From 77cc9d8defee182861daa7e92b9ea3515e30d1bb Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sat, 10 Oct 2026 11:37:53 +0200 Subject: [PATCH] fix(pdf): extract Adobe small capitals as letters Fonts with glyphs such as `Asmall` or `oneoldstyle` map them to Adobe's corporate-use code points U+F721 to U+F7FF, so the text layer held private-use characters that no search found and no copy could read. These code points mirror ASCII and Latin-1 at U+F700. Fold them back when a shown string is resolved to text, as pdf.js does. The glyphs drawn stay the same. Fixes #1187 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01S5yx1nDeVcNX3BxqK1WpJz --- CHANGELOG.md | 2 ++ src/odr/internal/pdf/pdf_page_extractor.cpp | 15 ++++++++++++++- test/src/internal/pdf/pdf_page_extractor.cpp | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57e463169..5ee2d3526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,8 @@ The release run heads these entries with the version and opens a fresh - A text page that holds a frame anchored to the page, such as a letter template, prints whole on one sheet, so the frame stays where the page puts it. +- pdf text in Adobe small capitals and old-style figures (`Asmall`, + `oneoldstyle`) is found by a search and copies as letters and digits. ## v7.5.0 - 2026-10-06 diff --git a/src/odr/internal/pdf/pdf_page_extractor.cpp b/src/odr/internal/pdf/pdf_page_extractor.cpp index 9a72063bb..43a260a0b 100644 --- a/src/odr/internal/pdf/pdf_page_extractor.cpp +++ b/src/odr/internal/pdf/pdf_page_extractor.cpp @@ -64,6 +64,19 @@ struct ResolvedText { bool no_unicode{false}; }; +/// Adobe's corporate-use small capitals and old-style figures (`Asmall` is +/// U+F761, `zerooldstyle` U+F730) mirror ASCII and Latin-1 at U+F700. Their +/// letters are what a search or a copy wants; the glyphs drawn stay the same. +std::string fold_adobe_small_caps(const std::string &text) { + std::string result; + for (std::string_view rest = text; !rest.empty();) { + const char32_t c = util::string::next_utf8(rest); + util::string::append_c32(c >= 0xf721 && c <= 0xf7ff ? c - 0xf700 : c, + result); + } + return result; +} + ResolvedText resolve_text(MarkedContentStack &marked, const Font *font, const std::string &codes) { for (MarkedContent &entry : marked | std::views::reverse) { @@ -78,7 +91,7 @@ ResolvedText resolve_text(MarkedContentStack &marked, const Font *font, if (font == nullptr) { return {codes, false}; // unknown font: historic raw-code passthrough } - std::string unicode = font->to_unicode(codes); + std::string unicode = fold_adobe_small_caps(font->to_unicode(codes)); const bool no_unicode = unicode.empty() && !codes.empty(); return {std::move(unicode), no_unicode}; } diff --git a/test/src/internal/pdf/pdf_page_extractor.cpp b/test/src/internal/pdf/pdf_page_extractor.cpp index 09be13756..4739105c7 100644 --- a/test/src/internal/pdf/pdf_page_extractor.cpp +++ b/test/src/internal/pdf/pdf_page_extractor.cpp @@ -636,6 +636,24 @@ Font type3_font(std::uint32_t first_char, const std::string &name, double width, } // namespace +// Adobe's private-use small capitals and old-style figures fold back to their +// letters and digits in the text, as pdf.js extracts them. +TEST(PdfPageExtractor, adobe_small_caps_extract_as_letters) { + Font font = simple_font('A', {500, 500, 500, 500}); + Encoding encoding(BaseEncoding::standard); + encoding.set_difference('A', "Asmall"); + encoding.set_difference('B', "Bsmall"); + encoding.set_difference('C', "oneoldstyle"); + font.encoding = encoding; + Resources res; + res.font["F1"] = &font; + + const std::vector text = run("BT /F1 10 Tf (ABCD) Tj ET", res); + + ASSERT_EQ(text.size(), 1); + EXPECT_EQ(text[0].text, "ab1D"); +} + // A Type3 glyph runs its char proc through the page machinery at the glyph // transform (`/FontMatrix` x size x `Tm` x CTM): the glyph paints as ordinary // path elements, and the shown run stays selectable but paints no visible text