diff --git a/crates/pdf-cmap/src/cmap/parser.rs b/crates/pdf-cmap/src/cmap/parser.rs index 09e27871..20dc8265 100644 --- a/crates/pdf-cmap/src/cmap/parser.rs +++ b/crates/pdf-cmap/src/cmap/parser.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use pdf_object_reader::object_variant::ObjectVariant; use pdf_parser::{error::ParserError, parser::PdfParser}; -use crate::{cmap::token::CMapToken, error::CMapError}; +use crate::{UnicodeSequence, cmap::token::CMapToken, error::CMapError}; /// Token reader for the small PostScript-like subset used in embedded CMaps. /// @@ -11,7 +11,7 @@ use crate::{cmap::token::CMapToken, error::CMapError}; /// needed by font CMap consumers rather than the full PDF object grammar. pub struct CMapParser<'a> { parser: PdfParser<'a>, - pub(super) to_unicode_map: HashMap>, + pub(super) to_unicode_map: HashMap, } impl<'a> From<&'a [u8]> for CMapParser<'a> { @@ -179,7 +179,7 @@ impl<'a> CMapParser<'a> { } /// Parse and return the ToUnicode mappings from this parser. - pub fn into_unicode_map(mut self) -> Result>, CMapError> { + pub fn into_unicode_map(mut self) -> Result, CMapError> { loop { let token = self.next_token_lenient()?; diff --git a/crates/pdf-cmap/src/cmap/to_unicode_sections.rs b/crates/pdf-cmap/src/cmap/to_unicode_sections.rs index bd7d2a61..68dffa75 100644 --- a/crates/pdf-cmap/src/cmap/to_unicode_sections.rs +++ b/crates/pdf-cmap/src/cmap/to_unicode_sections.rs @@ -1,27 +1,32 @@ use crate::{ + UnicodeSequence, cmap::{ parser::CMapParser, token::CMapToken, - unicode::{ - bytes_to_char_code, codepoint_to_chars, sequential_base_code, utf16_bytes_to_chars, - utf16_bytes_to_chars_non_empty, - }, + unicode::{SequentialDestination, utf16_destination}, }, + cmap_support::bytes_to_u32, error::CMapError, }; +/// Largest number of source codes one sequential `bfrange` may map. +/// +/// Source codes are up to four bytes wide, so this bounds the work and memory a +/// single malformed range such as `<00000000> ` can demand. +const MAX_SEQUENTIAL_RANGE_SPAN: u16 = u16::MAX; + /// Parser state while reading a `beginbfchar` section. enum BfCharState { NeedSource, - NeedDestination(u16), + NeedDestination(u32), } /// Parser state while reading a `beginbfrange` section. enum BfRangeState { NeedStart, - NeedEnd(u16), - NeedValue { start: u16, end: u16 }, - Array { code: u16, end: u16 }, + NeedEnd(u32), + NeedValue { start: u32, end: u32 }, + Array { code: u32, end: u32 }, } impl CMapParser<'_> { @@ -103,29 +108,23 @@ impl CMapParser<'_> { /// The next bfchar parser state after consuming the token. fn handle_bfchar_hex_token(&mut self, state: BfCharState, bytes: &[u8]) -> BfCharState { match state { - BfCharState::NeedSource => BfCharState::NeedDestination(bytes_to_char_code(bytes)), + BfCharState::NeedSource => BfCharState::NeedDestination(bytes_to_u32(bytes)), BfCharState::NeedDestination(source) => { - self.insert_bfchar_mapping(source, bytes); + self.insert_mapping(source, utf16_destination(bytes)); BfCharState::NeedSource } } } - /// Insert one direct ToUnicode mapping from a bfchar destination string. + /// Insert one ToUnicode mapping, omitting destinations that decoded to nothing. /// /// # Parameters /// - /// - `source`: The source character code to map. - /// - `bytes`: The destination UTF-16BE byte string. - /// - /// # Returns - /// - /// Nothing. The mapping is inserted into the parser's output map when the - /// decoded destination is not empty. - fn insert_bfchar_mapping(&mut self, source: u16, bytes: &[u8]) { - let chars = utf16_bytes_to_chars(bytes); - if !chars.is_empty() { - self.to_unicode_map.insert(source, chars); + /// - `source`: The packed source character code to map. + /// - `destination`: The decoded Unicode destination. + fn insert_mapping(&mut self, source: u32, destination: UnicodeSequence) { + if destination != UnicodeSequence::Empty { + self.to_unicode_map.insert(source, destination); } } @@ -164,17 +163,17 @@ impl CMapParser<'_> { /// The next bfrange parser state after consuming the token. fn handle_bfrange_hex_token(&mut self, state: BfRangeState, bytes: &[u8]) -> BfRangeState { match state { - BfRangeState::NeedStart => BfRangeState::NeedEnd(bytes_to_char_code(bytes)), + BfRangeState::NeedStart => BfRangeState::NeedEnd(bytes_to_u32(bytes)), BfRangeState::NeedEnd(start) => BfRangeState::NeedValue { start, - end: bytes_to_char_code(bytes), + end: bytes_to_u32(bytes), }, BfRangeState::NeedValue { start, end } => { self.insert_sequential_range(start, end, bytes); BfRangeState::NeedStart } BfRangeState::Array { code, end } => { - self.insert_bfrange_array_mapping(code, bytes); + self.insert_mapping(code, utf16_destination(bytes)); Self::next_bfrange_array_state(code, end) } } @@ -245,23 +244,6 @@ impl CMapParser<'_> { } } - /// Insert one array entry from a `beginbfrange` destination list. - /// - /// # Parameters - /// - /// - `code`: The source code that corresponds to the current array entry. - /// - `bytes`: The destination UTF-16BE byte string. - /// - /// # Returns - /// - /// Nothing. The mapping is inserted into the parser's output map when the - /// decoded destination is not empty. - fn insert_bfrange_array_mapping(&mut self, code: u16, bytes: &[u8]) { - if let Some(chars) = utf16_bytes_to_chars_non_empty(bytes) { - self.to_unicode_map.insert(code, chars); - } - } - /// Advance the array-entry state for a `beginbfrange` mapping. /// /// # Parameters @@ -272,7 +254,7 @@ impl CMapParser<'_> { /// # Returns /// /// The next array state, or `NeedStart` once the inclusive end is reached. - fn next_bfrange_array_state(code: u16, end: u16) -> BfRangeState { + fn next_bfrange_array_state(code: u32, end: u32) -> BfRangeState { if code >= end { BfRangeState::NeedStart } else { @@ -290,26 +272,25 @@ impl CMapParser<'_> { /// - `start`: The first source code in the inclusive range. /// - `end`: The last source code in the inclusive range. /// - `base_bytes`: The UTF-16BE destination bytes for the first entry in - /// the range. + /// the range. Each later source code advances the final code unit by one. /// /// # Returns /// /// Nothing. Each valid sequential destination is inserted into the parser's - /// output map. - fn insert_sequential_range(&mut self, start: u16, end: u16, base_bytes: &[u8]) { + /// output map. Ranges stop after [`MAX_SEQUENTIAL_RANGE_SPAN`] codes or once + /// the destination's final code unit would overflow. + fn insert_sequential_range(&mut self, start: u32, end: u32, base_bytes: &[u8]) { + let destination = SequentialDestination::from(base_bytes); let mut code = start; - let mut base = sequential_base_code(base_bytes); + let mut offset = 0_u16; - loop { - let chars = codepoint_to_chars(base); - if !chars.is_empty() { - self.to_unicode_map.insert(code, chars); - } - if code >= end { + while let Some(sequence) = destination.at(offset) { + self.insert_mapping(code, sequence); + if code >= end || offset == MAX_SEQUENTIAL_RANGE_SPAN { break; } code = code.saturating_add(1); - base = base.saturating_add(1); + offset = offset.saturating_add(1); } } } diff --git a/crates/pdf-cmap/src/cmap/unicode.rs b/crates/pdf-cmap/src/cmap/unicode.rs index 89a4e7f9..e059ae0f 100644 --- a/crates/pdf-cmap/src/cmap/unicode.rs +++ b/crates/pdf-cmap/src/cmap/unicode.rs @@ -1,167 +1,75 @@ -use pdf_object_reader::text_encoding::BigEndianU16Units; +use std::iter; -use crate::cmap_support::bytes_to_u32; +use pdf_object_reader::text_encoding::Utf16BeUnits; -/// Convert a ToUnicode source hex string into the parser's `u16` character code. -/// -/// PDF CMap source codes are variable-width byte strings. This crate stores -/// ToUnicode source codes as `u16`, so empty input maps to `.notdef` (`0`), -/// one-byte input maps directly, two-byte input is big-endian, and longer -/// input keeps the final two bytes. -pub(super) fn bytes_to_char_code(bytes: &[u8]) -> u16 { - match bytes { - [] => 0, - [byte] => u16::from(*byte), - [hi, lo] => u16::from_be_bytes([*hi, *lo]), - _ => trailing_char_code(bytes), - } -} +use crate::UnicodeSequence; -/// Decode a PDF UTF-16BE destination byte string into Unicode characters. +/// Decode a PDF UTF-16BE destination byte string into Unicode scalars. /// /// ToUnicode destinations are encoded as big-endian UTF-16 code units. This is -/// intentionally lossy for malformed PDF input: odd trailing bytes, unpaired -/// low surrogates, and invalid surrogate pairs are skipped instead of failing -/// the whole CMap parse. -pub(super) fn utf16_bytes_to_chars(bytes: &[u8]) -> Vec { - let mut chars = Vec::new(); - let mut units = BigEndianU16Units::from(bytes).units.into_iter(); - - while let Some(unit) = units.next() { - if is_high_surrogate(unit) { - if let Some(low) = units.next() - && let Some(c) = surrogate_pair_to_char(unit, low) - { - chars.push(c); - } - } else if let Some(c) = bmp_code_unit_to_char(unit) { - chars.push(c); - } - } - - chars -} - -/// Decode a UTF-16BE byte string and discard empty results. -/// -/// Callers use this when an empty Unicode mapping should be treated the same -/// as a malformed mapping and therefore omitted from the final map. -pub(super) fn utf16_bytes_to_chars_non_empty(bytes: &[u8]) -> Option> { - let chars = utf16_bytes_to_chars(bytes); - if chars.is_empty() { None } else { Some(chars) } -} - -/// Convert a Unicode scalar value to the map value representation. -/// -/// Invalid scalar values, including surrogate code points and values above the -/// Unicode range, produce an empty vector so malformed sequential ranges can be -/// skipped by the caller. -pub(super) fn codepoint_to_chars(cp: u32) -> Vec { - match char::from_u32(cp) { - Some(c) => vec![c], - None => Vec::new(), - } -} - -/// Convert a sequential `bfrange` destination byte string to its numeric base. -/// -/// Sequential range destinations are incremented as integers after each source -/// code, so this delegates to the shared big-endian byte packing helper. -pub(super) fn sequential_base_code(bytes: &[u8]) -> u32 { - bytes_to_u32(bytes) -} - -/// Return the final two bytes of an overlong source code as a `u16`. -fn trailing_char_code(bytes: &[u8]) -> u16 { - const PDF_CHAR_CODE_BYTES: usize = 2; - - let mut trailing = bytes.iter().rev().take(PDF_CHAR_CODE_BYTES); - let Some(lo) = trailing.next().copied() else { - return 0; - }; - let Some(hi) = trailing.next().copied() else { - return u16::from(lo); - }; - - u16::from_be_bytes([hi, lo]) +/// intentionally lossy for malformed PDF input: an odd trailing byte and +/// unpaired surrogates are skipped instead of failing the whole CMap parse. +pub(super) fn utf16_destination(bytes: &[u8]) -> UnicodeSequence { + Utf16BeUnits::from(bytes) + .chars() + .filter_map(Result::ok) + .collect() } -/// Return whether `unit` is a UTF-16 high surrogate. -fn is_high_surrogate(unit: u16) -> bool { - const HIGH_SURROGATE_END: u16 = 0xDBFF; - const HIGH_SURROGATE_START: u16 = 0xD800; - - (HIGH_SURROGATE_START..=HIGH_SURROGATE_END).contains(&unit) -} - -/// Return whether `unit` is a UTF-16 low surrogate. -fn is_low_surrogate(unit: u16) -> bool { - const LOW_SURROGATE_START: u16 = 0xDC00; - const LOW_SURROGATE_END: u16 = 0xDFFF; - - (LOW_SURROGATE_START..=LOW_SURROGATE_END).contains(&unit) +/// UTF-16 destination of a sequential `bfrange`, whose final code unit advances +/// by one for each source code in the range. +pub(super) struct SequentialDestination { + units: Vec, } -/// Decode one non-surrogate BMP code unit to a character. -/// -/// Low surrogates are invalid unless consumed after a high surrogate, so they -/// are ignored here. -fn bmp_code_unit_to_char(unit: u16) -> Option { - if is_low_surrogate(unit) { - None - } else { - char::from_u32(u32::from(unit)) +impl From<&[u8]> for SequentialDestination { + /// Split the base destination into code units. + /// + /// A single-byte destination is accepted as one code unit, since some + /// producers write `<41>` rather than `<0041>` for range bases. + fn from(bytes: &[u8]) -> Self { + let decoded = Utf16BeUnits::from(bytes); + let mut units: Vec = decoded.units().collect(); + if units.is_empty() + && let Some(byte) = decoded.trailing_byte() + { + units.push(u16::from(byte)); + } + Self { units } } } -/// Decode a valid UTF-16 surrogate pair into one Unicode character. -fn surrogate_pair_to_char(high: u16, low: u16) -> Option { - if !is_low_surrogate(low) { - return None; +impl SequentialDestination { + /// Return the destination for the source code `offset` places after the + /// range start, or `None` once the final code unit would overflow. + pub(super) fn at(&self, offset: u16) -> Option { + let (last, prefix) = self.units.split_last()?; + let last = last.checked_add(offset)?; + Some( + char::decode_utf16(prefix.iter().copied().chain(iter::once(last))) + .filter_map(Result::ok) + .collect(), + ) } - - char::from_u32(surrogate_pair_to_codepoint(high, low)?) -} - -/// Convert a known-valid UTF-16 surrogate pair into its scalar value. -/// -/// The checked additions are defensive: validated surrogate payloads cannot -/// exceed the Unicode range, but the workspace lints require arithmetic to make -/// overflow behavior explicit. -fn surrogate_pair_to_codepoint(high: u16, low: u16) -> Option { - const SURROGATE_HIGH_PAYLOAD_SHIFT: u32 = 10; - const SUPPLEMENTARY_PLANE_BASE: u32 = 0x1_0000; - const SURROGATE_PAYLOAD_MASK: u16 = 0x03FF; - - let high_bits = u32::from(high & SURROGATE_PAYLOAD_MASK) << SURROGATE_HIGH_PAYLOAD_SHIFT; - let low_bits = u32::from(low & SURROGATE_PAYLOAD_MASK); - - SUPPLEMENTARY_PLANE_BASE - .checked_add(high_bits)? - .checked_add(low_bits) } #[cfg(test)] mod tests { use super::*; - #[test] - fn packs_pdf_character_codes() { - assert_eq!(bytes_to_char_code(&[]), 0); - assert_eq!(bytes_to_char_code(&[0x41]), 0x41); - assert_eq!(bytes_to_char_code(&[0x12, 0x34]), 0x1234); - assert_eq!(bytes_to_char_code(&[0xAA, 0xBB, 0xCC]), 0xBBCC); + fn chars(sequence: &UnicodeSequence) -> &[char] { + sequence.as_slice() } #[test] fn decodes_bmp_utf16be_units() { - assert_eq!(utf16_bytes_to_chars(&[0x00, 0x41]), vec!['A']); + assert_eq!(chars(&utf16_destination(&[0x00, 0x41])), ['A']); } #[test] fn decodes_valid_surrogate_pairs() { assert_eq!( - utf16_bytes_to_chars(&[0xD8, 0x3D, 0xDE, 0x00]) + chars(&utf16_destination(&[0xD8, 0x3D, 0xDE, 0x00])) .first() .copied(), char::from_u32(0x1F600) @@ -170,28 +78,17 @@ mod tests { #[test] fn skips_unpaired_high_surrogates() { - assert!(utf16_bytes_to_chars(&[0xD8, 0x3D]).is_empty()); - assert!(utf16_bytes_to_chars(&[0xD8, 0x3D, 0x00, 0x41]).is_empty()); + assert!(utf16_destination(&[0xD8, 0x3D]).as_slice().is_empty()); + assert_eq!(chars(&utf16_destination(&[0xD8, 0x3D, 0x00, 0x41])), ['A']); } #[test] fn skips_unpaired_low_surrogates() { - assert!(utf16_bytes_to_chars(&[0xDE, 0x00]).is_empty()); + assert!(utf16_destination(&[0xDE, 0x00]).as_slice().is_empty()); } #[test] fn ignores_odd_trailing_utf16_byte() { - assert_eq!(utf16_bytes_to_chars(&[0x00, 0x41, 0x00]), vec!['A']); - } - - #[test] - fn rejects_invalid_scalar_values() { - assert!(codepoint_to_chars(0x11_0000).is_empty()); - assert!(codepoint_to_chars(0xD800_u32).is_empty()); - } - - #[test] - fn returns_none_for_empty_utf16_mapping() { - assert_eq!(utf16_bytes_to_chars_non_empty(&[0xDE, 0x00]), None); + assert_eq!(chars(&utf16_destination(&[0x00, 0x41, 0x00])), ['A']); } } diff --git a/crates/pdf-cmap/src/cmap_support.rs b/crates/pdf-cmap/src/cmap_support.rs index bd14b1bb..3c51f7b3 100644 --- a/crates/pdf-cmap/src/cmap_support.rs +++ b/crates/pdf-cmap/src/cmap_support.rs @@ -1,3 +1,5 @@ +use crate::{Cid, CidMapping, PdfCode}; + /// Convert a big-endian byte slice into a `u32`. /// /// # Parameters @@ -27,30 +29,39 @@ pub(crate) fn bytes_to_u32(bytes: &[u8]) -> u32 { /// /// # Returns /// -/// The packed source code, consumed byte length, and mapped CID. Unknown or unmapped input returns -/// the first byte with CID 0 so a streaming caller can advance safely. Empty input returns `None`. +/// The consumed source code and its mapped CID. Unknown or unmapped input returns the first byte +/// with CID 0 so a streaming caller can advance safely. Empty input returns `None`. pub(crate) fn decode_next_with_code_space( text: &[u8], allowed_code_lengths: &[usize], mut has_code_space: HasCodeSpace, mut map_code: MapCode, -) -> Option<(u32, usize, u16)> +) -> Option where HasCodeSpace: FnMut(u32, usize) -> bool, MapCode: FnMut(u32) -> Option, { - let first = text.first().copied()?; for len in allowed_code_lengths.iter().rev() { - let Some(bytes) = text.get(..*len) else { + let Some(source) = text + .get(..*len) + .and_then(|bytes| PdfCode::try_from(bytes).ok()) + else { continue; }; - let code = bytes_to_u32(bytes); - if has_code_space(code, *len) { - return Some((code, *len, map_code(code).unwrap_or(0))); + if has_code_space(source.value(), *len) { + let cid = map_code(source.value()).unwrap_or(0); + return Some(CidMapping { + source, + cid: Cid(u32::from(cid)), + }); } } - Some((u32::from(first), 1, 0)) + let source = PdfCode::try_from(text.get(..1)?).ok()?; + Some(CidMapping { + source, + cid: Cid(0), + }) } /// Shared lookup contract for Type0 encoding CMaps. @@ -102,10 +113,10 @@ pub(crate) trait Type0CodeMap { /// /// # Returns /// - /// The source code, consumed length, and CID selected by the longest valid code-space match. + /// The source code and CID selected by the longest valid code-space match. /// Non-empty input always consumes at least one byte, including malformed or unmapped input, so /// repeated calls cannot stall a streaming decoder. - fn decode_next(&self, text: &[u8]) -> Option<(u32, usize, u16)> { + fn decode_next(&self, text: &[u8]) -> Option { decode_next_with_code_space( text, self.allowed_code_lengths(), diff --git a/crates/pdf-cmap/src/mapping.rs b/crates/pdf-cmap/src/mapping.rs index 7905ebea..64a65e42 100644 --- a/crates/pdf-cmap/src/mapping.rs +++ b/crates/pdf-cmap/src/mapping.rs @@ -1,6 +1,6 @@ //! Shared character-code and mapping contracts for PDF CMaps. -use crate::{WritingMode, error::CMapError}; +use crate::{WritingMode, cmap_support::bytes_to_u32, error::CMapError}; /// Packed one-to-four-byte character code consumed from a PDF string. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -49,6 +49,18 @@ impl PdfCode { } } +impl TryFrom<&[u8]> for PdfCode { + type Error = CMapError; + + /// Packs one to four big-endian source bytes, recording their width. + fn try_from(bytes: &[u8]) -> Result { + let byte_len = u8::try_from(bytes.len()).map_err(|_| { + CMapError::InvalidPdfCode("character codes must contain one to four bytes") + })?; + Self::new(bytes_to_u32(bytes), byte_len) + } +} + /// Character identifier produced by a composite font CMap. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct Cid(pub u32); diff --git a/crates/pdf-cmap/src/to_unicode.rs b/crates/pdf-cmap/src/to_unicode.rs index b97b6155..fecbea3d 100644 --- a/crates/pdf-cmap/src/to_unicode.rs +++ b/crates/pdf-cmap/src/to_unicode.rs @@ -1,6 +1,6 @@ //! Parsing and compact storage for PDF `/ToUnicode` character maps. //! -//! Parsed vectors are converted once into [`UnicodeSequence`], keeping the common zero- and +//! Destinations are parsed directly into [`UnicodeSequence`], keeping the common zero- and //! one-scalar mappings inline and sharing storage only for genuine multi-scalar expansions. use std::collections::HashMap; @@ -26,10 +26,12 @@ pub enum ToUnicodeMap { impl ToUnicodeMap { /// Returns all Unicode scalars represented by `code` when a mapping exists. pub fn map(&self, code: PdfCode) -> Option { - let value = u16::try_from(code.value()).ok()?; match self { - Self::CMap(map) => map.map_char_code(value).cloned(), - Self::Identity => char::from_u32(u32::from(value)).map(Into::into), + Self::CMap(map) => map.map_char_code(code.value()).cloned(), + Self::Identity => u16::try_from(code.value()) + .ok() + .and_then(|value| char::from_u32(u32::from(value))) + .map(Into::into), } } } @@ -51,14 +53,17 @@ impl FromPdfObject for ToUnicodeMap { /// A parsed ToUnicode CMap that maps PDF character codes to Unicode scalar values. #[derive(Debug, Default)] -pub struct ToUnicodeCMap(HashMap); +pub struct ToUnicodeCMap(HashMap); impl ToUnicodeCMap { - /// Looks up the Unicode characters for the given PDF character code. + /// Looks up the Unicode characters for the given packed PDF character code. + /// + /// Codes are matched by numeric value regardless of their byte width, so a two-byte + /// `<0041>` source also maps the one-byte code `0x41`. /// /// The returned sequence is borrowed from the map; callers that need to retain it can clone the /// compact value without cloning a multi-scalar buffer. - pub fn map_char_code(&self, code: u16) -> Option<&UnicodeSequence> { + pub fn map_char_code(&self, code: u32) -> Option<&UnicodeSequence> { self.0.get(&code) } } @@ -66,14 +71,9 @@ impl ToUnicodeCMap { impl TryFrom<&[u8]> for ToUnicodeCMap { type Error = CMapError; - /// Parses a `/ToUnicode` CMap stream and compacts each parsed destination sequence. + /// Parses a `/ToUnicode` CMap stream. fn try_from(data: &[u8]) -> Result { - let mappings = CMapParser::from(data) - .into_unicode_map()? - .into_iter() - .map(|(code, characters)| (code, UnicodeSequence::from(characters))) - .collect(); - Ok(Self(mappings)) + CMapParser::from(data).into_unicode_map().map(Self) } } @@ -94,7 +94,7 @@ mod tests { string_kind::StringKind, }; - fn mapped(map: &ToUnicodeCMap, code: u16) -> Option<&[char]> { + fn mapped(map: &ToUnicodeCMap, code: u32) -> Option<&[char]> { map.map_char_code(code).map(UnicodeSequence::as_slice) } diff --git a/crates/pdf-cmap/src/type0/mod.rs b/crates/pdf-cmap/src/type0/mod.rs index 965b3181..cd6488bf 100644 --- a/crates/pdf-cmap/src/type0/mod.rs +++ b/crates/pdf-cmap/src/type0/mod.rs @@ -1,11 +1,9 @@ //! Type0 font encoding CMap support. -use std::convert::TryFrom; +use std::{convert::TryFrom, sync::Arc}; use pdf_object_reader::{ - FromPdfObject, ObjectAccess, ObjectContext, ReadResult, dictionary::Dictionary, - object_resolver::ObjectResolver, object_variant::ObjectVariant, - text_encoding::BigEndianU16Units, + FromPdfObject, ObjectAccess, ObjectContext, ReadResult, object_variant::ObjectVariant, }; mod embedded; mod parser; @@ -29,23 +27,6 @@ pub enum Type0EncodingCMap { } impl Type0EncodingCMap { - /// Parse the optional `/Encoding` entry of a Type0 font dictionary. - pub fn from_dictionary( - dictionary: &Dictionary, - objects: &dyn ObjectResolver, - ) -> Result, CMapError> { - dictionary - .get(b"Encoding") - .map(|value| { - let resolved = objects.resolve_object(value)?; - match resolved { - ObjectVariant::Stream(stream) => Self::from_bytes(stream.raw_data()), - _ => Self::from_name(value.try_bytes(objects)?), - } - }) - .transpose() - } - /// Build a Type0 encoding CMap from a predefined CMap name. pub fn from_name(name: &[u8]) -> Result { if let Ok(writing_mode) = WritingMode::try_from(name) { @@ -66,84 +47,36 @@ impl Type0EncodingCMap { pub fn from_bytes(data: &[u8]) -> Result { EmbeddedCMap::try_from(data).map(Self::Embedded) } - - /// Decode raw text bytes using the predefined Identity-H/V mapping. - /// - /// Identity CMaps define fixed-width 2-byte big-endian character codes, - /// and each code maps directly to the resulting CID. - pub fn decode_identity(text: &[u8]) -> Vec { - let decoded = BigEndianU16Units::from(text); - let mut units = decoded.units; - - // A non-empty remainder means the source text ended with an incomplete - // final code. That is malformed input, but we keep the decode - // infallible and emit CID 0 (`.notdef`) as the best-effort replacement. - if decoded.trailing_byte.is_some() { - units.push(0); - } - - units - } - - /// Decodes all raw text bytes into CIDs using this CMap. - /// - /// This allocating convenience method is intended for callers that need the complete CID list. - /// The text engine instead calls [`PdfCMap::decode_next`] and consumes mappings one at a time. - /// Unknown codes and an incomplete final identity code become CID 0 (`.notdef`) while still - /// consuming input, guaranteeing forward progress on malformed text. - pub fn decode(&self, text: &[u8]) -> Vec { - let mut remaining = text; - let mut decoded = Vec::new(); - while let Some(mapping) = self.decode_mapping(remaining) { - decoded.push(u16::try_from(mapping.cid.0).unwrap_or_default()); - let Some(next) = remaining.get(usize::from(mapping.source.byte_len())..) else { - break; - }; - remaining = next; - } - decoded - } - - /// Return whether this CMap is one of the predefined identity mappings. - pub fn is_identity(&self) -> bool { - matches!(self, Self::Identity { .. }) - } } -impl Type0EncodingCMap { - /// Decodes one source code without allocating an intermediate collection. +impl PdfCMap for Type0EncodingCMap { + /// Streams the first mapping in `bytes` using the CMap's malformed-input recovery policy. /// /// The returned mapping preserves both the packed source code and its byte length for - /// `/ToUnicode` lookup and exact stream advancement. Identity CMaps consume two-byte codes when - /// possible; an unmatched trailing byte is consumed as a one-byte `.notdef` mapping. Embedded - /// and predefined maps apply their longest valid code-space match. - fn decode_mapping(&self, bytes: &[u8]) -> Option { - let (code, byte_len, cid) = match self { - Self::Identity { .. } => { - let first = bytes.first().copied()?; - if let Some([first, second]) = bytes.get(..2) { - let cid = u16::from_be_bytes([*first, *second]); - (u32::from(cid), 2, cid) - } else { - (u32::from(first), 1, 0) + /// `/ToUnicode` lookup and exact stream advancement. Identity CMaps consume two-byte codes, + /// mapping each directly to its CID; an unmatched trailing byte is consumed as a one-byte + /// `.notdef` mapping. Embedded and predefined maps apply their longest valid code-space match. + fn decode_next(&self, bytes: &[u8]) -> Result, CMapError> { + Ok(match self { + Self::Identity { .. } => match bytes.get(..2).or_else(|| bytes.get(..1)) { + Some(source) => { + let source = PdfCode::try_from(source)?; + let cid = if source.byte_len() == 2 { + source.value() + } else { + 0 + }; + Some(CidMapping { + source, + cid: Cid(cid), + }) } - } - Self::Predefined(cmap) => cmap.decode_next(bytes)?, - Self::Embedded(cmap) => cmap.decode_next(bytes)?, - }; - let byte_len = u8::try_from(byte_len).ok()?; - Some(CidMapping { - source: PdfCode::new(code, byte_len).ok()?, - cid: Cid(u32::from(cid)), + None => None, + }, + Self::Predefined(cmap) => cmap.decode_next(bytes), + Self::Embedded(cmap) => cmap.decode_next(bytes), }) } -} - -impl PdfCMap for Type0EncodingCMap { - /// Streams the first mapping in `bytes` using the CMap's malformed-input recovery policy. - fn decode_next(&self, bytes: &[u8]) -> Result, CMapError> { - Ok(self.decode_mapping(bytes)) - } /// Returns the writing mode declared by the identity, predefined, or embedded CMap. fn writing_mode(&self) -> WritingMode { @@ -164,7 +97,7 @@ impl FromPdfObject for Type0EncodingCMap { Ok(Self::from_bytes(context.stream().raw_data())?) } _ => { - let name = std::sync::Arc::<[u8]>::from_pdf_object(context)?; + let name = Arc::<[u8]>::from_pdf_object(context)?; Ok(Self::from_name(&name)?) } } @@ -176,12 +109,22 @@ impl FromPdfObject for Type0EncodingCMap { mod tests { use super::*; + /// Decodes all of `text` into CIDs, one mapping at a time. + fn decode(cmap: &Type0EncodingCMap, mut text: &[u8]) -> Vec { + let mut decoded = Vec::new(); + while let Some(mapping) = cmap.decode_next(text).unwrap() { + decoded.push(mapping.cid.0); + text = &text[usize::from(mapping.source.byte_len())..]; + } + decoded + } + #[test] fn identity_h_decodes_big_endian_pairs() { let cmap = Type0EncodingCMap::from_name(b"Identity-H").unwrap(); - assert_eq!(cmap.decode(&[0x00, 0x01, 0x12, 0x34]), vec![1, 0x1234]); - assert!(cmap.is_identity()); + assert_eq!(decode(&cmap, &[0x00, 0x01, 0x12, 0x34]), vec![1, 0x1234]); + assert!(matches!(cmap, Type0EncodingCMap::Identity { .. })); assert_eq!(cmap.writing_mode(), WritingMode::Horizontal); } @@ -189,17 +132,7 @@ mod tests { fn identity_h_decodes_trailing_incomplete_code_as_notdef() { let cmap = Type0EncodingCMap::from_name(b"Identity-H").unwrap(); - assert_eq!(cmap.decode(&[0x00, 0x01, 0xFF]), vec![1, 0]); - } - - #[test] - fn decode_identity_handles_complete_pairs_empty_input_and_trailing_byte() { - assert_eq!(Type0EncodingCMap::decode_identity(&[0xFF]), vec![0]); - assert_eq!( - Type0EncodingCMap::decode_identity(&[0x00, 0x01, 0x12, 0x34]), - vec![1, 0x1234] - ); - assert_eq!(Type0EncodingCMap::decode_identity(&[]), Vec::::new()); + assert_eq!(decode(&cmap, &[0x00, 0x01, 0xFF]), vec![1, 0]); } #[test] @@ -207,7 +140,7 @@ mod tests { let cmap = Type0EncodingCMap::from_name(b"Identity-V").unwrap(); assert_eq!( - cmap.decode(&[0x00, 0x01, 0x12, 0x34, 0xFF]), + decode(&cmap, &[0x00, 0x01, 0x12, 0x34, 0xFF]), vec![1, 0x1234, 0] ); assert_eq!(cmap.writing_mode(), WritingMode::Vertical); @@ -234,7 +167,7 @@ mod tests { let cmap = Type0EncodingCMap::from_bytes(data).unwrap(); assert_eq!( - cmap.decode(&[0x20, 0x01, 0x00, 0x01, 0x02]), + decode(&cmap, &[0x20, 0x01, 0x00, 0x01, 0x02]), vec![7, 50, 52] ); assert_eq!(cmap.writing_mode(), WritingMode::Horizontal); @@ -261,7 +194,7 @@ mod tests { let cmap = Type0EncodingCMap::from_bytes(data).unwrap(); - assert_eq!(cmap.decode(&[0x00, 0x41]), vec![7]); + assert_eq!(decode(&cmap, &[0x00, 0x41]), vec![7]); assert_eq!(cmap.writing_mode(), WritingMode::Horizontal); } @@ -291,7 +224,7 @@ mod tests { let cmap = Type0EncodingCMap::from_bytes(data).unwrap(); - assert_eq!(cmap.decode(&[0x20, 0xE3, 0x80, 0x81]), vec![1, 38]); + assert_eq!(decode(&cmap, &[0x20, 0xE3, 0x80, 0x81]), vec![1, 38]); } #[test] @@ -310,7 +243,10 @@ mod tests { let cmap = Type0EncodingCMap::from_bytes(data).unwrap(); - assert_eq!(cmap.decode(&[0x00, 0x01, 0x00, 0x02, 0xFF]), vec![4, 0, 0]); + assert_eq!( + decode(&cmap, &[0x00, 0x01, 0x00, 0x02, 0xFF]), + vec![4, 0, 0] + ); assert_eq!(cmap.writing_mode(), WritingMode::Vertical); } @@ -332,14 +268,17 @@ mod tests { let cmap = Type0EncodingCMap::from_bytes(data).unwrap(); - assert_eq!(cmap.decode(&[0x00, 0x20, 0x00, 0x43]), vec![3, 72]); + assert_eq!(decode(&cmap, &[0x00, 0x20, 0x00, 0x43]), vec![3, 72]); } #[test] fn predefined_japanese_shift_jis_cmap_decodes_single_and_double_byte_codes() { let cmap = Type0EncodingCMap::from_name(b"90ms-RKSJ-H").unwrap(); - assert_eq!(cmap.decode(&[0x20, 0x81, 0x40, 0x7E]), vec![231, 633, 631]); + assert_eq!( + decode(&cmap, &[0x20, 0x81, 0x40, 0x7E]), + vec![231, 633, 631] + ); assert_eq!(cmap.writing_mode(), WritingMode::Horizontal); } @@ -347,7 +286,7 @@ mod tests { fn predefined_vertical_cmap_uses_horizontal_base_cmap() { let cmap = Type0EncodingCMap::from_name(b"90ms-RKSJ-V").unwrap(); - assert_eq!(cmap.decode(&[0x20, 0x81, 0x40]), vec![231, 633]); + assert_eq!(decode(&cmap, &[0x20, 0x81, 0x40]), vec![231, 633]); assert_eq!(cmap.writing_mode(), WritingMode::Vertical); } } diff --git a/crates/pdf-cmap/src/unicode_sequence.rs b/crates/pdf-cmap/src/unicode_sequence.rs index dde25058..208c159d 100644 --- a/crates/pdf-cmap/src/unicode_sequence.rs +++ b/crates/pdf-cmap/src/unicode_sequence.rs @@ -48,12 +48,16 @@ impl From> for UnicodeSequence { } } -impl From> for UnicodeSequence { - fn from(characters: Vec) -> Self { - match characters.as_slice() { - [] => Self::Empty, - [character] => Self::Scalar(*character), - _ => Self::Sequence(Arc::from(characters)), - } +impl FromIterator for UnicodeSequence { + /// Collects `characters`, allocating shared storage only for two or more scalars. + fn from_iter>(characters: I) -> Self { + let mut characters = characters.into_iter(); + let Some(first) = characters.next() else { + return Self::Empty; + }; + let Some(second) = characters.next() else { + return Self::Scalar(first); + }; + Self::Sequence([first, second].into_iter().chain(characters).collect()) } } diff --git a/crates/pdf-object-reader/src/text_encoding.rs b/crates/pdf-object-reader/src/text_encoding.rs index ab046f03..901c1938 100644 --- a/crates/pdf-object-reader/src/text_encoding.rs +++ b/crates/pdf-object-reader/src/text_encoding.rs @@ -1,62 +1,65 @@ -/// Big-endian `u16` code units decoded from a byte string. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct BigEndianU16Units { - /// Complete two-byte code units from the input. - pub units: Vec, - /// The unmatched final byte, when the input length is odd. - pub trailing_byte: Option, +use std::char::DecodeUtf16Error; + +/// Big-endian UTF-16 code units borrowed from a byte string. +/// +/// The unmatched final byte of an odd-length input is kept separately so callers can apply the +/// malformed-input policy required by their PDF context. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Utf16BeUnits<'a> { + pairs: &'a [[u8; 2]], + trailing_byte: Option, } -impl From<&[u8]> for BigEndianU16Units { - /// Decodes complete pairs of bytes into big-endian `u16` code units. - /// - /// The unmatched final byte of an odd-length input is returned separately - /// so callers can apply the malformed-input policy required by their PDF - /// context. - fn from(bytes: &[u8]) -> Self { +impl<'a> From<&'a [u8]> for Utf16BeUnits<'a> { + /// Splits `bytes` into complete big-endian pairs without allocating. + fn from(bytes: &'a [u8]) -> Self { let (pairs, remainder) = bytes.as_chunks::<2>(); - Self { - units: pairs.iter().map(|&pair| u16::from_be_bytes(pair)).collect(), + pairs, trailing_byte: remainder.first().copied(), } } } +impl Utf16BeUnits<'_> { + /// Iterates over the complete big-endian `u16` code units. + pub fn units(&self) -> impl Iterator + '_ { + self.pairs.iter().map(|&pair| u16::from_be_bytes(pair)) + } + + /// Decodes the code units as UTF-16, yielding an error for each unpaired surrogate. + pub fn chars(&self) -> impl Iterator> + '_ { + char::decode_utf16(self.units()) + } + + /// Returns the unmatched final byte, when the input length is odd. + pub fn trailing_byte(&self) -> Option { + self.trailing_byte + } +} + #[cfg(test)] mod tests { use super::*; #[test] fn decodes_complete_big_endian_units() { - assert_eq!( - BigEndianU16Units::from(&[0x00, 0x01, 0x12, 0x34][..]), - BigEndianU16Units { - units: vec![1, 0x1234], - trailing_byte: None, - } - ); + let units = Utf16BeUnits::from(&[0x00, 0x01, 0x12, 0x34][..]); + assert_eq!(units.units().collect::>(), vec![1, 0x1234]); + assert_eq!(units.trailing_byte(), None); } #[test] fn returns_an_unmatched_trailing_byte() { - assert_eq!( - BigEndianU16Units::from(&[0x00, 0x01, 0xFF][..]), - BigEndianU16Units { - units: vec![1], - trailing_byte: Some(0xFF), - } - ); + let units = Utf16BeUnits::from(&[0x00, 0x01, 0xFF][..]); + assert_eq!(units.units().collect::>(), vec![1]); + assert_eq!(units.trailing_byte(), Some(0xFF)); } #[test] fn decodes_empty_input() { - assert_eq!( - BigEndianU16Units::from(&[][..]), - BigEndianU16Units { - units: Vec::new(), - trailing_byte: None, - } - ); + let units = Utf16BeUnits::from(&[][..]); + assert_eq!(units.units().count(), 0); + assert_eq!(units.trailing_byte(), None); } } diff --git a/crates/pdf-object-reader/src/text_string.rs b/crates/pdf-object-reader/src/text_string.rs index e9ae24e1..5cf0b8fe 100644 --- a/crates/pdf-object-reader/src/text_string.rs +++ b/crates/pdf-object-reader/src/text_string.rs @@ -1,4 +1,4 @@ -use crate::text_encoding::BigEndianU16Units; +use crate::text_encoding::Utf16BeUnits; use thiserror::Error; const UTF16_BIG_ENDIAN_BOM: &[u8] = &[0xFE, 0xFF]; @@ -49,12 +49,15 @@ pub fn encode(text: &str) -> Vec { } fn decode_utf16_big_endian(bytes: &[u8]) -> Result { - let decoded = BigEndianU16Units::from(bytes); - if decoded.trailing_byte.is_some() { + let units = Utf16BeUnits::from(bytes); + if units.trailing_byte().is_some() { return Err(TextStringError::IncompleteUtf16CodeUnit); } - String::from_utf16(&decoded.units).map_err(|_| TextStringError::InvalidUtf16) + units + .chars() + .collect::>() + .map_err(|_| TextStringError::InvalidUtf16) } fn decode_pdf_doc_encoding(bytes: &[u8]) -> Result {