Repository: J-F-Liu/lopdf (language: rust)

Issue:
Implement decoding of Unicode characters
Possible duplicate of #86 if decoding and encoding would need to be implemented together.

I've got a PDF with the text `打尼爾` and

```rust
println!("{:?}", doc.extract_text(&vec![1]).unwrap());
```

yields:

```
"?Identity-H Unimplemented??Identity-H Unimplemented??Identity-H Unimplemented?\n"
```


Discussion / hints:
Hello, any updates here?

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index ce244e9..90e5e57 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -34,6 +34,7 @@ serde = { version = "1.0", features = ["derive"], optional = true }
 time = { version = "0.3", features = ["formatting", "parsing"] }
 tokio = { version = "1", features = ["fs", "io-util"], optional = true }
 weezl = "0.1"
+rangemap = "1.5"
 
 [dev-dependencies]
 clap = { version = "4.0", features = ["derive"] }
diff --git a/assets/unicode.pdf b/assets/unicode.pdf
new file mode 100644
index 0000000..42416a7
Binary files /dev/null and b/assets/unicode.pdf differ
diff --git a/src/cmap_parser.rs b/src/cmap_parser.rs
new file mode 100644
index 0000000..986862f
--- /dev/null
+++ b/src/cmap_parser.rs
@@ -0,0 +1,720 @@
+use pom::parser::*;
+
+use crate::cmap_section::{ArrayOfTargetStrings, CMapParseError, CMapSection, SourceCharMapping, SourceRangeMapping};
+use crate::parser::{dictionary, eol, hex_char, name, space};
+
+fn hex_u16<'a>() -> Parser<'a, u8, u16> {
+    (hex_char() + hex_char()).map(|(u1, u2)| u1 as u16 * 256 + u2 as u16)
+}
+
+fn space_no_crlf<'a>() -> Parser<'a, u8, ()> {
+    one_of(b" \t").repeat(0..).discard()
+}
+
+fn ws_newline<'a>() -> Parser<'a, u8, ()> {
+    space_no_crlf() * eol().discard()
+}
+
+fn whitespace<'a>() -> Parser<'a, u8, ()> {
+    one_of(b" \t\n\r\0\x0C").repeat(1..).discard()
+}
+
+fn source_code<'a>() -> Parser<'a, u8, u16> {
+    sym(b'<') * hex_u16() - sym(b'>')
+}
+
+fn code_range_pair<'a>() -> Parser<'a, u8, (u16, u16)> {
+    space_no_crlf() * source_code() + space_no_crlf() * source_code()
+}
+
+fn codespace_range_section<'a>() -> Parser<'a, u8, Vec<(u16, u16)>> {
+    one_of(b"0123456789").repeat(1..)
+        * space_no_crlf()
+        * seq(b"begincodespacerange")
+        * ws_newline()
+        * (code_range_pair() - ws_newline()).repeat(1..)
+        - seq(b"endcodespacerange")
+        - ws_newline()
+}
+
+fn target_string<'a>() -> Parser<'a, u8, Vec<u16>> {
+    // no more than 512 bytes in a target string
+    sym(b'<') * hex_u16().repeat(1..256) - sym(b'>')
+}
+
+fn range_target_array<'a>() -> Parser<'a, u8, ArrayOfTargetStrings> {
+    sym(b'[') * space_no_crlf() * (target_string() - space_no_crlf()).repeat(1..) - sym(b']')
+}
+
+fn bf_range_line<'a>() -> Parser<'a, u8, SourceRangeMapping> {
+    code_range_pair() + space_no_crlf() * (target_string().map(|it| vec![it]) | range_target_array()) - ws_newline()
+}
+
+fn bf_range_section<'a>() -> Parser<'a, u8, Vec<SourceRangeMapping>> {
+    one_of(b"0123456789").repeat(1..)
+        * space_no_crlf()
+        * seq(b"beginbfrange")
+        * ws_newline()
+        * bf_range_line().repeat(1..)
+        - seq(b"endbfrange")
+        - ws_newline()
+}
+
+fn bf_char_line<'a>() -> Parser<'a, u8, SourceCharMapping> {
+    space_no_crlf() * source_code() + space_no_crlf() * target_string() - space_no_crlf() - eol().discard()
+}
+
+fn bf_char_section<'a>() -> Parser<'a, u8, Vec<SourceCharMapping>> {
+    one_of(b"0123456789").repeat(1..)
+        * space_no_crlf()
+        * seq(b"beginbfchar")
+        * ws_newline()
+        * bf_char_line().repeat(1..)
+        - seq(b"endbfchar")
+        - ws_newline()
+}
+
+fn cmap_type<'a>() -> Parser<'a, u8, ()> {
+    space_no_crlf() * seq(b"/CMapType") * space_no_crlf() * sym(b'2') * space_no_crlf() * seq(b"def") * ws_newline()
+}
+
+fn cmap_name<'a>() -> Parser<'a, u8, ()> {
+    space_no_crlf() * seq(b"/CMapName") * space_no_crlf() * name() * space_no_crlf() * seq(b"def") * ws_newline()
+}
+
+fn cid_system_info<'a>() -> Parser<'a, u8, ()> {
+    space_no_crlf() * seq(b"/CIDSystemInfo") * space() * dictionary() * space() * seq(b"def") * ws_newline()
+}
+
+fn cmap_stream<'a>() -> Parser<'a, u8, Vec<CMapSection>> {
+    use self::space_no_crlf as ws;
+    use self::ws_newline as nl;
+    ws() * seq(b"/CIDInit")
+        * ws()
+        * seq(b"/ProcSet")
+        * ws()
+        * seq(b"findresource")
+        * ws()
+        * seq(b"begin")
+        * nl()
+        * ws()
+        * one_of(b"0123456789").repeat(1..)
+        * ws()
+        * seq(b"dict")
+        * ws()
+        * seq(b"begin")
+        * nl()
+        * ws()
+        * seq(b"begincmap")
+        * nl()
+        * (cmap_type() | cmap_name() | cid_system_info()).repeat(1..4)
+        * (codespace_range_section().map(CMapSection::CsRange)
+            | bf_char_section().map(CMapSection::BfChar)
+            | bf_range_section().map(CMapSection::BfRange))
+        .repeat(1..)
+        - ws() * seq(b"endcmap") * nl()
+        - ws()
+            * seq(b"CMapName")
+            * ws()
+            * seq(b"currentdict")
+            * ws()
+            * seq(b"/CMap")
+            * ws()
+            * seq(b"defineresource")
+            * ws()
+            * seq(b"pop")
+            * nl()
+            * ws()
+            * seq(b"end")
+            * whitespace()
+            * seq(b"end")
+}
+
+pub(crate) fn parse(stream_content: &[u8]) -> Result<Vec<CMapSection>, CMapParseError> {
+    cmap_stream().parse(stream_content).map_err(CMapParseError::from)
+}
+
+impl From<pom::Error> for CMapParseError {
+    fn from(err: pom::Error) -> Self {
+        match err {
+            pom::Error::Incomplete => CMapParseError::Incomplete,
+            _ => CMapParseError::Error,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn parse_source_code() {
+        let data = "<080F>";
+        assert_eq!(source_code().parse(data.as_bytes()), Ok(0x080f))
+    }
+
+    #[test]
+    fn parse_invalid_source_code() {
+        let data = "<080f01>";
+        assert!(source_code().parse(data.as_bytes()).is_err())
+    }
+
+    #[test]
+    fn parse_code_range_pair() {
+        let data = "<080F> <08FF> ";
+        assert_eq!(code_range_pair().parse(data.as_bytes()), Ok((0x080f, 0x08ff)),)
+    }
+
+    #[test]
+    fn parse_bfrange_line() {
+        let data = "<080f> <08ff> <09000110>\n";
+        assert_eq!(
+            bf_range_line().parse(data.as_bytes()),
+            Ok(((0x080f, 0x08ff), vec![vec![0x0900, 0x0110]]))
+        )
+    }
+    #[test]
+    fn parse_bfrange_line_array() {
+        let data = "<080f> <08ff> [ <09000110> <08fe> ] \n";
+        assert_eq!(
+            bf_range_line().parse(data.as_bytes()),
+            Ok(((0x080f, 0x08ff), vec![vec![0x0900, 0x0110], vec![0x08fe]]))
+        )
+    }
+    #[test]
+    fn parse_invalid_bfrange_line() {
+        let data = "<080f> <08ff> [ <09000110> <08FF> <09fe80> ]\n";
+        assert!(bf_range_line().parse(data.as_bytes()).is_err())
+    }
+
+    #[test]
+    fn parse_codespace_range_section() {
+        let data = "1 begincodespacerange\n\
+            <0000> <FFFF> \n\
+        endcodespacerange\n";
+        assert_eq!(
+            codespace_range_section().parse(data.as_bytes()),
+            Ok(vec![(0x0000, 0xffff)])
+        )
+    }
+
+    #[test]
+    fn parse_bf_range_section() {
+        let data = "3 beginbfrange \n\
+            <0000> <000f> <0000>\n\
+            <0010> <001f> <00000010> \n\
+            <0020>  <002f> [<0000> <00000010> ]\n\
+        endbfrange\n";
+        assert_eq!(
+            bf_range_section().parse(data.as_bytes()),
+            Ok(vec![
+                ((0x0000, 0x000f), vec![vec![0x0000]]),
+                ((0x0010, 0x001f), vec![vec![0x0000, 0x0010]]),
+                ((0x0020, 0x002f), vec![vec![0x0000], vec![0x0000, 0x0010]]),
+            ])
+        )
+    }
+
+    #[test]
+    fn parse_cid_system_info() {
+        let data = "/CIDSystemInfo <<
+/Registry (Adobe)
+/Ordering (UCS)
+/Supplement 0
+>> def
+";
+        assert!(cid_system_info().parse(data.as_bytes()).is_ok())
+    }
+
+    #[test]
+    fn parse_cid_system_info_with_spaces() {
+        let data = "/CIDSystemInfo
+<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n";
+        assert!(cid_system_info().parse(data.as_bytes()).is_ok())
+    }
+
+    #[test]
+    fn parse_cmap_name() {
+        let data = "/CMapName /Adobe-Identity-UCS def\n";
+        assert!(cmap_name().parse(data.as_bytes()).is_ok())
+    }
+
+    #[test]
+    fn parse_cmap_name2() {
+        let data = "/CMapName /Adobe-UCS-0 def\n";
+        assert!(cmap_name().parse(data.as_bytes()).is_ok())
+    }
+
+    #[test]
+    fn parse_cmap_type() {
+        let data = "/CMapType 2 def\n";
+        assert!(cmap_type().parse(data.as_bytes()).is_ok())
+    }
+
+    #[test]
+    fn parse_cmap_section_1() {
+        let data = "/CIDInit /ProcSet findresource begin
+12 dict begin
+begincmap
+/CIDSystemInfo <<
+/Registry (Adobe)
+/Ordering (UCS)
+/Supplement 0
+>> def
+/CMapName /Adobe-Identity-UCS def
+/CMapType 2 def
+1 begincodespacerange
+<0000> <FFFF>
+endcodespacerange
+96 beginbfrange
+<0000> <00FF> <0000>
+<0100> <01FF> <0100>
+<0200> <02FF> <0200>
+<0300> <03FF> <0300>
+<0400> <04FF> <0400>
+<0500> <05FF> <0500>
+<0600> <06FF> <0600>
+<0700> <07FF> <0700>
+<0800> <08FF> <0800>
+<0900> <09FF> <0900>
+<0A00> <0AFF> <0A00>
+<0B00> <0BFF> <0B00>
+<0C00> <0CFF> <0C00>
+<0D00> <0DFF> <0D00>
+<0E00> <0EFF> <0E00>
+<0F00> <0FFF> <0F00>
+<1000> <10FF> <1000>
+<1100> <11FF> <1100>
+<1200> <12FF> <1200>
+<1300> <13FF> <1300>
+<1400> <14FF> <1400>
+<1500> <15FF> <1500>
+<1600> <16FF> <1600>
+<1700> <17FF> <1700>
+<1800> <18FF> <1800>
+<1900> <19FF> <1900>
+<1A00> <1AFF> <1A00>
+<1B00> <1BFF> <1B00>
+<1C00> <1CFF> <1C00>
+<1D00> <1DFF> <1D00>
+<1E00> <1EFF> <1E00>
+<1F00> <1FFF> <1F00>
+<2000> <20FF> <2000>
+<2100> <21FF> <2100>
+<2200> <22FF> <2200>
+<2300> <23FF> <2300>
+<2400> <24FF> <2400>
+<2500> <25FF> <2500>
+<2600> <26FF> <2600>
+<2700> <27FF> <2700>
+<2800> <28FF> <2800>
+<2900> <29FF> <2900>
+<2A00> <2AFF> <2A00>
+<2B00> <2BFF> <2B00>
+<2C00> <2CFF> <2C00>
+<2D00> <2DFF> <2D00>
+<2E00> <2EFF> <2E00>
+<2F00> <2FFF> <2F00>
+<3000> <30FF> <3000>
+<3100> <31FF> <3100>
+<3200> <32FF> <3200>
+<3300> <33FF> <3300>
+<3400> <34FF> <3400>
+<3500> <35FF> <3500>
+<3600> <36FF> <3600>
+<3700> <37FF> <3700>
+<3800> <38FF> <3800>
+<3900> <39FF> <3900>
+<3A00> <3AFF> <3A00>
+<3B00> <3BFF> <3B00>
+<3C00> <3CFF> <3C00>
+<3D00> <3DFF> <3D00>
+<3E00> <3EFF> <3E00>
+<3F00> <3FFF> <3F00>
+<4000> <40FF> <4000>
+<4100> <41FF> <4100>
+<4200> <42FF> <4200>
+<4300> <43FF> <4300>
+<4400> <44FF> <4400>
+<4500> <45FF> <4500>
+<4600> <46FF> <4600>
+<4700> <47FF> <4700>
+<4800> <48FF> <4800>
+<4900> <49FF> <4900>
+<4A00> <4AFF> <4A00>
+<4B00> <4BFF> <4B00>
+<4C00> <4CFF> <4C00>
+<4D00> <4DFF> <4D00>
+<4E00> <4EFF> <4E00>
+<4F00> <4FFF> <4F00>
+<5000> <50FF> <5000>
+<5100> <51FF> <5100>
+<5200> <52FF> <5200>
+<5300> <53FF> <5300>
+<5400> <54FF> <5400>
+<5500> <55FF> <5500>
+<5600> <56FF> <5600>
+<5700> <57FF> <5700>
+<5800> <58FF> <5800>
+<5900> <59FF> <5900>
+<5A00> <5AFF> <5A00>
+<5B00> <5BFF> <5B00>
+<5C00> <5CFF> <5C00>
+<5D00> <5DFF> <5D00>
+<5E00> <5EFF> <5E00>
+<5F00> <5FFF> <5F00>
+endbfrange
+96 beginbfrange
+<6000> <60FF> <6000>
+<6100> <61FF> <6100>
+<6200> <62FF> <6200>
+<6300> <63FF> <6300>
+<6400> <64FF> <6400>
+<6500> <65FF> <6500>
+<6600> <66FF> <6600>
+<6700> <67FF> <6700>
+<6800> <68FF> <6800>
+<6900> <69FF> <6900>
+<6A00> <6AFF> <6A00>
+<6B00> <6BFF> <6B00>
+<6C00> <6CFF> <6C00>
+<6D00> <6DFF> <6D00>
+<6E00> <6EFF> <6E00>
+<6F00> <6FFF> <6F00>
+<7000> <70FF> <7000>
+<7100> <71FF> <7100>
+<7200> <72FF> <7200>
+<7300> <73FF> <7300>
+<7400> <74FF> <7400>
+<7500> <75FF> <7500>
+<7600> <76FF> <7600>
+<7700> <77FF> <7700>
+<7800> <78FF> <7800>
+<7900> <79FF> <7900>
+<7A00> <7AFF> <7A00>
+<7B00> <7BFF> <7B00>
+<7C00> <7CFF> <7C00>
+<7D00> <7DFF> <7D00>
+<7E00> <7EFF> <7E00>
+<7F00> <7FFF> <7F00>
+<8000> <80FF> <8000>
+<8100> <81FF> <8100>
+<8200> <82FF> <8200>
+<8300> <83FF> <8300>
+<8400> <84FF> <8400>
+<8500> <85FF> <8500>
+<8600> <86FF> <8600>
+<8700> <87FF> <8700>
+<8800> <88FF> <8800>
+<8900> <89FF> <8900>
+<8A00> <8AFF> <8A00>
+<8B00> <8BFF> <8B00>
+<8C00> <8CFF> <8C00>
+<8D00> <8DFF> <8D00>
+<8E00> <8EFF> <8E00>
+<8F00> <8FFF> <8F00>
+<9000> <90FF> <9000>
+<9100> <91FF> <9100>
+<9200> <92FF> <9200>
+<9300> <93FF> <9300>
+<9400> <94FF> <9400>
+<9500> <95FF> <9500>
+<9600> <96FF> <9600>
+<9700> <97FF> <9700>
+<9800> <98FF> <9800>
+<9900> <99FF> <9900>
+<9A00> <9AFF> <9A00>
+<9B00> <9BFF> <9B00>
+<9C00> <9CFF> <9C00>
+<9D00> <9DFF> <9D00>
+<9E00> <9EFF> <9E00>
+<9F00> <9FFF> <9F00>
+<A000> <A0FF> <A000>
+<A100> <A1FF> <A100>
+<A200> <A2FF> <A200>
+<A300> <A3FF> <A300>
+<A400> <A4FF> <A400>
+<A500> <A5FF> <A500>
+<A600> <A6FF> <A600>
+<A700> <A7FF> <A700>
+<A800> <A8FF> <A800>
+<A900> <A9FF> <A900>
+<AA00> <AAFF> <AA00>
+<AB00> <ABFF> <AB00>
+<AC00> <ACFF> <AC00>
+<AD00> <ADFF> <AD00>
+<AE00> <AEFF> <AE00>
+<AF00> <AFFF> <AF00>
+<B000> <B0FF> <B000>
+<B100> <B1FF> <B100>
+<B200> <B2FF> <B200>
+<B300> <B3FF> <B300>
+<B400> <B4FF> <B400>
+<B500> <B5FF> <B500>
+<B600> <B6FF> <B600>
+<B700> <B7FF> <B700>
+<B800> <B8FF> <B800>
+<B900> <B9FF> <B900>
+<BA00> <BAFF> <BA00>
+<BB00> <BBFF> <BB00>
+<BC00> <BCFF> <BC00>
+<BD00> <BDFF> <BD00>
+<BE00> <BEFF> <BE00>
+<BF00> <BFFF> <BF00>
+endbfrange
+64 beginbfrange
+<C000> <C0FF> <C000>
+<C100> <C1FF> <C100>
+<C200> <C2FF> <C200>
+<C300> <C3FF> <C300>
+<C400> <C4FF> <C400>
+<C500> <C5FF> <C500>
+<C600> <C6FF> <C600>
+<C700> <C7FF> <C700>
+<C800> <C8FF> <C800>
+<C900> <C9FF> <C900>
+<CA00> <CAFF> <CA00>
+<CB00> <CBFF> <CB00>
+<CC00> <CCFF> <CC00>
+<CD00> <CDFF> <CD00>
+<CE00> <CEFF> <CE00>
+<CF00> <CFFF> <CF00>
+<D000> <D0FF> <D000>
+<D100> <D1FF> <D100>
+<D200> <D2FF> <D200>
+<D300> <D3FF> <D300>
+<D400> <D4FF> <D400>
+<D500> <D5FF> <D500>
+<D600> <D6FF> <D600>
+<D700> <D7FF> <D700>
+<D800> <D8FF> <D800>
+<D900> <D9FF> <D900>
+<DA00> <DAFF> <DA00>
+<DB00> <DBFF> <DB00>
+<DC00> <DCFF> <DC00>
+<DD00> <DDFF> <DD00>
+<DE00> <DEFF> <DE00>
+<DF00> <DFFF> <DF00>
+<E000> <E0FF> <E000>
+<E100> <E1FF> <E100>
+<E200> <E2FF> <E200>
+<E300> <E3FF> <E300>
+<E400> <E4FF> <E400>
+<E500> <E5FF> <E500>
+<E600> <E6FF> <E600>
+<E700> <E7FF> <E700>
+<E800> <E8FF> <E800>
+<E900> <E9FF> <E900>
+<EA00> <EAFF> <EA00>
+<EB00> <EBFF> <EB00>
+<EC00> <ECFF> <EC00>
+<ED00> <EDFF> <ED00>
+<EE00> <EEFF> <EE00>
+<EF00> <EFFF> <EF00>
+<F000> <F0FF> <F000>
+<F100> <F1FF> <F100>
+<F200> <F2FF> <F200>
+<F300> <F3FF> <F300>
+<F400> <F4FF> <F400>
+<F500> <F5FF> <F500>
+<F600> <F6FF> <F600>
+<F700> <F7FF> <F700>
+<F800> <F8FF> <F800>
+<F900> <F9FF> <F900>
+<FA00> <FAFF> <FA00>
+<FB00> <FBFF> <FB00>\r
+<FC00> <FCFF> <FC00>
+<FD00> <FDFF> <FD00>
+<FE00> <FEFF> <FE00>
+<FF00> <FFFF> <FF00>
+endbfrange
+endcmap
+CMapName currentdict /CMap defineresource pop
+end
+end";
+        assert!(cmap_stream().parse(data.as_bytes()).is_ok())
+    }
+
+    #[test]
+    fn parse_cmap_section_2() {
+        let data = "/CIDInit /ProcSet findresource begin
+12 dict begin
+begincmap
+/CMapType 2 def
+/CMapName/R27 def
+1 begincodespacerange
+<0000><ffff>
+endcodespacerange
+78 beginbfrange
+<0020><0020><0020>
+<0028><0028><0028>
+<0029><0029><0029>
+<002b><002b><002b>
+<002c><002c><002c>
+<002d><002d><002d>
+<002e><002e><002e>
+<002f><002f><002f>
+<0030><0030><0030>
+<0031><0031><0031>
+<0032><0032><0032>
+<0033><0033><0033>
+<0034><0034><0034>
+<0035><0035><0035>
+<0036><0036><0036>
+<0037><0037><0037>
+<0038><0038><0038>
+<0039><0039><0039>
+<003a><003a><003a>
+<003d><003d><003d>
+<0041><0041><0041>
+<0042><0042><0042>
+<0043><0043><0043>
+<0044><0044><0044>
+<0045><0045><0045>
+<0046><0046><0046>
+<0047><0047><0047>
+<0048><0048><0048>
+<0049><0049><0049>
+<004a><004a><004a>
+<004b><004b><004b>
+<004c><004c><004c>
+<004d><004d><004d>
+<004e><004e><004e>
+<004f><004f><004f>
+<0050><0050><0050>
+<0052><0052><0052>
+<0053><0053><0053>
+<0054><0054><0054>
+<0055><0055><0055>
+<0056><0056><0056>
+<0057><0057><0057>
+<0058><0058><0058>
+<005a><005a><005a>
+<005c><005c><005c>
+<0061><0061><0061>
+<0062><0062><0062>
+<0063><0063><0063>
+<0064><0064><0064>
+<0065><0065><0065>
+<0066><0066><0066>
+<0067><0067><0067>
+<0068><0068><0068>
+<0069><0069><0069>
+<006a><006a><006a>
+<006b><006b><006b>
+<006c><006c><006c>
+<006d><006d><006d>
+<006e><006e><006e>
+<006f><006f><006f>
+<0070><0070><0070>
+<0072><0072><0072>
+<0073><0073><0073>
+<0074><0074><0074>
+<0075><0075><0075>
+<0076><0076><0076>
+<0077><0077><0077>
+<0078><0078><0078>
+<0079><0079><0079>
+<007a><007a><007a>
+<007c><007c><007c>
+<00a3><00a3><00a3>
+<00d6><00d6><00d6>
+<00df><00df><00df>
+<00e4><00e4><00e4>
+<00f6><00f6><00f6>
+<00fc><00fc><00fc>
+<2019><2019><2019>
+endbfrange
+endcmap
+CMapName currentdict /CMap defineresource pop
+end end
+";
+        assert!(cmap_stream().parse(data.as_bytes()).is_ok())
+    }
+
+    #[test]
+    fn parse_cmap_section_bfchar() {
+        let data = "/CIDInit /ProcSet findresource begin
+12 dict begin
+begincmap
+/CIDSystemInfo
+<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def
+/CMapName /Adobe-UCS-0 def
+/CMapType 2 def
+1 begincodespacerange
+<0000> <FFFF>
+endcodespacerange
+36 beginbfchar
+<0000> <0000>
+<0001> <004C>
+<0002> <0069>
+<0003> <0073>
+<0004> <0074>
+<0005> <0061>
+<0006> <006F>
+<0007> <0070>
+<0008> <0065>
+<0009> <0072>
+<000A> <0063>
+<000B> <006A>
+<000C> <007A>
+<000D> <006B>
+<000E> <0064>
+<000F> <0032>
+<0010> <0030>
+<0011> <0033>
+<0012> <002D>
+<0013> <0035>
+<0014> <0031>
+<0015> <0039>
+<0016> <0034>
+<0017> <0057>
+<0018> <006C>
+<0019> <0075>
+<001A> <0142>
+<001B> <0079>
+<001C> <0077>
+<001D> <004F>
+<001E> <0044>
+<001F> <0052>
+<0020> <0068>
+<0021> <006E>
+<0022> <004B>
+<0023> <0067>
+endbfchar
+endcmap
+CMapName currentdict /CMap defineresource pop
+end
+end";
+        assert!(cmap_stream().parse(data.as_bytes()).is_ok())
+    }
+
+    #[test]
+    fn parse_cmap_section_with_discontigous_range() {
+        let data = "/CIDInit /ProcSet findresource begin
+12 dict begin
+begincmap
+/CIDSystemInfo
+<< /Registry (Adobe)
+/Ordering (UCS)
+/Supplement 0
+>> def
+/CMapName /Adobe-Identity-UCS def
+/CMapType 2 def
+1 begincodespacerange
+<0000> <FFFF>
+endcodespacerange
+2 beginbfrange
+<0000> <005E> <0020>
+<005F> <0061> [<D83DDE00> <D83DDD27> <D83DDD28>]
+endbfrange
+1 beginbfchar
+<3A51> <D840DC3E>
+endbfchar
+endcmap
+CMapName currentdict /CMap defineresource pop
+end
+end";
+        assert!(cmap_stream().parse(data.as_bytes()).is_ok())
+    }
+}
diff --git a/src/cmap_section.rs b/src/cmap_section.rs
new file mode 100644
index 0000000..b74acbf
--- /dev/null
+++ b/src/cmap_section.rs
@@ -0,0 +1,25 @@
+/*
+ToUnicode CMaps are special CMaps and thus can be parsed simpler. Assumptions:
+- /CMapType is always 2
+- codespace range always from <0000> to <ffff>
+- only bfchar and bfrange sections allowed
+- no glyph names as target allowed, only hex strings
+- source character codes always 2 bytes
+- target encoded in UTF16-BE
+ */
+
+pub(crate) type ArrayOfTargetStrings = Vec<Vec<u16>>;
+pub(crate) type SourceRangeMapping = ((u16, u16), ArrayOfTargetStrings);
+pub(crate) type SourceCharMapping = (u16, Vec<u16>);
+#[derive(Debug, PartialEq)]
+pub enum CMapSection {
+    CsRange(Vec<(u16, u16)>),
+    BfChar(Vec<SourceCharMapping>),
+    BfRange(Vec<SourceRangeMapping>),
+}
+
+#[derive(Debug)]
+pub enum CMapParseError {
+    Incomplete,
+    Error,
+}
diff --git a/src/common_data_structures/mod.rs b/src/common_data_structures/mod.rs
index a94195d..06feaa7 100644
--- a/src/common_data_structures/mod.rs
+++ b/src/common_data_structures/mod.rs
@@ -39,7 +39,7 @@ pub fn decode_text_string(obj: &Object) -> Result<String> {
         String::from_utf8(s.to_vec()).map_err(|_| Error::StringDecode)
     } else {
         // If neither BOM is detected, PDFDocEncoding is used
-        Ok(bytes_to_string(encodings::PDF_DOC_ENCODING, s))
+        Ok(bytes_to_string(&encodings::PDF_DOC_ENCODING, s))
     }
 }
 
@@ -52,7 +52,7 @@ mod test {
     #[test]
     fn spec_example1_encode() {
         let input = "text‰";
-        let text_string = encodings::string_to_bytes(encodings::PDF_DOC_ENCODING, input);
+        let text_string = encodings::string_to_bytes(&encodings::PDF_DOC_ENCODING, input);
         // let text_string = input.bytes().collect::<Vec<_>>();
         let dict = Object::Dictionary(dictionary!(
             "Key" => Object::String(text_string, StringFormat::Literal),
diff --git a/src/document.rs b/src/document.rs
index 23046fa..3bc8cde 100644
--- a/src/document.rs
+++ b/src/document.rs
@@ -1,11 +1,10 @@
-use super::encodings::{self, bytes_to_string, string_to_bytes};
+use super::encodings::Encoding;
 use super::{Bookmark, Dictionary, Object, ObjectId};
 use crate::encryption;
 use crate::xobject::PdfImage;
 use crate::xref::{Xref, XrefType};
 use crate::{Error, Result, Stream};
-use encoding_rs::UTF_16BE;
-use log::info;
+use log::debug;
 use std::cmp::max;
 use std::collections::{BTreeMap, HashMap, HashSet};
 use std::io::Write;
@@ -570,39 +569,13 @@ impl Document {
         Ok(images)
     }
 
-    pub fn decode_text(encoding: Option<&str>, bytes: &[u8]) -> String {
-        if let Some(encoding) = encoding {
-            info!("{}", encoding);
-            match encoding {
-                "StandardEncoding" => bytes_to_string(encodings::STANDARD_ENCODING, bytes),
-                "MacRomanEncoding" => bytes_to_string(encodings::MAC_ROMAN_ENCODING, bytes),
-                "MacExpertEncoding" => bytes_to_string(encodings::MAC_EXPERT_ENCODING, bytes),
-                "WinAnsiEncoding" => bytes_to_string(encodings::WIN_ANSI_ENCODING, bytes),
-                "PDFDocEncoding" => bytes_to_string(encodings::PDF_DOC_ENCODING, bytes),
-                "UniGB-UCS2-H" | "UniGB−UTF16−H" => UTF_16BE.decode(bytes).0.to_string(),
-                "Identity-H" => "?Identity-H Unimplemented?".to_string(), // Unimplemented
-                _ => String::from_utf8_lossy(bytes).to_string(),
-            }
-        } else {
-            bytes_to_string(encodings::STANDARD_ENCODING, bytes)
-        }
+    pub fn decode_text(encoding: &Encoding, bytes: &[u8]) -> Result<String> {
+        debug!("Decoding text with {:#?}", encoding);
+        encoding.bytes_to_string(bytes)
     }
 
-    pub fn encode_text(encoding: Option<&str>, text: &str) -> Vec<u8> {
-        if let Some(encoding) = encoding {
-            match encoding {
-                "StandardEncoding" => string_to_bytes(encodings::STANDARD_ENCODING, text),
-                "MacRomanEncoding" => string_to_bytes(encodings::MAC_ROMAN_ENCODING, text),
-                "MacExpertEncoding" => string_to_bytes(encodings::MAC_EXPERT_ENCODING, text),
-                "WinAnsiEncoding" => string_to_bytes(encodings::WIN_ANSI_ENCODING, text),
-                "PDFDocEncoding" => string_to_bytes(encodings::PDF_DOC_ENCODING, text),
-                "UniGB-UCS2-H" | "UniGB−UTF16−H" => encodings::encode_utf16_be(text).to_vec(),
-                "Identity-H" => vec![], // Unimplemented
-                _ => text.as_bytes().to_vec(),
-            }
-        } else {
-            string_to_bytes(encodings::STANDARD_ENCODING, text)
-        }
+    pub fn encode_text(encoding: &Encoding, text: &str) -> Vec<u8> {
+        encoding.string_to_bytes(text)
     }
 }
 
diff --git a/src/encodings/cmap.rs b/src/encodings/cmap.rs
new file mode 100644
index 0000000..8900e08
--- /dev/null
+++ b/src/encodings/cmap.rs
@@ -0,0 +1,125 @@
+use crate::cmap_parser::parse;
+use crate::cmap_section::{CMapParseError, CMapSection};
+
+use rangemap::RangeInclusiveMap;
+use std::fmt;
+
+#[derive(Debug, Default)]
+pub struct ToUnicodeCMap {
+    bf_ranges: RangeInclusiveMap<u16, BfRangeTarget>,
+}
+
+#[derive(Debug)]
+pub enum UnicodeCMapError {
+    Parse(CMapParseError),
+    UnsupportedCodeSpaceRange,
+    InvalidCodeRange,
+}
+
+impl fmt::Display for UnicodeCMapError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use UnicodeCMapError::*;
+        match self {
+            Parse(cmap_parse_error) => write!(f, "Could not parse ToUnicodeCMap: {:#?}!", cmap_parse_error),
+            UnsupportedCodeSpaceRange => write!(f, "Unsupported codespace range given!"),
+            InvalidCodeRange => write!(f, "Invalid code range given!"),
+        }
+    }
+}
+impl From<CMapParseError> for UnicodeCMapError {
+    fn from(err: CMapParseError) -> Self {
+        UnicodeCMapError::Parse(err)
+    }
+}
+
+impl ToUnicodeCMap {
+    pub fn new() -> ToUnicodeCMap {
+        ToUnicodeCMap {
+            bf_ranges: RangeInclusiveMap::new(),
+        }
+    }
+
+    pub(crate) fn parse(stream_content: Vec<u8>) -> Result<ToUnicodeCMap, UnicodeCMapError> {
+        let cmap_sections = parse(&stream_content[..])?;
+        Self::from_sections(cmap_sections)
+    }
+
+    fn from_sections(cmap_sections: Vec<CMapSection>) -> Result<ToUnicodeCMap, UnicodeCMapError> {
+        let mut cmap = Self::new();
+        for section in cmap_sections {
+            match section {
+                CMapSection::CsRange(ranges) => match ranges.len() {
+                    1 if ranges[0] == (0x0000, 0xffff) => {}
+                    _ => return Err(UnicodeCMapError::UnsupportedCodeSpaceRange),
+                },
+                CMapSection::BfChar(char_mappings) => {
+                    for (code, dst) in char_mappings {
+                        cmap.put_char(code, dst);
+                    }
+                }
+                CMapSection::BfRange(range_mappings) => {
+                    for ((start, end), dst_vec) in range_mappings {
+                        if end < start {
+                            return Err(UnicodeCMapError::InvalidCodeRange);
+                        }
+                        match dst_vec.len() {
+                            1 if dst_vec[0].len() == 1 => cmap.put(
+                                start,
+                                end,
+                                BfRangeTarget::UTF16CodePoint {
+                                    offset: u16::wrapping_sub(dst_vec[0][0], start),
+                                },
+                            ),
+                            1 => cmap.put(start, end, BfRangeTarget::HexString(dst_vec[0].clone())),
+                            0 => return Err(UnicodeCMapError::InvalidCodeRange),
+                            _ => cmap.put(start, end, BfRangeTarget::ArrayOfHexStrings(dst_vec.clone())),
+                        }
+                    }
+                }
+            }
+        }
+        Ok(cmap)
+    }
+
+    pub fn get(&self, code: u16) -> Option<Vec<u16>> {
+        use BfRangeTarget::*;
+        self.bf_ranges.get_key_value(&code).map(|(range, value)| match value {
+            HexString(ref vec) => {
+                let mut ret_vec = vec.clone();
+                *(ret_vec.last_mut().unwrap()) += code - range.start();
+                ret_vec
+            }
+            UTF16CodePoint { offset } => vec![u16::wrapping_add(code, *offset)],
+            ArrayOfHexStrings(ref vec_of_strings) => vec_of_strings[(code - range.start()) as usize].clone(),
+        })
+    }
+
+    pub fn get_or_replacement_char(&self, code: u16) -> Vec<u16> {
+        self.get(code).unwrap_or(vec![0xfffd])
+    }
+
+    pub fn put(&mut self, src_code_lo: u16, src_code_hi: u16, target: BfRangeTarget) {
+        self.bf_ranges.insert(src_code_lo..=src_code_hi, target)
+    }
+
+    pub fn put_char(&mut self, code: u16, dst: Vec<u16>) {
+        let target = if dst.len() == 1 {
+            BfRangeTarget::UTF16CodePoint {
+                offset: u16::wrapping_sub(dst[0], code),
+            }
+        } else {
+            BfRangeTarget::HexString(dst)
+        };
+        self.put(code, code, target)
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum BfRangeTarget {
+    // UTF16-BE encoding is used
+    HexString(Vec<u16>),
+    // don't store the actual codepoint but rather an offset to the src_code_lo
+    // so that consecutive ranges can be mapped to the same value in the range map
+    UTF16CodePoint { offset: u16 },
+    ArrayOfHexStrings(Vec<Vec<u16>>),
+}
diff --git a/src/encodings/mappings.rs b/src/encodings/mappings.rs
index 01fb577..be7468a 100644
--- a/src/encodings/mappings.rs
+++ b/src/encodings/mappings.rs
@@ -1,7 +1,7 @@
 use super::glyphnames::Glyph;
 
-/// MacRomanEncoding
-pub const MAC_ROMAN_ENCODING: [Option<u16>; 256] = [
+pub type CodedCharacterSet = [Option<u16>; 256];
+pub const MAC_ROMAN_ENCODING: CodedCharacterSet = [
     None,
     None,
     None,
@@ -260,8 +260,7 @@ pub const MAC_ROMAN_ENCODING: [Option<u16>; 256] = [
     Some(Glyph::caron),
 ];
 
-/// MacExpertEncoding
-pub const MAC_EXPERT_ENCODING: [Option<u16>; 256] = [
+pub const MAC_EXPERT_ENCODING: CodedCharacterSet = [
     None,
     None,
     None,
@@ -520,8 +519,7 @@ pub const MAC_EXPERT_ENCODING: [Option<u16>; 256] = [
     None,
 ];
 
-/// WinAnsiEncoding
-pub const WIN_ANSI_ENCODING: [Option<u16>; 256] = [
+pub const WIN_ANSI_ENCODING: CodedCharacterSet = [
     None,
     None,
     None,
@@ -780,8 +778,7 @@ pub const WIN_ANSI_ENCODING: [Option<u16>; 256] = [
     Some(Glyph::ydieresis),
 ];
 
-/// StandardEncoding
-pub const STANDARD_ENCODING: [Option<u16>; 256] = [
+pub const STANDARD_ENCODING: CodedCharacterSet = [
     None,
     None,
     None,
@@ -1042,7 +1039,7 @@ pub const STANDARD_ENCODING: [Option<u16>; 256] = [
 
 /// MacExpertEncoding
 #[allow(dead_code)]
-pub const EXPERT_ENCODING: [Option<u16>; 256] = [
+pub const EXPERT_ENCODING: CodedCharacterSet = [
     None,
     None,
     None,
@@ -1302,7 +1299,7 @@ pub const EXPERT_ENCODING: [Option<u16>; 256] = [
 ];
 
 #[allow(dead_code)]
-pub const SYMBOL_ENCODING: [Option<u16>; 256] = [
+pub const SYMBOL_ENCODING: CodedCharacterSet = [
     None,
     None,
     None,
@@ -1563,7 +1560,7 @@ pub const SYMBOL_ENCODING: [Option<u16>; 256] = [
 
 /// PDFDocEncoding
 #[allow(dead_code)]
-pub const PDF_DOC_ENCODING: [Option<u16>; 256] = [
+pub const PDF_DOC_ENCODING: CodedCharacterSet = [
     None,
     None,
     None,
diff --git a/src/encodings/mod.rs b/src/encodings/mod.rs
index 65b73f3..4a2f79e 100644
--- a/src/encodings/mod.rs
+++ b/src/encodings/mod.rs
@@ -1,8 +1,16 @@
+pub mod cmap;
 mod glyphnames;
 mod mappings;
+
+use crate::Error;
+use crate::Result;
+use cmap::ToUnicodeCMap;
+use encoding_rs::UTF_16BE;
+use log::debug;
+
 pub use self::mappings::*;
 
-pub fn bytes_to_string(encoding: [Option<u16>; 256], bytes: &[u8]) -> String {
+pub fn bytes_to_string(encoding: &CodedCharacterSet, bytes: &[u8]) -> String {
     let code_points = bytes
         .iter()
         .filter_map(|&byte| encoding[byte as usize])
@@ -10,13 +18,65 @@ pub fn bytes_to_string(encoding: [Option<u16>; 256], bytes: &[u8]) -> String {
     String::from_utf16_lossy(&code_points)
 }
 
-pub fn string_to_bytes(encoding: [Option<u16>; 256], text: &str) -> Vec<u8> {
+pub fn string_to_bytes(encoding: &CodedCharacterSet, text: &str) -> Vec<u8> {
     text.encode_utf16()
         .filter_map(|ch| encoding.iter().position(|&code| code == Some(ch)))
         .map(|byte| byte as u8)
         .collect()
 }
 
+pub enum Encoding<'a> {
+    OneByteEncoding(&'a CodedCharacterSet),
+    SimpleEncoding(&'a str),
+    UnicodeMapEncoding(ToUnicodeCMap),
+}
+
+impl<'a> std::fmt::Debug for Encoding<'a> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            // UnicodeCMap and Bytes encoding ommitted to not bloat debug log
+            Self::OneByteEncoding(_arg0) => f.debug_tuple("OneByteEncoding").finish(),
+            Self::SimpleEncoding(arg0) => f.debug_tuple("SimpleEncoding").field(arg0).finish(),
+            Self::UnicodeMapEncoding(_arg0) => f.debug_tuple("UnicodeMapEncoding").finish(),
+        }
+    }
+}
+
+impl<'a> Encoding<'a> {
+    pub fn bytes_to_string(&self, bytes: &[u8]) -> Result<String> {
+        match self {
+            Self::OneByteEncoding(map) => Ok(bytes_to_string(map, bytes)),
+            Self::SimpleEncoding(name) if ["UniGB-UCS2-H", "UniGB−UTF16−H"].contains(name) => {
+                Ok(UTF_16BE.decode(bytes).0.to_string())
+            }
+            Self::UnicodeMapEncoding(unicode_map) => {
+                let utf16_str: Vec<u8> = bytes
+                    .chunks_exact(2)
+                    .map(|chunk| chunk[0] as u16 * 256 + chunk[1] as u16)
+                    .flat_map(|cp| unicode_map.get_or_replacement_char(cp))
+                    .flat_map(|it| [(it / 256) as u8, (it % 256) as u8])
+                    .collect();
+                Ok(UTF_16BE.decode(&utf16_str).0.to_string())
+            }
+            Self::SimpleEncoding(_) => Err(Error::ContentDecode),
+        }
+    }
+    pub fn string_to_bytes(&self, text: &str) -> Vec<u8> {
+        match self {
+            Self::OneByteEncoding(map) => string_to_bytes(map, text),
+            Self::SimpleEncoding(name) if ["UniGB-UCS2-H", "UniGB-UTF16-H"].contains(name) => encode_utf16_be(text),
+            Self::UnicodeMapEncoding(_unicode_map) => {
+                // maybe only possible if the unicode map is an identity?
+                unimplemented!()
+            }
+            Self::SimpleEncoding(_) => {
+                debug!("Unknown encoding used to encode text {self:?}");
+                text.as_bytes().to_vec()
+            }
+        }
+    }
+}
+
 /// Encodes the given `str` to UTF-16BE.
 /// The recommended way to encode text strings, as it supports all of
 /// unicode and all major PDF readers support it.
diff --git a/src/error.rs b/src/error.rs
index 55f2dac..7a6dafb 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -1,6 +1,8 @@
 use crate::encryption;
 use std::fmt;
 
+use crate::encodings::cmap::UnicodeCMapError;
+
 #[derive(Debug)]
 pub enum Error {
     /// Brackets limit reached.
@@ -40,6 +42,8 @@ pub enum Error {
     StringDecode,
     /// Syntax error while parsing the file.
     Syntax(String),
+    /// Could not parse ToUnicodeCMap.
+    ToUnicodeCMap(UnicodeCMapError),
     /// The file trailer was invalid.
     Trailer,
     /// The object does not have the expected type.
@@ -73,6 +77,7 @@ impl fmt::Display for Error {
             Error::ReferenceLimit => write!(f, "Could not dereference an object; possible reference cycle"),
             Error::StringDecode => write!(f, "Could not decode string"),
             Error::Syntax(msg) => write!(f, "Syntax error: {}", msg),
+            Error::ToUnicodeCMap(err) => write!(f, "ToUnicode CMap error: {}", err),
             Error::Trailer => write!(f, "Invalid file trailer"),
             Error::Type => write!(f, "An object does not have the expected type"),
             Error::UTF8 => write!(f, "UTF-8 error"),
@@ -130,6 +135,12 @@ impl From<std::str::Utf8Error> for Error {
     }
 }
 
+impl From<UnicodeCMapError> for Error {
+    fn from(cmap_err: UnicodeCMapError) -> Self {
+        Error::ToUnicodeCMap(cmap_err)
+    }
+}
+
 #[cfg(feature = "embed_image")]
 impl From<image::ImageError> for Error {
     fn from(err: image::ImageError) -> Self {
diff --git a/src/lib.rs b/src/lib.rs
index b4eadde..50ba714 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -27,9 +27,17 @@ mod destinations;
 pub use crate::destinations::Destination;
 mod toc;
 pub use crate::toc::Toc;
+#[cfg(not(feature = "nom_parser"))]
+#[cfg(feature = "pom_parser")]
+mod cmap_parser;
+#[cfg(feature = "nom_parser")]
+#[path = "nom_cmap_parser.rs"]
+mod cmap_parser;
+mod cmap_section;
 pub mod content;
 mod creator;
 mod encodings;
+pub use encodings::Encoding;
 pub mod encryption;
 mod error;
 pub use error::XrefError;
diff --git a/src/nom_cmap_parser.rs b/src/nom_cmap_parser.rs
new file mode 100644
index 0000000..6261a01
--- /dev/null
+++ b/src/nom_cmap_parser.rs
@@ -0,0 +1,859 @@
+use crate::cmap_section::{ArrayOfTargetStrings, CMapParseError, CMapSection, SourceRangeMapping};
+use crate::parser::{dictionary, eol, hex_char, name, NomError, NomResult};
+use nom::branch::alt;
+pub use nom::bytes::complete::tag;
+use nom::combinator::map;
+use nom::multi::{fold_many0, fold_many1, fold_many_m_n, many1, many_m_n, separated_list1};
+use nom::sequence::{pair, preceded, separated_pair};
+use nom::Parser;
+use nom::{
+    character::complete::digit1,
+    sequence::{delimited, tuple},
+};
+
+type ParserInput<'a> = &'a [u8];
+
+impl<E> From<nom::Err<E>> for CMapParseError {
+    fn from(err: nom::Err<E>) -> Self {
+        match err {
+            nom::Err::Incomplete(_) => CMapParseError::Incomplete,
+            // normally nom::Err::Error is a recoverable error, but CMapParseError is the return type
+            // so we assume that there are no more parsing branches to check
+            nom::Err::Failure(_) | nom::Err::Error(_) => CMapParseError::Error,
+        }
+    }
+}
+
+pub(crate) fn parse(stream_content: ParserInput) -> Result<Vec<CMapSection>, CMapParseError> {
+    let result = cmap_stream(stream_content);
+    let result = result.map_err(CMapParseError::from)?;
+    Ok(result.1)
+}
+
+fn cmap_stream(input: ParserInput) -> NomResult<Vec<CMapSection>> {
+    delimited(
+        cidinit_procset,
+        cmap_resource_dictionary,
+        tuple((tag(b"end"), multispace0)),
+    )(input)
+}
+
+fn tuple_discard_result<'a, O, List>(seq: List, input: ParserInput<'a>) -> NomResult<()>
+where
+    List: nom::sequence::Tuple<ParserInput<'a>, O, NomError<'a>>,
+{
+    let result = tuple(seq)(input);
+    match result {
+        Ok((rest_of_input, _)) => Ok((rest_of_input, ())),
+        Err(e) => Err(e),
+    }
+}
+
+fn space0(input: ParserInput) -> NomResult<()> {
+    fold_many0(alt((tag(b" "), tag("\t"))), || {}, |_, _| ())(input)
+}
+
+fn space1(input: ParserInput) -> NomResult<()> {
+    fold_many1(alt((tag(b" "), tag("\t"))), || {}, |_, _| ())(input)
+}
+
+fn multispace0(input: ParserInput) -> NomResult<()> {
+    fold_many0(alt((tag(b" "), tag("\t"), eol)), || {}, |_, _| ())(input)
+}
+
+fn multispace1(input: ParserInput) -> NomResult<()> {
+    fold_many1(alt((tag(b" "), tag("\t"), eol)), || {}, |_, _| ())(input)
+}
+
+fn cidinit_procset(input: ParserInput) -> NomResult<()> {
+    tuple_discard_result(
+        (
+            tag(b"/CIDInit"),
+            space0,
+            tag(b"/ProcSet"),
+            space1,
+            tag(b"findresource"),
+            space1,
+            tag(b"begin"),
+            multispace1,
+        ),
+        input,
+    )
+}
+
+fn cmap_resource_dictionary(input: ParserInput) -> NomResult<Vec<CMapSection>> {
+    let begin_parser = tuple((digit1, space1, tag(b"dict"), space1, tag(b"begin"), multispace1));
+    let end_parser = tuple((tag(b"end"), multispace1));
+    delimited(begin_parser, cmap_data, end_parser)(input)
+}
+
+fn cmap_data(input: ParserInput) -> NomResult<Vec<CMapSection>> {
+    let cmap_end = tuple((
+        tag(b"endcmap"),
+        multispace1,
+        tag(b"CMapName"),
+        space1,
+        tag(b"currentdict"),
+        space1,
+        tag(b"/CMap"),
+        space1,
+        tag(b"defineresource"),
+        space1,
+        tag(b"pop"),
+        multispace1,
+    ));
+    delimited(
+        tuple((tag(b"begincmap"), multispace1)),
+        preceded(cmap_metadata, cmap_codespace_and_mappings),
+        cmap_end,
+    )(input)
+}
+
+fn cmap_metadata(input: ParserInput) -> NomResult<()> {
+    let metadata_parser = alt((cid_system_info, cmap_name, cmap_type));
+    fold_many_m_n(1, 4, metadata_parser, || (), |_, _| ())(input)
+}
+
+fn cid_system_info(input: ParserInput) -> NomResult<()> {
+    // Note: Can array of CIDSystemInfo occur here?
+    // Normally in cmap this can be an array, but can it be also if this is a ToUnicode cmap?
+    tuple_discard_result(
+        (
+            tag(b"/CIDSystemInfo"),
+            multispace0,
+            dictionary,
+            multispace1,
+            tag("def"),
+            multispace1,
+        ),
+        input,
+    )
+}
+
+fn cmap_name(input: ParserInput) -> NomResult<()> {
+    tuple_discard_result(
+        (tag(b"/CMapName"), space0, name, space1, tag(b"def"), multispace1),
+        input,
+    )
+}
+
+fn cmap_type(input: ParserInput) -> NomResult<()> {
+    tuple_discard_result(
+        (tag(b"/CMapType"), space1, digit1, space1, tag(b"def"), multispace1),
+        input,
+    )
+}
+
+fn cmap_codespace_and_mappings(input: ParserInput) -> NomResult<Vec<CMapSection>> {
+    many1(alt((codespace_range_section, bf_char_section, bf_range_section)))(input)
+}
+
+fn codespace_range_section(input: ParserInput) -> NomResult<CMapSection> {
+    let begin_section = tuple((digit1, space1, tag(b"begincodespacerange"), multispace1));
+    let end_section = tuple((tag(b"endcodespacerange"), multispace1));
+    let parse_range = delimited(space0, code_range_pair, multispace1);
+    let (rest_of_input, ranges_result) = delimited(begin_section, many1(parse_range), end_section)(input)?;
+    Ok((rest_of_input, CMapSection::CsRange(ranges_result)))
+}
+
+fn code_range_pair(input: ParserInput) -> NomResult<(u16, u16)> {
+    separated_pair(source_code, space0, source_code)(input)
+}
+
+fn source_code(input: ParserInput) -> NomResult<u16> {
+    delimited(tag(b"<"), hex_u16, tag(b">"))(input)
+}
+
+fn hex_u16(input: ParserInput) -> NomResult<u16> {
+    map(pair(hex_char, hex_char), |(h1, h2)| h1 as u16 * 256 + h2 as u16)(input)
+}
+
+fn bf_char_section(input: ParserInput) -> NomResult<CMapSection> {
+    let begin_section = tuple((digit1, space1, tag(b"beginbfchar"), multispace1));
+    let end_section = tuple((tag(b"endbfchar"), multispace1));
+    let bf_char_line = delimited(space0, separated_pair(source_code, space1, target_string), multispace1);
+    let (rest_of_input, bf_char_mappings) = delimited(begin_section, many1(bf_char_line), end_section)(input)?;
+    Ok((rest_of_input, CMapSection::BfChar(bf_char_mappings)))
+}
+
+fn target_string(input: ParserInput) -> NomResult<Vec<u16>> {
+    // according to specification dstString can be up to 512 bytes
+    // in ToUnicode cmap these should be 2-byte big endian Unicode values
+    delimited(tag(b"<"), many_m_n(1, 256, hex_u16), tag(b">"))(input)
+}
+
+fn bf_range_section(input: ParserInput) -> NomResult<CMapSection> {
+    let begin_section = tuple((digit1, space1, tag(b"beginbfrange"), multispace1));
+    let end_section = tuple((tag(b"endbfrange"), multispace1));
+    let (rest_of_input, bf_range_mappings) = delimited(begin_section, many1(bf_range_line), end_section)(input)?;
+    Ok((rest_of_input, CMapSection::BfRange(bf_range_mappings)))
+}
+
+fn bf_range_line(input: ParserInput) -> NomResult<SourceRangeMapping> {
+    let bf_range_parser = separated_pair(
+        code_range_pair,
+        space0,
+        alt((target_string.map(|res| vec![res]), range_target_array)),
+    );
+    delimited(space0, bf_range_parser, multispace1)(input)
+}
+
+fn range_target_array(input: ParserInput) -> NomResult<ArrayOfTargetStrings> {
+    delimited(
+        tuple((tag(b"["), space0)),
+        separated_list1(space1, target_string),
+        tuple((space0, tag(b"]"))),
+    )(input)
+}
+
+#[cfg(test)]
+mod tests {
+
+    use super::*;
+
+    #[test]
+    fn parse_source_code() {
+        let data = b"<080F>";
+        assert_eq!(source_code(data), NomResult::Ok((b"", 0x080f)))
+    }
+
+    #[test]
+    fn parse_invalid_source_code() {
+        let data = "<080f01>";
+        assert!(source_code(data.as_bytes()).is_err())
+    }
+
+    #[test]
+    fn parse_code_range_pair() {
+        let data = "<080F> <08FF> ";
+        assert_eq!(
+            code_range_pair(data.as_bytes()),
+            NomResult::Ok((b" ", (0x080f, 0x08ff)))
+        )
+    }
+
+    #[test]
+    fn parse_code_range_pair_without_spaces() {
+        let data = "<080F><08FF>";
+        assert_eq!(code_range_pair(data.as_bytes()), NomResult::Ok((b"", (0x080f, 0x08ff))))
+    }
+
+    #[test]
+    fn parse_bfrange_line() {
+        let data = "<080f> <08ff> <09000110>\n";
+        assert_eq!(
+            bf_range_line(data.as_bytes()),
+            NomResult::Ok((b"", ((0x080f, 0x08ff), vec![vec![0x0900, 0x0110]])))
+        )
+    }
+    #[test]
+    fn parse_bfrange_line_without_spaces() {
+        let data = "<080f><08ff><09000110>\n";
+        assert_eq!(
+            bf_range_line(data.as_bytes()),
+            NomResult::Ok((b"", ((0x080f, 0x08ff), vec![vec![0x0900, 0x0110]])))
+        )
+    }
+
+    #[test]
+    fn parse_bfrange_line_array() {
+        let data = "<080f> <08ff> [ <09000110> <08fe> ] \n";
+        assert_eq!(
+            bf_range_line(data.as_bytes()),
+            NomResult::Ok((b"", ((0x080f, 0x08ff), vec![vec![0x0900, 0x0110], vec![0x08fe]])))
+        )
+    }
+    #[test]
+    fn parse_invalid_bfrange_line() {
+        let data = "<080f> <08ff> [ <09000110> <08FF> <09fe80> ]\n";
+        assert!(bf_range_line(data.as_bytes()).is_err())
+    }
+
+    #[test]
+    fn parse_codespace_range_section() {
+        let data = "1 begincodespacerange\n\
+            <0000> <FFFF> \n\
+        endcodespacerange\n";
+        assert_eq!(
+            codespace_range_section(data.as_bytes()),
+            NomResult::Ok((b"", CMapSection::CsRange(vec![(0x0000, 0xffff)])))
+        )
+    }
+
+    #[test]
+    fn parse_bf_range_section() {
+        let data = "3 beginbfrange \n\
+            <0000> <000f> <0000>\n\
+            <0010> <001f> <00000010> \n\
+            <0020>  <002f> [<0000> <00000010> ]\n\
+        endbfrange\n";
+        assert_eq!(
+            bf_range_section(data.as_bytes()),
+            NomResult::Ok((
+                b"",
+                CMapSection::BfRange(vec![
+                    ((0x0000, 0x000f), vec![vec![0x0000]]),
+                    ((0x0010, 0x001f), vec![vec![0x0000, 0x0010]]),
+                    ((0x0020, 0x002f), vec![vec![0x0000], vec![0x0000, 0x0010]]),
+                ])
+            ))
+        )
+    }
+
+    #[test]
+    fn parse_cid_system_info() {
+        let data = "/CIDSystemInfo <<
+/Registry (Adobe)
+/Ordering (UCS)
+/Supplement 0
+>> def
+";
+        assert!(cid_system_info(data.as_bytes()).is_ok())
+    }
+
+    #[test]
+    fn parse_cid_system_info_with_spaces() {
+        let data = "/CIDSystemInfo
+<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n";
+        assert!(cid_system_info(data.as_bytes()).is_ok())
+    }
+
+    #[test]
+    fn parse_cmap_name() {
+        let data = "/CMapName /Adobe-Identity-UCS def\n";
+        assert!(cmap_name(data.as_bytes()).is_ok())
+    }
+
+    #[test]
+    fn parse_cmap_name2() {
+        let data = "/CMapName /Adobe-UCS-0 def\n";
+        assert!(cmap_name(data.as_bytes()).is_ok())
+    }
+
+    #[test]
+    fn parse_cmap_type() {
+        let data = "/CMapType 2 def\n";
+        assert!(cmap_type(data.as_bytes()).is_ok())
+    }
+
+    #[test]
+    fn parse_cmap_section_1() {
+        let data = "/CIDInit /ProcSet findresource begin
+12 dict begin
+begincmap
+/CIDSystemInfo <<
+/Registry (Adobe)
+/Ordering (UCS)
+/Supplement 0
+>> def
+/CMapName /Adobe-Identity-UCS def
+/CMapType 2 def
+1 begincodespacerange
+<0000> <FFFF>
+endcodespacerange
+96 beginbfrange
+<0000> <00FF> <0000>
+<0100> <01FF> <0100>
+<0200> <02FF> <0200>
+<0300> <03FF> <0300>
+<0400> <04FF> <0400>
+<0500> <05FF> <0500>
+<0600> <06FF> <0600>
+<0700> <07FF> <0700>
+<0800> <08FF> <0800>
+<0900> <09FF> <0900>
+<0A00> <0AFF> <0A00>
+<0B00> <0BFF> <0B00>
+<0C00> <0CFF> <0C00>
+<0D00> <0DFF> <0D00>
+<0E00> <0EFF> <0E00>
+<0F00> <0FFF> <0F00>
+<1000> <10FF> <1000>
+<1100> <11FF> <1100>
+<1200> <12FF> <1200>
+<1300> <13FF> <1300>
+<1400> <14FF> <1400>
+<1500> <15FF> <1500>
+<1600> <16FF> <1600>
+<1700> <17FF> <1700>
+<1800> <18FF> <1800>
+<1900> <19FF> <1900>
+<1A00> <1AFF> <1A00>
+<1B00> <1BFF> <1B00>
+<1C00> <1CFF> <1C00>
+<1D00> <1DFF> <1D00>
+<1E00> <1EFF> <1E00>
+<1F00> <1FFF> <1F00>
+<2000> <20FF> <2000>
+<2100> <21FF> <2100>
+<2200> <22FF> <2200>
+<2300> <23FF> <2300>
+<2400> <24FF> <2400>
+<2500> <25FF> <2500>
+<2600> <26FF> <2600>
+<2700> <27FF> <2700>
+<2800> <28FF> <2800>
+<2900> <29FF> <2900>
+<2A00> <2AFF> <2A00>
+<2B00> <2BFF> <2B00>
+<2C00> <2CFF> <2C00>
+<2D00> <2DFF> <2D00>
+<2E00> <2EFF> <2E00>
+<2F00> <2FFF> <2F00>
+<3000> <30FF> <3000>
+<3100> <31FF> <3100>
+<3200> <32FF> <3200>
+<3300> <33FF> <3300>
+<3400> <34FF> <3400>
+<3500> <35FF> <3500>
+<3600> <36FF> <3600>
+<3700> <37FF> <3700>
+<3800> <38FF> <3800>
+<3900> <39FF> <3900>
+<3A00> <3AFF> <3A00>
+<3B00> <3BFF> <3B00>
+<3C00> <3CFF> <3C00>
+<3D00> <3DFF> <3D00>
+<3E00> <3EFF> <3E00>
+<3F00> <3FFF> <3F00>
+<4000> <40FF> <4000>
+<4100> <41FF> <4100>
+<4200> <42FF> <4200>
+<4300> <43FF> <4300>
+<4400> <44FF> <4400>
+<4500> <45FF> <4500>
+<4600> <46FF> <4600>
+<4700> <47FF> <4700>
+<4800> <48FF> <4800>
+<4900> <49FF> <4900>
+<4A00> <4AFF> <4A00>
+<4B00> <4BFF> <4B00>
+<4C00> <4CFF> <4C00>
+<4D00> <4DFF> <4D00>
+<4E00> <4EFF> <4E00>
+<4F00> <4FFF> <4F00>
+<5000> <50FF> <5000>
+<5100> <51FF> <5100>
+<5200> <52FF> <5200>
+<5300> <53FF> <5300>
+<5400> <54FF> <5400>
+<5500> <55FF> <5500>
+<5600> <56FF> <5600>
+<5700> <57FF> <5700>
+<5800> <58FF> <5800>
+<5900> <59FF> <5900>
+<5A00> <5AFF> <5A00>
+<5B00> <5BFF> <5B00>
+<5C00> <5CFF> <5C00>
+<5D00> <5DFF> <5D00>
+<5E00> <5EFF> <5E00>
+<5F00> <5FFF> <5F00>
+endbfrange
+96 beginbfrange
+<6000> <60FF> <6000>
+<6100> <61FF> <6100>
+<6200> <62FF> <6200>
+<6300> <63FF> <6300>
+<6400> <64FF> <6400>
+<6500> <65FF> <6500>
+<6600> <66FF> <6600>
+<6700> <67FF> <6700>
+<6800> <68FF> <6800>
+<6900> <69FF> <6900>
+<6A00> <6AFF> <6A00>
+<6B00> <6BFF> <6B00>
+<6C00> <6CFF> <6C00>
+<6D00> <6DFF> <6D00>
+<6E00> <6EFF> <6E00>
+<6F00> <6FFF> <6F00>
+<7000> <70FF> <7000>
+<7100> <71FF> <7100>
+<7200> <72FF> <7200>
+<7300> <73FF> <7300>
+<7400> <74FF> <7400>
+<7500> <75FF> <7500>
+<7600> <76FF> <7600>
+<7700> <77FF> <7700>
+<7800> <78FF> <7800>
+<7900> <79FF> <7900>
+<7A00> <7AFF> <7A00>
+<7B00> <7BFF> <7B00>
+<7C00> <7CFF> <7C00>
+<7D00> <7DFF> <7D00>
+<7E00> <7EFF> <7E00>
+<7F00> <7FFF> <7F00>
+<8000> <80FF> <8000>
+<8100> <81FF> <8100>
+<8200> <82FF> <8200>
+<8300> <83FF> <8300>
+<8400> <84FF> <8400>
+<8500> <85FF> <8500>
+<8600> <86FF> <8600>
+<8700> <87FF> <8700>
+<8800> <88FF> <8800>
+<8900> <89FF> <8900>
+<8A00> <8AFF> <8A00>
+<8B00> <8BFF> <8B00>
+<8C00> <8CFF> <8C00>
+<8D00> <8DFF> <8D00>
+<8E00> <8EFF> <8E00>
+<8F00> <8FFF> <8F00>
+<9000> <90FF> <9000>
+<9100> <91FF> <9100>
+<9200> <92FF> <9200>
+<9300> <93FF> <9300>
+<9400> <94FF> <9400>
+<9500> <95FF> <9500>
+<9600> <96FF> <9600>
+<9700> <97FF> <9700>
+<9800> <98FF> <9800>
+<9900> <99FF> <9900>
+<9A00> <9AFF> <9A00>
+<9B00> <9BFF> <9B00>
+<9C00> <9CFF> <9C00>
+<9D00> <9DFF> <9D00>
+<9E00> <9EFF> <9E00>
+<9F00> <9FFF> <9F00>
+<A000> <A0FF> <A000>
+<A100> <A1FF> <A100>
+<A200> <A2FF> <A200>
+<A300> <A3FF> <A300>
+<A400> <A4FF> <A400>
+<A500> <A5FF> <A500>
+<A600> <A6FF> <A600>
+<A700> <A7FF> <A700>
+<A800> <A8FF> <A800>
+<A900> <A9FF> <A900>
+<AA00> <AAFF> <AA00>
+<AB00> <ABFF> <AB00>
+<AC00> <ACFF> <AC00>
+<AD00> <ADFF> <AD00>
+<AE00> <AEFF> <AE00>
+<AF00> <AFFF> <AF00>
+<B000> <B0FF> <B000>
+<B100> <B1FF> <B100>
+<B200> <B2FF> <B200>
+<B300> <B3FF> <B300>
+<B400> <B4FF> <B400>
+<B500> <B5FF> <B500>
+<B600> <B6FF> <B600>
+<B700> <B7FF> <B700>
+<B800> <B8FF> <B800>
+<B900> <B9FF> <B900>
+<BA00> <BAFF> <BA00>
+<BB00> <BBFF> <BB00>
+<BC00> <BCFF> <BC00>
+<BD00> <BDFF> <BD00>
+<BE00> <BEFF> <BE00>
+<BF00> <BFFF> <BF00>
+endbfrange
+64 beginbfrange
+<C000> <C0FF> <C000>
+<C100> <C1FF> <C100>
+<C200> <C2FF> <C200>
+<C300> <C3FF> <C300>
+<C400> <C4FF> <C400>
+<C500> <C5FF> <C500>
+<C600> <C6FF> <C600>
+<C700> <C7FF> <C700>
+<C800> <C8FF> <C800>
+<C900> <C9FF> <C900>
+<CA00> <CAFF> <CA00>
+<CB00> <CBFF> <CB00>
+<CC00> <CCFF> <CC00>
+<CD00> <CDFF> <CD00>
+<CE00> <CEFF> <CE00>
+<CF00> <CFFF> <CF00>
+<D000> <D0FF> <D000>
+<D100> <D1FF> <D100>
+<D200> <D2FF> <D200>
+<D300> <D3FF> <D300>
+<D400> <D4FF> <D400>
+<D500> <D5FF> <D500>
+<D600> <D6FF> <D600>
+<D700> <D7FF> <D700>
+<D800> <D8FF> <D800>
+<D900> <D9FF> <D900>
+<DA00> <DAFF> <DA00>
+<DB00> <DBFF> <DB00>
+<DC00> <DCFF> <DC00>
+<DD00> <DDFF> <DD00>
+<DE00> <DEFF> <DE00>
+<DF00> <DFFF> <DF00>
+<E000> <E0FF> <E000>
+<E100> <E1FF> <E100>
+<E200> <E2FF> <E200>
+<E300> <E3FF> <E300>
+<E400> <E4FF> <E400>
+<E500> <E5FF> <E500>
+<E600> <E6FF> <E600>
+<E700> <E7FF> <E700>
+<E800> <E8FF> <E800>
+<E900> <E9FF> <E900>
+<EA00> <EAFF> <EA00>
+<EB00> <EBFF> <EB00>
+<EC00> <ECFF> <EC00>
+<ED00> <EDFF> <ED00>
+<EE00> <EEFF> <EE00>
+<EF00> <EFFF> <EF00>
+<F000> <F0FF> <F000>
+<F100> <F1FF> <F100>
+<F200> <F2FF> <F200>
+<F300> <F3FF> <F300>
+<F400> <F4FF> <F400>
+<F500> <F5FF> <F500>
+<F600> <F6FF> <F600>
+<F700> <F7FF> <F700>
+<F800> <F8FF> <F800>
+<F900> <F9FF> <F900>
+<FA00> <FAFF> <FA00>
+<FB00> <FBFF> <FB00>\r
+<FC00> <FCFF> <FC00>
+<FD00> <FDFF> <FD00>
+<FE00> <FEFF> <FE00>
+<FF00> <FFFF> <FF00>
+endbfrange
+endcmap
+CMapName currentdict /CMap defineresource pop
+end
+end";
+        assert!(cmap_stream(data.as_bytes()).is_ok())
+    }
+
+    #[test]
+    fn parse_cmap_section_2() {
+        let data = "/CIDInit /ProcSet findresource begin
+12 dict begin
+begincmap
+/CMapType 2 def
+/CMapName/R27 def
+1 begincodespacerange
+<0000><ffff>
+endcodespacerange
+78 beginbfrange
+<0020><0020><0020>
+<0028><0028><0028>
+<0029><0029><0029>
+<002b><002b><002b>
+<002c><002c><002c>
+<002d><002d><002d>
+<002e><002e><002e>
+<002f><002f><002f>
+<0030><0030><0030>
+<0031><0031><0031>
+<0032><0032><0032>
+<0033><0033><0033>
+<0034><0034><0034>
+<0035><0035><0035>
+<0036><0036><0036>
+<0037><0037><0037>
+<0038><0038><0038>
+<0039><0039><0039>
+<003a><003a><003a>
+<003d><003d><003d>
+<0041><0041><0041>
+<0042><0042><0042>
+<0043><0043><0043>
+<0044><0044><0044>
+<0045><0045><0045>
+<0046><0046><0046>
+<0047><0047><0047>
+<0048><0048><0048>
+<0049><0049><0049>
+<004a><004a><004a>
+<004b><004b><004b>
+<004c><004c><004c>
+<004d><004d><004d>
+<004e><004e><004e>
+<004f><004f><004f>
+<0050><0050><0050>
+<0052><0052><0052>
+<0053><0053><0053>
+<0054><0054><0054>
+<0055><0055><0055>
+<0056><0056><0056>
+<0057><0057><0057>
+<0058><0058><0058>
+<005a><005a><005a>
+<005c><005c><005c>
+<0061><0061><0061>
+<0062><0062><0062>
+<0063><0063><0063>
+<0064><0064><0064>
+<0065><0065><0065>
+<0066><0066><0066>
+<0067><0067><0067>
+<0068><0068><0068>
+<0069><0069><0069>
+<006a><006a><006a>
+<006b><006b><006b>
+<006c><006c><006c>
+<006d><006d><006d>
+<006e><006e><006e>
+<006f><006f><006f>
+<0070><0070><0070>
+<0072><0072><0072>
+<0073><0073><0073>
+<0074><0074><0074>
+<0075><0075><0075>
+<0076><0076><0076>
+<0077><0077><0077>
+<0078><0078><0078>
+<0079><0079><0079>
+<007a><007a><007a>
+<007c><007c><007c>
+<00a3><00a3><00a3>
+<00d6><00d6><00d6>
+<00df><00df><00df>
+<00e4><00e4><00e4>
+<00f6><00f6><00f6>
+<00fc><00fc><00fc>
+<2019><2019><2019>
+endbfrange
+endcmap
+CMapName currentdict /CMap defineresource pop
+end end
+";
+        assert!(cmap_stream(data.as_bytes()).is_ok())
+    }
+
+    #[test]
+    fn parse_cmap_section_bfchar() {
+        let data = "/CIDInit /ProcSet findresource begin
+12 dict begin
+begincmap
+/CIDSystemInfo
+<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def
+/CMapName /Adobe-UCS-0 def
+/CMapType 2 def
+1 begincodespacerange
+<0000> <FFFF>
+endcodespacerange
+36 beginbfchar
+<0000> <0000>
+<0001> <004C>
+<0002> <0069>
+<0003> <0073>
+<0004> <0074>
+<0005> <0061>
+<0006> <006F>
+<0007> <0070>
+<0008> <0065>
+<0009> <0072>
+<000A> <0063>
+<000B> <006A>
+<000C> <007A>
+<000D> <006B>
+<000E> <0064>
+<000F> <0032>
+<0010> <0030>
+<0011> <0033>
+<0012> <002D>
+<0013> <0035>
+<0014> <0031>
+<0015> <0039>
+<0016> <0034>
+<0017> <0057>
+<0018> <006C>
+<0019> <0075>
+<001A> <0142>
+<001B> <0079>
+<001C> <0077>
+<001D> <004F>
+<001E> <0044>
+<001F> <0052>
+<0020> <0068>
+<0021> <006E>
+<0022> <004B>
+<0023> <0067>
+endbfchar
+endcmap
+CMapName currentdict /CMap defineresource pop
+end
+end";
+        assert!(cmap_stream(data.as_bytes()).is_ok())
+    }
+
+    #[test]
+    fn parse_cmap_section_with_discontigous_range() {
+        let data = "/CIDInit /ProcSet findresource begin
+12 dict begin
+begincmap
+/CIDSystemInfo
+<< /Registry (Adobe)
+/Ordering (UCS)
+/Supplement 0
+>> def
+/CMapName /Adobe-Identity-UCS def
+/CMapType 2 def
+1 begincodespacerange
+<0000> <FFFF>
+endcodespacerange
+2 beginbfrange
+<0000> <005E> <0020>
+<005F> <0061> [<D83DDE00> <D83DDD27> <D83DDD28>]
+endbfrange
+1 beginbfchar
+<3A51> <D840DC3E>
+endbfchar
+endcmap
+CMapName currentdict /CMap defineresource pop
+end
+end";
+        assert!(cmap_stream(data.as_bytes()).is_ok())
+    }
+
+    #[ignore = "2char ToUnicode cmaps are currently not implemented"]
+    #[test]
+    fn parse_truetype_cmap_section_with_2char_bfchars() {
+        let data = b"/CIDInit/ProcSet findresource begin
+12 dict begin
+begincmap
+/CIDSystemInfo<<
+/Registry (Adobe)
+/Ordering (UCS)
+/Supplement 0
+>> def
+/CMapName/Adobe-Identity-UCS def
+/CMapType 2 def
+1 begincodespacerange
+<00> <FF>
+endcodespacerange
+29 beginbfchar
+<01> <0054>
+<02> <0068>
+<03> <0065>
+<04> <0020>
+<05> <0044>
+<06> <0061>
+<07> <006E>
+<08> <0067>
+<09> <0072>
+<0A> <006F>
+<0B> <0066>
+<0C> <0045>
+<0D> <0062>
+<0E> <006B>
+<0F> <0073>
+<10> <004A>
+<11> <0069>
+<12> <0075>
+<13> <0063>
+<14> <003A>
+<15> <0070>
+<16> <0074>
+<17> <002F>
+<18> <0076>
+<19> <0042>
+<1A> <0079>
+<1B> <002E>
+<1C> <006D>
+<1D> <006C>
+endbfchar
+endcmap
+CMapName currentdict /CMap defineresource pop
+end
+end\n";
+        assert!(cmap_stream(data).is_ok())
+    }
+}
diff --git a/src/nom_parser.rs b/src/nom_parser.rs
index e8d2d3b..6e8bf9a 100644
--- a/src/nom_parser.rs
+++ b/src/nom_parser.rs
@@ -18,8 +18,8 @@ use nom::IResult;
 
 // Change this to something else that implements ParseError to get a
 // different error type out of nom.
-type NomError = ();
-type NomResult<'a, O, E = NomError> = IResult<&'a [u8], O, E>;
+pub(crate) type NomError<'a> = ();
+pub(crate) type NomResult<'a, O, E = NomError<'a>> = IResult<&'a [u8], O, E>;
 
 #[inline]
 fn strip_nom<O>(r: NomResult<O>) -> Option<O> {
@@ -29,8 +29,10 @@ fn strip_nom<O>(r: NomResult<O>) -> Option<O> {
 #[inline]
 fn convert_result<O, E>(result: Result<O, E>, input: &[u8], error_kind: ErrorKind) -> NomResult<O> {
     result.map(|o| (input, o)).map_err(|_| {
-        NomError::from_error_kind(input, error_kind);
-        nom::Err::Error(())
+        // this is a unit bind if NomError = ()
+        #[allow(clippy::let_unit_value)]
+        let err: NomError = NomError::from_error_kind(input, error_kind);
+        nom::Err::Error(err)
     })
 }
 
@@ -41,7 +43,7 @@ fn offset_stream(object: &mut Object, offset: usize) {
     }
 }
 
-fn eol(input: &[u8]) -> NomResult<&[u8]> {
+pub(crate) fn eol(input: &[u8]) -> NomResult<&[u8]> {
     alt((tag(b"\r\n"), tag(b"\n"), tag(b"\r")))(input)
 }
 
@@ -104,7 +106,7 @@ fn real(input: &[u8]) -> NomResult<f32> {
     convert_result(f32::from_str(str::from_utf8(float_input).unwrap()), i, ErrorKind::Digit)
 }
 
-fn hex_char(input: &[u8]) -> NomResult<u8> {
+pub(crate) fn hex_char(input: &[u8]) -> NomResult<u8> {
     map_res(
         verify(take(2usize), |h: &[u8]| h.iter().cloned().all(is_hex_digit)),
         |x| u8::from_str_radix(str::from_utf8(x).unwrap(), 16),
@@ -119,7 +121,7 @@ fn oct_char(input: &[u8]) -> NomResult<u8> {
     )(input)
 }
 
-fn name(input: &[u8]) -> NomResult<Vec<u8>> {
+pub(crate) fn name(input: &[u8]) -> NomResult<Vec<u8>> {
     preceded(
         tag(b"/"),
         many0(alt((
@@ -256,7 +258,7 @@ fn array(input: &[u8]) -> NomResult<Vec<Object>> {
     delimited(pair(tag(b"["), space), many0(_direct_object), tag(b"]"))(input)
 }
 
-fn dictionary(input: &[u8]) -> NomResult<Dictionary> {
+pub(crate) fn dictionary(input: &[u8]) -> NomResult<Dictionary> {
     delimited(
         pair(tag(b"<<"), space),
         fold_many0(
@@ -282,7 +284,9 @@ fn stream<'a>(input: &'a [u8], reader: &Reader, already_seen: &mut HashSet<Objec
         }
     }) {
         if length < 0 {
-            return Err(nom::Err::Failure(()));
+            // artificial error kind is created to allow descriptive nom errors
+            #[allow(clippy::unit_arg)]
+            return Err(nom::Err::Failure(NomError::from_error_kind(i, ErrorKind::LengthValue)));
         }
         let (i, data) = terminated(take(length as usize), pair(opt(eol), tag(b"endstream")))(i)?;
         Ok((i, Object::Stream(Stream::new(dict, data.to_vec()))))
@@ -416,14 +420,15 @@ fn trailer(input: &[u8]) -> NomResult<Dictionary> {
 }
 
 pub fn xref_and_trailer(input: &[u8], reader: &Reader) -> crate::Result<(Xref, Dictionary)> {
+    let xref_trailer = map(pair(xref, trailer), |(mut xref, trailer)| {
+        xref.size = trailer
+            .get(b"Size")
+            .and_then(Object::as_i64)
+            .map_err(|_| Error::Trailer)? as u32;
+        Ok((xref, trailer))
+    });
     alt((
-        map(pair(xref, trailer), |(mut xref, trailer)| {
-            xref.size = trailer
-                .get(b"Size")
-                .and_then(Object::as_i64)
-                .map_err(|_| Error::Trailer)? as u32;
-            Ok((xref, trailer))
-        }),
+        xref_trailer,
         (|input| {
             _indirect_object(input, 0, None, reader, &mut HashSet::new())
                 .map(|(_, obj)| {
@@ -433,7 +438,11 @@ pub fn xref_and_trailer(input: &[u8], reader: &Reader) -> crate::Result<(Xref, D
                     };
                     (input, res)
                 })
-                .map_err(|_| nom::Err::Error(()))
+                .map_err(|_| {
+                    // artificial error kind is created to allow descriptive nom errors
+                    #[allow(clippy::unit_arg)]
+                    nom::Err::Error(NomError::from_error_kind(input, ErrorKind::Fail))
+                })
         }),
     ))(input)
     .map(|(_, o)| o)
diff --git a/src/object.rs b/src/object.rs
index f06aec4..3cccf5a 100644
--- a/src/object.rs
+++ b/src/object.rs
@@ -1,3 +1,6 @@
+use crate::encodings;
+use crate::encodings::cmap::ToUnicodeCMap;
+use crate::encodings::Encoding;
 use crate::{Document, Error, Result};
 use indexmap::IndexMap;
 use log::warn;
@@ -353,10 +356,50 @@ impl Dictionary {
         self.0.iter_mut()
     }
 
-    pub fn get_font_encoding(&self) -> &str {
-        self.get(b"Encoding")
-            .and_then(Object::as_name_str)
-            .unwrap_or("StandardEncoding")
+    pub fn get_font_encoding(&self, doc: &Document) -> Result<Encoding> {
+        debug_assert!(
+            self.type_is(b"Font"),
+            "Encoding should be only retrieved from Font type dictionaries!"
+        );
+
+        // Note: currently not all encodings are handled, not implemented:
+        // - dictionary differences encoding
+        // - default base encoding in dictionary differences encoding
+        // - TrueType cmap tables
+        // - DescendantFonts in CID-Keyed fonts
+        // - predefined CJK CMAP other than indicated in SimpleEncoding
+        match self.get(b"Encoding").and_then(Object::as_name_str) {
+            Ok("StandardEncoding") => Ok(Encoding::OneByteEncoding(&encodings::STANDARD_ENCODING)),
+            Ok("MacRomanEncoding") => Ok(Encoding::OneByteEncoding(&encodings::MAC_ROMAN_ENCODING)),
+            Ok("MacExpertEncoding") => Ok(Encoding::OneByteEncoding(&encodings::MAC_EXPERT_ENCODING)),
+            Ok("WinAnsiEncoding") => Ok(Encoding::OneByteEncoding(&encodings::WIN_ANSI_ENCODING)),
+            // it is not customary to use PDFDocEncoding in fonts but it is not forbidden by the standard
+            Ok("PDFDocEncoding") => Ok(Encoding::OneByteEncoding(&encodings::PDF_DOC_ENCODING)),
+            Ok("Identity-H") | Ok("Identity-V") => {
+                let stream = self.get_deref(b"ToUnicode", doc)?.as_stream()?;
+                self.get_encoding_from_to_unicode_cmap(stream)
+            }
+            Ok(name) => Ok(Encoding::SimpleEncoding(name)),
+            Err(err) => {
+                warn!(
+                    "Could not parse the encoding, error: {:#?}\nFont: {:#?}\nTrying to retrieve ToUnicode.",
+                    err, self
+                );
+                let stream = self.get_deref(b"ToUnicode", doc).and_then(Object::as_stream);
+                if let Ok(stream) = stream {
+                    return self.get_encoding_from_to_unicode_cmap(stream);
+                }
+
+                warn!("Using standard encoding as a fallback!");
+                Ok(Encoding::OneByteEncoding(&encodings::STANDARD_ENCODING))
+            }
+        }
+    }
+
+    fn get_encoding_from_to_unicode_cmap(&self, stream: &Stream) -> Result<Encoding> {
+        let content = stream.get_plain_content()?;
+        let cmap = ToUnicodeCMap::parse(content)?;
+        Ok(Encoding::UnicodeMapEncoding(cmap))
     }
 
     pub fn extend(&mut self, other: &Dictionary) {
@@ -572,6 +615,13 @@ impl Stream {
         self.content = content;
     }
 
+    pub fn get_plain_content(&self) -> Result<Vec<u8>> {
+        match self.filters() {
+            Ok(vec) if !vec.is_empty() => self.decompressed_content(),
+            _ => Ok(self.content.clone()),
+        }
+    }
+
     pub fn compress(&mut self) -> Result<()> {
         use flate2::write::ZlibEncoder;
         use flate2::Compression;
diff --git a/src/parser.rs b/src/parser.rs
index e9751c7..742dad9 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -9,7 +9,7 @@ use pom::parser::*;
 use std::cmp::max;
 use std::str::{self, FromStr};
 
-fn eol<'a>() -> Parser<'a, u8, u8> {
+pub(crate) fn eol<'a>() -> Parser<'a, u8, u8> {
     (sym(b'\r') * sym(b'\n')) | sym(b'\n') | sym(b'\r')
 }
 
@@ -21,7 +21,7 @@ fn white_space<'a>() -> Parser<'a, u8, ()> {
     one_of(b" \t\n\r\0\x0C").repeat(0..).discard()
 }
 
-fn space<'a>() -> Parser<'a, u8, ()> {
+pub(crate) fn space<'a>() -> Parser<'a, u8, ()> {
     (one_of(b" \t\n\r\0\x0C").repeat(1..).discard() | comment())
         .repeat(0..)
         .discard()
@@ -39,7 +39,7 @@ fn real<'a>() -> Parser<'a, u8, f32> {
     number.collect().convert(str::from_utf8).convert(f32::from_str)
 }
 
-fn hex_char<'a>() -> Parser<'a, u8, u8> {
+pub(crate) fn hex_char<'a>() -> Parser<'a, u8, u8> {
     let number = is_a(hex_digit).repeat(2);
     number
         .collect()
@@ -53,7 +53,7 @@ fn oct_char<'a>() -> Parser<'a, u8, u8> {
         .convert(|v| u8::from_str_radix(str::from_utf8(v).unwrap(), 8))
 }
 
-fn name<'a>() -> Parser<'a, u8, Vec<u8>> {
+pub(crate) fn name<'a>() -> Parser<'a, u8, Vec<u8>> {
     sym(b'/') * (none_of(b" \t\n\r\x0C()<>[]{}/%#") | (sym(b'#') * hex_char())).repeat(0..)
 }
 
@@ -113,7 +113,7 @@ fn array<'a>() -> Parser<'a, u8, Vec<Object>> {
     sym(b'[') * space() * call(_direct_object).repeat(0..) - sym(b']')
 }
 
-fn dictionary<'a>() -> Parser<'a, u8, Dictionary> {
+pub(crate) fn dictionary<'a>() -> Parser<'a, u8, Dictionary> {
     let entry = name() - space() + call(_direct_object);
     let entries = seq(b"<<") * space() * entry.repeat(0..) - seq(b">>");
     entries.map(|entries| {
@@ -148,7 +148,7 @@ fn stream<'a>(reader: &'a Reader) -> Parser<'a, u8, Stream> {
 }
 
 fn object_id<'a>() -> Parser<'a, u8, ObjectId> {
-    let id = one_of(b"0123456789")
+    let id: Parser<u8, u32> = one_of(b"0123456789")
         .repeat(1..)
         .convert(|v| u32::from_str(str::from_utf8(&v).unwrap()));
     let gen = one_of(b"0123456789")
@@ -372,4 +372,36 @@ T* (encoded streams.) Tj
         println!("{:?}", content);
         assert!(content.is_some());
     }
+
+    #[test]
+    fn parse_cidsysteminfo_dictionary() {
+        let stream = b"<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >>";
+        let expected = dictionary!(
+            "Registry" => Object::string_literal("Adobe"),
+            "Ordering" => Object::string_literal("UCS"),
+            "Supplement" => Object::Integer(0));
+
+        let parsed_dict = dictionary().parse(stream);
+        assert_eq!(parsed_dict, Ok(expected));
+    }
+
+    #[test]
+    fn parse_font_dictionary() {
+        // from PDF 1.7 standard example font
+        let stream = b"<< /Type /Font
+    /Subtype /Type0
+    /BaseFont /HeiseiMin-W5-90ms-RKSJ-H
+    /Encoding /90ms-RKSJ-H
+    /DescendantFonts [15 0 R]
+>>";
+        let expected = dictionary!(
+            "Type" => "Font",
+            "Subtype"=> "Type0",
+            "BaseFont" => "HeiseiMin-W5-90ms-RKSJ-H",
+            "Encoding" => "90ms-RKSJ-H",
+            "DescendantFonts" => Object::Array(vec![Object::Reference((15, 0))]));
+        let parsed_dict = dictionary().parse(stream);
+        println!("{:#?}", parsed_dict.clone().unwrap().get(b"DescendantFonts"));
+        assert_eq!(parsed_dict, Ok(expected));
+    }
 }
diff --git a/src/parser_aux.rs b/src/parser_aux.rs
index b335c14..cbdfee5 100644
--- a/src/parser_aux.rs
+++ b/src/parser_aux.rs
@@ -1,15 +1,17 @@
 #![cfg(any(feature = "pom_parser", feature = "nom_parser"))]
 
+use log::warn;
+
 use crate::{
     content::{Content, Operation},
     document::Document,
+    encodings::Encoding,
     error::XrefError,
     object::Object::Name,
     xref::{Xref, XrefEntry, XrefType},
     Error, Result,
 };
 use crate::{parser, Dictionary, Object, ObjectId, Stream};
-use log::info;
 use std::{
     collections::BTreeMap,
     io::{Cursor, Read},
@@ -44,15 +46,14 @@ impl Document {
     }
 
     pub fn extract_text(&self, page_numbers: &[u32]) -> Result<String> {
-        fn collect_text(text: &mut String, encoding: Option<&str>, operands: &[Object]) {
+        fn collect_text(text: &mut String, encoding: &Encoding, operands: &[Object]) -> Result<()> {
             for operand in operands.iter() {
                 match *operand {
                     Object::String(ref bytes, _) => {
-                        let decoded_text = Document::decode_text(encoding, bytes);
-                        text.push_str(&decoded_text);
+                        text.push_str(&Document::decode_text(encoding, bytes)?);
                     }
                     Object::Array(ref arr) => {
-                        collect_text(text, encoding, arr);
+                        collect_text(text, encoding, arr)?;
                         text.push(' ');
                     }
                     Object::Integer(i) => {
@@ -63,16 +64,17 @@ impl Document {
                     _ => {}
                 }
             }
+            Ok(())
         }
         let mut text = String::new();
         let pages = self.get_pages();
         for page_number in page_numbers {
             let page_id = *pages.get(page_number).ok_or(Error::PageNumberNotFound(*page_number))?;
             let fonts = self.get_page_fonts(page_id)?;
-            let encodings = fonts
+            let encodings: BTreeMap<Vec<u8>, Encoding> = fonts
                 .into_iter()
-                .map(|(name, font)| (name, font.get_font_encoding()))
-                .collect::<BTreeMap<Vec<u8>, &str>>();
+                .map(|(name, font)| font.get_font_encoding(self).map(|it| (name, it)))
+                .collect::<Result<BTreeMap<Vec<u8>, Encoding>>>()?;
             let content_data = self.get_page_content(page_id)?;
             let content = Content::decode(&content_data)?;
             let mut current_encoding = None;
@@ -84,11 +86,12 @@ impl Document {
                             .first()
                             .ok_or_else(|| Error::Syntax("missing font operand".to_string()))?
                             .as_name()?;
-                        current_encoding = encodings.get(current_font).cloned();
-                    }
-                    "Tj" | "TJ" => {
-                        collect_text(&mut text, current_encoding, &operation.operands);
+                        current_encoding = encodings.get(current_font);
                     }
+                    "Tj" | "TJ" => match current_encoding {
+                        Some(encoding) => collect_text(&mut text, encoding, &operation.operands)?,
+                        None => warn!("Could not decode extracted text"),
+                    },
                     "ET" => {
                         if !text.ends_with('\n') {
                             text.push('\n')
@@ -107,11 +110,11 @@ impl Document {
             .page_iter()
             .nth(page)
             .ok_or(Error::PageNumberNotFound(page_number))?;
-        let encodings = self
+        let encodings: BTreeMap<Vec<u8>, Encoding> = self
             .get_page_fonts(page_id)?
             .into_iter()
-            .map(|(name, font)| (name, font.get_font_encoding().to_owned()))
-            .collect::<BTreeMap<Vec<u8>, String>>();
+            .map(|(name, font)| font.get_font_encoding(self).map(|it| (name, it)))
+            .collect::<Result<BTreeMap<Vec<u8>, Encoding>>>()?;
         let content_data = self.get_page_content(page_id)?;
         let mut content = Content::decode(&content_data)?;
         let mut current_encoding = None;
@@ -123,18 +126,14 @@ impl Document {
                         .first()
                         .ok_or_else(|| Error::Syntax("missing font operand".to_string()))?
                         .as_name()?;
-                    current_encoding = encodings.get(current_font).map(std::string::String::as_str);
+                    current_encoding = encodings.get(current_font);
                 }
-                "Tj" => {
-                    for bytes in operation.operands.iter_mut().flat_map(Object::as_str_mut) {
-                        let decoded_text = Document::decode_text(current_encoding, bytes);
-                        info!("{}", decoded_text);
-                        if decoded_text == text {
-                            let encoded_bytes = Document::encode_text(current_encoding, other_text);
-                            *bytes = encoded_bytes;
-                        }
+                "Tj" => match current_encoding {
+                    Some(encoding) => try_to_replace_encoded_text(operation, encoding, text, other_text)?,
+                    None => {
+                        warn!("Could not decode extracted text, some of the occurances might not be properly replaced")
                     }
-                }
+                },
                 _ => {}
             }
         }
@@ -188,6 +187,19 @@ impl Document {
     }
 }
 
+fn try_to_replace_encoded_text(
+    operation: &mut Operation, encoding: &Encoding, text_to_replace: &str, replacement: &str,
+) -> Result<()> {
+    for bytes in operation.operands.iter_mut().flat_map(Object::as_str_mut) {
+        let decoded_text = Document::decode_text(encoding, bytes)?;
+        if decoded_text == text_to_replace {
+            let encoded_bytes = Document::encode_text(encoding, replacement);
+            *bytes = encoded_bytes;
+        }
+    }
+    Ok(())
+}
+
 /// Decode CrossReferenceStream
 pub fn decode_xref_stream(mut stream: Stream) -> Result<(Xref, Dictionary)> {
     stream.decompress();


Test patch:
diff --git a/tests/unicode.rs b/tests/unicode.rs
new file mode 100644
index 0000000..03ee79b
--- /dev/null
+++ b/tests/unicode.rs
@@ -0,0 +1,148 @@
+use lopdf::content::{Content, Operation};
+use lopdf::{dictionary, Document, Object, Stream, StringFormat};
+
+#[test]
+fn unicode_can_be_written_to_pdf_and_read() {
+    let mut doc = Document::new();
+
+    // literal corresponds to the chars:
+    // U+1F600
+    // U+1F527
+    // U+1F528
+    // which are encoded in font cmap with the following hex
+    // <005F>
+    // <0060>
+    // <0061>
+    // please mind that indicated BaseFont might not contain
+    // those unicode emojis in practice
+    let unicode_literal = "😀🔧🔨";
+    let literal_encoded_with_cmap = [0x00, 0x5F, 0x00, 0x60, 0x00, 0x61];
+
+    // majority of below code correspond to create document example
+    // we are just inserting an appropriate unicode text
+    // and Type0 font with approriate characters encoding
+    let pages_id = doc.new_object_id();
+
+    let cmap_stream_id = doc.add_object(Stream::new(
+        dictionary! {
+            "Length" => 437
+        },
+        b"/CIDInit /ProcSet findresource begin
+12 dict begin
+begincmap
+/CIDSystemInfo
+<< /Registry (Adobe)
+/Ordering (UCS)
+/Supplement 0
+>> def
+/CMapName /Adobe-Identity-UCS def
+/CMapType 2 def
+1 begincodespacerange
+<0000> <FFFF>
+endcodespacerange
+2 beginbfrange
+<0000> <005E> <0020>
+<005F> <0061> [<D83DDE00> <D83DDD27> <D83DDD28>]
+endbfrange
+1 beginbfchar
+<3A51> <D840DC3E>
+endbfchar
+endcmap
+CMapName currentdict /CMap defineresource pop
+end
+end"
+        .to_vec(),
+    ));
+
+    let font_id = doc.add_object(dictionary! {
+        "Type" => "Font",
+        "Subtype" => "Type0",
+        "BaseFont" => "Ryumin-Light",
+        "Encoding" => "Identity-H",
+        "ToUnicode" => Object::Reference(cmap_stream_id)
+    });
+
+    let resources_id = doc.add_object(dictionary! {
+        "Font" => dictionary! {
+            "F1" => font_id,
+        },
+    });
+
+    // Create content with the prepared emoji string encoded according to values in our font cmap
+    let content = Content {
+        operations: vec![
+            Operation::new("BT", vec![]),
+            Operation::new("Tf", vec!["F1".into(), 48.into()]),
+            Operation::new("Td", vec![100.into(), 600.into()]),
+            Operation::new(
+                "Tj",
+                vec![Object::String(
+                    literal_encoded_with_cmap.to_vec(),
+                    StringFormat::Hexadecimal,
+                )],
+            ),
+            Operation::new("ET", vec![]),
+        ],
+    };
+
+    let content_id = doc.add_object(Stream::new(dictionary! {}, content.encode().unwrap()));
+
+    let page_id = doc.add_object(dictionary! {
+        "Type" => "Page",
+        "Parent" => pages_id,
+        "Contents" => content_id,
+    });
+
+    let pages = dictionary! {
+        "Type" => "Pages",
+        "Kids" => vec![page_id.into()],
+        "Count" => 1,
+        "Resources" => resources_id,
+        "MediaBox" => vec![0.into(), 0.into(), 595.into(), 842.into()],
+    };
+
+    // using insert() here, instead of add_object() since the id is already known.
+    doc.objects.insert(pages_id, Object::Dictionary(pages));
+
+    let catalog_id = doc.add_object(dictionary! {
+        "Type" => "Catalog",
+        "Pages" => pages_id,
+    });
+
+    doc.trailer.set("Root", catalog_id);
+    doc.compress();
+
+    let extracted_text = get_text_from_first_page(&doc);
+
+    assert_eq!(extracted_text.trim_end(), unicode_literal);
+}
+
+fn get_text_from_first_page(doc: &Document) -> String {
+    let mut pages = doc.get_pages();
+    let first_page = pages.first_entry().expect("Expected pages to be non empty");
+    let extracted_text = doc
+        .extract_text(&[first_page.key().clone()])
+        .expect("Expected to find text on the first page");
+    extracted_text
+}
+
+#[cfg(not(feature = "async"))]
+#[test]
+fn unicode_can_be_extracted_from_loaded_pdf() -> lopdf::Result<()> {
+    let doc = Document::load("assets/unicode.pdf")?;
+    let extracted_text = get_text_from_first_page(&doc);
+    // extract text can currently map a consecutive fragment of text
+    // to one divided into multiple lines, therefore we have to remove the
+    // new lines
+    assert_eq!(extracted_text.replace("\n", ""), "😀🔧🔨");
+    Ok(())
+}
+
+#[cfg(feature = "async")]
+#[tokio::test]
+async fn unicode_can_be_extracted_from_loaded_pdf() -> lopdf::Result<()> {
+    let doc = Document::load("assets/unicode.pdf").await?;
+    let extracted_text = get_text_from_first_page(&doc);
+    assert_eq!(extracted_text.replace("\n", ""), "😀🔧🔨");
+    Ok(())
+}