Problem
When a FontTrueType font (simple TrueType, Subtype: TrueType) is subset-embedded without a /ToUnicode CMap, page.extract_text raises HexaPDF::Error:
No Unicode mapping for code point 50 in font AAAAAC+Calibri
This is common in PDFs exported from Microsoft Word and Apple Pages, which subset-embed Calibri using this embedding strategy, often omitting the ToUnicode CMap. The PDF renders correctly in all viewers, but text extraction fails entirely.
Reproduction
Export any Word (.docx) or Pages document that uses Calibri body text to PDF on Windows (default Word PDF export settings) or macOS Pages. Then:
require "hexapdf"
doc = HexaPDF::Document.open("calibri_word_export.pdf")
doc.pages.each { |page| puts page.extract_text }
# => HexaPDF::Error: No Unicode mapping for code point 50 in font AAAAAC+Calibri
The stack trace goes through FontSimple#to_utf8 at lib/hexapdf/type/font_simple.rb.
Root cause
FontSimple#to_utf8 (lib/hexapdf/type/font_simple.rb) has the following fallback chain:
def to_utf8(code)
to_unicode_cmap&.to_unicode(code) || (encoding.unicode(code) rescue nil) ||
missing_unicode_mapping(code)
end
For this class of PDF:
to_unicode_cmap — absent (omitted by the exporter)
encoding.unicode(code) — returns nil for the affected code points (the encoding can't map them)
missing_unicode_mapping — raises (or invokes the user config callback)
The underlying reason encoding.unicode fails: FontTrueType#font_wrapper (lib/hexapdf/type/font_true_type.rb) explicitly excludes subset fonts from parsing the embedded font file:
def font_wrapper
if (tmp = super)
tmp
elsif (font_file = self.font_file) && self[:BaseFont].to_s !~ /\A[A-Z]{6}\+/
# ^^^ this excludes AAAAAC+Calibri and all subset-embedded fonts
font = HexaPDF::Font::TrueType::Font.new(StringIO.new(font_file.stream))
@font_wrapper = HexaPDF::Font::TrueTypeWrapper.new(document, font, subset: true)
end
end
The AAAAAC+ prefix marks the font as a subset, so font_wrapper is never set and the embedded font file is never read for Unicode extraction.
Suggested fix
In FontTrueType: implement encoding_from_font (or a to_utf8 override) that reads the SFNT cmap table from the embedded subset font file even when the full TrueTypeWrapper is not appropriate. The embedded subset's cmap table is valid for all glyphs present in the subset — reading it for text extraction is safe, even though using it for new text embedding would not be.
HexaPDF::Font::TrueType::Font already supports parsing the cmap table; the change is narrow: allow that parsing in the read/extraction path for subset fonts, separately from the write/embedding path.
Prior discussion
Issue #349 (Mar 2025) discussed missing Unicode mappings in the context of OpenType CFF fonts. The Calibri case is slightly different — the font is embedded as a simple FontTrueType, not a composite Type0 font — but the same underlying gap applies: the embedded font file contains a usable cmap table that HexaPDF doesn't consult for subsets.
Problem
When a
FontTrueTypefont (simple TrueType,Subtype: TrueType) is subset-embedded without a/ToUnicodeCMap,page.extract_textraisesHexaPDF::Error:This is common in PDFs exported from Microsoft Word and Apple Pages, which subset-embed Calibri using this embedding strategy, often omitting the ToUnicode CMap. The PDF renders correctly in all viewers, but text extraction fails entirely.
Reproduction
Export any Word (
.docx) or Pages document that uses Calibri body text to PDF on Windows (default Word PDF export settings) or macOS Pages. Then:The stack trace goes through
FontSimple#to_utf8atlib/hexapdf/type/font_simple.rb.Root cause
FontSimple#to_utf8(lib/hexapdf/type/font_simple.rb) has the following fallback chain:For this class of PDF:
to_unicode_cmap— absent (omitted by the exporter)encoding.unicode(code)— returnsnilfor the affected code points (the encoding can't map them)missing_unicode_mapping— raises (or invokes the user config callback)The underlying reason
encoding.unicodefails:FontTrueType#font_wrapper(lib/hexapdf/type/font_true_type.rb) explicitly excludes subset fonts from parsing the embedded font file:The
AAAAAC+prefix marks the font as a subset, sofont_wrapperis never set and the embedded font file is never read for Unicode extraction.Suggested fix
In
FontTrueType: implementencoding_from_font(or ato_utf8override) that reads the SFNTcmaptable from the embedded subset font file even when the fullTrueTypeWrapperis not appropriate. The embedded subset'scmaptable is valid for all glyphs present in the subset — reading it for text extraction is safe, even though using it for new text embedding would not be.HexaPDF::Font::TrueType::Fontalready supports parsing thecmaptable; the change is narrow: allow that parsing in the read/extraction path for subset fonts, separately from the write/embedding path.Prior discussion
Issue #349 (Mar 2025) discussed missing Unicode mappings in the context of OpenType CFF fonts. The Calibri case is slightly different — the font is embedded as a simple
FontTrueType, not a composite Type0 font — but the same underlying gap applies: the embedded font file contains a usablecmaptable that HexaPDF doesn't consult for subsets.