Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions src/parse/pdf_resources/page_font.h
Original file line number Diff line number Diff line change
Expand Up @@ -2596,19 +2596,45 @@ namespace pdflib
// mapping to this value means 'unknown character' and is treated as
// no mapping at all, so the glyph-name based methods can still
// recover the code (eg /bullet.003 mapped to U+FFFD by the cmap).
const std::string replacement_char = "\xEF\xBF\xBD";
const uint32_t replacement_char = 0xFFFD;

// Unicode noncharacters (U+FDD0..U+FDEF and the last two code points
// of every plane) are reserved for internal use and never stand for
// text, so a /ToUnicode mapping to one of them carries no information
// either (eg ligatures /f_f_i mapped to <ffff>, see
// docling-project/docling-parse#394).
auto is_noncharacter = [](uint32_t cp)
{
return (0xFDD0<=cp and cp<=0xFDEF) or (cp & 0xFFFE)==0xFFFE;
};

// PDF 32000-1 (section 9.10.2) defines the /ToUnicode cmap as the
// first and most authoritative method to map a character-code to
// unicode; the glyph-name based methods only apply when no (valid)
// cmap entry exists for the code.
auto has_to_unicode = [&](int numb)
{
return cmap_initialized
and cmap_numb_to_char.count(numb)==1
and cmap_numb_to_char.at(numb).size()>0
and cmap_numb_to_char.at(numb)!=std::string(1, '\0')
and cmap_numb_to_char.at(numb)!=replacement_char;
if(not cmap_initialized or cmap_numb_to_char.count(numb)!=1)
{
return false;
}

const std::string& value = cmap_numb_to_char.at(numb);
if(value.empty())
{
return false;
}

// Only a mapping to a single unusable code point is discarded.
auto itr = value.begin();
utf8::utfchar32_t cp = 0;
if(utf8::internal::validate_next(itr, value.end(), cp)!=utf8::internal::UTF8_OK
or itr!=value.end())
{
return true;
}

return cp!=0 and cp!=replacement_char and not is_noncharacter(cp);
};

// Subset generators (FontForge, fontTools, mPDF, ...) name glyphs by
Expand Down
11 changes: 11 additions & 0 deletions tests/test_unit_nul_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ def test_nul_mapping_recovers_from_known_glyph_identity(glyph_name: str, expecte
assert _extract_character(mapping="0000", glyph_name=glyph_name) == expected


@pytest.mark.parametrize("mapping", ["FFFF", "FFFE", "FDD0"])
@pytest.mark.parametrize(
("glyph_name", "expected"), [("f_f_i", "ffi"), ("f_i", "fi"), ("minus", "-")]
)
def test_noncharacter_mapping_recovers_from_glyph_name(
mapping: str, glyph_name: str, expected: str
):
# docling-project/docling-parse#394: LaTeX subsets map ligatures to <ffff>.
assert _extract_character(mapping=mapping, glyph_name=glyph_name) == expected


def test_nul_mapping_with_unknown_identity_remains_unresolved():
assert (
_extract_character(mapping="0000", glyph_name="gid00043")
Expand Down
Loading