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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 14 additions & 1 deletion src/odr/internal/pdf/pdf_page_extractor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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};
}
Expand Down
18 changes: 18 additions & 0 deletions test/src/internal/pdf/pdf_page_extractor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<TextElement> 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
Expand Down
Loading