From 09d42b8c82806810687efbb13f4c6396f5ec94a8 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Mon, 19 Oct 2026 17:58:24 +0300 Subject: [PATCH 01/16] Read and write with a document's own fonts Index every font face a document holds (form, page, form XObject, pattern, soft mask, Type 3 and appearance resources), with each program's format, embedding, permission, Unicode map, SHA-256 and line metrics, cached per program stream. Resolve which font a write would draw each run in, without writing. An annotation's default appearance can name a program the document holds whole, and its appearance draws with that program. Report characters drawn with no font. Read registered fonts' programs, coverage and fallback order. New text no longer draws with a registered preview-and-print font that was not authorized for editing. --- core/fpdfapi/parser/cpdf_document.h | 14 + core/fpdfdoc/BUILD.gn | 6 + core/fpdfdoc/cpdf_annotfontmap.cpp | 237 ++++-- core/fpdfdoc/cpdf_annotfontmap.h | 44 ++ core/fpdfdoc/cpdf_annotfontsubset.cpp | 37 +- core/fpdfdoc/cpdf_annotfontsubset.h | 8 + core/fpdfdoc/cpdf_appearancegenerator.h | 11 + .../cpdf_appearancegenerator_forms.cpp | 8 +- .../cpdf_appearancegenerator_freetext.cpp | 51 +- .../cpdf_appearancegenerator_internal.h | 12 +- .../cpdf_appearancegenerator_redaction.cpp | 4 +- .../fpdfdoc/cpdf_appearancegenerator_text.cpp | 38 +- core/fpdfdoc/cpdf_documentfonts.cpp | 490 +++++++++++++ core/fpdfdoc/cpdf_documentfonts.h | 141 ++++ core/fpdfdoc/cpdf_fontface.cpp | 65 ++ core/fpdfdoc/cpdf_fontface.h | 17 + core/fpdfdoc/cpdf_fontresolver.cpp | 116 +++ core/fpdfdoc/cpdf_fontresolver.h | 53 ++ core/fpdfdoc/cpdf_richtextlayout.cpp | 96 ++- core/fpdfdoc/cpdf_richtextlayout.h | 20 + core/fxge/cfx_fontregistry.cpp | 68 ++ core/fxge/cfx_fontregistry.h | 18 + fpdfsdk/BUILD.gn | 3 + fpdfsdk/epdf_annot.cpp | 110 ++- fpdfsdk/epdf_c_api_test.c | 42 ++ fpdfsdk/epdf_font.cpp | 588 +++++++++++++++ fpdfsdk/epdf_font_embeddertest.cpp | 672 ++++++++++++++++++ public/epdf_annot.h | 42 ++ public/epdf_font.h | 291 ++++++++ 29 files changed, 3180 insertions(+), 122 deletions(-) create mode 100644 core/fpdfdoc/cpdf_documentfonts.cpp create mode 100644 core/fpdfdoc/cpdf_documentfonts.h create mode 100644 core/fpdfdoc/cpdf_fontresolver.cpp create mode 100644 core/fpdfdoc/cpdf_fontresolver.h create mode 100644 fpdfsdk/epdf_font_embeddertest.cpp diff --git a/core/fpdfapi/parser/cpdf_document.h b/core/fpdfapi/parser/cpdf_document.h index a51fbf2454..9e39ee4f52 100644 --- a/core/fpdfapi/parser/cpdf_document.h +++ b/core/fpdfapi/parser/cpdf_document.h @@ -139,6 +139,18 @@ class CPDF_Document : public Observable, epdf_attachment_ = std::move(attachment); } + // EmbedPDF(fonts): the document's font index (CPDF_DocumentFonts in + // core/fpdfdoc), made on first use: what each embedded font program is, + // and the session's font state. Destroyed before the parser. + class FontIndex { + public: + virtual ~FontIndex() = default; + }; + FontIndex* font_index() const { return font_index_.get(); } + void SetFontIndex(std::unique_ptr index) { + font_index_ = std::move(index); + } + // EmbedPDF(fonts): session-scoped provenance for a resource name this // document instance handed out for a registered runtime font (a /DA font // alias reserved before its /DR entry exists). Never written to the file; a @@ -434,6 +446,8 @@ class CPDF_Document : public Observable, // EmbedPDF(signatures): destroyed before everything declared above it (the // parser included), after the extension and the stock font clearer. std::unique_ptr epdf_attachment_; + // EmbedPDF(fonts): see font_index(); destroyed before the attachment. + std::unique_ptr font_index_; // Must be second to last. StockFontClearer stock_font_clearer_; diff --git a/core/fpdfdoc/BUILD.gn b/core/fpdfdoc/BUILD.gn index 24fd87c902..b4070b9f2c 100644 --- a/core/fpdfdoc/BUILD.gn +++ b/core/fpdfdoc/BUILD.gn @@ -26,6 +26,12 @@ source_set("fpdfdoc") { # EmbedPDF: a font as a face (family, weight, italic), one reader for all. "cpdf_fontface.cpp", "cpdf_fontface.h", + # EmbedPDF: the fonts a document uses and what each program is, and the + # fonts a write would draw text with (a resolver read). + "cpdf_documentfonts.cpp", + "cpdf_documentfonts.h", + "cpdf_fontresolver.cpp", + "cpdf_fontresolver.h", "cpdf_richtext.h", "cpdf_richtextjson.cpp", "cpdf_richtextjson.h", diff --git a/core/fpdfdoc/cpdf_annotfontmap.cpp b/core/fpdfdoc/cpdf_annotfontmap.cpp index 5d754e61d3..c8ddcb9326 100644 --- a/core/fpdfdoc/cpdf_annotfontmap.cpp +++ b/core/fpdfdoc/cpdf_annotfontmap.cpp @@ -26,8 +26,10 @@ #include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fpdfapi/parser/cpdf_stream_acc.h" #include "core/fpdfdoc/cpdf_annotfontsubset.h" +#include "core/fpdfdoc/cpdf_documentfonts.h" #include "core/fpdfdoc/cpdf_fontface.h" #include "core/fpdfdoc/cpdf_interactiveform.h" +#include "core/fpdfdoc/cpdf_richtextlayout.h" #include "core/fxcrt/check.h" #include "core/fxcrt/code_point_view.h" #include "core/fxcrt/fx_codepage.h" @@ -76,27 +78,6 @@ RetainPtr GetOrCreateDrFontDict(CPDF_Document* doc) { // ---- Rich text helpers ------------------------------------------------------ -// Family names compare without case, spaces, hyphens and underscores: -// "Noto Sans" == "NotoSans" == "noto-sans". -ByteString FamilyKey(const WideString& family) { - ByteString key; - for (wchar_t ch : family) { - if (ch == L' ' || ch == L'-' || ch == L'_' || ch == L'\'' || ch == L'"') { - continue; - } - if (ch < 0x80) { - key += static_cast(std::tolower(static_cast(ch))); - } else { - key += WideString(ch).ToUTF8(); - } - } - return key; -} - -ByteString FamilyKey(const ByteString& family) { - return FamilyKey(WideString::FromUTF8(family.AsStringView())); -} - // The standard-14 family a rich text family stands for, if any. enum class StandardFamily { kNone, @@ -108,7 +89,7 @@ enum class StandardFamily { }; StandardFamily StandardFamilyFor(const WideString& family) { - const ByteString key = FamilyKey(family); + const ByteString key = FontFamilyKey(family); if (key == "helvetica" || key == "arial" || key == "arialmt" || key == "sansserif") { return StandardFamily::kHelvetica; @@ -223,29 +204,6 @@ void StandardMetrics(const ByteString& base_font_name, *descent = std::abs(desc) / 1000.0f; } -// hhea ascender/descender of an embedded program (Acrobat's choice for -// TrueType and CFF, plan §6). -void ProgramMetrics(const CFX_Font* font, float* ascent, float* descent) { - RetainPtr face = font ? font->GetFace() : nullptr; - const uint16_t upem = face ? face->GetUnitsPerEm() : 0; - if (!face || upem == 0) { - *ascent = 0.830f; - *descent = 0.170f; - return; - } - *ascent = static_cast(face->GetAscender()) / upem; - *descent = static_cast(-face->GetDescender()) / upem; - if (*ascent <= 0 || *descent < 0) { - *ascent = 0.830f; - *descent = 0.170f; - } -} - -// "ABCDEF+NotoSans": a producer's subset, cut for the text it was used for. -bool HasSubsetTag(const ByteString& base_font) { - return base_font.GetLength() > 7 && base_font[6] == '+'; -} - struct DocumentProgramCandidate { RetainPtr stream; ByteString base_font_name; @@ -398,13 +356,18 @@ CPDF_AnnotFontMap::CPDF_AnnotFontMap( font_id = CPDF_AnnotFontSubset::ResolveRegisteredFont( entry.font->GetFontDict().Get()); } + // A registered font not authorized for editing (preview-and-print) never + // lays out new text: the /DA font stays the dictionary /DR holds. + if (font_id.has_value() && !CFX_FontRegistry::IsEditingAuthorized(*font_id)) { + font_id.reset(); + } if (font_id.has_value()) { RetainPtr registered_font = CreateRegisteredLayoutFont(*font_id); if (registered_font) { entry.font = std::move(registered_font); entry.registered_font_id = *font_id; entry.source = Source::kRegistered; - ProgramMetrics(entry.font->GetFont(), &entry.ascent, &entry.descent); + ProgramLineMetrics(entry.font->GetFont(), &entry.ascent, &entry.descent); } } if (entry.source == Source::kDocumentFontDict && entry.font) { @@ -412,10 +375,23 @@ CPDF_AnnotFontMap::CPDF_AnnotFontMap( StandardMetrics(entry.font->GetBaseFontName(), entry.font.Get(), &entry.ascent, &entry.descent); } else { - ProgramMetrics(entry.font->GetFont(), &entry.ascent, &entry.descent); + ProgramLineMetrics(entry.font->GetFont(), &entry.ascent, &entry.descent); } } + // A composite /DA font over a program that can author text (what the + // writer installs for a document program) lays new text out with that + // program's own cmap: its ToUnicode only names the characters drawn so far. + std::optional candidate; + if (entry.source == Source::kDocumentFontDict && entry.font && + entry.font->GetFontDict()->GetNameFor("Subtype") == "Type0") { + candidate = CandidateFromFontDict(entry.font->GetFontDict().Get()); + } fonts_.push_back(std::move(entry)); + if (candidate.has_value()) { + UseDocumentProgramForDefault(std::move(candidate->stream), + candidate->base_font_name, + candidate->identity); + } } CPDF_AnnotFontMap::~CPDF_AnnotFontMap() { @@ -509,6 +485,13 @@ ByteString CPDF_AnnotFontMap::AllocateAppearanceAlias( return candidate; } +// static +bool CPDF_AnnotFontMap::IsDocumentProgramFont( + const CPDF_Dictionary* font_dict) { + return font_dict && font_dict->GetNameFor("Subtype") == "Type0" && + CandidateFromFontDict(font_dict).has_value(); +} + bool CPDF_AnnotFontMap::HasDefaultFont() const { return !fonts_.empty() && fonts_.front().font; } @@ -767,7 +750,11 @@ int32_t CPDF_AnnotFontMap::GetWordFontIndex(uint16_t word, } } + const WideString character(static_cast(word)); if (!allow_registered_fallbacks_ || !fonts_.front().font) { + if (!CPDF_RichTextLayout::IsInvisibleGrapheme(character.AsStringView())) { + NoteMissingCharacter(character); + } return -1; } @@ -778,6 +765,9 @@ int32_t CPDF_AnnotFontMap::GetWordFontIndex(uint16_t word, CFX_FontRegistry::FindFallbackFont(word, weight, italic, /*for_authoring=*/true); if (!font_id.has_value()) { + if (!CPDF_RichTextLayout::IsInvisibleGrapheme(character.AsStringView())) { + NoteMissingCharacter(character); + } return -1; } @@ -802,7 +792,8 @@ int32_t CPDF_AnnotFontMap::CharCodeFromUnicode(int32_t font_index, } if (fxcrt::IndexInBounds(fonts_, font_index)) { FontEntry& entry = fonts_[font_index]; - if (entry.registered_font_id != CFX_FontRegistry::kInvalidFontId) { + if (entry.registered_font_id != CFX_FontRegistry::kInvalidFontId || + entry.source == Source::kDocumentProgram) { entry.glyph_to_unicode.emplace(charcode, word); } } @@ -831,6 +822,11 @@ bool CPDF_AnnotFontMap::SupportsWord(int32_t font_index, uint16_t word) const { if (entry.registered_font_id != CFX_FontRegistry::kInvalidFontId) { return CFX_FontRegistry::SupportsUnicode(entry.registered_font_id, word); } + if (entry.source == Source::kDocumentProgram) { + RetainPtr face = + entry.program ? entry.program->GetFace() : nullptr; + return face && face->GetCharIndex(word) > 0; + } return PDFontSupportsUnicode(entry.font, word); } @@ -879,7 +875,7 @@ int32_t CPDF_AnnotFontMap::AddRegisteredFallbackFont( entry.alias = AllocateAppearanceAlias(ResourceKeyForRegisteredFont(font_id)); entry.registered_font_id = font_id; entry.source = Source::kRegistered; - ProgramMetrics(entry.font->GetFont(), &entry.ascent, &entry.descent); + ProgramLineMetrics(entry.font->GetFont(), &entry.ascent, &entry.descent); fonts_.push_back(std::move(entry)); return pdfium::checked_cast(fonts_.size() - 1); } @@ -902,7 +898,7 @@ void CPDF_AnnotFontMap::PinRichFace(const WideString& family, ByteString CPDF_AnnotFontMap::FaceRequestKey(const WideString& family, int weight, bool italic) { - return ByteString::Format("%s|%d|%d", FamilyKey(family).c_str(), + return ByteString::Format("%s|%d|%d", FontFamilyKey(family).c_str(), std::clamp(weight, 100, 900), italic ? 1 : 0); } @@ -950,10 +946,10 @@ int CPDF_AnnotFontMap::ResolveRichFaceUncached(const WideString& family, // 0. A face the caller pinned to an entry (the /DA font of a regenerated // plain box), if it can draw the text; a box's own font that lacks a // glyph the edit needs is not the edit's face. - const ByteString wanted_key = FamilyKey(family); + const ByteString wanted_key = FontFamilyKey(family); for (const PinnedFace& pinned : pinned_faces_) { if (pinned.weight == weight && pinned.italic == italic && - FamilyKey(pinned.family) == wanted_key) { + FontFamilyKey(pinned.family) == wanted_key) { if (CoversText(pinned.entry, text)) { return pinned.entry; } @@ -1193,7 +1189,7 @@ int CPDF_AnnotFontMap::FindDocumentProgram(const WideString& family, if (!doc_) { return -1; } - const ByteString wanted = FamilyKey(family); + const ByteString wanted = FontFamilyKey(family); if (wanted.IsEmpty()) { return -1; } @@ -1203,7 +1199,7 @@ int CPDF_AnnotFontMap::FindDocumentProgram(const WideString& family, std::optional candidate = CandidateFromFontDict(dict.Get()); if (!candidate.has_value() || - FamilyKey(candidate->identity.family) != wanted || + FontFamilyKey(candidate->identity.family) != wanted || !seen.insert(candidate->stream.Get()).second) { continue; } @@ -1219,8 +1215,8 @@ int CPDF_AnnotFontMap::FindDocumentProgram(const WideString& family, if (a.score != b.score) { return a.score < b.score; } - return !HasSubsetTag(a.base_font_name) && - HasSubsetTag(b.base_font_name); + return !HasFontSubsetTag(a.base_font_name) && + HasFontSubsetTag(b.base_font_name); }); for (DocumentProgramCandidate& candidate : candidates) { int entry = -1; @@ -1249,17 +1245,36 @@ int CPDF_AnnotFontMap::AddDocumentProgram( RetainPtr stream, const ByteString& base_font_name, const CPDF_AnnotFontSubset::FaceIdentity& identity) { + std::optional entry = + MakeDocumentProgramEntry(std::move(stream), base_font_name, identity); + if (!entry.has_value()) { + return -1; + } + entry->alias = AllocateAppearanceAlias( + ByteString::Format("EDocF%u", entry->program_stream->GetObjNum())); + fonts_.push_back(std::move(*entry)); + return static_cast(fonts_.size() - 1); +} + +std::optional +CPDF_AnnotFontMap::MakeDocumentProgramEntry( + RetainPtr stream, + const ByteString& base_font_name, + const CPDF_AnnotFontSubset::FaceIdentity& identity) { + if (!doc_ || !stream || stream->GetObjNum() == 0) { + return std::nullopt; + } auto* page_data = CPDF_DocPageData::FromDocument(doc_); RetainPtr data = page_data->GetFontFileStreamAcc(stream); if (!data || data->GetSpan().empty()) { - return -1; + return std::nullopt; } // Eligibility on the bytes (C note §1.3): an sfnt we can emit, whose // licence allows authoring. if (CFX_FontRegistry::DetectProgramFormat(data->GetSpan()) == CFX_FontRegistry::ProgramFormat::kUnsupported) { page_data->MaybePurgeFontFileStreamAcc(std::move(data)); - return -1; + return std::nullopt; } auto program = std::make_unique(); if (!program->LoadFaceZeroFromSpan(data->GetSpan(), /*force_vertical=*/false, @@ -1272,27 +1287,119 @@ int CPDF_AnnotFontMap::AddDocumentProgram( // asked for explicitly, a Mac Roman-only cmap is refused too. !program->GetFace()->SelectCharMap(fxge::FontEncoding::kUnicode)) { page_data->MaybePurgeFontFileStreamAcc(std::move(data)); - return -1; + return std::nullopt; } const CFX_FontRegistry::EmbeddingPermission permission = CFX_FontRegistry::ClassifyFsType(program->GetFace()->GetFsTypeFlags()); if (permission != CFX_FontRegistry::EmbeddingPermission::kInstallable && permission != CFX_FontRegistry::EmbeddingPermission::kEditable) { page_data->MaybePurgeFontFileStreamAcc(std::move(data)); - return -1; // preview-and-print: renders, never authors (no override) + return std::nullopt; // preview-and-print: renders, never authors } FontEntry entry; entry.source = Source::kDocumentProgram; - entry.alias = AllocateAppearanceAlias( - ByteString::Format("EDocF%u", stream->GetObjNum())); + CPDF_AnnotFontSubset::LayoutFont layout_font = + CPDF_AnnotFontSubset::CreateLayoutFontForProgram( + doc_, program.get(), base_font_name, identity); + if (layout_font.scratch) { + layout_scratch_.push_back(std::move(layout_font.scratch)); + } + entry.font = std::move(layout_font.font); entry.program = std::move(program); entry.program_data = std::move(data); entry.program_stream = std::move(stream); entry.base_font_name = base_font_name; entry.identity = identity; - ProgramMetrics(entry.program.get(), &entry.ascent, &entry.descent); - fonts_.push_back(std::move(entry)); - return static_cast(fonts_.size() - 1); + ProgramLineMetrics(entry.program.get(), &entry.ascent, &entry.descent); + return entry; +} + +bool CPDF_AnnotFontMap::UseDocumentProgramForDefault( + RetainPtr program, + const ByteString& base_font_name, + const CPDF_AnnotFontSubset::FaceIdentity& identity) { + if (fonts_.empty()) { + return false; + } + std::optional entry = + MakeDocumentProgramEntry(std::move(program), base_font_name, identity); + if (!entry.has_value()) { + return false; + } + entry->alias = fonts_.front().alias; + fonts_.front() = std::move(*entry); + return true; +} + +CPDF_AnnotFontMap::EntryFacts CPDF_AnnotFontMap::DescribeEntry( + int entry) const { + EntryFacts facts; + if (!fxcrt::IndexInBounds(fonts_, entry)) { + return facts; + } + const FontEntry& font_entry = fonts_[entry]; + facts.source = font_entry.source; + facts.degraded = font_entry.degraded; + switch (font_entry.source) { + case Source::kRegistered: + facts.registered_font_id = font_entry.registered_font_id; + facts.family = WideString::FromUTF8( + CFX_FontRegistry::GetFamilyName(font_entry.registered_font_id) + .AsStringView()); + facts.weight = + CFX_FontRegistry::GetStyleWeight(font_entry.registered_font_id); + facts.italic = + CFX_FontRegistry::IsStyleItalic(font_entry.registered_font_id); + break; + case Source::kDocumentProgram: + facts.program_objnum = font_entry.program_stream + ? font_entry.program_stream->GetObjNum() + : 0; + facts.base_font_name = font_entry.base_font_name; + facts.family = + WideString::FromUTF8(font_entry.identity.family.AsStringView()); + facts.weight = font_entry.identity.weight; + facts.italic = font_entry.identity.italic; + break; + case Source::kStandard14: { + facts.base_font_name = font_entry.base_font_name; + const CPDF_FontFace face = FaceOfBaseFontName(font_entry.base_font_name); + facts.family = face.family; + facts.weight = face.weight; + facts.italic = face.italic; + break; + } + case Source::kDocumentFontDict: { + RetainPtr font_dict = + font_entry.font ? font_entry.font->GetFontDict() : nullptr; + if (!font_dict) { + break; + } + const CPDF_FontFace face = FaceOfFontDict(font_dict.Get()); + facts.family = face.family; + facts.weight = face.weight; + facts.italic = face.italic; + facts.base_font_name = font_dict->GetNameFor("BaseFont"); + RetainPtr descriptor = + FontDescriptorOf(font_dict.Get()); + for (const char* key : {"FontFile2", "FontFile3", "FontFile"}) { + RetainPtr stream = + descriptor ? descriptor->GetStreamFor(key) : nullptr; + if (stream) { + facts.program_objnum = stream->GetObjNum(); + break; + } + } + break; + } + } + return facts; +} + +void CPDF_AnnotFontMap::NoteMissingCharacter(const WideString& grapheme) { + if (doc_) { + CPDF_DocumentFonts::For(doc_)->NoteMissingCharacter(grapheme); + } } int CPDF_AnnotFontMap::AddStandardFont(const ByteString& base_font_name) { diff --git a/core/fpdfdoc/cpdf_annotfontmap.h b/core/fpdfdoc/cpdf_annotfontmap.h index fb6a023df7..4b5afe49f8 100644 --- a/core/fpdfdoc/cpdf_annotfontmap.h +++ b/core/fpdfdoc/cpdf_annotfontmap.h @@ -74,6 +74,11 @@ class CPDF_AnnotFontMap final : public IPVT_FontMap { const CPDF_Document* doc, const ByteString& alias); + // Whether a /DA font is a composite font over a program the layout can + // write any character of: what the writer installs for a document + // program, which the map then lays out with that program's own cmap. + static bool IsDocumentProgramFont(const CPDF_Dictionary* font_dict); + bool HasDefaultFont() const; // The /DA font is one of the standard 14 (a non-embedded base-14 Type1): // its metrics are PDFium's, not Acrobat's, so the rich layout resolves a @@ -200,6 +205,38 @@ class CPDF_AnnotFontMap final : public IPVT_FontMap { void SetDefaultAppearanceEntry(int entry, const ByteString& alias); int GetDefaultAppearanceEntry() const { return da_entry_; } + // Makes the /DA entry a program already in the document, under the alias + // the /DA string names: what a /DA font a session reserved for a document + // program draws with before its /DR entry exists. False, and the entry + // stays as it was, when the program can't author text (see + // FindDocumentProgram()). + bool UseDocumentProgramForDefault( + RetainPtr program, + const ByteString& base_font_name, + const CPDF_AnnotFontSubset::FaceIdentity& identity); + + // What an entry is, for a resolver read: where its program comes from and + // which one it is. + struct EntryFacts { + Source source = Source::kStandard14; + CFX_FontRegistry::FontId registered_font_id = + CFX_FontRegistry::kInvalidFontId; + // kDocumentProgram's stream, and kDocumentFontDict's when it embeds one. + uint32_t program_objnum = 0; + // kStandard14: the standard name; else the font's /BaseFont. + ByteString base_font_name; + // The face it draws: its family, weight and italic. + WideString family; + int weight = 400; + bool italic = false; + bool degraded = false; + }; + EntryFacts DescribeEntry(int entry) const; + + // A grapheme drawn with no font that has it, for the write's report + // (CPDF_DocumentFonts::NoteMissingCharacter()). + void NoteMissingCharacter(const WideString& grapheme); + // IPVT_FontMap: RetainPtr GetPDFFont(int32_t font_index) override; ByteString GetPDFFontAlias(int32_t font_index) override; @@ -260,6 +297,13 @@ class CPDF_AnnotFontMap final : public IPVT_FontMap { int AddDocumentProgram(RetainPtr stream, const ByteString& base_font_name, const CPDF_AnnotFontSubset::FaceIdentity& identity); + // The entry for a document program, when it can author text: an sfnt that + // loads, has glyphs and a Unicode cmap, and whose licence allows editing. + // Its layout font is what CPVT reads widths and charcodes from. + std::optional MakeDocumentProgramEntry( + RetainPtr stream, + const ByteString& base_font_name, + const CPDF_AnnotFontSubset::FaceIdentity& identity); int AddStandardFont(const ByteString& base_font_name); const CPDF_AnnotFontSubset::GlyphIdentity* GlyphIdentityOf(FontEntry* entry); diff --git a/core/fpdfdoc/cpdf_annotfontsubset.cpp b/core/fpdfdoc/cpdf_annotfontsubset.cpp index 8c64116969..18183b04a4 100644 --- a/core/fpdfdoc/cpdf_annotfontsubset.cpp +++ b/core/fpdfdoc/cpdf_annotfontsubset.cpp @@ -618,18 +618,32 @@ CPDF_AnnotFontSubset::BuildEphemeralFontResource(StagedFontResource staged) { CPDF_AnnotFontSubset::LayoutFont CPDF_AnnotFontSubset::CreateLayoutFont( CPDF_Document* doc, CFX_FontRegistry::FontId font_id) { - LayoutFont result; if (!doc || !CFX_FontRegistry::IsValidFont(font_id)) { - return result; + return LayoutFont(); } - std::unique_ptr font = CFX_FontRegistry::CreateFont(font_id); - if (!font || !font->HasAnyGlyphs()) { + if (!font) { + return LayoutFont(); + } + return CreateLayoutFontForProgram( + doc, font.get(), BaseFontNameForRegisteredFont(font_id, font.get()), + IdentityForRegisteredFont(font_id)); +} + +// static +CPDF_AnnotFontSubset::LayoutFont +CPDF_AnnotFontSubset::CreateLayoutFontForProgram( + CPDF_Document* doc, + CFX_Font* program, + const ByteString& base_font_name, + const FaceIdentity& identity) { + LayoutFont result; + if (!doc || !program || !program->HasAnyGlyphs()) { return result; } auto char_codes_and_indices = - font->GetCharCodesAndIndices(pdfium::kMaximumSupplementaryCodePoint); + program->GetCharCodesAndIndices(pdfium::kMaximumSupplementaryCodePoint); if (char_codes_and_indices.empty()) { return result; } @@ -637,16 +651,16 @@ CPDF_AnnotFontSubset::LayoutFont CPDF_AnnotFontSubset::CreateLayoutFont( // The layout font has the same shape as the resource that will be written, // so the charcodes it hands out (CIDs under Identity-H) are the ones the // appearance bytes carry. For a CID-keyed CFF that is the charset's CID. - const GlyphIdentity identity(font.get()); + const GlyphIdentity glyphs(program); std::multimap to_unicode; std::map widths; for (const auto& item : char_codes_and_indices) { - std::optional cid = identity.CidOf(item.glyph_index); + std::optional cid = glyphs.CidOf(item.glyph_index); if (!cid.has_value()) { continue; } if (!pdfium::Contains(widths, *cid)) { - widths[*cid] = font->GetGlyphWidth(item.glyph_index); + widths[*cid] = program->GetGlyphWidth(item.glyph_index); } to_unicode.emplace(*cid, item.char_code); } @@ -654,12 +668,9 @@ CPDF_AnnotFontSubset::LayoutFont CPDF_AnnotFontSubset::CreateLayoutFont( return result; } - const ByteString base_font_name = - BaseFontNameForRegisteredFont(font_id, font.get()); StagedFontResource parts; - if (!BuildCompositeFontParts(font.get(), base_font_name, font->GetFontSpan(), - IdentityForRegisteredFont(font_id), widths, - to_unicode, &parts)) { + if (!BuildCompositeFontParts(program, base_font_name, program->GetFontSpan(), + identity, widths, to_unicode, &parts)) { return result; } // The streams go into a scratch holder of their own: CPDF_Font resolves diff --git a/core/fpdfdoc/cpdf_annotfontsubset.h b/core/fpdfdoc/cpdf_annotfontsubset.h index 7ebb5a2c9e..b6abe9ebcc 100644 --- a/core/fpdfdoc/cpdf_annotfontsubset.h +++ b/core/fpdfdoc/cpdf_annotfontsubset.h @@ -100,6 +100,14 @@ class CPDF_AnnotFontSubset final { }; static FaceIdentity IdentityForRegisteredFont(CFX_FontRegistry::FontId id); + // A layout font over any program the writer can emit (a document's own, + // when a widget's /DA font is one): the same shape CreateLayoutFont() + // builds for a registered font, named and identified as given. + static LayoutFont CreateLayoutFontForProgram(CPDF_Document* doc, + CFX_Font* program, + const ByteString& base_font_name, + const FaceIdentity& identity); + // How much of the program a resource carries (§2 of the Phase C note). // The caller resolves the document's policy and the resource's owner into // one of these; fsType "no subsetting" forces kFull regardless. diff --git a/core/fpdfdoc/cpdf_appearancegenerator.h b/core/fpdfdoc/cpdf_appearancegenerator.h index 8c4003a6ff..147832e7eb 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator.h +++ b/core/fpdfdoc/cpdf_appearancegenerator.h @@ -129,6 +129,17 @@ class CPDF_AppearanceGenerator { float font_size, const CFX_Color& color); + // EmbedPDF: set /DA to a program already in the document, by its stream's + // object number (CPDF_DocumentFonts). Like a registered font, /DA names a + // session alias; the appearance installs the /DR entry. False for a + // program the layout can't write new text with: not an sfnt, no Unicode + // cmap, or a licence that doesn't allow editing. + static bool UpdateDefaultAppearanceDocumentFont(CPDF_Document* doc, + CPDF_Dictionary* annot_dict, + uint32_t program_objnum, + float font_size, + const CFX_Color& color); + // EmbedPDF (Phase D): a FreeText appearance laid out from a rich text // document, in the two halves of the Phase C note §6. Prepare lays out // and builds the stream and its font resources off to the side: no diff --git a/core/fpdfdoc/cpdf_appearancegenerator_forms.cpp b/core/fpdfdoc/cpdf_appearancegenerator_forms.cpp index f728dbe543..6198038dee 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_forms.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_forms.cpp @@ -752,10 +752,15 @@ bool GenerateFormAPToTarget(APGenerationTarget* target, ResolveDaFontForEphemeralTarget(doc, dr_font_dict.Get(), font_name); } font_dict = da_font.font_dict; + // A /DA font that is a document program (reserved this session, or the + // composite font the writer installed for one) lays out through the same + // map: CPVT_FontMap can only write the characters its ToUnicode names. const bool use_registered_font_map = target->IsPersistent() && (CFX_FontRegistry::HasFallbackFonts() || - da_font.registered_font_id != CFX_FontRegistry::kInvalidFontId); + da_font.registered_font_id != CFX_FontRegistry::kInvalidFontId || + da_font.document_program || + CPDF_AnnotFontMap::IsDocumentProgramFont(font_dict.Get())); if (!font_dict && !use_registered_font_map) { // Ephemeral render of a widget whose /DA names a registered font that has // no /DR entry yet: draw with the fallback rather than nothing. @@ -834,6 +839,7 @@ bool GenerateFormAPToTarget(APGenerationTarget* target, doc, std::move(default_font), font_name, /*allow_registered_fallbacks=*/true, da_font.registered_font_id, /*install_dr_entry=*/true, CPDF_AnnotFontMap::Owner::kWidget); + da_font.ApplyTo(&map); if (!map.HasDefaultFont()) { return false; } diff --git a/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp b/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp index 544d8d7781..ab4400e46e 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp @@ -18,7 +18,6 @@ #include #include "constants/annotation_common.h" -#include "core/fxcrt/stream_write_utils.h" #include "core/fpdfapi/font/cpdf_font.h" #include "core/fpdfapi/page/cpdf_docpagedata.h" #include "core/fpdfapi/parser/cpdf_array.h" @@ -32,12 +31,14 @@ #include "core/fpdfdoc/cpdf_appearancegenerator_internal.h" #include "core/fpdfdoc/cpdf_color_utils.h" #include "core/fpdfdoc/cpdf_defaultappearance.h" +#include "core/fpdfdoc/cpdf_documentfonts.h" #include "core/fpdfdoc/cpdf_interactiveform.h" #include "core/fpdfdoc/cpdf_richtext.h" #include "core/fpdfdoc/cpdf_richtextlayout.h" #include "core/fpdfdoc/cpdf_richtextparser.h" #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/fx_system.h" +#include "core/fxcrt/stream_write_utils.h" #include "core/fxcrt/utf16.h" #include "core/fxge/cfx_fontregistry.h" @@ -353,6 +354,8 @@ struct RichFreeTextInputs { ByteString font_name; // its alias CFX_FontRegistry::FontId registered_font_id = CFX_FontRegistry::kInvalidFontId; + // The /DA alias resolved to a reserved document program, if it did. + DaFontResolution da_font; bool persistent = true; bool author_default_appearance = false; // writer: body face -> /DA }; @@ -373,6 +376,7 @@ PrepareRichFreeTextAPInternal(CPDF_Document* doc, /*install_dr_entry=*/in.persistent, CPDF_AnnotFontMap::Owner::kAnnotation); CPDF_AnnotFontMap& map = *prepared->fonts; + in.da_font.ApplyTo(&map); if (!in.author_default_appearance && in.document->source == CPDF_RichTextDocument::Source::kContents && map.HasDefaultFont() && !map.DefaultFontIsStandard()) { @@ -531,6 +535,7 @@ bool GenerateRichFreeTextAP(APGenerationTarget* target, in.default_font = std::move(default_font); in.font_name = font_name; in.registered_font_id = da_font.registered_font_id; + in.da_font = da_font; in.persistent = target->IsPersistent(); std::unique_ptr prepared = PrepareRichFreeTextAPInternal(doc, annot_dict, blend_name, std::move(in)); @@ -844,7 +849,8 @@ bool GenerateFreeTextAP(APGenerationTarget* target, RetainPtr default_font = da_font.font_dict ? doc_page_data->GetFont(da_font.font_dict) : nullptr; if (!default_font && - da_font.registered_font_id == CFX_FontRegistry::kInvalidFontId) { + da_font.registered_font_id == CFX_FontRegistry::kInvalidFontId && + !da_font.document_program) { return false; } @@ -1014,6 +1020,47 @@ bool CPDF_AppearanceGenerator::UpdateDefaultAppearanceRegisteredFont( return true; } +bool CPDF_AppearanceGenerator::UpdateDefaultAppearanceDocumentFont( + CPDF_Document* doc, + CPDF_Dictionary* annot_dict, + uint32_t program_objnum, + float font_size, + const CFX_Color& color) { + if (!doc || !annot_dict || program_objnum == 0) { + return false; + } + CPDF_DocumentFonts* fonts = CPDF_DocumentFonts::For(doc); + // The program must be one the walk found, and one the layout can author + // with (the same test the font map applies before it writes with one). + bool can_author = false; + for (const CPDF_DocumentFonts::Face& face : fonts->CollectFaces()) { + for (const CPDF_DocumentFonts::Program& program : face.programs) { + if (program.objnum != program_objnum) { + continue; + } + can_author = program.maps_unicode && program.permission.has_value() && + (*program.permission == + CFX_FontRegistry::EmbeddingPermission::kInstallable || + *program.permission == + CFX_FontRegistry::EmbeddingPermission::kEditable); + } + } + ByteString resource_key; + if (!can_author || + !fonts->ReserveProgramAlias(program_objnum, &resource_key)) { + return false; + } + ByteString da_font_part = DefaultAppearanceFontPart(resource_key, font_size); + if (da_font_part.IsEmpty()) { + return false; + } + ByteString da_color_part = GenerateColorAP(color, PaintOperation::kFill); + da_color_part.TrimBack('\n'); + + annot_dict->SetNewFor("DA", da_color_part + " " + da_font_part); + return true; +} + CPDF_AppearanceGenerator::PreparedRichFreeTextAP::PreparedRichFreeTextAP() = default; diff --git a/core/fpdfdoc/cpdf_appearancegenerator_internal.h b/core/fpdfdoc/cpdf_appearancegenerator_internal.h index 35ad59fa2d..d8038e5631 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_internal.h +++ b/core/fpdfdoc/cpdf_appearancegenerator_internal.h @@ -19,7 +19,7 @@ #include #include -#include "core/fxcrt/stream_write_utils.h" +#include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fpdfdoc/cpdf_annot.h" #include "core/fpdfdoc/cpdf_annotfontmap.h" #include "core/fpdfdoc/cpdf_appearancegenerator.h" @@ -30,6 +30,7 @@ #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/span.h" +#include "core/fxcrt/stream_write_utils.h" #include "core/fxcrt/widestring.h" #include "core/fxge/cfx_color.h" #include "core/fxge/cfx_fontregistry.h" @@ -116,6 +117,15 @@ struct DaFontResolution { RetainPtr font_dict; CFX_FontRegistry::FontId registered_font_id = CFX_FontRegistry::kInvalidFontId; + // An alias this session reserved for a program already in the document + // (CPDF_DocumentFonts): the program the /DA entry draws with until the + // appearance installs its /DR entry. + RetainPtr document_program; + ByteString document_program_base_font; + CPDF_AnnotFontSubset::FaceIdentity document_program_identity; + + // Makes |map|'s /DA entry the reserved document program, if there is one. + void ApplyTo(CPDF_AnnotFontMap* map) const; }; struct APGenerationTarget { diff --git a/core/fpdfdoc/cpdf_appearancegenerator_redaction.cpp b/core/fpdfdoc/cpdf_appearancegenerator_redaction.cpp index 69510cc564..f43c143a16 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_redaction.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_redaction.cpp @@ -333,7 +333,8 @@ bool AppendRedactOverlayOps(CPDF_Document* doc, ? CPDF_DocPageData::FromDocument(doc)->GetFont(da_font.font_dict) : nullptr; if (!default_font && - da_font.registered_font_id == CFX_FontRegistry::kInvalidFontId) { + da_font.registered_font_id == CFX_FontRegistry::kInvalidFontId && + !da_font.document_program) { return has_fill; } @@ -341,6 +342,7 @@ bool AppendRedactOverlayOps(CPDF_Document* doc, /*allow_registered_fallbacks=*/true, da_font.registered_font_id, /*install_dr_entry=*/true); + da_font.ApplyTo(&map); if (!map.HasDefaultFont()) { return has_fill; } diff --git a/core/fpdfdoc/cpdf_appearancegenerator_text.cpp b/core/fpdfdoc/cpdf_appearancegenerator_text.cpp index 4e49dc7bd0..a26d26d0cb 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_text.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_text.cpp @@ -14,7 +14,6 @@ #include #include "constants/font_encodings.h" -#include "core/fxcrt/stream_write_utils.h" #include "core/fpdfapi/font/cpdf_font.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_document.h" @@ -28,11 +27,13 @@ #include "core/fpdfdoc/cpdf_annotfontsubset.h" #include "core/fpdfdoc/cpdf_appearancegenerator_internal.h" #include "core/fpdfdoc/cpdf_defaultappearance.h" +#include "core/fpdfdoc/cpdf_documentfonts.h" #include "core/fpdfdoc/cpdf_embed_metadata.h" #include "core/fpdfdoc/cpvt_variabletext.h" #include "core/fpdfdoc/cpvt_word.h" #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/fx_system.h" +#include "core/fxcrt/stream_write_utils.h" #include "core/fxge/cfx_fontregistry.h" using appearance_generator::DaFontResolution; @@ -348,6 +349,27 @@ RetainPtr GetFontFromDrFontDictOrGenerateFallback( return new_font_dict; } +// A /DA alias this session reserved for a document program. +bool ResolveReservedDocumentProgram(const CPDF_Document* doc, + const ByteString& font_name, + DaFontResolution* result) { + CPDF_DocumentFonts* fonts = CPDF_DocumentFonts::Find(doc); + std::optional reserved = + fonts ? fonts->ProgramForAlias(font_name) : std::nullopt; + if (!reserved.has_value()) { + return false; + } + result->document_program = fonts->ProgramStream(reserved->objnum); + if (!result->document_program) { + return false; + } + result->document_program_base_font = reserved->base_font_name; + result->document_program_identity.family = reserved->family.ToUTF8(); + result->document_program_identity.weight = reserved->weight; + result->document_program_identity.italic = reserved->italic; + return true; +} + DaFontResolution ResolveDaFontForPersistentTarget(CPDF_Document* doc, CPDF_Dictionary* dr_font_dict, const ByteString& font_name) { @@ -364,6 +386,9 @@ DaFontResolution ResolveDaFontForPersistentTarget(CPDF_Document* doc, result.registered_font_id = *font_id; return result; } + if (ResolveReservedDocumentProgram(doc, font_name, &result)) { + return result; + } // EmbedPDF: the fallback is a write, so it goes into /DR/Font as the // document opens it for writing - a layer transaction copies the catalog's // chain up first. |dr_font_dict| may be a read-path view of a committed @@ -405,10 +430,21 @@ DaFontResolution ResolveDaFontForEphemeralTarget( result.registered_font_id = *font_id; return result; } + if (ResolveReservedDocumentProgram(doc, font_name, &result)) { + return result; + } result.font_dict = GenerateDirectFallbackFontDict(); return result; } +void DaFontResolution::ApplyTo(CPDF_AnnotFontMap* map) const { + if (document_program) { + map->UseDocumentProgramForDefault(document_program, + document_program_base_font, + document_program_identity); + } +} + RetainPtr GenerateResourceFontDict( CPDF_Document* doc, const ByteString& font_name, diff --git a/core/fpdfdoc/cpdf_documentfonts.cpp b/core/fpdfdoc/cpdf_documentfonts.cpp new file mode 100644 index 0000000000..204721f4ea --- /dev/null +++ b/core/fpdfdoc/cpdf_documentfonts.cpp @@ -0,0 +1,490 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fpdfdoc/cpdf_documentfonts.h" + +#include +#include +#include +#include + +#include "core/fdrm/fx_crypt_sha.h" +#include "core/fpdfapi/page/cpdf_docpagedata.h" +#include "core/fpdfapi/parser/cpdf_array.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fpdfapi/parser/cpdf_stream_acc.h" +#include "core/fpdfdoc/cpdf_fontface.h" +#include "core/fxcrt/utf16.h" +#include "core/fxge/cfx_face.h" +#include "core/fxge/cfx_font.h" +#include "core/fxge/fx_fontencoding.h" + +namespace { + +// Deep enough for any real nesting of pages, forms and patterns; a cycle the +// walked set misses (a direct object repeated) stops here. +constexpr int kMaxDepth = 64; + +// Every font dictionary the document's drawings can use, each once. +class FontWalk { + public: + explicit FontWalk(CPDF_Document* doc) : doc_(doc) {} + + std::vector> Run() { + const CPDF_Dictionary* root = doc_->GetRoot(); + RetainPtr acroform = + root ? root->GetDictFor("AcroForm") : nullptr; + if (acroform) { + Resources(acroform->GetDictFor("DR").Get(), 0); + Fields(acroform->GetArrayFor("Fields").Get(), 0); + } + const int page_count = doc_->GetPageCount(); + for (int i = 0; i < page_count; ++i) { + RetainPtr page = doc_->GetPageDictionary(i); + if (!page) { + continue; + } + Resources(InheritedResources(page.Get()).Get(), 0); + RetainPtr annots = page->GetArrayFor("Annots"); + for (size_t j = 0; annots && j < annots->size(); ++j) { + Appearance(annots->GetDictAt(j).Get(), 0); + } + } + return std::move(fonts_); + } + + private: + // A page's /Resources, or the nearest one up its /Parent chain. + static RetainPtr InheritedResources( + const CPDF_Dictionary* page) { + std::set seen; + RetainPtr node(page); + for (int i = 0; node && i < kMaxDepth && seen.insert(node.Get()).second; + ++i) { + if (RetainPtr resources = + node->GetDictFor("Resources")) { + return resources; + } + node = node->GetDictFor("Parent"); + } + return nullptr; + } + + void Resources(const CPDF_Dictionary* resources, int depth) { + if (!resources || depth > kMaxDepth || !walked_.insert(resources).second) { + return; + } + if (RetainPtr fonts = + resources->GetDictFor("Font")) { + CPDF_DictionaryLocker locker(fonts); + for (const auto& [key, value] : locker) { + Font(value ? ToDictionary(value->GetDirect()) : nullptr, depth); + } + } + if (RetainPtr xobjects = + resources->GetDictFor("XObject")) { + CPDF_DictionaryLocker locker(xobjects); + for (const auto& [key, value] : locker) { + RetainPtr stream = + value ? ToStream(value->GetDirect()) : nullptr; + if (stream && stream->GetDict()->GetNameFor("Subtype") == "Form") { + FormStream(stream.Get(), depth); + } + } + } + // Tiling patterns are streams with their own resources; shading + // patterns are dictionaries and draw no text. + if (RetainPtr patterns = + resources->GetDictFor("Pattern")) { + CPDF_DictionaryLocker locker(patterns); + for (const auto& [key, value] : locker) { + RetainPtr stream = + value ? ToStream(value->GetDirect()) : nullptr; + FormStream(stream.Get(), depth); + } + } + if (RetainPtr states = + resources->GetDictFor("ExtGState")) { + CPDF_DictionaryLocker locker(states); + for (const auto& [key, value] : locker) { + RetainPtr state = + value ? ToDictionary(value->GetDirect()) : nullptr; + RetainPtr mask = + state ? state->GetDictFor("SMask") : nullptr; + FormStream(mask ? mask->GetStreamFor("G").Get() : nullptr, depth); + } + } + } + + void FormStream(const CPDF_Stream* stream, int depth) { + if (!stream || depth > kMaxDepth || !walked_.insert(stream).second) { + return; + } + Resources(stream->GetDict()->GetDictFor("Resources").Get(), depth + 1); + } + + void Font(RetainPtr font, int depth) { + if (!font || !font_set_.insert(font.Get()).second) { + return; + } + if (font->GetNameFor("Subtype") == "Type3") { + // A Type 3 glyph is a drawing, and may draw text in other fonts. + Resources(font->GetDictFor("Resources").Get(), depth + 1); + } + fonts_.push_back(std::move(font)); + } + + // Every state of the normal, rollover and down appearances. + void Appearance(const CPDF_Dictionary* annot, int depth) { + RetainPtr ap = + annot ? annot->GetDictFor("AP") : nullptr; + if (!ap) { + return; + } + for (const char* key : {"N", "R", "D"}) { + RetainPtr entry = ap->GetDirectObjectFor(key); + if (const CPDF_Stream* stream = entry ? entry->AsStream() : nullptr) { + FormStream(stream, depth); + continue; + } + const CPDF_Dictionary* states = entry ? entry->AsDictionary() : nullptr; + if (!states) { + continue; + } + CPDF_DictionaryLocker locker(states); + for (const auto& [state, value] : locker) { + RetainPtr stream = + value ? ToStream(value->GetDirect()) : nullptr; + FormStream(stream.Get(), depth); + } + } + } + + // Fields' own /DR, down their /Kids. + void Fields(const CPDF_Array* fields, int depth) { + for (size_t i = 0; fields && depth <= kMaxDepth && i < fields->size(); + ++i) { + RetainPtr field = fields->GetDictAt(i); + if (!field || !walked_.insert(field.Get()).second) { + continue; + } + Resources(field->GetDictFor("DR").Get(), depth + 1); + Fields(field->GetArrayFor("Kids").Get(), depth + 1); + } + } + + UnownedPtr const doc_; + std::set walked_; + std::set font_set_; + std::vector> fonts_; +}; + +// The program stream a font dictionary carries, and what kind it says it is. +// An /OpenType /FontFile3 says nothing about its outlines: its bytes do. +std::pair, CPDF_DocumentFonts::Format> ProgramOf( + const CPDF_Dictionary* font) { + using Format = CPDF_DocumentFonts::Format; + if (font->GetNameFor("Subtype") == "Type3") { + return {nullptr, Format::kType3}; + } + RetainPtr descriptor = FontDescriptorOf(font); + if (!descriptor) { + return {nullptr, Format::kNone}; + } + if (RetainPtr stream = + descriptor->GetStreamFor("FontFile2")) { + return {std::move(stream), Format::kTrueType}; + } + if (RetainPtr stream = + descriptor->GetStreamFor("FontFile")) { + return {std::move(stream), Format::kType1}; + } + if (RetainPtr stream = + descriptor->GetStreamFor("FontFile3")) { + const bool sfnt = stream->GetDict()->GetNameFor("Subtype") == "OpenType"; + return {std::move(stream), sfnt ? Format::kTrueType : Format::kCFF}; + } + return {nullptr, Format::kNone}; +} + +// The /BaseFont that names the program: a Type0 font's descendant's. +ByteString ProgramBaseFont(const CPDF_Dictionary* font) { + if (font->GetNameFor("Subtype") == "Type0") { + RetainPtr descendants = + font->GetArrayFor("DescendantFonts"); + RetainPtr descendant = + descendants ? descendants->GetDictAt(0) : nullptr; + if (descendant && !descendant->GetNameFor("BaseFont").IsEmpty()) { + return descendant->GetNameFor("BaseFont"); + } + } + return font->GetNameFor("BaseFont"); +} + +bool IsSfnt(CPDF_DocumentFonts::Format format) { + return format == CPDF_DocumentFonts::Format::kTrueType || + format == CPDF_DocumentFonts::Format::kOpenTypeCFF; +} + +} // namespace + +CPDF_DocumentFonts::CPDF_DocumentFonts(CPDF_Document* doc) : doc_(doc) {} + +CPDF_DocumentFonts::~CPDF_DocumentFonts() = default; + +// static +CPDF_DocumentFonts* CPDF_DocumentFonts::For(CPDF_Document* doc) { + if (!doc->font_index()) { + doc->SetFontIndex(std::make_unique(doc)); + } + return static_cast(doc->font_index()); +} + +// static +CPDF_DocumentFonts* CPDF_DocumentFonts::Find(const CPDF_Document* doc) { + return doc ? static_cast(doc->font_index()) : nullptr; +} + +std::vector CPDF_DocumentFonts::CollectFaces() { + std::vector faces; + std::map face_index; // family key|weight|italic + for (const RetainPtr& font : FontWalk(doc_).Run()) { + const CPDF_FontFace face = FaceOfFontDict(font.Get()); + const ByteString key = + ByteString::Format("%s|%d|%d", FontFamilyKey(face.family).c_str(), + face.weight, face.italic ? 1 : 0); + auto [found, added] = face_index.emplace(key, faces.size()); + if (added) { + faces.push_back({face.family, face.weight, face.italic, {}}); + } + Face& entry = faces[found->second]; + + auto [stream, format] = ProgramOf(font.Get()); + const ByteString base_font = ProgramBaseFont(font.Get()); + Program program; + if (stream) { + program = Inspect(stream, format); + program.embedded = + HasFontSubsetTag(base_font) ? Embedded::kSubset : Embedded::kWhole; + } else { + program.format = format; + } + program.postscript_name = BaseFontNameWithoutSubsetTag(base_font); + const bool known = + std::ranges::any_of(entry.programs, [&](const Program& other) { + return program.objnum != 0 + ? other.objnum == program.objnum + : other.objnum == 0 && other.format == program.format && + other.sha256 == program.sha256; + }); + if (!known) { + entry.programs.push_back(std::move(program)); + } + } + return faces; +} + +CPDF_DocumentFonts::Program CPDF_DocumentFonts::Inspect( + RetainPtr stream, + Format format) { + const uint32_t objnum = stream->GetObjNum(); + if (objnum != 0) { + auto it = inspected_.find(objnum); + if (it != inspected_.end() && it->second.stream == stream && + it->second.raw_size == stream->GetRawSize()) { + return it->second.facts; + } + } + Program facts; + facts.objnum = objnum; + facts.format = format; + CPDF_DocPageData* page_data = CPDF_DocPageData::FromDocument(doc_); + RetainPtr data = page_data->GetFontFileStreamAcc(stream); + if (data && !data->GetSpan().empty()) { + facts.sha256 = CryptSha256Generate(data->GetSpan()); + if (IsSfnt(format) && + CFX_FontRegistry::DetectProgramFormat(data->GetSpan()) == + CFX_FontRegistry::ProgramFormat::kOpenTypeCFF) { + facts.format = Format::kOpenTypeCFF; + } + auto program = std::make_unique(); + if (program->LoadFaceZeroFromSpan(data->GetSpan(), + /*force_vertical=*/false, + /*object_tag=*/0) && + program->HasAnyGlyphs()) { + ProgramLineMetrics(program.get(), &facts.ascent, &facts.descent); + if (IsSfnt(facts.format)) { + RetainPtr face = program->GetFace(); + facts.permission = + CFX_FontRegistry::ClassifyFsType(face->GetFsTypeFlags()); + facts.maps_unicode = face->SelectCharMap(fxge::FontEncoding::kUnicode); + } + } + } + if (data) { + page_data->MaybePurgeFontFileStreamAcc(std::move(data)); + } + // An inline program is inspected again each time: nothing addresses it. + if (objnum != 0) { + inspected_[objnum] = {stream, stream->GetRawSize(), facts, std::nullopt}; + } + return facts; +} + +CPDF_DocumentFonts::Inspected* CPDF_DocumentFonts::FindInspected( + uint32_t objnum) { + if (objnum == 0) { + return nullptr; + } + auto found = [&]() -> Inspected* { + auto it = inspected_.find(objnum); + if (it == inspected_.end()) { + return nullptr; + } + // The version inspected must still be the document's. + RetainPtr current = doc_->GetIndirectObject(objnum); + if (current != it->second.stream || + it->second.stream->GetRawSize() != it->second.raw_size) { + return nullptr; + } + return &it->second; + }; + if (Inspected* inspected = found()) { + return inspected; + } + // Not inspected yet, or changed since: walk again, which inspects what + // the document uses now. + CollectFaces(); + return found(); +} + +DataVector CPDF_DocumentFonts::ProgramData(uint32_t objnum) { + Inspected* inspected = FindInspected(objnum); + if (!inspected || inspected->facts.sha256.empty()) { + return {}; + } + CPDF_DocPageData* page_data = CPDF_DocPageData::FromDocument(doc_); + RetainPtr data = + page_data->GetFontFileStreamAcc(inspected->stream); + if (!data) { + return {}; + } + DataVector bytes(data->GetSpan().begin(), data->GetSpan().end()); + page_data->MaybePurgeFontFileStreamAcc(std::move(data)); + return bytes; +} + +std::vector> CPDF_DocumentFonts::ProgramCoverage( + uint32_t objnum) { + Inspected* inspected = FindInspected(objnum); + if (!inspected || !inspected->facts.maps_unicode) { + return {}; + } + if (inspected->coverage.has_value()) { + return *inspected->coverage; + } + std::vector scalars; + { + DataVector bytes = ProgramData(objnum); + auto program = std::make_unique(); + if (!bytes.empty() && + program->LoadFaceZeroFromSpan(bytes, /*force_vertical=*/false, + /*object_tag=*/0) && + program->GetFace()->SelectCharMap(fxge::FontEncoding::kUnicode)) { + for (const CharCodeAndIndex& item : program->GetCharCodesAndIndices( + pdfium::kMaximumSupplementaryCodePoint)) { + if (item.glyph_index != 0) { + scalars.push_back(item.char_code); + } + } + } + } + std::ranges::sort(scalars); + std::vector> ranges; + for (uint32_t scalar : scalars) { + if (!ranges.empty() && + ranges.back().first + ranges.back().second >= scalar) { + ranges.back().second = + std::max(ranges.back().second, scalar - ranges.back().first + 1); + continue; + } + ranges.emplace_back(scalar, 1); + } + inspected->coverage = ranges; + return ranges; +} + +RetainPtr CPDF_DocumentFonts::ProgramStream(uint32_t objnum) { + Inspected* inspected = FindInspected(objnum); + if (!inspected) { + return nullptr; + } + return pdfium::WrapRetain(const_cast(inspected->stream.Get())); +} + +bool CPDF_DocumentFonts::ReserveProgramAlias(uint32_t objnum, + ByteString* alias) { + if (objnum == 0) { + return false; + } + for (const auto& [reserved, program] : program_aliases_) { + if (program.objnum == objnum) { + *alias = reserved; + return true; + } + } + std::optional found; + for (const Face& face : CollectFaces()) { + for (const Program& program : face.programs) { + if (program.objnum == objnum) { + found = ReservedProgram{objnum, program.postscript_name, face.family, + face.weight, face.italic}; + break; + } + } + if (found.has_value()) { + break; + } + } + if (!found.has_value()) { + return false; + } + const CPDF_Dictionary* root = doc_->GetRoot(); + RetainPtr acroform = + root ? root->GetDictFor("AcroForm") : nullptr; + RetainPtr dr = + acroform ? acroform->GetDictFor("DR") : nullptr; + RetainPtr dr_fonts = + dr ? dr->GetDictFor("Font") : nullptr; + const ByteString base = ByteString::Format("EDocF%u", objnum); + ByteString candidate = base; + for (int suffix = 1; + (dr_fonts && dr_fonts->KeyExist(candidate.AsStringView())) || + program_aliases_.contains(candidate); + ++suffix) { + candidate = ByteString::Format("%s_%d", base.c_str(), suffix); + } + program_aliases_[candidate] = std::move(*found); + *alias = candidate; + return true; +} + +std::optional +CPDF_DocumentFonts::ProgramForAlias(const ByteString& alias) const { + auto it = program_aliases_.find(alias); + if (it == program_aliases_.end()) { + return std::nullopt; + } + return it->second; +} + +void CPDF_DocumentFonts::NoteMissingCharacter(const WideString& grapheme) { + if (grapheme.IsEmpty() || + missing_characters_.size() >= kMaxMissingCharacters || + std::ranges::find(missing_characters_, grapheme) != + missing_characters_.end()) { + return; + } + missing_characters_.push_back(grapheme); +} diff --git a/core/fpdfdoc/cpdf_documentfonts.h b/core/fpdfdoc/cpdf_documentfonts.h new file mode 100644 index 0000000000..ecdcd87849 --- /dev/null +++ b/core/fpdfdoc/cpdf_documentfonts.h @@ -0,0 +1,141 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 +// +// EmbedPDF: the fonts a document uses, found in its dictionaries without +// loading a page, and the facts about each embedded program. Facts only: what +// may be written with a font, or sent where, is the engine's decision. + +#ifndef CORE_FPDFDOC_CPDF_DOCUMENTFONTS_H_ +#define CORE_FPDFDOC_CPDF_DOCUMENTFONTS_H_ + +#include +#include + +#include +#include +#include +#include + +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/data_vector.h" +#include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/unowned_ptr.h" +#include "core/fxcrt/widestring.h" +#include "core/fxge/cfx_fontregistry.h" + +class CPDF_Stream; + +class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { + public: + // What a font program is, from the font dictionary that holds it. + enum class Format : uint8_t { + kNone, // nothing embedded: the PDF only names the font + kTrueType, // /FontFile2, or /FontFile3 /OpenType with glyf outlines + kOpenTypeCFF, // /FontFile3 /OpenType with CFF outlines + kCFF, // /FontFile3 /Type1C or /CIDFontType0C: bare CFF + kType1, // /FontFile + kType3, // glyphs drawn by content streams: no program + }; + enum class Embedded : uint8_t { kNone, kWhole, kSubset }; + + struct Program { + // The program stream's object number; 0 when there is no program, or + // when the stream is inline (it can't be addressed). + uint32_t objnum = 0; + Format format = Format::kNone; + Embedded embedded = Embedded::kNone; + // /BaseFont without its subset tag (a Type0 font's descendant's). + ByteString postscript_name; + // What the OS/2 fsType grants; nullopt when the program has no OS/2 + // table to say (Type 1, bare CFF) or there is no program. + std::optional permission; + // The program loads, has glyphs and maps characters through a Unicode + // cmap: the layout can write any character it maps. + bool maps_unicode = false; + // SHA-256 of the decoded program; empty without one. + DataVector sha256; + // Line metrics as the layout reads them (em, descent positive); 0 when + // the program doesn't load. + float ascent = 0; + float descent = 0; + }; + + struct Face { + WideString family; + int weight = 400; + bool italic = false; + // One per program stream, in the order the walk found them; fonts + // without a program (not embedded, Type 3) add one entry per format. + std::vector programs; + }; + + explicit CPDF_DocumentFonts(CPDF_Document* doc); + ~CPDF_DocumentFonts() override; + + // The document's index, made on first use. + static CPDF_DocumentFonts* For(CPDF_Document* doc); + // The document's index if it has one yet; null otherwise. + static CPDF_DocumentFonts* Find(const CPDF_Document* doc); + + // Every face the document's fonts are, grouped by family, weight and + // italic. Found in /AcroForm /DR, fields' own /DR, every page's resources + // (inherited ones too), form XObjects, tiling patterns, soft masks, Type 3 + // glyph resources and every state of every annotation appearance. Each + // dictionary is walked once; cycles stop. Programs are inspected once per + // stream version. + std::vector CollectFaces(); + + // A program found by the walk, by its stream's object number: its decoded + // bytes, the program as the layout reads it. Empty for any other number. + DataVector ProgramData(uint32_t objnum); + // The scalars it maps through its Unicode cmap, as [start, start + count) + // ranges in order; empty when it maps none or |objnum| isn't one. + std::vector> ProgramCoverage(uint32_t objnum); + // The program stream itself, for the writer; null when |objnum| isn't one. + RetainPtr ProgramStream(uint32_t objnum); + + // A /DA font naming a document program: like a registered font's, the + // alias is this session's until an appearance installs the /DR entry. + // Reserve picks "EDocF", suffixed when /DR holds the key already; + // false when |objnum| isn't a program the walk found. + struct ReservedProgram { + uint32_t objnum = 0; + ByteString base_font_name; // without its subset tag + WideString family; + int weight = 400; + bool italic = false; + }; + bool ReserveProgramAlias(uint32_t objnum, ByteString* alias); + std::optional ProgramForAlias(const ByteString& alias) const; + + // Characters an appearance drew with no font that has them (each a + // grapheme, drawn as its face's missing glyph), first seen first, at most + // kMaxMissingCharacters. The engine clears them before a write and reads + // them after it. + static constexpr size_t kMaxMissingCharacters = 256; + void NoteMissingCharacter(const WideString& grapheme); + const std::vector& missing_characters() const { + return missing_characters_; + } + void ClearMissingCharacters() { missing_characters_.clear(); } + + private: + struct Inspected { + RetainPtr stream; // the version inspected + size_t raw_size = 0; + Program facts; + std::optional>> coverage; + }; + + // The facts about |stream|, inspected once per version. + Program Inspect(RetainPtr stream, Format format); + Inspected* FindInspected(uint32_t objnum); + + UnownedPtr const doc_; + std::map inspected_; // by program stream objnum + std::map program_aliases_; + std::vector missing_characters_; +}; + +#endif // CORE_FPDFDOC_CPDF_DOCUMENTFONTS_H_ diff --git a/core/fpdfdoc/cpdf_fontface.cpp b/core/fpdfdoc/cpdf_fontface.cpp index 4e312fca3a..6b9a3f3943 100644 --- a/core/fpdfdoc/cpdf_fontface.cpp +++ b/core/fpdfdoc/cpdf_fontface.cpp @@ -4,11 +4,15 @@ #include "core/fpdfdoc/cpdf_fontface.h" #include +#include #include #include "core/fpdfapi/parser/cpdf_array.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfdoc/cpdf_annotfontmap.h" +#include "core/fpdfdoc/cpdf_documentfonts.h" +#include "core/fxge/cfx_face.h" +#include "core/fxge/cfx_font.h" #include "core/fxge/cfx_fontregistry.h" #include "core/fxge/fx_font.h" @@ -66,6 +70,59 @@ std::optional StandardFontFace(const ByteString& name) { return std::nullopt; } +ByteString FontFamilyKey(const WideString& family) { + ByteString key; + for (wchar_t ch : family) { + if (ch == L' ' || ch == L'-' || ch == L'_' || ch == L'\'' || ch == L'"') { + continue; + } + if (ch < 0x80) { + key += static_cast(std::tolower(static_cast(ch))); + } else { + key += WideString(ch).ToUTF8(); + } + } + return key; +} + +ByteString FontFamilyKey(const ByteString& family_utf8) { + return FontFamilyKey(WideString::FromUTF8(family_utf8.AsStringView())); +} + +void ProgramLineMetrics(const CFX_Font* program, + float* ascent, + float* descent) { + RetainPtr face = program ? program->GetFace() : nullptr; + const uint16_t upem = face ? face->GetUnitsPerEm() : 0; + if (!face || upem == 0) { + *ascent = 0.830f; + *descent = 0.170f; + return; + } + *ascent = static_cast(face->GetAscender()) / upem; + *descent = static_cast(-face->GetDescender()) / upem; + if (*ascent <= 0 || *descent < 0) { + *ascent = 0.830f; + *descent = 0.170f; + } +} + +bool HasFontSubsetTag(const ByteString& base_font) { + if (base_font.GetLength() <= 7 || base_font[6] != '+') { + return false; + } + for (size_t i = 0; i < 6; ++i) { + if (base_font[i] < 'A' || base_font[i] > 'Z') { + return false; + } + } + return true; +} + +ByteString BaseFontNameWithoutSubsetTag(const ByteString& base_font) { + return HasFontSubsetTag(base_font) ? base_font.Substr(7) : base_font; +} + CPDF_FontFace FaceOfBaseFontName(ByteString base_font) { if (base_font.GetLength() > 7 && base_font[6] == '+') { base_font = base_font.Substr(7); @@ -164,6 +221,14 @@ CPDF_FontFace FaceOfDefaultAppearanceFont( CFX_FontRegistry::GetStyleWeight(*registered), CFX_FontRegistry::IsStyleItalic(*registered)}; } + // The same for a document program's alias (CPDF_DocumentFonts). + CPDF_DocumentFonts* document_fonts = CPDF_DocumentFonts::Find(doc); + std::optional program = + document_fonts ? document_fonts->ProgramForAlias(font_name) + : std::nullopt; + if (program.has_value()) { + return CPDF_FontFace{program->family, program->weight, program->italic}; + } } if (std::optional standard = StandardFontFace(font_name)) { return *standard; diff --git a/core/fpdfdoc/cpdf_fontface.h b/core/fpdfdoc/cpdf_fontface.h index 7734a209b8..5a93a78346 100644 --- a/core/fpdfdoc/cpdf_fontface.h +++ b/core/fpdfdoc/cpdf_fontface.h @@ -11,6 +11,7 @@ #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/widestring.h" +class CFX_Font; class CPDF_Dictionary; class CPDF_Document; @@ -22,6 +23,22 @@ struct CPDF_FontFace { bool italic = false; }; +// The key two family names compare by: case, spaces, hyphens, underscores +// and quotes don't count, so "Noto Sans" == "NotoSans" == "noto-sans". +ByteString FontFamilyKey(const WideString& family); +ByteString FontFamilyKey(const ByteString& family_utf8); + +// A program's line metrics as the layout reads them: its hhea ascender and +// descender over its units per em (Acrobat's choice for TrueType and CFF), +// descent positive. 0.83 and 0.17 when the program has none. +void ProgramLineMetrics(const CFX_Font* program, float* ascent, float* descent); + +// A /BaseFont name without its subset tag: "ABCDEF+MinionPro-Bold" is +// "MinionPro-Bold". +ByteString BaseFontNameWithoutSubsetTag(const ByteString& base_font); +// "ABCDEF+NotoSans": six capital letters and a plus, a producer's subset. +bool HasFontSubsetTag(const ByteString& base_font); + // The face a standard 14 name or one of Acrobat's aliases (Helv, TiBo, ...) // stands for: its standard family ("Arial" is Helvetica). std::optional StandardFontFace(const ByteString& name); diff --git a/core/fpdfdoc/cpdf_fontresolver.cpp b/core/fpdfdoc/cpdf_fontresolver.cpp new file mode 100644 index 0000000000..2e7e6ba5ff --- /dev/null +++ b/core/fpdfdoc/cpdf_fontresolver.cpp @@ -0,0 +1,116 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fpdfdoc/cpdf_fontresolver.h" + +#include +#include +#include +#include + +#include "core/fpdfapi/font/cpdf_font.h" +#include "core/fpdfapi/page/cpdf_docpagedata.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfdoc/cpdf_appearancegenerator_internal.h" +#include "core/fpdfdoc/cpdf_fontface.h" +#include "core/fpdfdoc/cpdf_richtextlayout.h" + +// static +CPDF_FontResolver::Result CPDF_FontResolver::Resolve( + CPDF_Document* doc, + const CPDF_Dictionary* annot_dict, + const std::vector& runs) { + Result result; + if (!doc) { + return result; + } + const CPDF_Dictionary* root = doc->GetRoot(); + RetainPtr acroform = + root ? root->GetDictFor("AcroForm") : nullptr; + RetainPtr dr = + acroform ? acroform->GetDictFor("DR") : nullptr; + RetainPtr dr_fonts = + dr ? dr->GetDictFor("Font") : nullptr; + + // The box's /DA font, as its appearance would find it (read only). + RetainPtr default_font; + ByteString font_name; + appearance_generator::DaFontResolution da_font; + if (annot_dict) { + std::optional info = + appearance_generator::GetDefaultAppearanceInfo(annot_dict, + acroform.Get()); + if (info.has_value()) { + font_name = info->font_name; + da_font = appearance_generator::ResolveDaFontForEphemeralTarget( + doc, dr_fonts.Get(), font_name); + if (da_font.font_dict) { + default_font = + CPDF_DocPageData::FromDocument(doc)->GetFont(da_font.font_dict); + } + } + } + const bool widget = + annot_dict && annot_dict->GetNameFor("Subtype") == "Widget"; + CPDF_AnnotFontMap map(doc, std::move(default_font), font_name, + /*allow_registered_fallbacks=*/true, + da_font.registered_font_id, + /*install_dr_entry=*/false, + widget ? CPDF_AnnotFontMap::Owner::kWidget + : CPDF_AnnotFontMap::Owner::kAnnotation); + da_font.ApplyTo(&map); + // The box keeps its own font while that font covers the text: requests + // for its face answer with it first. + if (map.HasDefaultFont() && !map.DefaultFontIsStandard()) { + const CPDF_FontFace face = + FaceOfDefaultAppearanceFont(doc, font_name, {dr.Get()}); + map.PinRichFace(face.family, face.weight, face.italic, 0); + } + + // Each face resolves once over every character asked of it, as the + // layout primes it, so two runs of one face get one program. + std::map text_of; + for (const Run& run : runs) { + text_of[CPDF_AnnotFontMap::FaceRequestKey(run.family, run.weight, + run.italic)] += run.text; + } + for (const Run& run : runs) { + map.ResolveRichFace(run.family, run.weight, run.italic, + text_of[CPDF_AnnotFontMap::FaceRequestKey( + run.family, run.weight, run.italic)] + .AsStringView()); + } + + for (const Run& run : runs) { + const int entry = map.ResolveRichFace(run.family, run.weight, run.italic); + ResolvedRun resolved; + resolved.face = map.DescribeEntry(entry); + resolved.fallbacks = CFX_FontRegistry::GetFallbackOrder( + run.weight, run.italic, /*for_authoring=*/true); + const std::vector starts = + CPDF_RichTextLayout::GraphemeStarts(run.text); + const size_t length = run.text.GetLength(); + for (size_t begin = 0; begin < length;) { + size_t end = begin + 1; + while (end < length && !starts[end]) { + ++end; + } + const WideStringView grapheme = + run.text.AsStringView().Substr(begin, end - begin); + const CPDF_RichTextLayout::GraphemeMatch match = + CPDF_RichTextLayout::MatchGrapheme(&map, entry, grapheme, run.weight, + run.italic); + if (!match.covered && + !CPDF_RichTextLayout::IsInvisibleGrapheme(grapheme) && + std::ranges::none_of( + result.missing_characters, + [&](const WideString& missing) { return missing == grapheme; })) { + result.missing_characters.emplace_back(grapheme); + } + begin = end; + } + result.runs.push_back(std::move(resolved)); + } + return result; +} diff --git a/core/fpdfdoc/cpdf_fontresolver.h b/core/fpdfdoc/cpdf_fontresolver.h new file mode 100644 index 0000000000..90373ec2b9 --- /dev/null +++ b/core/fpdfdoc/cpdf_fontresolver.h @@ -0,0 +1,53 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 +// +// EmbedPDF: the fonts a write would draw text with, found the way the write +// finds them, without writing: the same /DA resolution, the same font map, +// each face resolved once over all of its text, and the same per-grapheme +// fallback. What an editor shows while someone types. + +#ifndef CORE_FPDFDOC_CPDF_FONTRESOLVER_H_ +#define CORE_FPDFDOC_CPDF_FONTRESOLVER_H_ + +#include + +#include "core/fpdfdoc/cpdf_annotfontmap.h" +#include "core/fxcrt/widestring.h" +#include "core/fxge/cfx_fontregistry.h" + +class CPDF_Dictionary; +class CPDF_Document; + +class CPDF_FontResolver { + public: + // A run of text and the face it asks for. + struct Run { + WideString text; + WideString family; // a family name, or a registered font's family + int weight = 400; + bool italic = false; + }; + + struct ResolvedRun { + // The entry the run's face resolves to: where its program comes from, + // which one, the face it draws, and whether it substitutes the request. + CPDF_AnnotFontMap::EntryFacts face; + // The registered fallback fonts in the order the layout tries them for + // the run's style (authorized for editing only). + std::vector fallbacks; + }; + + struct Result { + std::vector runs; + // Graphemes no font covers, first seen first: drawn as missing glyphs. + std::vector missing_characters; + }; + + // |annot_dict| is the box being edited (its /DA font is the box's own, + // kept while it covers the text), or null for a new box. + static Result Resolve(CPDF_Document* doc, + const CPDF_Dictionary* annot_dict, + const std::vector& runs); +}; + +#endif // CORE_FPDFDOC_CPDF_FONTRESOLVER_H_ diff --git a/core/fpdfdoc/cpdf_richtextlayout.cpp b/core/fpdfdoc/cpdf_richtextlayout.cpp index d739e6bc82..ff135809cc 100644 --- a/core/fpdfdoc/cpdf_richtextlayout.cpp +++ b/core/fpdfdoc/cpdf_richtextlayout.cpp @@ -227,6 +227,55 @@ std::vector CPDF_RichTextLayout::GraphemeStarts(const WideString& text) { return starts; } +// static +CPDF_RichTextLayout::GraphemeMatch CPDF_RichTextLayout::MatchGrapheme( + CPDF_AnnotFontMap* fonts, + int face_entry, + WideStringView grapheme, + int weight, + bool italic) { + if (face_entry < 0) { + return {}; + } + if (fonts->CoversText(face_entry, grapheme)) { + return {face_entry, true}; + } + // The grapheme's base scalar picks the fallback font. + uint32_t base = 0; + for (char32_t cp : pdfium::CodePointView(grapheme)) { + if (base == 0 || !IsMark(static_cast(cp))) { + base = static_cast(cp); + if (!IsMark(base)) { + break; + } + } + } + const int fallback = fonts->FindRichFallback(base, weight, italic); + if (fallback >= 0 && fonts->CoversText(fallback, grapheme)) { + return {fallback, true}; + } + return {face_entry, false}; +} + +// static +bool CPDF_RichTextLayout::IsInvisibleGrapheme(WideStringView grapheme) { + for (char32_t cp : pdfium::CodePointView(grapheme)) { + const bool invisible = + cp < 0x20 || (cp >= 0x7F && cp <= 0x9F) || cp == 0x00AD || + cp == 0x034F || cp == 0x061C || (cp >= 0x115F && cp <= 0x1160) || + (cp >= 0x17B4 && cp <= 0x17B5) || (cp >= 0x180B && cp <= 0x180F) || + (cp >= 0x200B && cp <= 0x200F) || (cp >= 0x202A && cp <= 0x202E) || + (cp >= 0x2060 && cp <= 0x206F) || cp == 0x3164 || + (cp >= 0xFE00 && cp <= 0xFE0F) || cp == 0xFEFF || cp == 0xFFA0 || + (cp >= 0xFFF0 && cp <= 0xFFF8) || (cp >= 0x1BCA0 && cp <= 0x1BCA3) || + (cp >= 0x1D173 && cp <= 0x1D17A) || (cp >= 0xE0000 && cp <= 0xE0FFF); + if (!invisible) { + return false; + } + } + return true; +} + // static std::vector CPDF_RichTextLayout::BreakOpportunities( const WideString& text) { @@ -318,23 +367,6 @@ struct CPDF_RichTextLayout::Impl { return fonts->RichGlyphFor(entry, static_cast(ch)); } - // Does |entry| have a glyph for every character of the grapheme at - // [begin, end)? Joiners and variation selectors may be missing. - bool Covers(int entry, const WideString& text, size_t begin, size_t end) { - // Scalars, not code units: a supplementary character is one grapheme - // of two units on a 16-bit wchar_t platform. - for (char32_t cp : pdfium::CodePointView( - text.AsStringView().Substr(begin, end - begin))) { - if (cp == 0x200D || IsVariationSelector(cp)) { - continue; - } - if (fonts->RichGlyphFor(entry, static_cast(cp)) == 0) { - return false; - } - } - return true; - } - // Shapes one item into glyphs with absolute clusters. void Shape(Item* item, const WideString& text, @@ -555,27 +587,17 @@ struct CPDF_RichTextLayout::Impl { } const int style = segment->style_of[begin]; const CPDF_RichTextStyle& resolved = styles[style].style; - int entry = fonts->ResolveRichFace(resolved.family, resolved.weight, - resolved.italic); - if (entry >= 0 && !Covers(entry, text, begin, end)) { - // The grapheme's base scalar picks the fallback font. - uint32_t base = 0; - for (char32_t cp : pdfium::CodePointView( - text.AsStringView().Substr(begin, end - begin))) { - if (base == 0 || !IsMark(static_cast(cp))) { - base = static_cast(cp); - if (!IsMark(base)) { - break; - } - } - } - const int fallback = - fonts->FindRichFallback(base, resolved.weight, resolved.italic); - if (fallback >= 0 && Covers(fallback, text, begin, end)) { - entry = fallback; - } + const WideStringView grapheme = + text.AsStringView().Substr(begin, end - begin); + const GraphemeMatch match = + MatchGrapheme(fonts, + fonts->ResolveRichFace(resolved.family, resolved.weight, + resolved.italic), + grapheme, resolved.weight, resolved.italic); + if (!match.covered && !IsInvisibleGrapheme(grapheme)) { + fonts->NoteMissingCharacter(WideString(grapheme)); } - pieces.push_back({begin, end, style, script, entry}); + pieces.push_back({begin, end, style, script, match.entry}); begin = end; } // A leading common-script piece takes the script that follows it. diff --git a/core/fpdfdoc/cpdf_richtextlayout.h b/core/fpdfdoc/cpdf_richtextlayout.h index e37f04514c..d4ff43be16 100644 --- a/core/fpdfdoc/cpdf_richtextlayout.h +++ b/core/fpdfdoc/cpdf_richtextlayout.h @@ -99,6 +99,26 @@ class CPDF_RichTextLayout { // selectors, ZWJ sequences and low surrogates extend the previous one). static std::vector GraphemeStarts(const WideString& text); + // The font a grapheme draws with when its run's face is |face_entry|: the + // face, when it maps every character of the grapheme; else the fallback + // font its base character picks (the closest style, then the order the + // fallbacks were added in), when that one maps every character. |covered| + // is false when neither does: the grapheme draws as the face's missing + // glyph. Joiners and variation selectors don't need a glyph. + struct GraphemeMatch { + int entry = -1; + bool covered = false; + }; + static GraphemeMatch MatchGrapheme(CPDF_AnnotFontMap* fonts, + int face_entry, + WideStringView grapheme, + int weight, + bool italic); + // Whether a grapheme draws nothing a font must provide: controls and + // default-ignorable characters (joiners, direction marks, variation + // selectors, the zero width space, the soft hyphen, ...). + static bool IsInvisibleGrapheme(WideStringView grapheme); + private: struct Impl; UnownedPtr const fonts_; diff --git a/core/fxge/cfx_fontregistry.cpp b/core/fxge/cfx_fontregistry.cpp index 8d2cde7773..9742730a3e 100644 --- a/core/fxge/cfx_fontregistry.cpp +++ b/core/fxge/cfx_fontregistry.cpp @@ -481,6 +481,44 @@ pdfium::span CFX_FontRegistry::GetSourceHash(FontId font_id) { : pdfium::span(); } +// static +DataVector CFX_FontRegistry::GetProgramData(FontId font_id) { + RegisteredFont* font = GetRegisteredFont(font_id); + if (!font) { + return {}; + } + if (!font->memory_data.empty()) { + return font->memory_data; + } + return ReadStreamToData(font->stream.Get()); +} + +// static +CFX_FontRegistry::ProgramFormat CFX_FontRegistry::GetProgramFormat( + FontId font_id) { + RegisteredFont* font = GetRegisteredFont(font_id); + if (!font) { + return ProgramFormat::kUnsupported; + } + if (!font->memory_data.empty()) { + return DetectProgramFormat(font->memory_data); + } + uint8_t head[4] = {}; + if (!font->stream || + !font->stream->ReadBlockAtOffset(pdfium::span(head), /*offset=*/0)) { + return ProgramFormat::kUnsupported; + } + return DetectProgramFormat(head); +} + +// static +pdfium::span CFX_FontRegistry::GetSupportedUnicodes( + FontId font_id) { + RegisteredFont* font = GetRegisteredFont(font_id); + return font ? pdfium::span(font->supported_unicodes) + : pdfium::span(); +} + // static bool CFX_FontRegistry::IsInstanced(FontId font_id) { RegisteredFont* font = GetRegisteredFont(font_id); @@ -594,6 +632,36 @@ std::optional CFX_FontRegistry::FindFallbackFont( return best_font_id; } +// static +std::vector CFX_FontRegistry::GetFallbackOrder( + int weight, + bool italic, + bool for_authoring) { + struct Ranked { + FontId id; + int score; + }; + std::vector ranked; + if (g_registry) { + for (FontId font_id : g_registry->fallback_order) { + RegisteredFont* font = GetRegisteredFont(font_id); + if (!font || (for_authoring && !font->editing_authorized)) { + continue; + } + ranked.push_back({font_id, StyleScore(*font, weight, italic)}); + } + } + // Stable: among equal scores the font added first stays first, as in + // FindFallbackFont(). + std::ranges::stable_sort(ranked, {}, &Ranked::score); + std::vector order; + order.reserve(ranked.size()); + for (const Ranked& entry : ranked) { + order.push_back(entry.id); + } + return order; +} + // static std::unique_ptr CFX_FontRegistry::CreateFont(FontId font_id) { RegisteredFont* registered_font = GetRegisteredFont(font_id); diff --git a/core/fxge/cfx_fontregistry.h b/core/fxge/cfx_fontregistry.h index 0a6d1a3ed5..270ec6d6b8 100644 --- a/core/fxge/cfx_fontregistry.h +++ b/core/fxge/cfx_fontregistry.h @@ -11,8 +11,10 @@ #include #include +#include #include "core/fxcrt/bytestring.h" +#include "core/fxcrt/data_vector.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/span.h" @@ -72,6 +74,16 @@ class CFX_FontRegistry { // SHA-256 of the registered (instanced) bytes: the source identity a font // pool shares programs by. Empty for an unknown id. static pdfium::span GetSourceHash(FontId font_id); + // The registered program's bytes: exactly what the writer embeds and + // GetSourceHash() names (the static instance for a variable font). Empty + // for an unknown id. + static DataVector GetProgramData(FontId font_id); + // What the registered program is (registration accepts kTrueType and + // kOpenTypeCFF only); kUnsupported for an unknown id. + static ProgramFormat GetProgramFormat(FontId font_id); + // The scalars the program maps to a glyph, sorted and unique. Empty for an + // unknown id. + static pdfium::span GetSupportedUnicodes(FontId font_id); // True when the registered bytes are a static instance made from a // variable font at registration. A variable font that cannot be instanced // is refused, so every registered program is static. @@ -100,6 +112,12 @@ class CFX_FontRegistry { int weight, bool italic, bool for_authoring = false); + // The fallback fonts in the order FindFallbackFont() tries them for a + // style: the closest weight and italic first, then the order they were + // added in. The first of them that maps a scalar is the one it returns. + static std::vector GetFallbackOrder(int weight, + bool italic, + bool for_authoring); static std::unique_ptr CreateFont(FontId font_id); static void DestroyGlobals(); diff --git a/fpdfsdk/BUILD.gn b/fpdfsdk/BUILD.gn index 39f6de5230..6213a27da6 100644 --- a/fpdfsdk/BUILD.gn +++ b/fpdfsdk/BUILD.gn @@ -230,6 +230,9 @@ pdfium_embeddertest_source_set("embeddertests") { "epdf_annot_embeddertest.cpp", # EmbedPDF: covers attachments (public/epdf_attachment.h). "epdf_attachment_embeddertest.cpp", + # EmbedPDF: covers a document's fonts, the resolver read and the + # registry's program reads (public/epdf_font.h). + "epdf_font_embeddertest.cpp", # EmbedPDF: covers flattening and annotation export (public/epdf_flatten.h). "epdf_flatten_embeddertest.cpp", # EmbedPDF: covers base documents and layers (public/epdf_layer.h). diff --git a/fpdfsdk/epdf_annot.cpp b/fpdfsdk/epdf_annot.cpp index 25be04b584..5bf2591fc0 100644 --- a/fpdfsdk/epdf_annot.cpp +++ b/fpdfsdk/epdf_annot.cpp @@ -23,7 +23,6 @@ #include "constants/appearance.h" #include "constants/form_fields.h" #include "core/fdrm/fx_crypt.h" -#include "core/fxcrt/stream_write_utils.h" #include "core/fpdfapi/edit/cpdf_stringarchivestream.h" #include "core/fpdfapi/page/cpdf_annotcontext.h" #include "core/fpdfapi/page/cpdf_form.h" @@ -46,6 +45,7 @@ #include "core/fpdfdoc/cpdf_annot_rotation.h" #include "core/fpdfdoc/cpdf_appearancegenerator.h" #include "core/fpdfdoc/cpdf_color_utils.h" +#include "core/fpdfdoc/cpdf_documentfonts.h" #include "core/fpdfdoc/cpdf_embed_metadata.h" #include "core/fpdfdoc/cpdf_fontface.h" #include "core/fpdfdoc/cpdf_formfield.h" @@ -56,6 +56,7 @@ #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/numerics/safe_conversions.h" #include "core/fxcrt/stl_util.h" +#include "core/fxcrt/stream_write_utils.h" #include "core/fxge/cfx_color.h" #include "core/fxge/cfx_fontregistry.h" #include "fpdfsdk/cpdfsdk_helpers.h" @@ -2285,6 +2286,113 @@ EPDFAnnot_GetDefaultAppearanceFontFace(FPDF_ANNOTATION annot, face.family, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(family, buflen))); } +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAnnot_GetDefaultAppearanceFontPostScriptName(FPDF_ANNOTATION annot, + char* buffer, + unsigned long buflen) { + ScopedFPDFAnnotationView annot_view(annot); + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + if (!context) { + return 0; + } + FPDF_ANNOTATION_SUBTYPE subtype = FPDFAnnot_GetSubtype(annot); + if (subtype != FPDF_ANNOT_FREETEXT && subtype != FPDF_ANNOT_WIDGET && + subtype != FPDF_ANNOT_REDACT) { + return 0; + } + CPDF_Document* doc = context->GetPage()->GetDocument(); + if (!doc) { + return 0; + } + CPDF_DocumentViewScope document_view(doc); + const CPDF_Dictionary* annot_dict = context->GetAnnotDict(); + if (!annot_dict) { + return 0; + } + const CPDF_Dictionary* root_dict = doc->GetRoot(); + RetainPtr acroform_dict = + root_dict ? root_dict->GetDictFor("AcroForm") : nullptr; + CPDF_DefaultAppearance da(annot_dict, acroform_dict.Get()); + std::optional font_info = + da.GetFont(); + if (!font_info.has_value() || font_info.value().name.IsEmpty()) { + return 0; + } + const ByteString& font_name = font_info.value().name; + ByteString base_font; + // The same /DR lookup as EPDFAnnot_GetDefaultAppearanceFontFace(). + RetainPtr inherited_dr = + ToDictionary(CPDF_FormField::GetFieldAttrForDict(annot_dict, "DR")); + RetainPtr acroform_dr = + acroform_dict ? acroform_dict->GetDictFor("DR") : nullptr; + for (const CPDF_Dictionary* dr : {inherited_dr.Get(), acroform_dr.Get()}) { + RetainPtr fonts = + dr ? dr->GetDictFor("Font") : nullptr; + RetainPtr font_dict = + fonts ? fonts->GetDictFor(font_name.AsStringView()) : nullptr; + if (!font_dict) { + continue; + } + base_font = font_dict->GetNameFor("BaseFont"); + if (font_dict->GetNameFor("Subtype") == "Type0") { + RetainPtr descendants = + font_dict->GetArrayFor("DescendantFonts"); + RetainPtr descendant = + descendants ? descendants->GetDictAt(0) : nullptr; + if (descendant && !descendant->GetNameFor("BaseFont").IsEmpty()) { + base_font = descendant->GetNameFor("BaseFont"); + } + } + break; + } + if (base_font.IsEmpty()) { + CPDF_DocumentFonts* fonts = CPDF_DocumentFonts::Find(doc); + std::optional program = + fonts ? fonts->ProgramForAlias(font_name) : std::nullopt; + if (program.has_value()) { + base_font = program->base_font_name; + } + } + base_font = BaseFontNameWithoutSubsetTag(base_font); + if (base_font.IsEmpty()) { + return 0; + } + // SAFETY: required from caller. + return NulTerminateMaybeCopyAndReturnLength( + base_font, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetDefaultAppearanceDocumentFont(FPDF_ANNOTATION annot, + unsigned long program_objnum, + float font_size, + unsigned int R, + unsigned int G, + unsigned int B) { + ScopedFPDFAnnotationView annot_view(annot); + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + if (!context) { + return false; + } + RetainPtr annot_dict = context->GetMutableAnnotDict(); + if (!annot_dict) { + return false; + } + FPDF_ANNOTATION_SUBTYPE subtype = FPDFAnnot_GetSubtype(annot); + if (subtype != FPDF_ANNOT_FREETEXT && subtype != FPDF_ANNOT_WIDGET && + subtype != FPDF_ANNOT_REDACT) { + return false; + } + CPDF_Document* doc = context->GetPage()->GetDocument(); + if (!doc || program_objnum == 0 || font_size < 0 || R > 255 || G > 255 || + B > 255) { + return false; + } + return CPDF_AppearanceGenerator::UpdateDefaultAppearanceDocumentFont( + doc, annot_dict.Get(), static_cast(program_objnum), font_size, + CFX_Color::MakeRGBBytes(R, G, B)); +} + FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetTextAlignment(FPDF_ANNOTATION annot, FPDF_TEXT_ALIGNMENT alignment) { diff --git a/fpdfsdk/epdf_c_api_test.c b/fpdfsdk/epdf_c_api_test.c index 60458bab55..2cdc0b20a8 100644 --- a/fpdfsdk/epdf_c_api_test.c +++ b/fpdfsdk/epdf_c_api_test.c @@ -111,6 +111,7 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFAnnot_GetColor); CHK(EPDFAnnot_GetDefaultAppearance); CHK(EPDFAnnot_GetDefaultAppearanceFontFace); + CHK(EPDFAnnot_GetDefaultAppearanceFontPostScriptName); CHK(EPDFAnnot_GetDrawingRect); CHK(EPDFAnnot_GetEmbedMetadataBoolean); CHK(EPDFAnnot_GetEmbedMetadataJSON); @@ -153,6 +154,7 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFAnnot_SetCalloutLine); CHK(EPDFAnnot_SetColor); CHK(EPDFAnnot_SetDefaultAppearance); + CHK(EPDFAnnot_SetDefaultAppearanceDocumentFont); CHK(EPDFAnnot_SetDefaultAppearanceRegisteredFont); CHK(EPDFAnnot_SetEmbedMetadataBoolean); CHK(EPDFAnnot_SetEmbedMetadataJSON); @@ -271,14 +273,54 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFPage_FlattenAnnotations); // epdf_font.h + CHK(EPDFDoc_ClearMissingCharacters); + CHK(EPDFDoc_CountMissingCharacters); CHK(EPDFDoc_GetFontEmbeddingPolicy); + CHK(EPDFDoc_GetFontProgramCoverage); + CHK(EPDFDoc_GetFontProgramData); + CHK(EPDFDoc_GetMissingCharacter); + CHK(EPDFDoc_ResolveFonts); CHK(EPDFDoc_SetFontEmbeddingPolicy); + CHK(EPDFFontFaces_Close); + CHK(EPDFFontFaces_Count); + CHK(EPDFFontFaces_CountPrograms); + CHK(EPDFFontFaces_GetFamily); + CHK(EPDFFontFaces_GetProgramEmbedded); + CHK(EPDFFontFaces_GetProgramEmbeddingPermission); + CHK(EPDFFontFaces_GetProgramFormat); + CHK(EPDFFontFaces_GetProgramHash); + CHK(EPDFFontFaces_GetProgramMetrics); + CHK(EPDFFontFaces_GetProgramObjNum); + CHK(EPDFFontFaces_GetProgramPostScriptName); + CHK(EPDFFontFaces_GetWeight); + CHK(EPDFFontFaces_IsItalic); + CHK(EPDFFontFaces_Load); + CHK(EPDFFontFaces_ProgramMapsUnicode); + CHK(EPDFFontResolution_Close); + CHK(EPDFFontResolution_CountMissingCharacters); + CHK(EPDFFontResolution_GetBaseFontName); + CHK(EPDFFontResolution_GetFallbacks); + CHK(EPDFFontResolution_GetFamily); + CHK(EPDFFontResolution_GetMissingCharacter); + CHK(EPDFFontResolution_GetProgramObjNum); + CHK(EPDFFontResolution_GetRegisteredFont); + CHK(EPDFFontResolution_GetSource); + CHK(EPDFFontResolution_GetWeight); + CHK(EPDFFontResolution_IsDegraded); + CHK(EPDFFontResolution_IsItalic); CHK(EPDFFont_AddFallbackFont); CHK(EPDFFont_AuthorizeEditing); CHK(EPDFFont_ClearFallbackFonts); CHK(EPDFFont_ClearRegisteredFonts); + CHK(EPDFFont_GetCoverage); CHK(EPDFFont_GetEmbeddingPermission); + CHK(EPDFFont_GetFallbackOrder); CHK(EPDFFont_GetFamilyName); + CHK(EPDFFont_GetMetrics); + CHK(EPDFFont_GetPostScriptName); + CHK(EPDFFont_GetProgramData); + CHK(EPDFFont_GetProgramFormat); + CHK(EPDFFont_GetProgramHash); CHK(EPDFFont_GetWeight); CHK(EPDFFont_IsEditingAuthorized); CHK(EPDFFont_IsInstanced); diff --git a/fpdfsdk/epdf_font.cpp b/fpdfsdk/epdf_font.cpp index 8ee3de5514..4156f40a0a 100644 --- a/fpdfsdk/epdf_font.cpp +++ b/fpdfsdk/epdf_font.cpp @@ -6,13 +6,27 @@ #include #include +#include +#include #include +#include +#include +#include "core/fpdfapi/page/cpdf_annotcontext.h" #include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfdoc/cpdf_annotfontmap.h" +#include "core/fpdfdoc/cpdf_documentfonts.h" +#include "core/fpdfdoc/cpdf_fontface.h" +#include "core/fpdfdoc/cpdf_fontresolver.h" #include "core/fxcrt/bytestring.h" #include "core/fxcrt/compiler_specific.h" +#include "core/fxcrt/data_vector.h" +#include "core/fxcrt/numerics/safe_conversions.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/span.h" +#include "core/fxcrt/span_util.h" +#include "core/fxcrt/stl_util.h" +#include "core/fxge/cfx_font.h" #include "core/fxge/cfx_fontregistry.h" #include "fpdfsdk/cpdfsdk_customaccess.h" #include "fpdfsdk/cpdfsdk_helpers.h" @@ -173,3 +187,577 @@ EPDFFont_AddFallbackFont(EPDF_FONT_ID font_id) { FPDF_EXPORT void FPDF_CALLCONV EPDFFont_ClearFallbackFonts(void) { CFX_FontRegistry::ClearFallbackFonts(); } + +// ---- Registered fonts: programs, coverage, fallback order ------------------- + +namespace { + +// Copies |bytes| into the caller's buffer when it is big enough; returns how +// many bytes there are either way. +unsigned long CopyBytesAndReturnLength(pdfium::span bytes, + void* buffer, + unsigned long buflen) { + // SAFETY: required from caller. + pdfium::span destination = + UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen)); + fxcrt::try_spancpy(pdfium::as_writable_bytes(destination), bytes); + return pdfium::checked_cast(bytes.size()); +} + +// Writes (first, count) pairs into the caller's array, up to |capacity| +// pairs; returns how many ranges there are either way. +unsigned long CopyRangesAndReturnCount( + const std::vector>& ranges, + uint32_t* out, + unsigned long capacity) { + if (out) { + const size_t count = std::min(ranges.size(), capacity); + // SAFETY: required from caller: |out| holds 2 * |capacity| values. + pdfium::span destination = + UNSAFE_BUFFERS(pdfium::span(out, count * 2)); + for (size_t i = 0; i < count; ++i) { + destination[2 * i] = ranges[i].first; + destination[2 * i + 1] = ranges[i].second; + } + } + return pdfium::checked_cast(ranges.size()); +} + +unsigned long CopyFontIdsAndReturnCount( + const std::vector& ids, + EPDF_FONT_ID* out, + unsigned long capacity) { + if (out) { + const size_t count = std::min(ids.size(), capacity); + // SAFETY: required from caller. + pdfium::span destination = + UNSAFE_BUFFERS(pdfium::span(out, count)); + for (size_t i = 0; i < count; ++i) { + destination[i] = ids[i]; + } + } + return pdfium::checked_cast(ids.size()); +} + +bool CopySha256(pdfium::span hash, unsigned char* out) { + if (!out || hash.size() != 32) { + return false; + } + // SAFETY: required from caller: |out| holds 32 bytes. + fxcrt::spancpy(UNSAFE_BUFFERS(pdfium::span(out, 32u)), hash); + return true; +} + +} // namespace + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFont_GetFallbackOrder(int weight, + FPDF_BOOL italic, + EPDF_FONT_ID* ids, + unsigned long capacity) { + return CopyFontIdsAndReturnCount( + CFX_FontRegistry::GetFallbackOrder(weight, !!italic, + /*for_authoring=*/true), + ids, capacity); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFont_GetProgramData(EPDF_FONT_ID font_id, + void* buffer, + unsigned long buflen) { + if (!CFX_FontRegistry::IsValidFont(font_id)) { + return 0; + } + const DataVector data = CFX_FontRegistry::GetProgramData(font_id); + return CopyBytesAndReturnLength(data, buffer, buflen); +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFFont_GetProgramFormat(EPDF_FONT_ID font_id) { + switch (CFX_FontRegistry::GetProgramFormat(font_id)) { + case CFX_FontRegistry::ProgramFormat::kTrueType: + return EPDF_FONT_FORMAT_TRUETYPE; + case CFX_FontRegistry::ProgramFormat::kOpenTypeCFF: + return EPDF_FONT_FORMAT_OPENTYPE_CFF; + case CFX_FontRegistry::ProgramFormat::kUnsupported: + return -1; + } +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFont_GetProgramHash(EPDF_FONT_ID font_id, unsigned char* sha256) { + return CopySha256(CFX_FontRegistry::GetSourceHash(font_id), sha256); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFont_GetCoverage(EPDF_FONT_ID font_id, + uint32_t* ranges, + unsigned long range_capacity) { + std::vector> result; + for (uint32_t scalar : CFX_FontRegistry::GetSupportedUnicodes(font_id)) { + if (!result.empty() && + result.back().first + result.back().second == scalar) { + ++result.back().second; + } else { + result.emplace_back(scalar, 1); + } + } + return CopyRangesAndReturnCount(result, ranges, range_capacity); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFFont_GetMetrics(EPDF_FONT_ID font_id, + float* ascent, + float* descent) { + if (!ascent || !descent) { + return false; + } + std::unique_ptr program = CFX_FontRegistry::CreateFont(font_id); + if (!program) { + return false; + } + ProgramLineMetrics(program.get(), ascent, descent); + return true; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFont_GetPostScriptName(EPDF_FONT_ID font_id, + char* buffer, + unsigned long buflen) { + if (!CFX_FontRegistry::IsValidFont(font_id)) { + return 0; + } + // SAFETY: required from caller. + return NulTerminateMaybeCopyAndReturnLength( + BaseFontNameWithoutSubsetTag(CFX_FontRegistry::GetBaseFontName(font_id)), + UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +// ---- A document's fonts +// ------------------------------------------------------ + +struct epdf_font_faces_t__ { + std::vector faces; +}; + +namespace { + +const CPDF_DocumentFonts::Face* FaceAt(EPDF_FONT_FACES faces, int face) { + if (!faces || !fxcrt::IndexInBounds(faces->faces, face)) { + return nullptr; + } + return &faces->faces[face]; +} + +const CPDF_DocumentFonts::Program* ProgramAt(EPDF_FONT_FACES faces, + int face, + int program) { + const CPDF_DocumentFonts::Face* entry = FaceAt(faces, face); + if (!entry || !fxcrt::IndexInBounds(entry->programs, program)) { + return nullptr; + } + return &entry->programs[program]; +} + +int FormatCode(CPDF_DocumentFonts::Format format) { + switch (format) { + case CPDF_DocumentFonts::Format::kNone: + return EPDF_FONT_FORMAT_NONE; + case CPDF_DocumentFonts::Format::kTrueType: + return EPDF_FONT_FORMAT_TRUETYPE; + case CPDF_DocumentFonts::Format::kOpenTypeCFF: + return EPDF_FONT_FORMAT_OPENTYPE_CFF; + case CPDF_DocumentFonts::Format::kCFF: + return EPDF_FONT_FORMAT_CFF; + case CPDF_DocumentFonts::Format::kType1: + return EPDF_FONT_FORMAT_TYPE1; + case CPDF_DocumentFonts::Format::kType3: + return EPDF_FONT_FORMAT_TYPE3; + } +} + +int EmbeddedCode(CPDF_DocumentFonts::Embedded embedded) { + switch (embedded) { + case CPDF_DocumentFonts::Embedded::kNone: + return EPDF_FONT_EMBEDDED_NONE; + case CPDF_DocumentFonts::Embedded::kWhole: + return EPDF_FONT_EMBEDDED_WHOLE; + case CPDF_DocumentFonts::Embedded::kSubset: + return EPDF_FONT_EMBEDDED_SUBSET; + } +} + +} // namespace + +FPDF_EXPORT EPDF_FONT_FACES FPDF_CALLCONV +EPDFFontFaces_Load(FPDF_DOCUMENT document) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + if (!doc) { + return nullptr; + } + auto faces = std::make_unique(); + faces->faces = CPDF_DocumentFonts::For(doc)->CollectFaces(); + return faces.release(); +} + +FPDF_EXPORT void FPDF_CALLCONV EPDFFontFaces_Close(EPDF_FONT_FACES faces) { + delete faces; +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFFontFaces_Count(EPDF_FONT_FACES faces) { + return faces ? fxcrt::CollectionSize(faces->faces) : -1; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontFaces_GetFamily(EPDF_FONT_FACES faces, + int face, + FPDF_WCHAR* buffer, + unsigned long buflen) { + const CPDF_DocumentFonts::Face* entry = FaceAt(faces, face); + if (!entry) { + return 0; + } + // SAFETY: required from caller. + return Utf16EncodeMaybeCopyAndReturnLength( + entry->family, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFFontFaces_GetWeight(EPDF_FONT_FACES faces, + int face) { + const CPDF_DocumentFonts::Face* entry = FaceAt(faces, face); + return entry ? entry->weight : 0; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_IsItalic(EPDF_FONT_FACES faces, int face) { + const CPDF_DocumentFonts::Face* entry = FaceAt(faces, face); + return entry && entry->italic; +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFFontFaces_CountPrograms(EPDF_FONT_FACES faces, + int face) { + const CPDF_DocumentFonts::Face* entry = FaceAt(faces, face); + return entry ? fxcrt::CollectionSize(entry->programs) : -1; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontFaces_GetProgramObjNum(EPDF_FONT_FACES faces, int face, int program) { + const CPDF_DocumentFonts::Program* entry = ProgramAt(faces, face, program); + return entry ? entry->objnum : 0; +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontFaces_GetProgramFormat(EPDF_FONT_FACES faces, int face, int program) { + const CPDF_DocumentFonts::Program* entry = ProgramAt(faces, face, program); + return entry ? FormatCode(entry->format) : -1; +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontFaces_GetProgramEmbedded(EPDF_FONT_FACES faces, int face, int program) { + const CPDF_DocumentFonts::Program* entry = ProgramAt(faces, face, program); + return entry ? EmbeddedCode(entry->embedded) : -1; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontFaces_GetProgramPostScriptName(EPDF_FONT_FACES faces, + int face, + int program, + char* buffer, + unsigned long buflen) { + const CPDF_DocumentFonts::Program* entry = ProgramAt(faces, face, program); + if (!entry) { + return 0; + } + // SAFETY: required from caller. + return NulTerminateMaybeCopyAndReturnLength( + entry->postscript_name, + UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontFaces_GetProgramEmbeddingPermission(EPDF_FONT_FACES faces, + int face, + int program) { + const CPDF_DocumentFonts::Program* entry = ProgramAt(faces, face, program); + if (!entry || !entry->permission.has_value()) { + return -1; + } + return static_cast(*entry->permission); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_ProgramMapsUnicode(EPDF_FONT_FACES faces, int face, int program) { + const CPDF_DocumentFonts::Program* entry = ProgramAt(faces, face, program); + return entry && entry->maps_unicode; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_GetProgramHash(EPDF_FONT_FACES faces, + int face, + int program, + unsigned char* sha256) { + const CPDF_DocumentFonts::Program* entry = ProgramAt(faces, face, program); + return entry && CopySha256(entry->sha256, sha256); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_GetProgramMetrics(EPDF_FONT_FACES faces, + int face, + int program, + float* ascent, + float* descent) { + const CPDF_DocumentFonts::Program* entry = ProgramAt(faces, face, program); + if (!entry || !ascent || !descent || entry->ascent <= 0) { + return false; + } + *ascent = entry->ascent; + *descent = entry->descent; + return true; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetFontProgramData(FPDF_DOCUMENT document, + unsigned long program_objnum, + void* buffer, + unsigned long buflen) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + if (!doc || program_objnum == 0) { + return 0; + } + const DataVector data = CPDF_DocumentFonts::For(doc)->ProgramData( + static_cast(program_objnum)); + return CopyBytesAndReturnLength(data, buffer, buflen); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetFontProgramCoverage(FPDF_DOCUMENT document, + unsigned long program_objnum, + uint32_t* ranges, + unsigned long range_capacity) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + if (!doc || program_objnum == 0) { + return 0; + } + return CopyRangesAndReturnCount(CPDF_DocumentFonts::For(doc)->ProgramCoverage( + static_cast(program_objnum)), + ranges, range_capacity); +} + +// ---- The fonts a write would use +// --------------------------------------------- + +struct epdf_font_resolution_t__ { + CPDF_FontResolver::Result result; +}; + +namespace { + +const CPDF_FontResolver::ResolvedRun* RunAt(EPDF_FONT_RESOLUTION resolution, + int run) { + if (!resolution || !fxcrt::IndexInBounds(resolution->result.runs, run)) { + return nullptr; + } + return &resolution->result.runs[run]; +} + +} // namespace + +FPDF_EXPORT EPDF_FONT_RESOLUTION FPDF_CALLCONV +EPDFDoc_ResolveFonts(FPDF_DOCUMENT document, + FPDF_ANNOTATION annot, + int run_count, + const FPDF_WIDESTRING* texts, + const FPDF_WIDESTRING* families, + const int* weights, + const FPDF_BOOL* italics) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + if (!doc || run_count < 0 || + (run_count > 0 && (!texts || !families || !weights || !italics))) { + return nullptr; + } + const CPDF_Dictionary* annot_dict = nullptr; + if (annot) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + annot_dict = context ? context->GetAnnotDict() : nullptr; + } + const size_t count = static_cast(run_count); + std::vector runs; + runs.reserve(count); + // SAFETY: required from caller: each array holds |run_count| entries. + auto text_span = UNSAFE_BUFFERS(pdfium::span(texts, count)); + auto family_span = UNSAFE_BUFFERS(pdfium::span(families, count)); + auto weight_span = UNSAFE_BUFFERS(pdfium::span(weights, count)); + auto italic_span = UNSAFE_BUFFERS(pdfium::span(italics, count)); + for (size_t i = 0; i < count; ++i) { + CPDF_FontResolver::Run run; + run.text = text_span[i] ? WideStringFromFPDFWideString(text_span[i]) + : WideString(); + run.family = family_span[i] ? WideStringFromFPDFWideString(family_span[i]) + : WideString(); + run.weight = weight_span[i]; + run.italic = !!italic_span[i]; + runs.push_back(std::move(run)); + } + auto resolution = std::make_unique(); + resolution->result = CPDF_FontResolver::Resolve(doc, annot_dict, runs); + return resolution.release(); +} + +FPDF_EXPORT void FPDF_CALLCONV +EPDFFontResolution_Close(EPDF_FONT_RESOLUTION resolution) { + delete resolution; +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontResolution_GetSource(EPDF_FONT_RESOLUTION resolution, int run) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + if (!entry) { + return -1; + } + switch (entry->face.source) { + case CPDF_AnnotFontMap::Source::kStandard14: + return EPDF_FONT_SOURCE_STANDARD; + case CPDF_AnnotFontMap::Source::kRegistered: + return EPDF_FONT_SOURCE_REGISTERED; + case CPDF_AnnotFontMap::Source::kDocumentProgram: + return EPDF_FONT_SOURCE_DOCUMENT_PROGRAM; + case CPDF_AnnotFontMap::Source::kDocumentFontDict: + return EPDF_FONT_SOURCE_DOCUMENT_FONT; + } +} + +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV +EPDFFontResolution_GetRegisteredFont(EPDF_FONT_RESOLUTION resolution, int run) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + return entry ? entry->face.registered_font_id + : CFX_FontRegistry::kInvalidFontId; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetProgramObjNum(EPDF_FONT_RESOLUTION resolution, int run) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + return entry ? entry->face.program_objnum : 0; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetBaseFontName(EPDF_FONT_RESOLUTION resolution, + int run, + char* buffer, + unsigned long buflen) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + if (!entry || entry->face.base_font_name.IsEmpty()) { + return 0; + } + // SAFETY: required from caller. + return NulTerminateMaybeCopyAndReturnLength( + entry->face.base_font_name, + UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetFamily(EPDF_FONT_RESOLUTION resolution, + int run, + FPDF_WCHAR* buffer, + unsigned long buflen) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + if (!entry) { + return 0; + } + // SAFETY: required from caller. + return Utf16EncodeMaybeCopyAndReturnLength( + entry->face.family, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontResolution_GetWeight(EPDF_FONT_RESOLUTION resolution, int run) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + return entry ? entry->face.weight : 0; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontResolution_IsItalic(EPDF_FONT_RESOLUTION resolution, int run) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + return entry && entry->face.italic; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontResolution_IsDegraded(EPDF_FONT_RESOLUTION resolution, int run) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + return entry && entry->face.degraded; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetFallbacks(EPDF_FONT_RESOLUTION resolution, + int run, + EPDF_FONT_ID* ids, + unsigned long capacity) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + if (!entry) { + return 0; + } + return CopyFontIdsAndReturnCount(entry->fallbacks, ids, capacity); +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontResolution_CountMissingCharacters(EPDF_FONT_RESOLUTION resolution) { + return resolution + ? fxcrt::CollectionSize(resolution->result.missing_characters) + : -1; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetMissingCharacter(EPDF_FONT_RESOLUTION resolution, + int index, + FPDF_WCHAR* buffer, + unsigned long buflen) { + if (!resolution || + !fxcrt::IndexInBounds(resolution->result.missing_characters, index)) { + return 0; + } + // SAFETY: required from caller. + return Utf16EncodeMaybeCopyAndReturnLength( + resolution->result.missing_characters[index], + UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +// ---- Characters a write drew with no font +// ------------------------------------- + +FPDF_EXPORT void FPDF_CALLCONV +EPDFDoc_ClearMissingCharacters(FPDF_DOCUMENT document) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (doc) { + CPDF_DocumentFonts::For(doc)->ClearMissingCharacters(); + } +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_CountMissingCharacters(FPDF_DOCUMENT document) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc) { + return -1; + } + return fxcrt::CollectionSize( + CPDF_DocumentFonts::For(doc)->missing_characters()); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetMissingCharacter(FPDF_DOCUMENT document, + int index, + FPDF_WCHAR* buffer, + unsigned long buflen) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc) { + return 0; + } + const std::vector& missing = + CPDF_DocumentFonts::For(doc)->missing_characters(); + if (!fxcrt::IndexInBounds(missing, index)) { + return 0; + } + // SAFETY: required from caller. + return Utf16EncodeMaybeCopyAndReturnLength( + missing[index], UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} diff --git a/fpdfsdk/epdf_font_embeddertest.cpp b/fpdfsdk/epdf_font_embeddertest.cpp new file mode 100644 index 0000000000..eafa45b89d --- /dev/null +++ b/fpdfsdk/epdf_font_embeddertest.cpp @@ -0,0 +1,672 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "public/epdf_font.h" + +#include + +#include +#include +#include + +#include "core/fdrm/fx_crypt_sha.h" +#include "core/fpdfapi/page/cpdf_annotcontext.h" +#include "core/fpdfapi/parser/cpdf_array.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/widestring.h" +#include "fpdfsdk/cpdfsdk_helpers.h" +#include "fpdfsdk/epdf_edit_test_util.h" +#include "public/cpp/fpdf_scopers.h" +#include "public/epdf_annot.h" +#include "public/epdf_form.h" +#include "public/fpdf_annot.h" +#include "public/fpdf_edit.h" +#include "testing/embedder_test.h" +#include "testing/embedpdf_form_writes.h" +#include "testing/fx_string_testhelpers.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/utils/file_util.h" +#include "testing/utils/path_service.h" + +namespace { + +class ScopedRegisteredFonts { + public: + ScopedRegisteredFonts() { EPDFFont_ClearRegisteredFonts(); } + ~ScopedRegisteredFonts() { EPDFFont_ClearRegisteredFonts(); } +}; + +std::vector LoadThirdPartyFont(const char* path) { + std::string font_path = PathService::GetThirdPartyFilePath(path); + if (font_path.empty()) { + ADD_FAILURE() << "Failed to find " << path; + return {}; + } + return GetFileContents(font_path.c_str()); +} + +std::vector LoadRoboto() { + return LoadThirdPartyFont("harfbuzz/src/perf/fonts/Roboto-Regular.ttf"); +} + +std::vector LoadDroidSansFallback() { + std::string font_path = + PathService::GetTestFilePath("fonts/DroidSansFallbackFull.ttf"); + if (font_path.empty()) { + ADD_FAILURE() << "Failed to find DroidSansFallbackFull"; + return {}; + } + return GetFileContents(font_path.c_str()); +} + +// Roboto with its OS/2 fsType set to preview-and-print. +std::vector RobotoPreviewAndPrint() { + std::vector font = LoadRoboto(); + if (font.size() < 12) { + return {}; + } + const size_t tables = (font[4] << 8) | font[5]; + for (size_t i = 0; i < tables; ++i) { + const size_t record = 12 + 16 * i; + if (record + 16 > font.size()) { + break; + } + if (font[record] == 'O' && font[record + 1] == 'S' && + font[record + 2] == '/' && font[record + 3] == '2') { + const size_t offset = (static_cast(font[record + 8]) << 24) | + (font[record + 9] << 16) | + (font[record + 10] << 8) | font[record + 11]; + font[offset + 8] = 0x00; + font[offset + 9] = 0x04; // fsType: preview-and-print + return font; + } + } + return {}; +} + +std::wstring Utf16Of(std::vector buffer) { + return GetPlatformWString(buffer.data()); +} + +std::wstring FaceFamily(EPDF_FONT_FACES faces, int face) { + const unsigned long length = EPDFFontFaces_GetFamily(faces, face, nullptr, 0); + if (length == 0) { + return L""; + } + std::vector buffer = GetFPDFWideStringBuffer(length); + EPDFFontFaces_GetFamily(faces, face, buffer.data(), length); + return Utf16Of(std::move(buffer)); +} + +std::string ProgramPostScriptName(EPDF_FONT_FACES faces, + int face, + int program) { + const unsigned long length = + EPDFFontFaces_GetProgramPostScriptName(faces, face, program, nullptr, 0); + if (length == 0) { + return ""; + } + std::vector buffer(length); + EPDFFontFaces_GetProgramPostScriptName(faces, face, program, buffer.data(), + length); + return std::string(buffer.data()); +} + +struct FoundProgram { + int face = -1; + int program = -1; +}; + +// The first program of the first face named |family|. +std::optional FindProgram(EPDF_FONT_FACES faces, + const std::wstring& family) { + for (int face = 0; face < EPDFFontFaces_Count(faces); ++face) { + if (FaceFamily(faces, face) != family) { + continue; + } + if (EPDFFontFaces_CountPrograms(faces, face) > 0) { + return FoundProgram{face, 0}; + } + } + return std::nullopt; +} + +bool HasFace(EPDF_FONT_FACES faces, const std::wstring& family) { + for (int face = 0; face < EPDFFontFaces_Count(faces); ++face) { + if (FaceFamily(faces, face) == family) { + return true; + } + } + return false; +} + +std::vector MissingCharacters(FPDF_DOCUMENT document) { + std::vector missing; + for (int i = 0; i < EPDFDoc_CountMissingCharacters(document); ++i) { + const unsigned long length = + EPDFDoc_GetMissingCharacter(document, i, nullptr, 0); + std::vector buffer = GetFPDFWideStringBuffer(length); + EPDFDoc_GetMissingCharacter(document, i, buffer.data(), length); + missing.push_back(Utf16Of(std::move(buffer))); + } + return missing; +} + +ByteString DefaultAppearanceFontName(FPDF_ANNOTATION annot) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + const ByteString da = context->GetAnnotDict()->GetByteStringFor("DA"); + // "0 g /EDocF12 12 Tf": the name after the slash. + std::optional slash = da.Find('/'); + if (!slash.has_value()) { + return ByteString(); + } + ByteString rest = da.Substr(*slash + 1); + std::optional space = rest.Find(' '); + return space.has_value() ? rest.First(*space) : rest; +} + +// The object number of the program the appearance's |alias| font embeds. +uint32_t AppearanceFontProgram(FPDF_ANNOTATION annot, const ByteString& alias) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + RetainPtr ap = + context->GetAnnotDict()->GetDictFor("AP"); + RetainPtr normal = ap ? ap->GetStreamFor("N") : nullptr; + RetainPtr resources = + normal ? normal->GetDict()->GetDictFor("Resources") : nullptr; + RetainPtr fonts = + resources ? resources->GetDictFor("Font") : nullptr; + RetainPtr font = + fonts ? fonts->GetDictFor(alias.AsStringView()) : nullptr; + if (!font || font->GetNameFor("Subtype") != "Type0") { + return 0; + } + RetainPtr descendants = + font->GetArrayFor("DescendantFonts"); + RetainPtr descendant = + descendants ? descendants->GetDictAt(0) : nullptr; + RetainPtr descriptor = + descendant ? descendant->GetDictFor("FontDescriptor") : nullptr; + RetainPtr program = + descriptor ? descriptor->GetStreamFor("FontFile2") : nullptr; + return program ? program->GetObjNum() : 0; +} + +class EPDFFontEmbedderTest : public EmbedderTest { + protected: + // A page with a text field whose widget writes with |font_id|, filled + // with |value|: under the default embedding policy, the font's whole + // program goes into the document. + uint32_t AddRegisteredFontField(FPDF_PAGE page, + EPDF_FONT_ID font_id, + const wchar_t* name, + float top, + const std::wstring& value) { + ScopedFPDFAnnotation annot(EPDFPage_CreateAnnot(page, FPDF_ANNOT_WIDGET)); + EXPECT_TRUE(annot); + const FS_RECTF rect{50.0f, top, 350.0f, top - 30.0f}; + EXPECT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + EXPECT_TRUE(EPDFAnnot_SetDefaultAppearanceRegisteredFont( + annot.get(), font_id, 12.0f, 0, 0, 0)); + return AttachAndFill(annot.get(), name, value); + } + + uint32_t AttachAndFill(FPDF_ANNOTATION annot, + const wchar_t* name, + const std::wstring& value) { + ScopedFPDFWideString field_name = GetFPDFWideString(name); + const uint32_t field = EPDFForm_CreateField( + document(), 4 /* EPDF_FORMFIELD_FAMILY_TEXT */, field_name.get(), 0); + EXPECT_GT(field, 0u); + EXPECT_TRUE(EPDFForm_AttachWidget( + document(), field, EPDFAnnot_GetObjectNumber(annot), nullptr, 0)); + EXPECT_TRUE(embedpdf_test::FillTextField(document(), field, value)); + EXPECT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot)); + return field; + } +}; + +} // namespace + +// The registry hands out a font's program as the writer embeds it, its +// hash, the characters it maps and the order fallbacks are tried in. +TEST_F(EPDFFontEmbedderTest, RegisteredFontProgramCoverageAndFallbackOrder) { + ScopedRegisteredFonts scoped_fonts; + std::vector roboto = LoadRoboto(); + ASSERT_FALSE(roboto.empty()); + const EPDF_FONT_ID regular = EPDFFont_RegisterMemFont64( + "Roboto", /*weight=*/400, /*italic=*/0, roboto.data(), roboto.size()); + const EPDF_FONT_ID bold = + EPDFFont_RegisterMemFont64("Roboto Heavy", /*weight=*/700, /*italic=*/0, + roboto.data(), roboto.size()); + ASSERT_NE(0u, regular); + ASSERT_NE(0u, bold); + + ASSERT_EQ(roboto.size(), EPDFFont_GetProgramData(regular, nullptr, 0)); + std::vector bytes(roboto.size()); + ASSERT_EQ(roboto.size(), + EPDFFont_GetProgramData(regular, bytes.data(), bytes.size())); + EXPECT_EQ(roboto, bytes); + + unsigned char hash[32] = {}; + ASSERT_TRUE(EPDFFont_GetProgramHash(regular, hash)); + const DataVector expected = CryptSha256Generate(roboto); + EXPECT_EQ(std::vector(expected.begin(), expected.end()), + std::vector(hash, hash + 32)); + EXPECT_FALSE(EPDFFont_GetProgramHash(9999, hash)); + + const unsigned long range_count = EPDFFont_GetCoverage(regular, nullptr, 0); + ASSERT_GT(range_count, 0u); + std::vector ranges(range_count * 2); + ASSERT_EQ(range_count, + EPDFFont_GetCoverage(regular, ranges.data(), range_count)); + bool covers_a = false; + for (unsigned long i = 0; i < range_count; ++i) { + covers_a |= ranges[2 * i] <= 'A' && 'A' < ranges[2 * i] + ranges[2 * i + 1]; + } + EXPECT_TRUE(covers_a); + + float ascent = 0; + float descent = 0; + ASSERT_TRUE(EPDFFont_GetMetrics(regular, &ascent, &descent)); + EXPECT_GT(ascent, 0.5f); + EXPECT_GT(descent, 0.0f); + EXPECT_GT(EPDFFont_GetPostScriptName(regular, nullptr, 0), 1u); + EXPECT_EQ(EPDF_FONT_FORMAT_TRUETYPE, EPDFFont_GetProgramFormat(regular)); + EXPECT_EQ(-1, EPDFFont_GetProgramFormat(9999)); + + ASSERT_TRUE(EPDFFont_AddFallbackFont(regular)); + ASSERT_TRUE(EPDFFont_AddFallbackFont(bold)); + EPDF_FONT_ID order[2] = {}; + ASSERT_EQ(2u, EPDFFont_GetFallbackOrder(700, false, order, 2)); + EXPECT_EQ(bold, order[0]); + EXPECT_EQ(regular, order[1]); + ASSERT_EQ(2u, EPDFFont_GetFallbackOrder(400, false, order, 2)); + EXPECT_EQ(regular, order[0]); + EXPECT_EQ(bold, order[1]); +} + +// A whole program the document holds is listed with its facts, and its bytes +// and coverage are handed out by object number; nothing else is. +TEST_F(EPDFFontEmbedderTest, ListsADocumentProgramAndHandsOutItsBytes) { + ScopedRegisteredFonts scoped_fonts; + std::vector roboto = LoadRoboto(); + ASSERT_FALSE(roboto.empty()); + const EPDF_FONT_ID roboto_id = EPDFFont_RegisterMemFont64( + "Roboto", /*weight=*/400, /*italic=*/0, roboto.data(), roboto.size()); + ASSERT_NE(0u, roboto_id); + + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ASSERT_TRUE(page); + AddRegisteredFontField(page.get(), roboto_id, L"name", 320.0f, L"hello"); + + EPDF_FONT_FACES faces = EPDFFontFaces_Load(document()); + ASSERT_TRUE(faces); + std::optional found = FindProgram(faces, L"Roboto"); + ASSERT_TRUE(found.has_value()); + const unsigned long objnum = + EPDFFontFaces_GetProgramObjNum(faces, found->face, found->program); + EXPECT_NE(0u, objnum); + EXPECT_EQ(400, EPDFFontFaces_GetWeight(faces, found->face)); + EXPECT_FALSE(EPDFFontFaces_IsItalic(faces, found->face)); + EXPECT_EQ(EPDF_FONT_FORMAT_TRUETYPE, + EPDFFontFaces_GetProgramFormat(faces, found->face, found->program)); + EXPECT_EQ(EPDF_FONT_EMBEDDED_WHOLE, EPDFFontFaces_GetProgramEmbedded( + faces, found->face, found->program)); + EXPECT_EQ(EPDF_FONT_EMBEDDING_INSTALLABLE, + EPDFFontFaces_GetProgramEmbeddingPermission(faces, found->face, + found->program)); + EXPECT_TRUE( + EPDFFontFaces_ProgramMapsUnicode(faces, found->face, found->program)); + EXPECT_FALSE( + ProgramPostScriptName(faces, found->face, found->program).empty()); + unsigned char hash[32] = {}; + unsigned char registered_hash[32] = {}; + ASSERT_TRUE( + EPDFFontFaces_GetProgramHash(faces, found->face, found->program, hash)); + ASSERT_TRUE(EPDFFont_GetProgramHash(roboto_id, registered_hash)); + EXPECT_EQ(std::vector(registered_hash, registered_hash + 32), + std::vector(hash, hash + 32)); + float ascent = 0; + float descent = 0; + EXPECT_TRUE(EPDFFontFaces_GetProgramMetrics( + faces, found->face, found->program, &ascent, &descent)); + EPDFFontFaces_Close(faces); + + ASSERT_EQ(roboto.size(), + EPDFDoc_GetFontProgramData(document(), objnum, nullptr, 0)); + std::vector bytes(roboto.size()); + ASSERT_EQ(roboto.size(), EPDFDoc_GetFontProgramData( + document(), objnum, bytes.data(), bytes.size())); + EXPECT_EQ(roboto, bytes); + EXPECT_GT(EPDFDoc_GetFontProgramCoverage(document(), objnum, nullptr, 0), 0u); + // The catalog is an object, not a font program. + EXPECT_EQ(0u, EPDFDoc_GetFontProgramData(document(), 1, nullptr, 0)); +} + +// A widget writes with a document program by its object number: /DA names a +// session alias, the appearance embeds that very program, characters it +// lacks are reported, and the box's font reads back as that program. +TEST_F(EPDFFontEmbedderTest, WidgetWritesWithADocumentProgram) { + ScopedRegisteredFonts scoped_fonts; + std::vector roboto = LoadRoboto(); + ASSERT_FALSE(roboto.empty()); + const EPDF_FONT_ID roboto_id = EPDFFont_RegisterMemFont64( + "Roboto", /*weight=*/400, /*italic=*/0, roboto.data(), roboto.size()); + ASSERT_NE(0u, roboto_id); + + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ASSERT_TRUE(page); + AddRegisteredFontField(page.get(), roboto_id, L"first", 320.0f, L"hello"); + EPDF_FONT_FACES faces = EPDFFontFaces_Load(document()); + std::optional found = FindProgram(faces, L"Roboto"); + ASSERT_TRUE(found.has_value()); + const unsigned long objnum = + EPDFFontFaces_GetProgramObjNum(faces, found->face, found->program); + const std::string postscript_name = + ProgramPostScriptName(faces, found->face, found->program); + EPDFFontFaces_Close(faces); + + // The font is the document's alone now. + EPDFFont_ClearRegisteredFonts(); + + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_WIDGET)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 250.0f, 350.0f, 220.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + EXPECT_FALSE( + EPDFAnnot_SetDefaultAppearanceDocumentFont(annot.get(), 1, 12, 0, 0, 0)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearanceDocumentFont(annot.get(), objnum, + 12, 0, 0, 0)); + const ByteString alias = DefaultAppearanceFontName(annot.get()); + EXPECT_EQ(ByteString::Format("EDocF%lu", objnum), alias); + + EPDFDoc_ClearMissingCharacters(document()); + const uint32_t second = + AttachAndFill(annot.get(), L"second", L"\x03A9 and \x4E2D"); + EXPECT_EQ(objnum, AppearanceFontProgram(annot.get(), alias)); + EXPECT_EQ(std::vector{L"\x4E2D"}, + MissingCharacters(document())); + + const unsigned long name_length = + EPDFAnnot_GetDefaultAppearanceFontPostScriptName(annot.get(), nullptr, 0); + ASSERT_GT(name_length, 1u); + std::vector name(name_length); + EPDFAnnot_GetDefaultAppearanceFontPostScriptName(annot.get(), name.data(), + name_length); + EXPECT_EQ(postscript_name, std::string(name.data())); + + int weight = 0; + FPDF_BOOL italic = true; + const unsigned long family_length = EPDFAnnot_GetDefaultAppearanceFontFace( + annot.get(), nullptr, 0, &weight, &italic); + std::vector family = GetFPDFWideStringBuffer(family_length); + EPDFAnnot_GetDefaultAppearanceFontFace(annot.get(), family.data(), + family_length, &weight, &italic); + EXPECT_EQ(L"Roboto", Utf16Of(std::move(family))); + EXPECT_EQ(400, weight); + EXPECT_FALSE(italic); + + // Once the appearance exists, /DR holds the composite font over the + // program, and a new value with a character no earlier one used lays out + // with the program's own cmap. + RetainPtr acroform = + CPDFDocumentFromFPDFDocument(document()) + ->GetRoot() + ->GetDictFor("AcroForm"); + RetainPtr dr_font = + acroform->GetDictFor("DR")->GetDictFor("Font")->GetDictFor( + alias.AsStringView()); + ASSERT_TRUE(dr_font); + EXPECT_EQ("Type0", dr_font->GetNameFor("Subtype")); + EPDFDoc_ClearMissingCharacters(document()); + ASSERT_TRUE(embedpdf_test::FillTextField(document(), second, L"\x03A8")); + ASSERT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot.get())); + EXPECT_EQ(objnum, AppearanceFontProgram(annot.get(), alias)); + EXPECT_TRUE(MissingCharacters(document()).empty()); +} + +// What a write would draw with, without writing: the standard face, a +// registered font, a substitute, the fallbacks in order, and the characters +// no font has. +TEST_F(EPDFFontEmbedderTest, ResolvesTheFontsAWriteWouldUse) { + ScopedRegisteredFonts scoped_fonts; + std::vector roboto = LoadRoboto(); + std::vector droid = LoadDroidSansFallback(); + ASSERT_FALSE(roboto.empty()); + ASSERT_FALSE(droid.empty()); + const EPDF_FONT_ID roboto_id = EPDFFont_RegisterMemFont64( + "Roboto", /*weight=*/400, /*italic=*/0, roboto.data(), roboto.size()); + const EPDF_FONT_ID droid_id = + EPDFFont_RegisterMemFont64("DroidSansFallbackFull", /*weight=*/400, + /*italic=*/0, droid.data(), droid.size()); + ASSERT_NE(0u, roboto_id); + ASSERT_NE(0u, droid_id); + ASSERT_TRUE(EPDFFont_AddFallbackFont(droid_id)); + + CreateEmptyDocument(); + ScopedFPDFWideString text0 = GetFPDFWideString(L"Hello \x4E2D"); + ScopedFPDFWideString text1 = GetFPDFWideString(L"Roboto here"); + ScopedFPDFWideString text2 = GetFPDFWideString(L"X"); + ScopedFPDFWideString family0 = GetFPDFWideString(L"Helvetica"); + ScopedFPDFWideString family1 = GetFPDFWideString(L"Roboto"); + ScopedFPDFWideString family2 = GetFPDFWideString(L"NoSuchFamily"); + FPDF_WIDESTRING texts[] = {text0.get(), text1.get(), text2.get()}; + FPDF_WIDESTRING families[] = {family0.get(), family1.get(), family2.get()}; + const int weights[] = {400, 400, 700}; + const FPDF_BOOL italics[] = {false, false, false}; + + EPDF_FONT_RESOLUTION resolution = EPDFDoc_ResolveFonts( + document(), nullptr, 3, texts, families, weights, italics); + ASSERT_TRUE(resolution); + EXPECT_EQ(EPDF_FONT_SOURCE_STANDARD, + EPDFFontResolution_GetSource(resolution, 0)); + char base_font[64] = {}; + EPDFFontResolution_GetBaseFontName(resolution, 0, base_font, + sizeof(base_font)); + EXPECT_STREQ("Helvetica", base_font); + EXPECT_FALSE(EPDFFontResolution_IsDegraded(resolution, 0)); + EPDF_FONT_ID fallbacks[2] = {}; + ASSERT_EQ(1u, EPDFFontResolution_GetFallbacks(resolution, 0, fallbacks, 2)); + EXPECT_EQ(droid_id, fallbacks[0]); + + EXPECT_EQ(EPDF_FONT_SOURCE_REGISTERED, + EPDFFontResolution_GetSource(resolution, 1)); + EXPECT_EQ(roboto_id, EPDFFontResolution_GetRegisteredFont(resolution, 1)); + + EXPECT_EQ(EPDF_FONT_SOURCE_STANDARD, + EPDFFontResolution_GetSource(resolution, 2)); + EXPECT_TRUE(EPDFFontResolution_IsDegraded(resolution, 2)); + EXPECT_EQ(700, EPDFFontResolution_GetWeight(resolution, 2)); + EXPECT_EQ(0, EPDFFontResolution_CountMissingCharacters(resolution)); + EPDFFontResolution_Close(resolution); + + // Without the fallback, the CJK character has no font. + EPDFFont_ClearFallbackFonts(); + resolution = EPDFDoc_ResolveFonts(document(), nullptr, 1, texts, families, + weights, italics); + ASSERT_TRUE(resolution); + EXPECT_EQ(0u, EPDFFontResolution_GetFallbacks(resolution, 0, nullptr, 0)); + ASSERT_EQ(1, EPDFFontResolution_CountMissingCharacters(resolution)); + const unsigned long length = + EPDFFontResolution_GetMissingCharacter(resolution, 0, nullptr, 0); + std::vector missing = GetFPDFWideStringBuffer(length); + EPDFFontResolution_GetMissingCharacter(resolution, 0, missing.data(), length); + EXPECT_EQ(L"\x4E2D", Utf16Of(std::move(missing))); + EPDFFontResolution_Close(resolution); +} + +// A family the document holds as a whole program, and nobody registered, +// resolves to that program. +TEST_F(EPDFFontEmbedderTest, ResolvesADocumentProgram) { + ScopedRegisteredFonts scoped_fonts; + std::vector roboto = LoadRoboto(); + ASSERT_FALSE(roboto.empty()); + const EPDF_FONT_ID roboto_id = EPDFFont_RegisterMemFont64( + "Roboto", /*weight=*/400, /*italic=*/0, roboto.data(), roboto.size()); + ASSERT_NE(0u, roboto_id); + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ASSERT_TRUE(page); + AddRegisteredFontField(page.get(), roboto_id, L"name", 320.0f, L"hello"); + EPDF_FONT_FACES faces = EPDFFontFaces_Load(document()); + std::optional found = FindProgram(faces, L"Roboto"); + ASSERT_TRUE(found.has_value()); + const unsigned long objnum = + EPDFFontFaces_GetProgramObjNum(faces, found->face, found->program); + EPDFFontFaces_Close(faces); + EPDFFont_ClearRegisteredFonts(); + + ScopedFPDFWideString text = GetFPDFWideString(L"Hi"); + ScopedFPDFWideString family = GetFPDFWideString(L"Roboto"); + FPDF_WIDESTRING texts[] = {text.get()}; + FPDF_WIDESTRING families[] = {family.get()}; + const int weights[] = {400}; + const FPDF_BOOL italics[] = {false}; + EPDF_FONT_RESOLUTION resolution = EPDFDoc_ResolveFonts( + document(), nullptr, 1, texts, families, weights, italics); + ASSERT_TRUE(resolution); + EXPECT_EQ(EPDF_FONT_SOURCE_DOCUMENT_PROGRAM, + EPDFFontResolution_GetSource(resolution, 0)); + EXPECT_EQ(objnum, EPDFFontResolution_GetProgramObjNum(resolution, 0)); + EPDFFontResolution_Close(resolution); +} + +// The walk finds fonts wherever a drawing can use them: inherited page +// resources, a form XObject (that names itself), every appearance state, a +// field's own /DR, the form's /DR, and a Type 3 font's resources. +TEST_F(EPDFFontEmbedderTest, FindsFontsWhereverADrawingUsesThem) { + const std::string pdf = MakePdf({ + "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [8 0 R] " + "/DR << /Font << /FDR 9 0 R >> >> >> >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 /Resources << /Font << /FInh 10 " + "0 R /FT3 14 0 R >> /XObject << /X1 4 0 R >> >> >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Annots [5 0 R] " + ">>", + "<< /Type /XObject /Subtype /Form /BBox [0 0 10 10] /Resources << /Font " + "<< /FX 11 0 R >> /XObject << /Self 4 0 R >> >> /Length 0 >>\n" + "stream\n\nendstream", + "<< /Type /Annot /Subtype /Square /Rect [10 10 50 50] /AP << /N << /On " + "6 0 R /Off 7 0 R >> >> >>", + "<< /BBox [0 0 10 10] /Resources << /Font << /FOn 12 0 R >> >> /Length " + "0 >>\nstream\n\nendstream", + Stream(""), + "<< /T (field) /FT /Tx /DR << /Font << /FField 13 0 R >> >> >>", + "<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>", + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", + "<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman >>", + "<< /Type /Font /Subtype /Type1 /BaseFont /Symbol >>", + "<< /Type /Font /Subtype /Type1 /BaseFont /ZapfDingbats >>", + "<< /Type /Font /Subtype /Type3 /FontBBox [0 0 1 1] /FontMatrix [1 0 0 1 " + "0 0] /CharProcs << >> /Encoding << /Differences [] >> /FirstChar 0 " + "/LastChar 0 /Widths [0] /Resources << /Font << /FG 15 0 R >> >> >>", + "<< /Type /Font /Subtype /TrueType /BaseFont /Georgia >>", + }); + ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(doc); + EPDF_FONT_FACES faces = EPDFFontFaces_Load(doc.get()); + ASSERT_TRUE(faces); + for (const wchar_t* family : {L"Courier", L"Helvetica", L"Times", L"Symbol", + L"ZapfDingbats", L"Georgia"}) { + EXPECT_TRUE(HasFace(faces, family)) << family; + } + std::optional georgia = FindProgram(faces, L"Georgia"); + ASSERT_TRUE(georgia.has_value()); + EXPECT_EQ(EPDF_FONT_FORMAT_NONE, EPDFFontFaces_GetProgramFormat( + faces, georgia->face, georgia->program)); + EXPECT_EQ( + EPDF_FONT_EMBEDDED_NONE, + EPDFFontFaces_GetProgramEmbedded(faces, georgia->face, georgia->program)); + EXPECT_EQ(0u, EPDFFontFaces_GetProgramObjNum(faces, georgia->face, + georgia->program)); + EXPECT_EQ(-1, EPDFFontFaces_GetProgramEmbeddingPermission( + faces, georgia->face, georgia->program)); + unsigned char hash[32] = {}; + EXPECT_FALSE(EPDFFontFaces_GetProgramHash(faces, georgia->face, + georgia->program, hash)); + bool type3 = false; + for (int face = 0; face < EPDFFontFaces_Count(faces); ++face) { + for (int program = 0; program < EPDFFontFaces_CountPrograms(faces, face); + ++program) { + type3 |= EPDFFontFaces_GetProgramFormat(faces, face, program) == + EPDF_FONT_FORMAT_TYPE3; + } + } + EXPECT_TRUE(type3); + EPDFFontFaces_Close(faces); +} + +// A free text drawn with no font for a character reports it once; marks, +// line breaks and zero width characters are never reported. +TEST_F(EPDFFontEmbedderTest, FreeTextReportsCharactersNoFontHas) { + ScopedRegisteredFonts scoped_fonts; + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FREETEXT)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 250.0f, 350.0f, 320.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ScopedFPDFWideString contents = GetFPDFWideString(L"a\x4E2D\x200B b\r\x4E2D"); + ASSERT_TRUE( + FPDFAnnot_SetStringValue(annot.get(), "Contents", contents.get())); + EPDFDoc_ClearMissingCharacters(document()); + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + EXPECT_EQ(std::vector{L"\x4E2D"}, + MissingCharacters(document())); + EPDFDoc_ClearMissingCharacters(document()); + EXPECT_EQ(0, EPDFDoc_CountMissingCharacters(document())); +} + +// A registered font not authorized for editing never lays out a box's new +// text, even when the box's /DR font was written with it: the box keeps the +// font dictionary it has. +TEST_F(EPDFFontEmbedderTest, UnauthorizedRegisteredFontDoesNotTakeOverABox) { + ScopedRegisteredFonts scoped_fonts; + std::vector font = RobotoPreviewAndPrint(); + ASSERT_FALSE(font.empty()); + EPDF_FONT_ID font_id = EPDFFont_RegisterMemFont64( + "RobotoPP", /*weight=*/400, /*italic=*/0, font.data(), font.size()); + ASSERT_NE(0u, font_id); + ASSERT_EQ(EPDF_FONT_EMBEDDING_PREVIEW_AND_PRINT, + EPDFFont_GetEmbeddingPermission(font_id)); + ASSERT_TRUE(EPDFFont_AuthorizeEditing(font_id)); + + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_WIDGET)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 320.0f, 350.0f, 290.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearanceRegisteredFont(annot.get(), font_id, + 12, 0, 0, 0)); + AttachAndFill(annot.get(), L"name", L"hello"); + + // The same font again, this time not authorized. + EPDFFont_ClearRegisteredFonts(); + font_id = EPDFFont_RegisterMemFont64("RobotoPP", /*weight=*/400, + /*italic=*/0, font.data(), font.size()); + ASSERT_NE(0u, font_id); + ASSERT_FALSE(EPDFFont_IsEditingAuthorized(font_id)); + + ScopedFPDFWideString text = GetFPDFWideString(L"hello"); + ScopedFPDFWideString family = GetFPDFWideString(L"RobotoPP"); + FPDF_WIDESTRING texts[] = {text.get()}; + FPDF_WIDESTRING families[] = {family.get()}; + const int weights[] = {400}; + const FPDF_BOOL italics[] = {false}; + EPDF_FONT_RESOLUTION resolution = EPDFDoc_ResolveFonts( + document(), annot.get(), 1, texts, families, weights, italics); + ASSERT_TRUE(resolution); + EXPECT_EQ(EPDF_FONT_SOURCE_DOCUMENT_FONT, + EPDFFontResolution_GetSource(resolution, 0)); + EPDFFontResolution_Close(resolution); +} diff --git a/public/epdf_annot.h b/public/epdf_annot.h index 684ce09072..a6848ed52a 100644 --- a/public/epdf_annot.h +++ b/public/epdf_annot.h @@ -620,6 +620,28 @@ EPDFAnnot_SetDefaultAppearanceRegisteredFont(FPDF_ANNOTATION annot, unsigned int G, unsigned int B); +// Experimental EmbedPDF Extension API. +// Set the default appearance of a FreeText, Redact or Widget annotation to a +// font program the document already holds, by the object number of its +// stream (EPDFFontFaces_GetProgramObjNum()). /DA names an alias for it this +// session; the appearance generated next installs its /DR entry. Fails for +// a program the layout can't write new text with: not TrueType or OpenType, +// no Unicode cmap, or a licence (OS/2 fsType) that doesn't allow editing. +// +// annot - handle to an annotation. +// program_objnum - the font program stream's object number. +// font_size - the font size to be set. +// R, G, B - the color to be set. +// +// Returns true on success. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetDefaultAppearanceDocumentFont(FPDF_ANNOTATION annot, + unsigned long program_objnum, + float font_size, + unsigned int R, + unsigned int G, + unsigned int B); + // Experimental EmbedPDF Extension API. // Get the default appearance of a FreeText annotation. // @@ -661,6 +683,26 @@ EPDFAnnot_GetDefaultAppearanceFontFace(FPDF_ANNOTATION annot, int* weight, FPDF_BOOL* italic); +// Experimental EmbedPDF Extension API. +// Get the PostScript name of the font a FreeText, Redact or Widget +// annotation's default appearance (/DA) names: the /BaseFont of the font +// dictionary /DR holds for it (a Type0 font's descendant's), without its +// subset tag, or the program's for an alias reserved by +// EPDFAnnot_SetDefaultAppearanceDocumentFont(). +// +// annot - handle to an annotation. +// buffer - receives the name's bytes and a trailing NUL; may be NULL to +// query the length. +// buflen - the size of |buffer| in bytes. +// +// Returns the name's length in bytes, including the trailing NUL, or 0 when +// /DA names no font dictionary (a standard alias, a registered font's alias) +// or the dictionary has no /BaseFont. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAnnot_GetDefaultAppearanceFontPostScriptName(FPDF_ANNOTATION annot, + char* buffer, + unsigned long buflen); + // Experimental EmbedPDF Extension API. // Set the text alignment (/Q) of a FreeText, Redact or Widget annotation. A // widget's own /Q wins over its field's. diff --git a/public/epdf_font.h b/public/epdf_font.h index ef73518bd1..ce23bbfc66 100644 --- a/public/epdf_font.h +++ b/public/epdf_font.h @@ -152,6 +152,297 @@ EPDFFont_AddFallbackFont(EPDF_FONT_ID font_id); // Clear the ordered fallback font list without unregistering fonts. FPDF_EXPORT void FPDF_CALLCONV EPDFFont_ClearFallbackFonts(void); +// Experimental EmbedPDF Extension API. +// The registered fallback fonts in the order new text tries them for a +// style: the closest weight and italic first, then the order they were +// added in; fonts not authorized for editing are left out. The first of +// them that maps a character is the one that draws it. +// +// weight, italic - the style. +// ids - receives up to |capacity| font ids; may be NULL. +// capacity - the number of ids |ids| holds. +// +// Returns the number of fonts in the order, whatever |capacity| is. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFont_GetFallbackOrder(int weight, + FPDF_BOOL italic, + EPDF_FONT_ID* ids, + unsigned long capacity); + +// Experimental EmbedPDF Extension API. +// A registered font's program: the bytes the writer embeds (the static +// instance for a variable font). Returns the byte length, 0 for an unknown +// id; pass NULL/0 to query it. Nothing is copied when |buflen| is too small. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFont_GetProgramData(EPDF_FONT_ID font_id, + void* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// What a registered font's program is: EPDF_FONT_FORMAT_TRUETYPE or +// EPDF_FONT_FORMAT_OPENTYPE_CFF; -1 for an unknown id. +FPDF_EXPORT int FPDF_CALLCONV EPDFFont_GetProgramFormat(EPDF_FONT_ID font_id); + +// Experimental EmbedPDF Extension API. +// The SHA-256 of EPDFFont_GetProgramData()'s bytes into |sha256| (32 +// bytes). Returns false for an unknown id. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFont_GetProgramHash(EPDF_FONT_ID font_id, unsigned char* sha256); + +// Experimental EmbedPDF Extension API. +// The characters a registered font maps, as ranges: |ranges| receives up to +// |range_capacity| pairs (first code point, count). Returns the number of +// ranges, 0 for an unknown id; pass NULL/0 to query it. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFont_GetCoverage(EPDF_FONT_ID font_id, + uint32_t* ranges, + unsigned long range_capacity); + +// Experimental EmbedPDF Extension API. +// A registered font's line metrics as the layout reads them: its hhea +// ascender and descender over its units per em (descent positive). Returns +// false for an unknown id. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFFont_GetMetrics(EPDF_FONT_ID font_id, + float* ascent, + float* descent); + +// Experimental EmbedPDF Extension API. +// A registered font's PostScript name, as its embedded /BaseFont spells it +// (without a subset tag). Bytes and a trailing NUL; returns the length +// including the NUL, 0 for an unknown id; pass NULL/0 to query it. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFont_GetPostScriptName(EPDF_FONT_ID font_id, + char* buffer, + unsigned long buflen); + +// ---- A document's fonts ----------------------------------------------------- + +// Experimental EmbedPDF Extension API. +// The fonts a document uses, as faces (family, weight, italic) and the +// programs behind each: a detached snapshot, read with the EPDFFontFaces_* +// getters and freed with EPDFFontFaces_Close(). Found in the document's +// dictionaries without loading a page: /AcroForm /DR, fields' own /DR, every +// page's resources (inherited ones too), form XObjects, tiling patterns, +// soft masks, Type 3 glyph resources and every state of every annotation +// appearance. Facts only: what may be written with a font is the caller's +// decision. A snapshot goes stale with the next write. +typedef struct epdf_font_faces_t__* EPDF_FONT_FACES; + +// What a program is (EPDFFontFaces_GetProgramFormat). +#define EPDF_FONT_FORMAT_NONE 0 // the PDF only names the font +#define EPDF_FONT_FORMAT_TRUETYPE 1 // TrueType outlines (sfnt) +#define EPDF_FONT_FORMAT_OPENTYPE_CFF 2 // OpenType with CFF outlines (sfnt) +#define EPDF_FONT_FORMAT_CFF 3 // bare CFF (/FontFile3 /Type1C ...) +#define EPDF_FONT_FORMAT_TYPE1 4 // Type 1 (/FontFile) +#define EPDF_FONT_FORMAT_TYPE3 5 // glyphs drawn by content streams + +// How much of the font the PDF holds (EPDFFontFaces_GetProgramEmbedded). +#define EPDF_FONT_EMBEDDED_NONE 0 +#define EPDF_FONT_EMBEDDED_WHOLE 1 +#define EPDF_FONT_EMBEDDED_SUBSET 2 // the /BaseFont carries a subset tag + +// Returns NULL for a NULL document. +FPDF_EXPORT EPDF_FONT_FACES FPDF_CALLCONV +EPDFFontFaces_Load(FPDF_DOCUMENT document); +FPDF_EXPORT void FPDF_CALLCONV EPDFFontFaces_Close(EPDF_FONT_FACES faces); + +// The number of faces; -1 for a NULL snapshot. +FPDF_EXPORT int FPDF_CALLCONV EPDFFontFaces_Count(EPDF_FONT_FACES faces); + +// A face's family as UTF-16LE with a trailing NUL. Returns the length in +// bytes including the NUL, 0 for an index out of range; pass NULL/0 to +// query it. Empty families (some Type 3 fonts) return 2. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontFaces_GetFamily(EPDF_FONT_FACES faces, + int face, + FPDF_WCHAR* buffer, + unsigned long buflen); +// 100 to 900; 0 for an index out of range. +FPDF_EXPORT int FPDF_CALLCONV EPDFFontFaces_GetWeight(EPDF_FONT_FACES faces, + int face); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_IsItalic(EPDF_FONT_FACES faces, int face); + +// The programs behind a face: one per program stream, in the order found, +// plus one per kind for fonts without a program. -1 out of range. +FPDF_EXPORT int FPDF_CALLCONV EPDFFontFaces_CountPrograms(EPDF_FONT_FACES faces, + int face); + +// The program stream's object number: what EPDFDoc_GetFontProgramData() and +// EPDFAnnot_SetDefaultAppearanceDocumentFont() take. 0 for a font without a +// program, an inline program stream, or an index out of range. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontFaces_GetProgramObjNum(EPDF_FONT_FACES faces, int face, int program); +// One of EPDF_FONT_FORMAT_*; -1 out of range. +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontFaces_GetProgramFormat(EPDF_FONT_FACES faces, int face, int program); +// One of EPDF_FONT_EMBEDDED_*; -1 out of range. +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontFaces_GetProgramEmbedded(EPDF_FONT_FACES faces, int face, int program); +// The /BaseFont without its subset tag (a Type0 font's descendant's): bytes +// and a trailing NUL. Returns the length including the NUL, 0 out of range; +// pass NULL/0 to query it. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontFaces_GetProgramPostScriptName(EPDF_FONT_FACES faces, + int face, + int program, + char* buffer, + unsigned long buflen); +// What the program's OS/2 fsType grants: one of EPDF_FONT_EMBEDDING_*; -1 +// when it has no OS/2 table to say (Type 1, bare CFF, no program) or out of +// range. +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontFaces_GetProgramEmbeddingPermission(EPDF_FONT_FACES faces, + int face, + int program); +// Whether the program loads, has glyphs and maps characters through a +// Unicode cmap: whether the layout can write any character it maps. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_ProgramMapsUnicode(EPDF_FONT_FACES faces, int face, int program); +// The SHA-256 of the decoded program into |sha256| (32 bytes). Returns +// false for a font without a program or an index out of range. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_GetProgramHash(EPDF_FONT_FACES faces, + int face, + int program, + unsigned char* sha256); +// The program's line metrics as the layout reads them (em, descent +// positive). Returns false when it doesn't load or out of range. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_GetProgramMetrics(EPDF_FONT_FACES faces, + int face, + int program, + float* ascent, + float* descent); + +// Experimental EmbedPDF Extension API. +// A program the document's fonts use, by its stream's object number: its +// decoded bytes. Returns the length, 0 when |program_objnum| isn't such a +// program; pass NULL/0 to query it. Nothing is copied when |buflen| is too +// small. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetFontProgramData(FPDF_DOCUMENT document, + unsigned long program_objnum, + void* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// The characters such a program maps through its Unicode cmap, as ranges: +// |ranges| receives up to |range_capacity| pairs (first code point, count). +// Returns the number of ranges, 0 when it maps none or isn't such a +// program; pass NULL/0 to query it. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetFontProgramCoverage(FPDF_DOCUMENT document, + unsigned long program_objnum, + uint32_t* ranges, + unsigned long range_capacity); + +// ---- The fonts a write would use -------------------------------------------- + +// Experimental EmbedPDF Extension API. +// Which font each run of text would be drawn with, found the way a write +// finds it, without writing: the same /DA resolution, each face resolved +// once over all of its text, the same per-character fallback. A detached +// snapshot, read with the EPDFFontResolution_* getters and freed with +// EPDFFontResolution_Close(). +typedef struct epdf_font_resolution_t__* EPDF_FONT_RESOLUTION; + +// Where a run's font comes from (EPDFFontResolution_GetSource). +#define EPDF_FONT_SOURCE_STANDARD 0 // one of the standard 14 +#define EPDF_FONT_SOURCE_REGISTERED 1 // a registered font +#define EPDF_FONT_SOURCE_DOCUMENT_PROGRAM 2 // a program the document holds +#define EPDF_FONT_SOURCE_DOCUMENT_FONT 3 // the box's /DA font as it is + +// document - the document. +// annot - the box being edited (its own font counts first), or NULL +// for a new one. +// run_count - the number of runs. +// texts - each run's text, UTF-16LE, NUL-terminated. +// families - each run's family (a family name or a registered font's). +// weights - each run's weight, 100 to 900. +// italics - whether each run is italic. +// +// Returns NULL for a NULL document or arrays. +FPDF_EXPORT EPDF_FONT_RESOLUTION FPDF_CALLCONV +EPDFDoc_ResolveFonts(FPDF_DOCUMENT document, + FPDF_ANNOTATION annot, + int run_count, + const FPDF_WIDESTRING* texts, + const FPDF_WIDESTRING* families, + const int* weights, + const FPDF_BOOL* italics); +FPDF_EXPORT void FPDF_CALLCONV +EPDFFontResolution_Close(EPDF_FONT_RESOLUTION resolution); + +// One of EPDF_FONT_SOURCE_*; -1 out of range. +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontResolution_GetSource(EPDF_FONT_RESOLUTION resolution, int run); +// The registered font for EPDF_FONT_SOURCE_REGISTERED; 0 otherwise. +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV +EPDFFontResolution_GetRegisteredFont(EPDF_FONT_RESOLUTION resolution, int run); +// The program stream's object number for EPDF_FONT_SOURCE_DOCUMENT_PROGRAM, +// and for EPDF_FONT_SOURCE_DOCUMENT_FONT when it embeds one; 0 otherwise. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetProgramObjNum(EPDF_FONT_RESOLUTION resolution, int run); +// The font's /BaseFont: the standard name for EPDF_FONT_SOURCE_STANDARD +// ("Helvetica-Bold"). Bytes and a trailing NUL; returns the length +// including the NUL, 0 when there is none; pass NULL/0 to query it. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetBaseFontName(EPDF_FONT_RESOLUTION resolution, + int run, + char* buffer, + unsigned long buflen); +// The face the run is drawn in: its family (UTF-16LE with a trailing NUL; +// the length in bytes, 0 out of range), weight and italic. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetFamily(EPDF_FONT_RESOLUTION resolution, + int run, + FPDF_WCHAR* buffer, + unsigned long buflen); +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontResolution_GetWeight(EPDF_FONT_RESOLUTION resolution, int run); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontResolution_IsItalic(EPDF_FONT_RESOLUTION resolution, int run); +// Whether the face isn't the one the run asked for (a substitute). +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontResolution_IsDegraded(EPDF_FONT_RESOLUTION resolution, int run); +// The fallback fonts for the run's style, in the order they are tried +// (EPDFFont_GetFallbackOrder()). Returns their number; pass NULL/0 to +// query it. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetFallbacks(EPDF_FONT_RESOLUTION resolution, + int run, + EPDF_FONT_ID* ids, + unsigned long capacity); +// The characters (graphemes) no font has: they would be drawn as missing +// glyphs. Count, then each as UTF-16LE with a trailing NUL (the length in +// bytes, 0 out of range). +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontResolution_CountMissingCharacters(EPDF_FONT_RESOLUTION resolution); +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetMissingCharacter(EPDF_FONT_RESOLUTION resolution, + int index, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// ---- Characters a write drew with no font ----------------------------------- + +// Experimental EmbedPDF Extension API. +// The characters (graphemes) appearances generated for |document| drew with +// no font that has them, first seen first, since the list was last cleared: +// clear it before a write and read it after. At most 256 are kept. +FPDF_EXPORT void FPDF_CALLCONV +EPDFDoc_ClearMissingCharacters(FPDF_DOCUMENT document); +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_CountMissingCharacters(FPDF_DOCUMENT document); +// Each as UTF-16LE with a trailing NUL: the length in bytes, 0 out of +// range; pass NULL/0 to query it. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetMissingCharacter(FPDF_DOCUMENT document, + int index, + FPDF_WCHAR* buffer, + unsigned long buflen); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus From fee7adaa0aa1a2f795dbc9570cf573d6faf9393a Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Mon, 19 Oct 2026 20:27:25 +0300 Subject: [PATCH 02/16] Build display programs for document fonts a browser can't load A document font that is Type 1, bare CFF or a TrueType without a Unicode cmap gets a display program: an OpenType font with the program's outlines (read unscaled and unhinted, written as CFF charstrings), a Unicode cmap holding exactly the characters the engine writes with the font through the PDF's encoding, and its /Widths advances. Built once per program stream, the same bytes every time. EPDFFontFaces_GetProgramDisplayHash names what a browser loads for a program, EPDFDoc_GetFontDisplayProgramData returns it, and coverage reports the display program's characters. --- core/fpdfdoc/BUILD.gn | 7 +- core/fpdfdoc/cpdf_displayprogram.cpp | 187 ++++++++ core/fpdfdoc/cpdf_displayprogram.h | 50 ++ core/fpdfdoc/cpdf_documentfonts.cpp | 96 +++- core/fpdfdoc/cpdf_documentfonts.h | 28 +- core/fxge/BUILD.gn | 6 + core/fxge/cfx_face.h | 6 + core/fxge/cfx_glyphoutline.cpp | 106 +++++ core/fxge/cfx_glyphoutline.h | 33 ++ core/fxge/cfx_opentypewriter.cpp | 661 +++++++++++++++++++++++++++ core/fxge/cfx_opentypewriter.h | 54 +++ fpdfsdk/epdf_c_api_test.c | 2 + fpdfsdk/epdf_font.cpp | 25 + fpdfsdk/epdf_font_embeddertest.cpp | 248 ++++++++++ public/epdf_font.h | 36 +- 15 files changed, 1525 insertions(+), 20 deletions(-) create mode 100644 core/fpdfdoc/cpdf_displayprogram.cpp create mode 100644 core/fpdfdoc/cpdf_displayprogram.h create mode 100644 core/fxge/cfx_glyphoutline.cpp create mode 100644 core/fxge/cfx_glyphoutline.h create mode 100644 core/fxge/cfx_opentypewriter.cpp create mode 100644 core/fxge/cfx_opentypewriter.h diff --git a/core/fpdfdoc/BUILD.gn b/core/fpdfdoc/BUILD.gn index b4070b9f2c..4f4bd25da6 100644 --- a/core/fpdfdoc/BUILD.gn +++ b/core/fpdfdoc/BUILD.gn @@ -26,8 +26,11 @@ source_set("fpdfdoc") { # EmbedPDF: a font as a face (family, weight, italic), one reader for all. "cpdf_fontface.cpp", "cpdf_fontface.h", - # EmbedPDF: the fonts a document uses and what each program is, and the - # fonts a write would draw text with (a resolver read). + # EmbedPDF: the fonts a document uses and what each program is, a + # document font as a browser can draw it, and the fonts a write would + # draw text with (a resolver read). + "cpdf_displayprogram.cpp", + "cpdf_displayprogram.h", "cpdf_documentfonts.cpp", "cpdf_documentfonts.h", "cpdf_fontresolver.cpp", diff --git a/core/fpdfdoc/cpdf_displayprogram.cpp b/core/fpdfdoc/cpdf_displayprogram.cpp new file mode 100644 index 0000000000..548dd5adcd --- /dev/null +++ b/core/fpdfdoc/cpdf_displayprogram.cpp @@ -0,0 +1,187 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fpdfdoc/cpdf_displayprogram.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "core/fpdfapi/font/cpdf_font.h" +#include "core/fpdfapi/page/cpdf_docpagedata.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfdoc/cpdf_fontface.h" +#include "core/fxcrt/fx_string.h" +#include "core/fxge/cfx_face.h" +#include "core/fxge/cfx_font.h" +#include "core/fxge/cfx_glyphoutline.h" +#include "core/fxge/cfx_opentypewriter.h" +#include "core/fxge/fx_font.h" + +namespace { + +// What the engine writes for a character: the glyph it draws and the advance +// it lays it out with, in thousandths of the font size. +struct Written { + uint32_t glyph; + int width; +}; + +bool operator<(const Written& left, const Written& right) { + return std::tie(left.glyph, left.width) < std::tie(right.glyph, right.width); +} + +// Every character the engine can write with |font|: the layout asks the +// font's encoding for each one's code (CharCodeFromUnicode) and draws that +// code's glyph, so a character is written when that code exists and has a +// glyph. The layout writes Basic Multilingual Plane characters only. The +// candidates are what the font's codes read as. +std::map WrittenCharacters(CPDF_Font* font) { + std::map written; + const uint32_t last_code = font->IsCIDFont() ? 0xffff : 0xff; + for (uint32_t code = 0; code <= last_code; ++code) { + const WideString text = font->UnicodeFromCharCode(code); + if (text.GetLength() != 1) { + continue; + } + const uint32_t scalar = static_cast(text[0]); + if (scalar == 0 || scalar > 0xffff || + (scalar >= 0xd800 && scalar <= 0xdfff) || written.contains(scalar)) { + continue; + } + const uint32_t write_code = + font->CharCodeFromUnicode(static_cast(scalar)); + if (write_code == CPDF_Font::kInvalidCharCode || write_code == 0) { + continue; + } + bool vertical = false; + const int glyph = font->GlyphFromCharCode(write_code, &vertical); + if (glyph <= 0) { + continue; + } + written[scalar] = {static_cast(glyph), + font->GetCharWidth(write_code)}; + } + return written; +} + +// |text| with only what a name table and a PostScript name may hold. +ByteString AsciiName(const WideString& text, bool postscript) { + ByteString name; + for (wchar_t c : text) { + const bool letter_or_digit = (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9'); + if (letter_or_digit || c == '-' || (!postscript && c == ' ')) { + name += static_cast(c); + } + if (name.GetLength() >= 63) { + break; + } + } + return name; +} + +} // namespace + +CPDF_DisplayProgram::CPDF_DisplayProgram() = default; +CPDF_DisplayProgram::CPDF_DisplayProgram(CPDF_DisplayProgram&&) noexcept = + default; +CPDF_DisplayProgram& CPDF_DisplayProgram::operator=( + CPDF_DisplayProgram&&) noexcept = default; +CPDF_DisplayProgram::~CPDF_DisplayProgram() = default; + +std::optional BuildDisplayProgram( + CPDF_Document* doc, + const CPDF_Dictionary* font_dict, + const ByteString& postscript_name, + const CPDF_FontFace& face) { + RetainPtr font = CPDF_DocPageData::FromDocument(doc)->GetFont( + pdfium::WrapRetain(const_cast(font_dict))); + if (!font || font->IsType3Font() || !font->IsEmbedded()) { + return std::nullopt; + } + CFX_Font* program = font->GetFont(); + RetainPtr program_face = program->GetFace(); + // A program that didn't load is drawn with a substitute: nothing of its own. + if (!program_face || program->GetSubstFont()) { + return std::nullopt; + } + const std::map written = WrittenCharacters(font.Get()); + if (written.empty()) { + return std::nullopt; + } + + CFX_OpenTypeFont display; + display.units_per_em = + program_face->GetUnitsPerEm() ? program_face->GetUnitsPerEm() : 1000; + const float em = display.units_per_em / 1000.0f; + auto advance = [&](int thousandths) { + return static_cast( + std::clamp(std::lround(thousandths * em), 0, 0xffff)); + }; + auto glyph_of = [&](uint32_t source) { + CFX_OpenTypeFont::Glyph glyph; + std::optional outline = + program_face->LoadUnscaledGlyphOutline(source); + if (outline.has_value()) { + glyph.outline = std::move(*outline); + } + return glyph; + }; + + // .notdef first; a browser never draws it, the cmap only maps written + // characters. + display.glyphs.push_back(glyph_of(0)); + display.glyphs.back().advance = advance(500); + // One glyph per source glyph and advance: two codes may reach one glyph + // with different widths. + std::map glyph_index; + for (const auto& [scalar, character] : written) { + auto [it, added] = glyph_index.emplace( + character, static_cast(display.glyphs.size())); + if (added) { + if (display.glyphs.size() >= 0xffff) { + return std::nullopt; + } + display.glyphs.push_back(glyph_of(character.glyph)); + display.glyphs.back().advance = advance(character.width); + } + display.cmap.emplace_back(scalar, it->second); + } + + float ascent = 0; + float descent = 0; + ProgramLineMetrics(program, &ascent, &descent); + display.ascent = static_cast(std::lround(ascent * display.units_per_em)); + display.descent = + -static_cast(std::lround(descent * display.units_per_em)); + display.postscript_name = + AsciiName(WideString::FromLatin1(postscript_name.AsStringView()), + /*postscript=*/true); + if (display.postscript_name.IsEmpty()) { + display.postscript_name = "EmbedPDFDisplay"; + } + display.family = AsciiName(face.family, /*postscript=*/false); + if (display.family.IsEmpty()) { + display.family = display.postscript_name; + } + display.weight = face.weight; + display.italic = face.italic; + display.italic_angle = static_cast(font->GetItalicAngle()); + display.fixed_pitch = + (font->GetFontFlags() & pdfium::kFontStyleFixedPitch) != 0; + CPDF_DisplayProgram result; + result.bytes = WriteOpenTypeCFF(display); + if (result.bytes.empty()) { + return std::nullopt; + } + for (const auto& [scalar, glyph] : display.cmap) { + result.characters.push_back(scalar); + } + return result; +} diff --git a/core/fpdfdoc/cpdf_displayprogram.h b/core/fpdfdoc/cpdf_displayprogram.h new file mode 100644 index 0000000000..fa06df5789 --- /dev/null +++ b/core/fpdfdoc/cpdf_displayprogram.h @@ -0,0 +1,50 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 +// +// EmbedPDF(fonts): a document font as a browser can draw it. A browser loads +// TrueType and OpenType, not Type 1 or bare CFF, and it reaches glyphs through +// a font's own cmap and spaces them by its own metrics, where the engine goes +// through the PDF's encoding and its /Widths. So the engine's view of the font +// is written as an OpenType font of its own: the same glyphs, reached by the +// same characters, at the same advances. + +#ifndef CORE_FPDFDOC_CPDF_DISPLAYPROGRAM_H_ +#define CORE_FPDFDOC_CPDF_DISPLAYPROGRAM_H_ + +#include + +#include +#include + +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/data_vector.h" + +class CPDF_Dictionary; +class CPDF_Document; +struct CPDF_FontFace; + +struct CPDF_DisplayProgram { + CPDF_DisplayProgram(); + CPDF_DisplayProgram(CPDF_DisplayProgram&&) noexcept; + CPDF_DisplayProgram& operator=(CPDF_DisplayProgram&&) noexcept; + ~CPDF_DisplayProgram(); + + // An OpenType font with CFF outlines. + DataVector bytes; + // The characters its cmap maps, in order. + std::vector characters; +}; + +// The display program of |font_dict|: the glyphs its embedded program draws; +// a Unicode cmap holding exactly the characters the engine writes with the +// font, each mapped through the PDF's encoding to the glyph it draws; and the +// advances the engine lays them out with. |postscript_name| and |face| name +// it. nullopt when the font has no outlines of its own (Type 3, nothing +// embedded, a program that doesn't load) or writes no character. +std::optional BuildDisplayProgram( + CPDF_Document* doc, + const CPDF_Dictionary* font_dict, + const ByteString& postscript_name, + const CPDF_FontFace& face); + +#endif // CORE_FPDFDOC_CPDF_DISPLAYPROGRAM_H_ diff --git a/core/fpdfdoc/cpdf_documentfonts.cpp b/core/fpdfdoc/cpdf_documentfonts.cpp index 204721f4ea..5e6fbd0750 100644 --- a/core/fpdfdoc/cpdf_documentfonts.cpp +++ b/core/fpdfdoc/cpdf_documentfonts.cpp @@ -14,6 +14,7 @@ #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fpdfapi/parser/cpdf_stream_acc.h" +#include "core/fpdfdoc/cpdf_displayprogram.h" #include "core/fpdfdoc/cpdf_fontface.h" #include "core/fxcrt/utf16.h" #include "core/fxge/cfx_face.h" @@ -227,6 +228,23 @@ bool IsSfnt(CPDF_DocumentFonts::Format format) { format == CPDF_DocumentFonts::Format::kOpenTypeCFF; } +// |scalars| as [start, start + count) ranges, in order. +std::vector> RangesOf( + std::vector scalars) { + std::ranges::sort(scalars); + std::vector> ranges; + for (uint32_t scalar : scalars) { + if (!ranges.empty() && + ranges.back().first + ranges.back().second >= scalar) { + ranges.back().second = + std::max(ranges.back().second, scalar - ranges.back().first + 1); + continue; + } + ranges.emplace_back(scalar, 1); + } + return ranges; +} + } // namespace CPDF_DocumentFonts::CPDF_DocumentFonts(CPDF_Document* doc) : doc_(doc) {} @@ -271,6 +289,10 @@ std::vector CPDF_DocumentFonts::CollectFaces() { program.format = format; } program.postscript_name = BaseFontNameWithoutSubsetTag(base_font); + if (stream) { + program.display_sha256 = DisplayHashOf(program, font.Get(), face.family, + face.weight, face.italic); + } const bool known = std::ranges::any_of(entry.programs, [&](const Program& other) { return program.objnum != 0 @@ -375,10 +397,69 @@ DataVector CPDF_DocumentFonts::ProgramData(uint32_t objnum) { return bytes; } +DataVector CPDF_DocumentFonts::DisplayHashOf( + const Program& program, + const CPDF_Dictionary* font_dict, + const WideString& family, + int weight, + bool italic) { + if (program.sha256.empty()) { + return {}; + } + // A browser loads it as it is. + if (IsSfnt(program.format) && program.maps_unicode) { + return program.sha256; + } + // An inline program can't be addressed, so nothing could fetch it. + if (program.objnum == 0) { + return {}; + } + auto it = inspected_.find(program.objnum); + if (it == inspected_.end()) { + return {}; + } + Inspected& inspected = it->second; + if (!inspected.display_built) { + inspected.display_built = true; + std::optional built = + BuildDisplayProgram(doc_, font_dict, program.postscript_name, + CPDF_FontFace{family, weight, italic}); + if (built.has_value()) { + Display display; + display.sha256 = CryptSha256Generate(built->bytes); + display.coverage = RangesOf(std::move(built->characters)); + display.bytes = std::move(built->bytes); + inspected.display = std::move(display); + } + } + return inspected.display.has_value() ? inspected.display->sha256 + : DataVector(); +} + +DataVector CPDF_DocumentFonts::DisplayProgramData(uint32_t objnum) { + Inspected* inspected = FindInspected(objnum); + if (!inspected) { + return {}; + } + if (inspected->display.has_value()) { + return inspected->display->bytes; + } + if (IsSfnt(inspected->facts.format) && inspected->facts.maps_unicode) { + return ProgramData(objnum); + } + return {}; +} + std::vector> CPDF_DocumentFonts::ProgramCoverage( uint32_t objnum) { Inspected* inspected = FindInspected(objnum); - if (!inspected || !inspected->facts.maps_unicode) { + if (!inspected) { + return {}; + } + if (inspected->display.has_value()) { + return inspected->display->coverage; + } + if (!inspected->facts.maps_unicode) { return {}; } if (inspected->coverage.has_value()) { @@ -400,17 +481,8 @@ std::vector> CPDF_DocumentFonts::ProgramCoverage( } } } - std::ranges::sort(scalars); - std::vector> ranges; - for (uint32_t scalar : scalars) { - if (!ranges.empty() && - ranges.back().first + ranges.back().second >= scalar) { - ranges.back().second = - std::max(ranges.back().second, scalar - ranges.back().first + 1); - continue; - } - ranges.emplace_back(scalar, 1); - } + std::vector> ranges = + RangesOf(std::move(scalars)); inspected->coverage = ranges; return ranges; } diff --git a/core/fpdfdoc/cpdf_documentfonts.h b/core/fpdfdoc/cpdf_documentfonts.h index ecdcd87849..20d0bc6489 100644 --- a/core/fpdfdoc/cpdf_documentfonts.h +++ b/core/fpdfdoc/cpdf_documentfonts.h @@ -55,6 +55,10 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { bool maps_unicode = false; // SHA-256 of the decoded program; empty without one. DataVector sha256; + // SHA-256 of what a browser loads for it: the program itself when it is + // an sfnt that maps characters through a Unicode cmap, else its display + // program (cpdf_displayprogram.h). Empty when a browser can't draw it. + DataVector display_sha256; // Line metrics as the layout reads them (em, descent positive); 0 when // the program doesn't load. float ascent = 0; @@ -89,8 +93,12 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { // A program found by the walk, by its stream's object number: its decoded // bytes, the program as the layout reads it. Empty for any other number. DataVector ProgramData(uint32_t objnum); - // The scalars it maps through its Unicode cmap, as [start, start + count) - // ranges in order; empty when it maps none or |objnum| isn't one. + // What a browser loads for such a program (see Program::display_sha256); + // empty when it can't draw it or |objnum| isn't one. + DataVector DisplayProgramData(uint32_t objnum); + // The scalars a browser draws with it, which are the ones the engine writes + // with it, as [start, start + count) ranges in order; empty when it maps + // none or |objnum| isn't one. std::vector> ProgramCoverage(uint32_t objnum); // The program stream itself, for the writer; null when |objnum| isn't one. RetainPtr ProgramStream(uint32_t objnum); @@ -121,16 +129,32 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { void ClearMissingCharacters() { missing_characters_.clear(); } private: + struct Display { + DataVector bytes; + DataVector sha256; + std::vector> coverage; + }; + struct Inspected { RetainPtr stream; // the version inspected size_t raw_size = 0; Program facts; std::optional>> coverage; + // Built from the first font dictionary found to use the stream. + bool display_built = false; + std::optional display; }; // The facts about |stream|, inspected once per version. Program Inspect(RetainPtr stream, Format format); Inspected* FindInspected(uint32_t objnum); + // |program|'s display hash, its display program built through + // |font_dict| once per stream version when it needs one. + DataVector DisplayHashOf(const Program& program, + const CPDF_Dictionary* font_dict, + const WideString& family, + int weight, + bool italic); UnownedPtr const doc_; std::map inspected_; // by program stream objnum diff --git a/core/fxge/BUILD.gn b/core/fxge/BUILD.gn index 202413795b..b94673a5fa 100644 --- a/core/fxge/BUILD.gn +++ b/core/fxge/BUILD.gn @@ -52,10 +52,16 @@ source_set("fxge") { "cfx_glyphbitmap.h", "cfx_glyphcache.cpp", "cfx_glyphcache.h", + # EmbedPDF(fonts): a glyph's unscaled outline, and an OpenType font + # written from outlines (a document font's display program). + "cfx_glyphoutline.cpp", + "cfx_glyphoutline.h", "cfx_graphstate.cpp", "cfx_graphstate.h", "cfx_graphstatedata.cpp", "cfx_graphstatedata.h", + "cfx_opentypewriter.cpp", + "cfx_opentypewriter.h", "cfx_path.cpp", "cfx_path.h", "cfx_renderdevice.cpp", diff --git a/core/fxge/cfx_face.h b/core/fxge/cfx_face.h index 223b2cc480..e68cb5f826 100644 --- a/core/fxge/cfx_face.h +++ b/core/fxge/cfx_face.h @@ -19,6 +19,7 @@ #include "core/fxcrt/observed_ptr.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/span.h" +#include "core/fxge/cfx_glyphoutline.h" // EmbedPDF(fonts) #include "core/fxge/fx_font.h" #include "core/fxge/fx_fontencoding.h" @@ -125,6 +126,11 @@ class CFX_Face final : public Retainable, public Observable { int dest_width, bool is_vertical, const CFX_SubstFont* subst_font); + // EmbedPDF(fonts): |glyph_index| in font units, unscaled and unhinted, so + // another program can carry it exactly (cfx_glyphoutline.cpp). nullopt + // when the glyph doesn't load or isn't an outline. + std::optional LoadUnscaledGlyphOutline( + uint32_t glyph_index); int GetGlyphTTWidth(uint32_t glyph_index) const; int GetGlyphWidth(uint32_t glyph_index, int dest_width, diff --git a/core/fxge/cfx_glyphoutline.cpp b/core/fxge/cfx_glyphoutline.cpp new file mode 100644 index 0000000000..7b3b3697ad --- /dev/null +++ b/core/fxge/cfx_glyphoutline.cpp @@ -0,0 +1,106 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fxge/cfx_glyphoutline.h" + +#include + +#include "core/fxge/cfx_face.h" + +#if defined(PDF_ENABLE_FREETYPE) +#include "core/fxge/freetype/fx_freetype.h" +#endif + +#if defined(PDF_ENABLE_FREETYPE) +namespace { + +struct Decomposition { + CFX_GlyphOutline* outline; + CFX_PointF current; +}; + +CFX_PointF PointOf(const FT_Vector* vector) { + return CFX_PointF(static_cast(vector->x), + static_cast(vector->y)); +} + +int MoveTo(const FT_Vector* to, void* user) { + auto* decomposition = static_cast(user); + decomposition->current = PointOf(to); + decomposition->outline->segments.push_back( + {CFX_GlyphOutline::Op::kMove, {decomposition->current, {}, {}}}); + return 0; +} + +int LineTo(const FT_Vector* to, void* user) { + auto* decomposition = static_cast(user); + decomposition->current = PointOf(to); + decomposition->outline->segments.push_back( + {CFX_GlyphOutline::Op::kLine, {decomposition->current, {}, {}}}); + return 0; +} + +// A quadratic Bézier is the cubic whose control points are two thirds of the +// way from each end to its one control point. +int ConicTo(const FT_Vector* control, const FT_Vector* to, void* user) { + auto* decomposition = static_cast(user); + const CFX_PointF from = decomposition->current; + const CFX_PointF via = PointOf(control); + const CFX_PointF end = PointOf(to); + const CFX_PointF first(from.x + (via.x - from.x) * 2 / 3, + from.y + (via.y - from.y) * 2 / 3); + const CFX_PointF second(end.x + (via.x - end.x) * 2 / 3, + end.y + (via.y - end.y) * 2 / 3); + decomposition->current = end; + decomposition->outline->segments.push_back( + {CFX_GlyphOutline::Op::kCubic, {first, second, end}}); + return 0; +} + +int CubicTo(const FT_Vector* first, + const FT_Vector* second, + const FT_Vector* to, + void* user) { + auto* decomposition = static_cast(user); + decomposition->current = PointOf(to); + decomposition->outline->segments.push_back( + {CFX_GlyphOutline::Op::kCubic, + {PointOf(first), PointOf(second), decomposition->current}}); + return 0; +} + +} // namespace +#endif // defined(PDF_ENABLE_FREETYPE) + +std::optional CFX_Face::LoadUnscaledGlyphOutline( + uint32_t glyph_index) { +#if defined(PDF_ENABLE_FREETYPE) + FT_FaceRec* rec = GetRec(); + if (!rec) { + return std::nullopt; + } + if (FT_Load_Glyph( + rec, glyph_index, + FT_LOAD_NO_SCALE | FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP)) { + return std::nullopt; + } + if (rec->glyph->format != FT_GLYPH_FORMAT_OUTLINE) { + return std::nullopt; + } + CFX_GlyphOutline outline; + Decomposition decomposition{&outline, CFX_PointF()}; + FT_Outline_Funcs funcs; + funcs.move_to = MoveTo; + funcs.line_to = LineTo; + funcs.conic_to = ConicTo; + funcs.cubic_to = CubicTo; + funcs.shift = 0; + funcs.delta = 0; + if (FT_Outline_Decompose(&rec->glyph->outline, &funcs, &decomposition)) { + return std::nullopt; + } + return outline; +#else + return std::nullopt; +#endif // defined(PDF_ENABLE_FREETYPE) +} diff --git a/core/fxge/cfx_glyphoutline.h b/core/fxge/cfx_glyphoutline.h new file mode 100644 index 0000000000..9738dfb020 --- /dev/null +++ b/core/fxge/cfx_glyphoutline.h @@ -0,0 +1,33 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 +// +// EmbedPDF(fonts): a glyph's outline in font units, as the font program +// draws it: unscaled and unhinted, so another program can carry it exactly. + +#ifndef CORE_FXGE_CFX_GLYPHOUTLINE_H_ +#define CORE_FXGE_CFX_GLYPHOUTLINE_H_ + +#include + +#include +#include + +#include "core/fxcrt/fx_coordinates.h" + +struct CFX_GlyphOutline { + enum class Op : uint8_t { + kMove, // starts a contour at points[0] + kLine, // to points[0] + kCubic, // through points[0] and points[1] to points[2] + }; + struct Segment { + Op op; + std::array points; + }; + + // Contours close where they started. Empty for a glyph that draws nothing + // (a space). A quadratic curve is the cubic it is. + std::vector segments; +}; + +#endif // CORE_FXGE_CFX_GLYPHOUTLINE_H_ diff --git a/core/fxge/cfx_opentypewriter.cpp b/core/fxge/cfx_opentypewriter.cpp new file mode 100644 index 0000000000..ed578b3894 --- /dev/null +++ b/core/fxge/cfx_opentypewriter.cpp @@ -0,0 +1,661 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fxge/cfx_opentypewriter.h" + +#include + +#include +#include +#include +#include + +#include "core/fxcrt/numerics/safe_conversions.h" + +CFX_OpenTypeFont::CFX_OpenTypeFont() = default; +CFX_OpenTypeFont::CFX_OpenTypeFont(CFX_OpenTypeFont&&) noexcept = default; +CFX_OpenTypeFont& CFX_OpenTypeFont::operator=(CFX_OpenTypeFont&&) noexcept = + default; +CFX_OpenTypeFont::~CFX_OpenTypeFont() = default; + +namespace { + +// Big-endian bytes, as every OpenType table and CFF structure is. +class Writer { + public: + // Each writes the low bytes of |value|: the caller has checked it fits. + Writer& U8(uint64_t value) { + bytes_.push_back(static_cast(value)); + return *this; + } + Writer& U16(uint64_t value) { return U8(value >> 8).U8(value); } + Writer& I16(int value) { return U16(static_cast(value)); } + Writer& U32(uint64_t value) { return U16(value >> 16).U16(value); } + // A 16.16 fixed-point number. + Writer& Fixed(double value) { + return U32(static_cast( + static_cast(std::lround(value * 65536.0)))); + } + Writer& Tag(const char* tag) { + for (int i = 0; i < 4; ++i) { + U8(static_cast(tag[i])); + } + return *this; + } + Writer& Zeros(size_t count) { + bytes_.insert(bytes_.end(), count, 0); + return *this; + } + Writer& Bytes(const DataVector& bytes) { + bytes_.insert(bytes_.end(), bytes.begin(), bytes.end()); + return *this; + } + size_t size() const { return bytes_.size(); } + DataVector Take() { return std::move(bytes_); } + + private: + DataVector bytes_; +}; + +// ---- Bounds ---------------------------------------------------------------- + +struct Bounds { + float x_min = std::numeric_limits::max(); + float y_min = std::numeric_limits::max(); + float x_max = std::numeric_limits::lowest(); + float y_max = std::numeric_limits::lowest(); + + bool empty() const { return x_min > x_max; } + void Add(const CFX_PointF& point) { + x_min = std::min(x_min, point.x); + y_min = std::min(y_min, point.y); + x_max = std::max(x_max, point.x); + y_max = std::max(y_max, point.y); + } + void Add(const Bounds& other) { + if (!other.empty()) { + Add(CFX_PointF(other.x_min, other.y_min)); + Add(CFX_PointF(other.x_max, other.y_max)); + } + } +}; + +// The bounds of every point the outline names, control points included: the +// glyph lies inside them. +Bounds BoundsOf(const CFX_GlyphOutline& outline) { + Bounds bounds; + for (const CFX_GlyphOutline::Segment& segment : outline.segments) { + const size_t points = segment.op == CFX_GlyphOutline::Op::kCubic ? 3 : 1; + for (size_t i = 0; i < points; ++i) { + bounds.Add(segment.points[i]); + } + } + return bounds; +} + +int Floor(float value) { + return pdfium::saturated_cast(std::floor(value)); +} + +int Ceil(float value) { + return pdfium::saturated_cast(std::ceil(value)); +} + +// ---- CFF ------------------------------------------------------------------- + +// A Type 2 charstring operand: an integer in its shortest form, anything +// else as 16.16 fixed point. +void CharstringNumber(DataVector* out, float value) { + const float rounded = std::round(value); + if (std::fabs(value - rounded) < 1.0f / 65536 && std::fabs(rounded) < 32768) { + const int number = static_cast(rounded); + if (number >= -107 && number <= 107) { + out->push_back(static_cast(number + 139)); + } else if (number >= 108 && number <= 1131) { + const int offset = number - 108; + out->push_back(static_cast((offset >> 8) + 247)); + out->push_back(static_cast(offset & 0xff)); + } else if (number >= -1131 && number <= -108) { + const int offset = -number - 108; + out->push_back(static_cast((offset >> 8) + 251)); + out->push_back(static_cast(offset & 0xff)); + } else { + out->push_back(28); + out->push_back(static_cast((number >> 8) & 0xff)); + out->push_back(static_cast(number & 0xff)); + } + return; + } + const uint32_t fixed = static_cast( + static_cast(std::lround(static_cast(value) * 65536.0))); + out->push_back(255); + out->push_back(static_cast(fixed >> 24)); + out->push_back(static_cast(fixed >> 16)); + out->push_back(static_cast(fixed >> 8)); + out->push_back(static_cast(fixed)); +} + +// What a charstring operand encodes as: the value a reader gets back. +float CharstringValue(float value) { + const float rounded = std::round(value); + if (std::fabs(value - rounded) < 1.0f / 65536 && std::fabs(rounded) < 32768) { + return rounded; + } + return static_cast(std::lround(static_cast(value) * 65536.0) / + 65536.0); +} + +// The glyph as Type 2 operators: rmoveto, rlineto and rrcurveto from the +// pen's position, then endchar. Each contour closes at the next rmoveto or +// at endchar. No width: the advance is hmtx's. +DataVector Charstring(const CFX_GlyphOutline& outline) { + constexpr uint8_t kRLineTo = 5; + constexpr uint8_t kRRCurveTo = 8; + constexpr uint8_t kEndChar = 14; + constexpr uint8_t kRMoveTo = 21; + DataVector out; + // The pen as a reader will have it, so rounding never accumulates. + CFX_PointF pen; + auto relative = [&](const CFX_PointF& point) { + const float dx = CharstringValue(point.x - pen.x); + const float dy = CharstringValue(point.y - pen.y); + CharstringNumber(&out, dx); + CharstringNumber(&out, dy); + pen = CFX_PointF(pen.x + dx, pen.y + dy); + }; + for (const CFX_GlyphOutline::Segment& segment : outline.segments) { + switch (segment.op) { + case CFX_GlyphOutline::Op::kMove: + relative(segment.points[0]); + out.push_back(kRMoveTo); + break; + case CFX_GlyphOutline::Op::kLine: + relative(segment.points[0]); + out.push_back(kRLineTo); + break; + case CFX_GlyphOutline::Op::kCubic: + relative(segment.points[0]); + relative(segment.points[1]); + relative(segment.points[2]); + out.push_back(kRRCurveTo); + break; + } + } + out.push_back(kEndChar); + return out; +} + +// A CFF DICT integer operand in its shortest form. +void DictInteger(Writer* out, int number) { + if (number >= -107 && number <= 107) { + out->U8(number + 139); + } else if (number >= 108 && number <= 1131) { + out->U8(((number - 108) >> 8) + 247).U8((number - 108) & 0xff); + } else if (number >= -1131 && number <= -108) { + out->U8(((-number - 108) >> 8) + 251).U8((-number - 108) & 0xff); + } else if (number >= -32768 && number <= 32767) { + out->U8(28).I16(number); + } else { + out->U8(29).U32(static_cast(number)); + } +} + +// An offset operand, always five bytes, so a DICT's size doesn't depend on +// the offsets it holds. +void DictOffset(Writer* out, uint32_t offset) { + out->U8(29).U32(offset); +} + +// A CFF DICT real operand: its decimal digits as nibbles. +void DictReal(Writer* out, double value) { + char text[32]; + snprintf(text, sizeof(text), "%.10g", value); + std::vector nibbles; + for (const char* c = text; *c; ++c) { + if (*c >= '0' && *c <= '9') { + nibbles.push_back(static_cast(*c - '0')); + } else if (*c == '.') { + nibbles.push_back(0xa); + } else if (*c == '-') { + nibbles.push_back(0xe); + } else if (*c == 'e' || *c == 'E') { + if (c[1] == '-') { + nibbles.push_back(0xc); + ++c; + } else { + nibbles.push_back(0xb); + if (c[1] == '+') { + ++c; + } + } + } + } + nibbles.push_back(0xf); + if (nibbles.size() % 2) { + nibbles.push_back(0xf); + } + out->U8(30); + for (size_t i = 0; i < nibbles.size(); i += 2) { + out->U8((nibbles[i] << 4) | nibbles[i + 1]); + } +} + +// A CFF INDEX of |items|. +void Index(Writer* out, const std::vector>& items) { + out->U16(pdfium::checked_cast(items.size())); + if (items.empty()) { + return; + } + size_t total = 1; + for (const DataVector& item : items) { + total += item.size(); + } + const uint8_t offset_size = total <= 0xff ? 1 + : total <= 0xffff ? 2 + : total <= 0xffffff ? 3 + : 4; + out->U8(offset_size); + auto offset = [&](size_t value) { + for (int shift = (offset_size - 1) * 8; shift >= 0; shift -= 8) { + out->U8(static_cast(value >> shift)); + } + }; + size_t at = 1; + offset(at); + for (const DataVector& item : items) { + at += item.size(); + offset(at); + } + for (const DataVector& item : items) { + out->Bytes(item); + } +} + +DataVector BytesOf(const ByteString& text) { + return DataVector(text.unsigned_span().begin(), + text.unsigned_span().end()); +} + +// The first string ID after CFF's standard strings. +constexpr int kFirstCustomString = 391; + +// A name-keyed CFF font: glyph names "g1", "g2", … (the cmap names the +// characters), no hints, no subroutines. +DataVector CffTable(const CFX_OpenTypeFont& font, + const Bounds& font_bounds) { + const size_t glyph_count = font.glyphs.size(); + std::vector> names = {BytesOf(font.postscript_name)}; + std::vector> strings; + for (size_t glyph = 1; glyph < glyph_count; ++glyph) { + strings.push_back(BytesOf(ByteString::Format("g%zu", glyph))); + } + std::vector> charstrings; + for (const CFX_OpenTypeFont::Glyph& glyph : font.glyphs) { + charstrings.push_back(Charstring(glyph.outline)); + } + + Writer charset; + charset.U8(0); + for (size_t glyph = 1; glyph < glyph_count; ++glyph) { + charset.U16(pdfium::checked_cast(kFirstCustomString + glyph - 1)); + } + Writer private_dict; + DictInteger(&private_dict, 0); + private_dict.U8(20); // defaultWidthX + DictInteger(&private_dict, 0); + private_dict.U8(21); // nominalWidthX + + // The top DICT, with the offsets of what follows it. + auto top_dict = [&](uint32_t charset_offset, uint32_t charstrings_offset, + uint32_t private_offset) { + Writer dict; + DictInteger(&dict, font_bounds.empty() ? 0 : Floor(font_bounds.x_min)); + DictInteger(&dict, font_bounds.empty() ? 0 : Floor(font_bounds.y_min)); + DictInteger(&dict, font_bounds.empty() ? 0 : Ceil(font_bounds.x_max)); + DictInteger(&dict, font_bounds.empty() ? 0 : Ceil(font_bounds.y_max)); + dict.U8(5); // FontBBox + if (font.units_per_em != 1000) { + const double scale = 1.0 / font.units_per_em; + for (double value : {scale, 0.0, 0.0, scale, 0.0, 0.0}) { + DictReal(&dict, value); + } + dict.U8(12).U8(7); // FontMatrix + } + DictOffset(&dict, charset_offset); + dict.U8(15); // charset + DictOffset(&dict, charstrings_offset); + dict.U8(17); // CharStrings + DictInteger(&dict, pdfium::checked_cast(private_dict.size())); + DictOffset(&dict, private_offset); + dict.U8(18); // Private + return dict.Take(); + }; + + // Lay it out once to learn where each part starts, then write it. + auto layout = [&](uint32_t charset_offset, uint32_t charstrings_offset, + uint32_t private_offset, Writer* out) { + out->U8(1).U8(0).U8(4).U8(4); // header: version 1.0, offsets of 4 bytes + Index(out, names); + Index(out, {top_dict(charset_offset, charstrings_offset, private_offset)}); + Index(out, strings); + Index(out, {}); // global subroutines + }; + Writer probe; + layout(0, 0, 0, &probe); + const uint32_t charset_offset = pdfium::checked_cast(probe.size()); + const uint32_t charstrings_offset = + charset_offset + pdfium::checked_cast(charset.size()); + Writer charstrings_index; + Index(&charstrings_index, charstrings); + const uint32_t private_offset = + charstrings_offset + + pdfium::checked_cast(charstrings_index.size()); + + Writer cff; + layout(charset_offset, charstrings_offset, private_offset, &cff); + cff.Bytes(charset.Take()); + cff.Bytes(charstrings_index.Take()); + cff.Bytes(private_dict.Take()); + return cff.Take(); +} + +// ---- The other tables +// -------------------------------------------------------- + +// cmap: Windows Unicode BMP (format 4), runs of consecutive characters on +// consecutive glyphs as one segment; when that doesn't fit its 16-bit +// lengths, Windows Unicode full (format 12) instead. +DataVector CmapTable( + const std::vector>& cmap) { + struct Run { + uint32_t start; + uint32_t end; + uint16_t glyph; + }; + std::vector runs; + for (const auto& [scalar, glyph] : cmap) { + if (!runs.empty() && runs.back().end + 1 == scalar && + runs.back().glyph + (scalar - runs.back().start) == glyph) { + runs.back().end = scalar; + continue; + } + runs.push_back({scalar, scalar, glyph}); + } + const bool bmp = cmap.empty() || cmap.back().first <= 0xfffe; + const size_t seg_count = runs.size() + 1; + const size_t format4_length = 16 + seg_count * 8; + Writer table; + table.U16(0).U16(1); + if (bmp && format4_length <= 0xffff) { + table.U16(3).U16(1).U32(12); + size_t entry_selector = 0; + while ((size_t{2} << entry_selector) <= seg_count) { + ++entry_selector; + } + const size_t search_range = size_t{2} << entry_selector; + table.U16(4).U16(format4_length).U16(0); + table.U16(seg_count * 2) + .U16(search_range) + .U16(entry_selector) + .U16(seg_count * 2 - search_range); + for (const Run& run : runs) { + table.U16(run.end); + } + table.U16(0xffff).U16(0); + for (const Run& run : runs) { + table.U16(run.start); + } + table.U16(0xffff); + for (const Run& run : runs) { + table.U16((run.glyph - run.start) & 0xffff); + } + table.U16(1); + table.Zeros(seg_count * 2); // idRangeOffset + return table.Take(); + } + table.U16(3).U16(10).U32(12); + table.U16(12).U16(0).U32(16 + runs.size() * 12).U32(0).U32(runs.size()); + for (const Run& run : runs) { + table.U32(run.start).U32(run.end).U32(run.glyph); + } + return table.Take(); +} + +DataVector NameTable( + const std::vector>& records) { + Writer table; + table.U16(0).U16(records.size()).U16(6 + records.size() * 12); + size_t offset = 0; + for (const auto& [name_id, text] : records) { + table.U16(3) + .U16(1) + .U16(0x409) + .U16(name_id) + .U16(text.GetLength() * 2) + .U16(offset); + offset += text.GetLength() * 2; + } + for (const auto& [name_id, text] : records) { + for (char c : text) { + table.U16(static_cast(c)); + } + } + return table.Take(); +} + +uint32_t Checksum(const DataVector& bytes) { + uint32_t sum = 0; + for (size_t i = 0; i < bytes.size(); i += 4) { + uint32_t word = 0; + for (size_t j = 0; j < 4; ++j) { + word = (word << 8) | (i + j < bytes.size() ? bytes[i + j] : 0); + } + sum += word; + } + return sum; +} + +// An sfnt from its tables, in tag order: the table directory, each table +// padded to four bytes, then head's checksum adjustment. +DataVector Sfnt( + uint32_t version, + const std::map>& tables) { + const size_t count = tables.size(); + size_t entry_selector = 0; + while ((size_t{2} << entry_selector) <= count) { + ++entry_selector; + } + const size_t search_range = (size_t{1} << entry_selector) * 16; + Writer font; + font.U32(version) + .U16(count) + .U16(search_range) + .U16(entry_selector) + .U16(count * 16 - search_range); + size_t offset = 12 + count * 16; + size_t head_offset = 0; + for (const auto& [tag, table] : tables) { + if (tag == "head") { + head_offset = offset; + } + font.Tag(tag.c_str()).U32(Checksum(table)).U32(offset).U32(table.size()); + offset += (table.size() + 3) / 4 * 4; + } + for (const auto& [tag, table] : tables) { + font.Bytes(table).Zeros((4 - table.size() % 4) % 4); + } + DataVector bytes = font.Take(); + const uint32_t adjustment = 0xb1b0afba - Checksum(bytes); + for (int i = 0; i < 4; ++i) { + bytes[head_offset + 8 + i] = + static_cast(adjustment >> (24 - 8 * i)); + } + return bytes; +} + +ByteString StyleName(const CFX_OpenTypeFont& font) { + const bool bold = font.weight >= 600; + if (bold && font.italic) { + return "Bold Italic"; + } + return bold ? "Bold" : font.italic ? "Italic" : "Regular"; +} + +} // namespace + +DataVector WriteOpenTypeCFF(const CFX_OpenTypeFont& font) { + const size_t glyph_count = font.glyphs.size(); + if (glyph_count == 0 || glyph_count > 0xffff || font.units_per_em < 16) { + return {}; + } + const float em = font.units_per_em / 1000.0f; + auto units = [&](float thousandths) { + return static_cast(std::lround(thousandths * em)); + }; + + Bounds font_bounds; + std::vector glyph_bounds; + int advance_max = 0; + int min_lsb = std::numeric_limits::max(); + int min_rsb = std::numeric_limits::max(); + int x_max_extent = 0; + int advance_sum = 0; + int advance_count = 0; + for (const CFX_OpenTypeFont::Glyph& glyph : font.glyphs) { + glyph_bounds.push_back(BoundsOf(glyph.outline)); + const Bounds& bounds = glyph_bounds.back(); + font_bounds.Add(bounds); + advance_max = std::max(advance_max, glyph.advance); + if (glyph.advance > 0) { + advance_sum += glyph.advance; + ++advance_count; + } + if (!bounds.empty()) { + min_lsb = std::min(min_lsb, Floor(bounds.x_min)); + min_rsb = std::min(min_rsb, glyph.advance - Ceil(bounds.x_max)); + x_max_extent = std::max(x_max_extent, Ceil(bounds.x_max)); + } + } + if (min_lsb == std::numeric_limits::max()) { + min_lsb = 0; + min_rsb = 0; + } + const bool bold = font.weight >= 600; + + std::map> tables; + tables["CFF "] = CffTable(font, font_bounds); + + Writer head; + head.Fixed(1) + .Fixed(1) + .U32(0) // checkSumAdjustment, set once the font is assembled + .U32(0x5f0f3cf5) + .U16(0x0003) + .U16(font.units_per_em) + .Zeros(16) // created and modified: none, so the bytes never change + .I16(font_bounds.empty() ? 0 : Floor(font_bounds.x_min)) + .I16(font_bounds.empty() ? 0 : Floor(font_bounds.y_min)) + .I16(font_bounds.empty() ? 0 : Ceil(font_bounds.x_max)) + .I16(font_bounds.empty() ? 0 : Ceil(font_bounds.y_max)) + .U16((bold ? 1 : 0) | (font.italic ? 2 : 0)) + .U16(8) + .I16(2) + .I16(0) + .I16(0); + tables["head"] = head.Take(); + + Writer hhea; + hhea.Fixed(1) + .I16(font.ascent) + .I16(font.descent) + .I16(0) + .U16(advance_max) + .I16(min_lsb) + .I16(min_rsb) + .I16(x_max_extent) + .I16(1) + .I16(0) + .I16(0) + .Zeros(8) + .I16(0) + .U16(glyph_count); + tables["hhea"] = hhea.Take(); + + Writer hmtx; + for (size_t i = 0; i < glyph_count; ++i) { + hmtx.U16(font.glyphs[i].advance) + .I16(glyph_bounds[i].empty() ? 0 : Floor(glyph_bounds[i].x_min)); + } + tables["hmtx"] = hmtx.Take(); + + Writer maxp; + maxp.U32(0x00005000).U16(glyph_count); + tables["maxp"] = maxp.Take(); + + uint32_t first_char = 0xffff; + uint32_t last_char = 0; + for (const auto& [scalar, glyph] : font.cmap) { + first_char = std::min(first_char, scalar); + last_char = std::max(last_char, scalar); + } + if (font.cmap.empty()) { + first_char = 0; + } + Writer os2; + os2.U16(4) + .I16(advance_count ? advance_sum / advance_count : 0) + .U16(std::clamp(font.weight, 100, 900)) + .U16(5) + .U16(0) // fsType: no restriction; the engine decides who receives it + .I16(units(650)) + .I16(units(600)) + .I16(0) + .I16(units(75)) + .I16(units(650)) + .I16(units(600)) + .I16(0) + .I16(units(350)) + .I16(units(50)) + .I16(units(250)) + .I16(0) + .Zeros(10) // panose + .Zeros(16) // ulUnicodeRange + .Tag("EPDF") + .U16((font.italic ? 1 : 0) | (bold ? 0x20 : 0) | + (!bold && !font.italic ? 0x40 : 0) | 0x80) + .U16(std::min(first_char, 0xffff)) + .U16(std::min(last_char, 0xffff)) + .I16(font.ascent) + .I16(font.descent) + .I16(0) + .U16(std::max(font.ascent, 0)) + .U16(std::max(-font.descent, 0)) + .U32(1) // ulCodePageRange1: Latin 1 + .U32(0) + .I16(0) + .I16(0) + .U16(0) + .U16(32) + .U16(1); + tables["OS/2"] = os2.Take(); + + Writer post; + post.Fixed(3) + .Fixed(font.italic_angle) + .I16(units(-100)) + .I16(units(50)) + .U32(font.fixed_pitch ? 1 : 0) + .Zeros(16); + tables["post"] = post.Take(); + + const ByteString style = StyleName(font); + const ByteString full_name = + style == "Regular" ? font.family : font.family + " " + style; + tables["name"] = NameTable({{1, font.family}, + {2, style}, + {3, font.postscript_name + ";EmbedPDF"}, + {4, full_name}, + {6, font.postscript_name}}); + tables["cmap"] = CmapTable(font.cmap); + return Sfnt(0x4f54544f, tables); // 'OTTO' +} diff --git a/core/fxge/cfx_opentypewriter.h b/core/fxge/cfx_opentypewriter.h new file mode 100644 index 0000000000..b0c686f9b4 --- /dev/null +++ b/core/fxge/cfx_opentypewriter.h @@ -0,0 +1,54 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 +// +// EmbedPDF(fonts): an OpenType font with CFF outlines, written from glyph +// outlines, a Unicode cmap and metrics: a font a browser loads. + +#ifndef CORE_FXGE_CFX_OPENTYPEWRITER_H_ +#define CORE_FXGE_CFX_OPENTYPEWRITER_H_ + +#include + +#include +#include + +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/data_vector.h" +#include "core/fxge/cfx_glyphoutline.h" + +struct CFX_OpenTypeFont { + CFX_OpenTypeFont(); + CFX_OpenTypeFont(CFX_OpenTypeFont&&) noexcept; + CFX_OpenTypeFont& operator=(CFX_OpenTypeFont&&) noexcept; + ~CFX_OpenTypeFont(); + + struct Glyph { + CFX_GlyphOutline outline; + // In font units. + uint16_t advance = 0; + }; + + // The names its 'name' table carries; ASCII. + ByteString postscript_name; + ByteString family; + int weight = 400; + bool italic = false; + bool fixed_pitch = false; + // Degrees, counter-clockwise from vertical. + float italic_angle = 0; + uint16_t units_per_em = 1000; + // Line metrics in font units; descent is negative. + int ascent = 0; + int descent = 0; + // Glyph 0 is .notdef. + std::vector glyphs; + // Unicode scalars and the glyph each maps to, in scalar order, each scalar + // once. + std::vector> cmap; +}; + +// The font's bytes: the same bytes for the same font. Empty when it has no +// glyphs or more than an OpenType font can hold. +DataVector WriteOpenTypeCFF(const CFX_OpenTypeFont& font); + +#endif // CORE_FXGE_CFX_OPENTYPEWRITER_H_ diff --git a/fpdfsdk/epdf_c_api_test.c b/fpdfsdk/epdf_c_api_test.c index 2cdc0b20a8..08eaba4ad7 100644 --- a/fpdfsdk/epdf_c_api_test.c +++ b/fpdfsdk/epdf_c_api_test.c @@ -275,6 +275,7 @@ int CheckEmbedPDFCApi(void) { // epdf_font.h CHK(EPDFDoc_ClearMissingCharacters); CHK(EPDFDoc_CountMissingCharacters); + CHK(EPDFDoc_GetFontDisplayProgramData); CHK(EPDFDoc_GetFontEmbeddingPolicy); CHK(EPDFDoc_GetFontProgramCoverage); CHK(EPDFDoc_GetFontProgramData); @@ -285,6 +286,7 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFFontFaces_Count); CHK(EPDFFontFaces_CountPrograms); CHK(EPDFFontFaces_GetFamily); + CHK(EPDFFontFaces_GetProgramDisplayHash); CHK(EPDFFontFaces_GetProgramEmbedded); CHK(EPDFFontFaces_GetProgramEmbeddingPermission); CHK(EPDFFontFaces_GetProgramFormat); diff --git a/fpdfsdk/epdf_font.cpp b/fpdfsdk/epdf_font.cpp index 4156f40a0a..0cdd15cb74 100644 --- a/fpdfsdk/epdf_font.cpp +++ b/fpdfsdk/epdf_font.cpp @@ -499,6 +499,15 @@ EPDFFontFaces_GetProgramHash(EPDF_FONT_FACES faces, return entry && CopySha256(entry->sha256, sha256); } +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_GetProgramDisplayHash(EPDF_FONT_FACES faces, + int face, + int program, + unsigned char* sha256) { + const CPDF_DocumentFonts::Program* entry = ProgramAt(faces, face, program); + return entry && CopySha256(entry->display_sha256, sha256); +} + FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFFontFaces_GetProgramMetrics(EPDF_FONT_FACES faces, int face, @@ -529,6 +538,22 @@ EPDFDoc_GetFontProgramData(FPDF_DOCUMENT document, return CopyBytesAndReturnLength(data, buffer, buflen); } +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetFontDisplayProgramData(FPDF_DOCUMENT document, + unsigned long program_objnum, + void* buffer, + unsigned long buflen) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + if (!doc || program_objnum == 0) { + return 0; + } + const DataVector data = + CPDF_DocumentFonts::For(doc)->DisplayProgramData( + static_cast(program_objnum)); + return CopyBytesAndReturnLength(data, buffer, buflen); +} + FPDF_EXPORT unsigned long FPDF_CALLCONV EPDFDoc_GetFontProgramCoverage(FPDF_DOCUMENT document, unsigned long program_objnum, diff --git a/fpdfsdk/epdf_font_embeddertest.cpp b/fpdfsdk/epdf_font_embeddertest.cpp index eafa45b89d..177b8da679 100644 --- a/fpdfsdk/epdf_font_embeddertest.cpp +++ b/fpdfsdk/epdf_font_embeddertest.cpp @@ -10,13 +10,19 @@ #include #include "core/fdrm/fx_crypt_sha.h" +#include "core/fpdfapi/font/cpdf_font.h" #include "core/fpdfapi/page/cpdf_annotcontext.h" +#include "core/fpdfapi/page/cpdf_docpagedata.h" #include "core/fpdfapi/parser/cpdf_array.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_document.h" #include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fxcrt/bytestring.h" #include "core/fxcrt/widestring.h" +#include "core/fxge/cfx_face.h" +#include "core/fxge/cfx_font.h" +#include "core/fxge/cfx_glyphoutline.h" +#include "core/fxge/fx_fontencoding.h" #include "fpdfsdk/cpdfsdk_helpers.h" #include "fpdfsdk/epdf_edit_test_util.h" #include "public/cpp/fpdf_scopers.h" @@ -330,6 +336,12 @@ TEST_F(EPDFFontEmbedderTest, ListsADocumentProgramAndHandsOutItsBytes) { ASSERT_TRUE(EPDFFont_GetProgramHash(roboto_id, registered_hash)); EXPECT_EQ(std::vector(registered_hash, registered_hash + 32), std::vector(hash, hash + 32)); + // A browser loads it as it is. + unsigned char display_hash[32] = {}; + ASSERT_TRUE(EPDFFontFaces_GetProgramDisplayHash( + faces, found->face, found->program, display_hash)); + EXPECT_EQ(std::vector(hash, hash + 32), + std::vector(display_hash, display_hash + 32)); float ascent = 0; float descent = 0; EXPECT_TRUE(EPDFFontFaces_GetProgramMetrics( @@ -342,6 +354,9 @@ TEST_F(EPDFFontEmbedderTest, ListsADocumentProgramAndHandsOutItsBytes) { ASSERT_EQ(roboto.size(), EPDFDoc_GetFontProgramData( document(), objnum, bytes.data(), bytes.size())); EXPECT_EQ(roboto, bytes); + ASSERT_EQ(roboto.size(), EPDFDoc_GetFontDisplayProgramData( + document(), objnum, bytes.data(), bytes.size())); + EXPECT_EQ(roboto, bytes); EXPECT_GT(EPDFDoc_GetFontProgramCoverage(document(), objnum, nullptr, 0), 0u); // The catalog is an object, not a font program. EXPECT_EQ(0u, EPDFDoc_GetFontProgramData(document(), 1, nullptr, 0)); @@ -589,6 +604,8 @@ TEST_F(EPDFFontEmbedderTest, FindsFontsWhereverADrawingUsesThem) { unsigned char hash[32] = {}; EXPECT_FALSE(EPDFFontFaces_GetProgramHash(faces, georgia->face, georgia->program, hash)); + EXPECT_FALSE(EPDFFontFaces_GetProgramDisplayHash(faces, georgia->face, + georgia->program, hash)); bool type3 = false; for (int face = 0; face < EPDFFontFaces_Count(faces); ++face) { for (int program = 0; program < EPDFFontFaces_CountPrograms(faces, face); @@ -670,3 +687,234 @@ TEST_F(EPDFFontEmbedderTest, UnauthorizedRegisteredFontDoesNotTakeOverABox) { EPDFFontResolution_GetSource(resolution, 0)); EPDFFontResolution_Close(resolution); } + +namespace { + +std::vector LoadTestFont(const char* path) { + std::string font_path = PathService::GetTestFilePath(path); + if (font_path.empty()) { + ADD_FAILURE() << "Failed to find " << path; + return {}; + } + return GetFileContents(font_path.c_str()); +} + +// A form whose field writes with a simple font over |program| (embedded +// under |font_file|, with |file_subtype| as the stream's /Subtype when it has +// one), WinAnsi-encoded, every character 600 wide but "D" 700. +std::string FormWithFieldFont(const std::vector& program, + const char* font_file, + const char* file_subtype, + const char* base_font) { + std::string widths; + for (int code = 32; code <= 126; ++code) { + widths += code == 'D' ? "700 " : "600 "; + } + const std::string data(program.begin(), program.end()); + std::string file = "<< "; + if (file_subtype) { + file += std::string("/Subtype /") + file_subtype + " "; + } + file += "/Length " + std::to_string(data.size()) + " >>\nstream\n" + data + + "\nendstream"; + return MakePdf({ + "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [5 0 R] /DR << " + "/Font << /FKept 6 0 R >> >> >> >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Annots [5 0 R] " + ">>", + file, + "<< /Type /Annot /Subtype /Widget /FT /Tx /T (kept) /P 3 0 R /Rect [20 " + "200 280 230] /DA (/FKept 12 Tf 0 g) /V (AB) >>", + std::string("<< /Type /Font /Subtype /Type1 /BaseFont /") + base_font + + " /FirstChar 32 /LastChar 126 /Widths [" + widths + + "] /Encoding /WinAnsiEncoding /FontDescriptor 7 0 R >>", + std::string("<< /Type /FontDescriptor /FontName /") + base_font + + " /Flags 32 /FontBBox [0 -250 1000 900] /ItalicAngle 0 /Ascent 900 " + "/Descent -250 /CapHeight 700 /StemV 80 /" + + font_file + " 4 0 R >>", + }); +} + +// An outline with zero-length lines left out: a contour's closing line is +// written by one program and implied by another. +std::vector Drawn(const CFX_GlyphOutline& outline) { + std::vector drawn; + CFX_PointF pen; + for (const CFX_GlyphOutline::Segment& segment : outline.segments) { + const CFX_PointF end = segment.op == CFX_GlyphOutline::Op::kCubic + ? segment.points[2] + : segment.points[0]; + if (segment.op == CFX_GlyphOutline::Op::kLine && end == pen) { + continue; + } + pen = end; + drawn.push_back(segment); + } + return drawn; +} + +void ExpectSameOutline(const CFX_GlyphOutline& expected, + const CFX_GlyphOutline& actual) { + const std::vector left = Drawn(expected); + const std::vector right = Drawn(actual); + ASSERT_EQ(left.size(), right.size()); + ASSERT_FALSE(left.empty()); + for (size_t i = 0; i < left.size(); ++i) { + ASSERT_EQ(left[i].op, right[i].op) << i; + const size_t points = left[i].op == CFX_GlyphOutline::Op::kCubic ? 3 : 1; + for (size_t j = 0; j < points; ++j) { + EXPECT_NEAR(left[i].points[j].x, right[i].points[j].x, 0.01) << i; + EXPECT_NEAR(left[i].points[j].y, right[i].points[j].y, 0.01) << i; + } + } +} + +bool Covers(const std::vector& ranges, uint32_t scalar) { + for (size_t i = 0; i + 1 < ranges.size(); i += 2) { + if (ranges[i] <= scalar && scalar < ranges[i] + ranges[i + 1]) { + return true; + } + } + return false; +} + +// The field font of a form from FormWithFieldFont(), as the engine sees it, +// and its display program: an OpenType font a browser loads, with the same +// glyphs, reached by exactly the characters the engine writes, at the +// advances it lays them out with. +void ExpectDisplayProgramDrawsTheKeptFont(const std::string& pdf, + int expected_format) { + ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(doc); + EPDF_FONT_FACES faces = EPDFFontFaces_Load(doc.get()); + ASSERT_TRUE(faces); + std::optional found; + for (int face = 0; face < EPDFFontFaces_Count(faces) && !found; ++face) { + for (int program = 0; program < EPDFFontFaces_CountPrograms(faces, face); + ++program) { + if (EPDFFontFaces_GetProgramFormat(faces, face, program) == + expected_format) { + found = FoundProgram{face, program}; + break; + } + } + } + ASSERT_TRUE(found.has_value()); + EXPECT_FALSE( + EPDFFontFaces_ProgramMapsUnicode(faces, found->face, found->program)); + unsigned char source_hash[32] = {}; + unsigned char display_hash[32] = {}; + ASSERT_TRUE(EPDFFontFaces_GetProgramHash(faces, found->face, found->program, + source_hash)); + ASSERT_TRUE(EPDFFontFaces_GetProgramDisplayHash( + faces, found->face, found->program, display_hash)); + EXPECT_NE(std::vector(source_hash, source_hash + 32), + std::vector(display_hash, display_hash + 32)); + const unsigned long objnum = + EPDFFontFaces_GetProgramObjNum(faces, found->face, found->program); + EPDFFontFaces_Close(faces); + + const unsigned long length = + EPDFDoc_GetFontDisplayProgramData(doc.get(), objnum, nullptr, 0); + ASSERT_GT(length, 0u); + std::vector display(length); + ASSERT_EQ(length, EPDFDoc_GetFontDisplayProgramData(doc.get(), objnum, + display.data(), length)); + const DataVector hashed = CryptSha256Generate(display); + EXPECT_EQ(std::vector(display_hash, display_hash + 32), + std::vector(hashed.begin(), hashed.end())); + ASSERT_EQ('O', display[0]); // 'OTTO': OpenType with CFF outlines + + auto display_font = std::make_unique(); + ASSERT_TRUE(display_font->LoadFaceZeroFromSpan( + display, /*force_vertical=*/false, /*object_tag=*/0)); + RetainPtr display_face = display_font->GetFace(); + ASSERT_TRUE(display_face->SelectCharMap(fxge::FontEncoding::kUnicode)); + + // The engine's view of the same font. + CPDF_Document* pdf_doc = CPDFDocumentFromFPDFDocument(doc.get()); + RetainPtr font_dict = pdf_doc->GetMutableRoot() + ->GetMutableDictFor("AcroForm") + ->GetMutableDictFor("DR") + ->GetMutableDictFor("Font") + ->GetMutableDictFor("FKept"); + RetainPtr font = + CPDF_DocPageData::FromDocument(pdf_doc)->GetFont(font_dict); + ASSERT_TRUE(font); + RetainPtr source_face = font->GetFont()->GetFace(); + + // Every printable character the font has. + int compared = 0; + for (int character = 0x21; character <= 0x7e; ++character) { + const uint32_t code = + font->CharCodeFromUnicode(static_cast(character)); + const int source_glyph = font->GlyphFromCharCode(code, nullptr); + if (source_glyph <= 0) { + EXPECT_EQ(0, display_face->GetCharIndex(character)) << character; + continue; + } + ++compared; + const int display_glyph = display_face->GetCharIndex(character); + ASSERT_GT(display_glyph, 0) << character; + std::optional expected = + source_face->LoadUnscaledGlyphOutline(source_glyph); + std::optional actual = + display_face->LoadUnscaledGlyphOutline(display_glyph); + ASSERT_TRUE(expected.has_value()); + ASSERT_TRUE(actual.has_value()); + ExpectSameOutline(*expected, *actual); + ASSERT_EQ(0, display_face->LoadGlyph(display_glyph, /*scale=*/false)); + EXPECT_EQ(character == 'D' ? 700 : 600, + display_face->GetGlyphTTWidth(display_glyph)) + << character; + } + EXPECT_GE(compared, 5); + + // The characters it draws are exactly the ones the engine writes. + const unsigned long range_count = + EPDFDoc_GetFontProgramCoverage(doc.get(), objnum, nullptr, 0); + ASSERT_GT(range_count, 0u); + std::vector ranges(range_count * 2); + EPDFDoc_GetFontProgramCoverage(doc.get(), objnum, ranges.data(), range_count); + for (uint32_t scalar = 1; scalar < 0x3000; ++scalar) { + const uint32_t code = + font->CharCodeFromUnicode(static_cast(scalar)); + const bool written = code != CPDF_Font::kInvalidCharCode && code != 0 && + font->GlyphFromCharCode(code, nullptr) > 0 && + !font->UnicodeFromCharCode(code).IsEmpty(); + EXPECT_EQ(written, Covers(ranges, scalar)) << scalar; + EXPECT_EQ(written, display_face->GetCharIndex(scalar) > 0) << scalar; + } + + // The same bytes from another opening of the same file. + ScopedFPDFDocument again( + FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(again); + std::vector rebuilt(length); + ASSERT_EQ(length, EPDFDoc_GetFontDisplayProgramData(again.get(), objnum, + rebuilt.data(), length)); + EXPECT_EQ(display, rebuilt); +} + +} // namespace + +// A form's field font that is bare CFF, which a browser can't load, gets a +// display program it can. +TEST_F(EPDFFontEmbedderTest, BuildsADisplayProgramForABareCffFont) { + std::vector cff = LoadTestFont("fonts/foxit_fixed.cff"); + ASSERT_FALSE(cff.empty()); + ExpectDisplayProgramDrawsTheKeptFont( + FormWithFieldFont(cff, "FontFile3", "Type1C", "FoxitFixed"), + EPDF_FONT_FORMAT_CFF); +} + +// So does one that is Type 1. +TEST_F(EPDFFontEmbedderTest, BuildsADisplayProgramForAType1Font) { + std::vector type1 = + LoadThirdPartyFont("skia/resources/fonts/Roboto2-Regular.pfb"); + ASSERT_FALSE(type1.empty()); + ExpectDisplayProgramDrawsTheKeptFont( + FormWithFieldFont(type1, "FontFile", nullptr, "Roboto2-Regular"), + EPDF_FONT_FORMAT_TYPE1); +} diff --git a/public/epdf_font.h b/public/epdf_font.h index ce23bbfc66..e6a7e7c91b 100644 --- a/public/epdf_font.h +++ b/public/epdf_font.h @@ -306,6 +306,17 @@ EPDFFontFaces_GetProgramHash(EPDF_FONT_FACES faces, int face, int program, unsigned char* sha256); +// The SHA-256 of what a browser loads for the program into |sha256| (32 +// bytes): the program itself when it is TrueType or OpenType with a Unicode +// cmap, else the display program built for it +// (EPDFDoc_GetFontDisplayProgramData()). Returns false when a browser can't +// draw it (Type 3, nothing embedded, a program that doesn't load) or for an +// index out of range. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_GetProgramDisplayHash(EPDF_FONT_FACES faces, + int face, + int program, + unsigned char* sha256); // The program's line metrics as the layout reads them (em, descent // positive). Returns false when it doesn't load or out of range. FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV @@ -327,10 +338,27 @@ EPDFDoc_GetFontProgramData(FPDF_DOCUMENT document, unsigned long buflen); // Experimental EmbedPDF Extension API. -// The characters such a program maps through its Unicode cmap, as ranges: -// |ranges| receives up to |range_capacity| pairs (first code point, count). -// Returns the number of ranges, 0 when it maps none or isn't such a -// program; pass NULL/0 to query it. +// What a browser loads for such a program: the program itself when it is +// TrueType or OpenType with a Unicode cmap. Otherwise (Type 1, bare CFF, a +// program without a Unicode cmap) an OpenType font built from it: its +// glyphs, a Unicode cmap holding exactly the characters the engine writes +// with the font (each through the PDF's encoding to the glyph it draws), and +// the advances the engine lays them out with (/Widths). The same bytes every +// time. Returns the length, 0 when a browser can't draw it or +// |program_objnum| isn't such a program; pass NULL/0 to query it. Nothing is +// copied when |buflen| is too small. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetFontDisplayProgramData(FPDF_DOCUMENT document, + unsigned long program_objnum, + void* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// The characters a browser draws with such a program, which are the ones the +// engine writes with it, as ranges: |ranges| receives up to +// |range_capacity| pairs (first code point, count). Returns the number of +// ranges, 0 when it maps none or isn't such a program; pass NULL/0 to query +// it. FPDF_EXPORT unsigned long FPDF_CALLCONV EPDFDoc_GetFontProgramCoverage(FPDF_DOCUMENT document, unsigned long program_objnum, From c43414b81200fc33ff0cb0b8abef43782c967da4 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Mon, 19 Oct 2026 21:27:12 +0300 Subject: [PATCH 03/16] Lay out rich text with the Unicode bidi algorithm CFX_BidiParagraph resolves a paragraph's levels with ICU's ubidi, sets a line's trailing whitespace to the paragraph's level and orders a line's runs. The rich layout splits items by level and shapes them in its direction, orders each line's runs, and takes a paragraph's direction from its dir or, for form fields, from its first strong character. A line's trailing spaces hang past its end, a right-to-left paragraph's indent is on its right, and a justified line no longer stretches the spaces it ends with. --- core/fpdfdoc/cpdf_richtextlayout.cpp | 207 ++++++++++----- core/fpdfdoc/cpdf_richtextlayout.h | 12 +- core/fxcrt/BUILD.gn | 3 + core/fxcrt/cfx_bidiparagraph.cpp | 170 +++++++++++++ core/fxcrt/cfx_bidiparagraph.h | 50 ++++ core/fxcrt/cfx_bidiparagraph_unittest.cpp | 67 +++++ fpdfsdk/BUILD.gn | 2 + fpdfsdk/epdf_richtextlayout_embeddertest.cpp | 252 +++++++++++++++++++ 8 files changed, 694 insertions(+), 69 deletions(-) create mode 100644 core/fxcrt/cfx_bidiparagraph.cpp create mode 100644 core/fxcrt/cfx_bidiparagraph.h create mode 100644 core/fxcrt/cfx_bidiparagraph_unittest.cpp create mode 100644 fpdfsdk/epdf_richtextlayout_embeddertest.cpp diff --git a/core/fpdfdoc/cpdf_richtextlayout.cpp b/core/fpdfdoc/cpdf_richtextlayout.cpp index ff135809cc..0c8f871cf4 100644 --- a/core/fpdfdoc/cpdf_richtextlayout.cpp +++ b/core/fpdfdoc/cpdf_richtextlayout.cpp @@ -13,6 +13,7 @@ #include "core/fpdfapi/font/cpdf_font.h" #include "core/fpdfdoc/cpdf_annotfontmap.h" +#include "core/fxcrt/cfx_bidiparagraph.h" #include "core/fxcrt/code_point_view.h" #include "core/fxcrt/fx_linebreak.h" #include "core/fxcrt/fx_unicode.h" @@ -109,10 +110,6 @@ bool IsCommonScript(hb_script_t script) { script == HB_SCRIPT_UNKNOWN; } -bool IsRtlScript(hb_script_t script) { - return hb_script_get_horizontal_direction(script) == HB_DIRECTION_RTL; -} - // Scripts whose shaping is a matter of default features only: the Latin // features Acrobat leaves off are turned off for them (C note §4.2). bool IsSimpleScript(hb_script_t script) { @@ -178,14 +175,15 @@ struct ResolvedStyle { bool has_size_override = false; }; -// One shaping unit: one font entry, one style, one script and direction, -// covering [begin, end) of the segment's text. +// One shaping unit: one font entry, one style, one script and one bidi +// level, covering [begin, end) of the segment's text. struct Item { int entry = -1; int style_index = 0; size_t begin = 0; size_t end = 0; hb_script_t script = HB_SCRIPT_COMMON; + uint8_t level = 0; // odd: shaped right to left bool rtl = false; std::vector glyphs; // clusters are absolute segment indices float ascent = 0; // pt @@ -198,6 +196,7 @@ struct Segment { WideString text; // without the hard break std::vector style_of; // per character std::vector source_pos; // per character: index in the paragraph + std::optional bidi; std::vector items; int break_style = 0; // the style at the hard break, for an empty line bool ends_with_hard_break = false; @@ -379,14 +378,19 @@ struct CPDF_RichTextLayout::Impl { item->ascent = face.ascent * size; item->descent = face.descent * size; item->degraded = face.degraded; - item->rtl = IsRtlScript(item->script); + item->rtl = (item->level & 1) != 0; HbFont* hb = face.program ? HbFontOf(item->entry) : nullptr; if (!hb) { // Standard 14 (or a program HarfBuzz refused): one glyph per - // character through the font's encoding. + // character through the font's encoding; right to left, in reverse + // and mirrored, as HarfBuzz would. for (size_t i = item->begin; i < item->end; ++i) { - const uint32_t code = GlyphFor(item->entry, text[i]); + uint32_t ch = static_cast(text[i]); + if (item->rtl) { + ch = hb_unicode_mirroring(hb_unicode_funcs_get_default(), ch); + } + const uint32_t code = GlyphFor(item->entry, static_cast(ch)); ShapedGlyph glyph; glyph.gid = code; glyph.cluster = pdfium::checked_cast(i); @@ -395,7 +399,10 @@ struct CPDF_RichTextLayout::Impl { glyph.x_advance = (width * size / 1000.0f + letter_spacing) * scale_x; item->glyphs.push_back(glyph); } - item->needs_actual_text = false; + if (item->rtl) { + std::reverse(item->glyphs.begin(), item->glyphs.end()); + } + item->needs_actual_text = item->rtl; return; } @@ -499,12 +506,8 @@ struct CPDF_RichTextLayout::Impl { return width; } - // Ink width of the segment's text in [begin, end): trailing spaces do - // not count (Acrobat centres and wraps without them). - static float InkWidth(const Segment& segment, size_t begin, size_t end) { - while (end > begin && IsSpace(segment.text[end - 1])) { - --end; - } + // Width of the segment's text in [begin, end). + static float RangeWidth(const Segment& segment, size_t begin, size_t end) { float width = 0; for (const Item& item : segment.items) { if (item.end <= begin || item.begin >= end) { @@ -516,6 +519,19 @@ struct CPDF_RichTextLayout::Impl { return width; } + // Where the ink of [begin, end) ends: trailing spaces do not count + // (Acrobat centres and wraps without them). + static size_t InkEnd(const Segment& segment, size_t begin, size_t end) { + while (end > begin && IsSpace(segment.text[end - 1])) { + --end; + } + return end; + } + + static float InkWidth(const Segment& segment, size_t begin, size_t end) { + return RangeWidth(segment, begin, InkEnd(segment, begin, end)); + } + static bool IsClusterBoundary(const Segment& segment, size_t pos) { for (const Item& item : segment.items) { if (pos == item.begin || pos == item.end) { @@ -554,22 +570,28 @@ struct CPDF_RichTextLayout::Impl { } } - // Builds the items of a segment: runs of one style, split by script and - // by the face that covers each grapheme, then shaped. - void Itemize(Segment* segment, const std::vector& styles) { + // Builds the items of a segment of a paragraph that reads right to left + // when |rtl|: runs of one style, split by script, by bidi level and by the + // face that covers each grapheme, then shaped. + void Itemize(Segment* segment, + bool rtl, + const std::vector& styles) { const WideString& text = segment->text; const size_t length = text.GetLength(); segment->items.clear(); + segment->bidi.emplace(text, rtl); if (length == 0) { return; } const std::vector grapheme_starts = GraphemeStarts(text); + const std::vector& levels = segment->bidi->levels(); - // Per grapheme: style, script, entry. + // Per grapheme: style, script, level, entry. struct Piece { size_t begin, end; int style; hb_script_t script; + uint8_t level; int entry; }; std::vector pieces; @@ -597,7 +619,7 @@ struct CPDF_RichTextLayout::Impl { if (!match.covered && !IsInvisibleGrapheme(grapheme)) { fonts->NoteMissingCharacter(WideString(grapheme)); } - pieces.push_back({begin, end, style, script, match.entry}); + pieces.push_back({begin, end, style, script, levels[begin], match.entry}); begin = end; } // A leading common-script piece takes the script that follows it. @@ -610,7 +632,7 @@ struct CPDF_RichTextLayout::Impl { for (const Piece& piece : pieces) { Item* last = segment->items.empty() ? nullptr : &segment->items.back(); if (last && last->style_index == piece.style && - last->entry == piece.entry && + last->entry == piece.entry && last->level == piece.level && (last->script == piece.script || IsCommonScript(piece.script))) { last->end = piece.end; continue; @@ -621,6 +643,7 @@ struct CPDF_RichTextLayout::Impl { item.begin = piece.begin; item.end = piece.end; item.script = piece.script; + item.level = piece.level; segment->items.push_back(std::move(item)); } for (Item& item : segment->items) { @@ -631,46 +654,73 @@ struct CPDF_RichTextLayout::Impl { } } - // The runs of a line covering [begin, end) of the segment. - std::vector RunsFor(const Segment& segment, - size_t begin, - size_t end, - const std::vector& styles) { + // The run drawing [begin, end) of |item| at bidi level |level|. + static GlyphRun RunOf(const Segment& segment, + const Item& item, + size_t begin, + size_t end, + uint8_t level, + const ResolvedStyle& resolved) { + GlyphRun run; + run.font_entry = item.entry; + run.style = resolved.style; + run.style.size = resolved.drawn_size; + run.rise = resolved.rise; + run.text = segment.text.Substr(begin, end - begin); + run.text_begin = segment.source_pos[begin]; + run.text_end = end < segment.source_pos.size() + ? segment.source_pos[end] + : segment.source_pos[end - 1] + 1; + run.rtl = item.rtl; + run.bidi_level = level; + run.ascent = item.ascent; + run.descent = item.descent; + run.needs_actual_text = item.needs_actual_text; + run.degraded = item.degraded; + for (const ShapedGlyph& glyph : item.glyphs) { + if (glyph.cluster >= begin && glyph.cluster < end) { + ShapedGlyph copy = glyph; + copy.cluster -= pdfium::checked_cast(begin); + run.glyphs.push_back(copy); + run.width += glyph.x_advance; + } + } + return run; + } + + // The runs of a line covering [begin, end) of the segment, in the order + // they are drawn, left to right. + static std::vector RunsFor( + const Segment& segment, + size_t begin, + size_t end, + const std::vector& styles) { + // Whitespace ending the line takes the paragraph's level, so an item's + // last characters may run at another level than the rest of it. + const std::vector levels = segment.bidi->LineLevels(begin, end); std::vector runs; + std::vector run_levels; for (const Item& item : segment.items) { - if (item.end <= begin || item.begin >= end) { - continue; - } - const size_t run_begin = std::max(begin, item.begin); - const size_t run_end = std::min(end, item.end); - const ResolvedStyle& resolved = styles[item.style_index]; - GlyphRun run; - run.font_entry = item.entry; - run.style = resolved.style; - run.style.size = resolved.drawn_size; - run.rise = resolved.rise; - run.text = segment.text.Substr(run_begin, run_end - run_begin); - run.text_begin = segment.source_pos[run_begin]; - run.text_end = run_end < segment.source_pos.size() - ? segment.source_pos[run_end] - : segment.source_pos[run_end - 1] + 1; - run.rtl = item.rtl; - run.bidi_level = item.rtl ? 1 : 0; - run.ascent = item.ascent; - run.descent = item.descent; - run.needs_actual_text = item.needs_actual_text; - run.degraded = item.degraded; - for (const ShapedGlyph& glyph : item.glyphs) { - if (glyph.cluster >= run_begin && glyph.cluster < run_end) { - ShapedGlyph copy = glyph; - copy.cluster -= pdfium::checked_cast(run_begin); - run.glyphs.push_back(copy); - run.width += glyph.x_advance; + const size_t item_end = std::min(end, item.end); + for (size_t run_begin = std::max(begin, item.begin); + run_begin < item_end;) { + const uint8_t level = levels[run_begin - begin]; + size_t run_end = run_begin + 1; + while (run_end < item_end && levels[run_end - begin] == level) { + ++run_end; } + runs.push_back(RunOf(segment, item, run_begin, run_end, level, + styles[item.style_index])); + run_levels.push_back(level); + run_begin = run_end; } - runs.push_back(std::move(run)); } - return runs; + std::vector visual; + visual.reserve(runs.size()); + for (size_t index : CFX_BidiParagraph::VisualOrder(run_levels)) { + visual.push_back(std::move(runs[index])); + } + return visual; } }; @@ -842,12 +892,23 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( } segments.back().break_style = last_style; + // The direction the paragraph reads in: its dir, or its text's. + bool rtl = props.rtl; + if (options_.direction_from_text) { + WideString text; + for (const Segment& segment : segments) { + text += segment.text; + } + rtl = CFX_BidiParagraph::FirstStrongIsRtl(text.AsStringView()) + .value_or(false); + } + // 3. Each segment: itemise, shape, break into lines. const float margin_left = std::max(0.0f, props.margin_left); const float margin_right = std::max(0.0f, props.margin_right); bool first_line_of_paragraph = true; for (Segment& segment : segments) { - impl.Itemize(&segment, styles); + impl.Itemize(&segment, rtl, styles); const size_t length = segment.text.GetLength(); const std::vector opportunities = BreakOpportunities(segment.text); @@ -942,11 +1003,13 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( line.leading = kLeadingFactor * max_size; } // 5. Horizontal placement. - line.width = Impl::InkWidth(segment, pos, best); + const size_t ink_end = Impl::InkEnd(segment, pos, best); + line.width = Impl::RangeWidth(segment, pos, ink_end); const float slack = std::max(0.0f, avail - line.width); + // A paragraph whose dir is rtl names its sides from where it + // starts: left (the default) is its right side. Align align = props.align; if (props.rtl) { - std::reverse(line.runs.begin(), line.runs.end()); if (align == Align::kLeft) { align = Align::kRight; } else if (align == Align::kRight) { @@ -956,7 +1019,8 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( const bool justify_this_line = align == Align::kJustify && best < length && !line.ends_with_hard_break; - float start = margin_left + indent; + // The indent is on the side the paragraph starts from. + float start = margin_left + (rtl ? 0 : indent); switch (align) { case Align::kLeft: case Align::kJustify: @@ -969,11 +1033,20 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( break; } if (justify_this_line) { + // The spaces between words stretch; the line's trailing ones + // hang. + const size_t ink_source_end = + segment.source_pos[pos] + (ink_end - pos); + auto stretches = [&](const GlyphRun& run, + const ShapedGlyph& glyph) { + return glyph.cluster < run.text.GetLength() && + IsSpace(run.text[glyph.cluster]) && + run.text_begin + glyph.cluster < ink_source_end; + }; int spaces = 0; for (const GlyphRun& run : line.runs) { for (const ShapedGlyph& glyph : run.glyphs) { - if (glyph.cluster < run.text.GetLength() && - IsSpace(run.text[glyph.cluster])) { + if (stretches(run, glyph)) { ++spaces; } } @@ -982,8 +1055,7 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( const float extra = slack / spaces; for (GlyphRun& run : line.runs) { for (ShapedGlyph& glyph : run.glyphs) { - if (glyph.cluster < run.text.GetLength() && - IsSpace(run.text[glyph.cluster])) { + if (stretches(run, glyph)) { glyph.x_advance += extra; run.width += extra; } @@ -991,8 +1063,13 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( } } } + // The line's trailing spaces hang past its end: past the left + // edge when the paragraph reads right to left. line.start_x = start; float x = start; + if (rtl) { + x -= Impl::RangeWidth(segment, ink_end, best); + } for (GlyphRun& run : line.runs) { run.x = x; x += run.width; diff --git a/core/fpdfdoc/cpdf_richtextlayout.h b/core/fpdfdoc/cpdf_richtextlayout.h index d4ff43be16..cfa8c5acd0 100644 --- a/core/fpdfdoc/cpdf_richtextlayout.h +++ b/core/fpdfdoc/cpdf_richtextlayout.h @@ -4,8 +4,9 @@ // EmbedPDF: lays a CPDF_RichTextDocument out into lines of shaped glyph // runs, the way Acrobat does it (plan §4.4, Phase C note §4): HarfBuzz // shaping per run, UAX #14 break opportunities snapped to grapheme -// boundaries, and the measured line model (ascent = max, descent = the last -// run's, leading = 0.2 em of the largest size). Fonts come from a +// boundaries, the Unicode bidi algorithm (UAX #9) ordering each line, and the +// measured line model (ascent = max, descent = the last run's, leading = +// 0.2 em of the largest size). Fonts come from a // CPDF_AnnotFontMap, which the layout extends with fallbacks as it goes. // Nothing here touches a document. @@ -46,8 +47,8 @@ class CPDF_RichTextLayout { WideString text; // the logical text this run draws size_t text_begin = 0; // range within the paragraph's logical text size_t text_end = 0; - uint8_t bidi_level = 0; // D: 0 or 1 from the run's script - bool rtl = false; // shaped right-to-left + uint8_t bidi_level = 0; // resolved embedding level; odd = right to left + bool rtl = false; // shaped right to left std::vector glyphs; // visual order for the run's direction float x = 0; // start, from the text area's left edge, pt float width = 0; // sum of advances @@ -73,6 +74,9 @@ class CPDF_RichTextLayout { bool typographic_features = false; // Acrobat's line descent is the last run's; false = max over runs. bool parity_descent = true; + // A paragraph reads in the direction of its first strong character (left + // to right when it has none), as a form field does, instead of its dir. + bool direction_from_text = false; }; struct Result { diff --git a/core/fxcrt/BUILD.gn b/core/fxcrt/BUILD.gn index 854777c19c..ab79a93305 100644 --- a/core/fxcrt/BUILD.gn +++ b/core/fxcrt/BUILD.gn @@ -33,6 +33,8 @@ source_set("fxcrt") { "bytestring_pool.h", "cfx_bidi_resolver.cpp", "cfx_bidi_resolver.h", + "cfx_bidiparagraph.cpp", + "cfx_bidiparagraph.h", "cfx_bitstream.cpp", "cfx_bitstream.h", "cfx_datetime.cpp", @@ -238,6 +240,7 @@ pdfium_unittest_source_set("unittests") { "bytestring_pool_unittest.cpp", "bytestring_unittest.cpp", "cfx_bidi_resolver_unittest.cpp", + "cfx_bidiparagraph_unittest.cpp", "cfx_bitstream_unittest.cpp", "cfx_datetime_unittest.cpp", "cfx_read_only_container_stream_unittest.cpp", diff --git a/core/fxcrt/cfx_bidiparagraph.cpp b/core/fxcrt/cfx_bidiparagraph.cpp new file mode 100644 index 0000000000..18f53d6e39 --- /dev/null +++ b/core/fxcrt/cfx_bidiparagraph.cpp @@ -0,0 +1,170 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fxcrt/cfx_bidiparagraph.h" + +#include +#include +#include +#include + +#include "core/fxcrt/check_op.h" +#include "core/fxcrt/code_point_view.h" +#include "core/fxcrt/numerics/safe_conversions.h" +#include "core/fxcrt/utf16.h" + +#if defined(USE_SYSTEM_ICUUC) +#include +#include +#else +#include "third_party/icu/source/common/unicode/ubidi.h" +#include "third_party/icu/source/common/unicode/uchar.h" +#endif + +namespace { + +struct UBiDiDeleter { + void operator()(UBiDi* bidi) const { ubidi_close(bidi); } +}; + +// What rule L1 resets at the end of a line: whitespace, separators, and the +// formatting characters the algorithm otherwise passes over. +bool TrailsAtParagraphLevel(uint32_t code_point) { + switch (u_charDirection(static_cast(code_point))) { + case U_WHITE_SPACE_NEUTRAL: + case U_SEGMENT_SEPARATOR: + case U_BLOCK_SEPARATOR: + case U_BOUNDARY_NEUTRAL: + case U_LEFT_TO_RIGHT_EMBEDDING: + case U_LEFT_TO_RIGHT_OVERRIDE: + case U_RIGHT_TO_LEFT_EMBEDDING: + case U_RIGHT_TO_LEFT_OVERRIDE: + case U_POP_DIRECTIONAL_FORMAT: + case U_LEFT_TO_RIGHT_ISOLATE: + case U_RIGHT_TO_LEFT_ISOLATE: + case U_FIRST_STRONG_ISOLATE: + case U_POP_DIRECTIONAL_ISOLATE: + return true; + default: + return false; + } +} + +} // namespace + +CFX_BidiParagraph::CFX_BidiParagraph(const WideString& text, bool rtl) + : text_(text), + base_level_(rtl ? 1 : 0), + levels_(text.GetLength(), base_level_) { + // ICU counts UTF-16 code units; each character's level is its first + // unit's. + std::u16string units; + std::vector first_unit; + first_unit.reserve(text.GetLength()); + for (wchar_t ch : text) { + first_unit.push_back(units.size()); + const uint32_t code_point = static_cast(ch); + if (pdfium::IsSupplementary(code_point)) { + const pdfium::SurrogatePair pair(static_cast(code_point)); + units += pair.high(); + units += pair.low(); + } else if (code_point > 0xFFFF) { + units += u'�'; + } else { + units += static_cast(code_point); + } + } + // ICU's run bookkeeping overflows past this length (crbug.com/504629701). + constexpr size_t kMaxUnits = + std::numeric_limits::max() / (3 * sizeof(int32_t)); + if (units.empty() || units.size() > kMaxUnits) { + return; + } + const int32_t length = pdfium::checked_cast(units.size()); + UErrorCode status = U_ZERO_ERROR; + std::unique_ptr bidi( + ubidi_openSized(length, /*maxRunCount=*/0, &status)); + if (U_FAILURE(status)) { + return; + } + ubidi_setPara(bidi.get(), units.data(), length, base_level_, + /*embeddingLevels=*/nullptr, &status); + if (U_FAILURE(status)) { + return; + } + const UBiDiLevel* levels = ubidi_getLevels(bidi.get(), &status); + if (U_FAILURE(status) || !levels) { + return; + } + for (size_t i = 0; i < first_unit.size(); ++i) { + levels_[i] = levels[first_unit[i]]; + } +} + +CFX_BidiParagraph::~CFX_BidiParagraph() = default; + +std::vector CFX_BidiParagraph::LineLevels(size_t begin, + size_t end) const { + CHECK_LE(begin, end); + CHECK_LE(end, levels_.size()); + std::vector line(levels_.begin() + begin, levels_.begin() + end); + for (size_t i = line.size(); + i > 0 && + TrailsAtParagraphLevel(static_cast(text_[begin + i - 1])); + --i) { + line[i - 1] = base_level_; + } + return line; +} + +// static +std::optional CFX_BidiParagraph::FirstStrongIsRtl(WideStringView text) { + int open_isolates = 0; + for (char32_t code_point : pdfium::CodePointView(text)) { + switch (u_charDirection(static_cast(code_point))) { + case U_LEFT_TO_RIGHT: + if (open_isolates == 0) { + return false; + } + break; + case U_RIGHT_TO_LEFT: + case U_RIGHT_TO_LEFT_ARABIC: + if (open_isolates == 0) { + return true; + } + break; + case U_LEFT_TO_RIGHT_ISOLATE: + case U_RIGHT_TO_LEFT_ISOLATE: + case U_FIRST_STRONG_ISOLATE: + ++open_isolates; + break; + case U_POP_DIRECTIONAL_ISOLATE: + if (open_isolates > 0) { + --open_isolates; + } + break; + default: + break; + } + } + return std::nullopt; +} + +// static +std::vector CFX_BidiParagraph::VisualOrder( + pdfium::span levels) { + std::vector order(levels.size()); + std::iota(order.begin(), order.end(), 0); + if (levels.empty()) { + return order; + } + std::vector logical_index(levels.size()); + std::iota(logical_index.begin(), logical_index.end(), 0); + ubidi_reorderVisual(levels.data(), + pdfium::checked_cast(levels.size()), + logical_index.data()); + for (size_t i = 0; i < order.size(); ++i) { + order[i] = pdfium::checked_cast(logical_index[i]); + } + return order; +} diff --git a/core/fxcrt/cfx_bidiparagraph.h b/core/fxcrt/cfx_bidiparagraph.h new file mode 100644 index 0000000000..f12bc3199f --- /dev/null +++ b/core/fxcrt/cfx_bidiparagraph.h @@ -0,0 +1,50 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 +// +// EmbedPDF(fonts): the Unicode bidirectional algorithm (UAX #9) over one +// paragraph, through ICU's ubidi: the level each character resolves to, the +// levels of one line of it, and the order a line's runs are drawn in. + +#ifndef CORE_FXCRT_CFX_BIDIPARAGRAPH_H_ +#define CORE_FXCRT_CFX_BIDIPARAGRAPH_H_ + +#include +#include + +#include +#include + +#include "core/fxcrt/span.h" +#include "core/fxcrt/widestring.h" + +class CFX_BidiParagraph { + public: + // Resolves |text| as one paragraph whose level is 1 when |rtl|, else 0. + CFX_BidiParagraph(const WideString& text, bool rtl); + ~CFX_BidiParagraph(); + + uint8_t base_level() const { return base_level_; } + + // One level per character of the text; an odd level runs right to left. + const std::vector& levels() const { return levels_; } + + // The levels of [begin, end) laid out as one line: whitespace at the end of + // the line takes the paragraph's level (rule L1), so it trails the line in + // the paragraph's direction. + std::vector LineLevels(size_t begin, size_t end) const; + + // Whether |text| reads right to left by its first strong character outside + // isolates (rules P2 and P3). nullopt when it has no strong character. + static std::optional FirstStrongIsRtl(WideStringView text); + + // The order to draw runs with these levels in, left to right, as their + // logical indices (rule L2). + static std::vector VisualOrder(pdfium::span levels); + + private: + const WideString text_; + const uint8_t base_level_; + std::vector levels_; +}; + +#endif // CORE_FXCRT_CFX_BIDIPARAGRAPH_H_ diff --git a/core/fxcrt/cfx_bidiparagraph_unittest.cpp b/core/fxcrt/cfx_bidiparagraph_unittest.cpp new file mode 100644 index 0000000000..73e356f9c9 --- /dev/null +++ b/core/fxcrt/cfx_bidiparagraph_unittest.cpp @@ -0,0 +1,67 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fxcrt/cfx_bidiparagraph.h" + +#include + +#include "testing/gtest/include/gtest/gtest.h" + +using Levels = std::vector; + +TEST(CFXBidiParagraphTest, HebrewAndDigitsInALeftToRightParagraph) { + // "Hello שלום 123": the space before the digits sits between two + // right-to-left neighbours (digits count as such, rule N1), and digits + // take the next even level (rule I1). + CFX_BidiParagraph bidi(L"Hello \x05E9\x05DC\x05D5\x05DD 123", false); + EXPECT_EQ(0, bidi.base_level()); + EXPECT_EQ((Levels{0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2}), bidi.levels()); +} + +TEST(CFXBidiParagraphTest, DigitsBeforeHebrewInARightToLeftParagraph) { + // "123 שלום" at level 1: digits at an odd level go up one (rule I2). + CFX_BidiParagraph bidi(L"123 \x05E9\x05DC\x05D5\x05DD", true); + EXPECT_EQ(1, bidi.base_level()); + EXPECT_EQ((Levels{2, 2, 2, 1, 1, 1, 1, 1}), bidi.levels()); +} + +TEST(CFXBidiParagraphTest, OneLevelPerCharacter) { + // A supplementary character is one character, whatever wchar_t holds. + const WideString text(L"a\U0001D400\x05E9"); + CFX_BidiParagraph bidi(text, false); + ASSERT_EQ(text.GetLength(), bidi.levels().size()); + EXPECT_EQ(0, bidi.levels().front()); + EXPECT_EQ(1, bidi.levels().back()); +} + +TEST(CFXBidiParagraphTest, LineLevelsResetTrailingWhitespace) { + // Latin in a right-to-left paragraph is at level 2, its inner space too; + // ending a line, the space goes to the paragraph's level (rule L1). + CFX_BidiParagraph bidi(L"abc def", true); + EXPECT_EQ((Levels{2, 2, 2, 2, 2, 2, 2}), bidi.levels()); + EXPECT_EQ((Levels{2, 2, 2, 1}), bidi.LineLevels(0, 4)); + EXPECT_EQ((Levels{2, 2, 2}), bidi.LineLevels(4, 7)); + EXPECT_TRUE(bidi.LineLevels(3, 3).empty()); +} + +TEST(CFXBidiParagraphTest, FirstStrongIsRtl) { + EXPECT_EQ(true, CFX_BidiParagraph::FirstStrongIsRtl(L"123 \x05E9\x05DC")); + EXPECT_EQ(false, CFX_BidiParagraph::FirstStrongIsRtl(L"abc \x05E9\x05DC")); + EXPECT_EQ(true, CFX_BidiParagraph::FirstStrongIsRtl(L"\x0634\x0643")); + EXPECT_EQ(std::nullopt, CFX_BidiParagraph::FirstStrongIsRtl(L"123 !")); + EXPECT_EQ(std::nullopt, CFX_BidiParagraph::FirstStrongIsRtl(L"")); + // An isolate's content doesn't count (rule P2). + EXPECT_EQ(true, CFX_BidiParagraph::FirstStrongIsRtl(L"\x2066" + L"abc\x2069 \x05E9")); +} + +TEST(CFXBidiParagraphTest, VisualOrder) { + EXPECT_EQ((std::vector{0, 2, 1, 3}), + CFX_BidiParagraph::VisualOrder(Levels{0, 1, 1, 0})); + // Higher levels reverse first, then the whole odd stretch. + EXPECT_EQ((std::vector{3, 1, 2, 0}), + CFX_BidiParagraph::VisualOrder(Levels{1, 2, 2, 1})); + EXPECT_EQ((std::vector{0, 1, 2}), + CFX_BidiParagraph::VisualOrder(Levels{0, 0, 0})); + EXPECT_TRUE(CFX_BidiParagraph::VisualOrder(Levels{}).empty()); +} diff --git a/fpdfsdk/BUILD.gn b/fpdfsdk/BUILD.gn index 6213a27da6..adbb5a204d 100644 --- a/fpdfsdk/BUILD.gn +++ b/fpdfsdk/BUILD.gn @@ -233,6 +233,8 @@ pdfium_embeddertest_source_set("embeddertests") { # EmbedPDF: covers a document's fonts, the resolver read and the # registry's program reads (public/epdf_font.h). "epdf_font_embeddertest.cpp", + # EmbedPDF: covers the rich text layout with real fonts. + "epdf_richtextlayout_embeddertest.cpp", # EmbedPDF: covers flattening and annotation export (public/epdf_flatten.h). "epdf_flatten_embeddertest.cpp", # EmbedPDF: covers base documents and layers (public/epdf_layer.h). diff --git a/fpdfsdk/epdf_richtextlayout_embeddertest.cpp b/fpdfsdk/epdf_richtextlayout_embeddertest.cpp new file mode 100644 index 0000000000..97bbf1e3c2 --- /dev/null +++ b/fpdfsdk/epdf_richtextlayout_embeddertest.cpp @@ -0,0 +1,252 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fpdfdoc/cpdf_richtextlayout.h" + +#include + +#include +#include + +#include "core/fpdfapi/font/cpdf_font.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfdoc/cpdf_annotfontmap.h" +#include "core/fpdfdoc/cpdf_richtext.h" +#include "core/fxcrt/fx_coordinates.h" +#include "core/fxcrt/widestring.h" +#include "fpdfsdk/cpdfsdk_helpers.h" +#include "public/cpp/fpdf_scopers.h" +#include "public/epdf_font.h" +#include "public/fpdf_edit.h" +#include "testing/embedder_test.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/utils/file_util.h" +#include "testing/utils/path_service.h" + +namespace { + +using Line = CPDF_RichTextLayout::Line; +using GlyphRun = CPDF_RichTextLayout::GlyphRun; +using Align = CPDF_RichTextParagraphProps::Align; + +constexpr float kBoxWidth = 400.0f; +constexpr float kSize = 12.0f; + +// A line's runs as drawn, left to right: "[text]level" each. +std::wstring Describe(const Line& line) { + std::wstring description; + for (const GlyphRun& run : line.runs) { + description += L"["; + description += run.text.c_str(); + description += L"]"; + description += std::to_wstring(run.bidi_level); + } + return description; +} + +} // namespace + +// The rich text layout with real fonts. Arimo covers Latin, digits and Hebrew +// with its points; Helvetica is a standard font, drawn without HarfBuzz. +class EPDFRichTextLayoutEmbedderTest : public EmbedderTest { + protected: + void SetUp() override { + EmbedderTest::SetUp(); + EPDFFont_ClearRegisteredFonts(); + const std::string path = PathService::GetThirdPartyFilePath( + "test_fonts/test_fonts/Arimo-Regular.ttf"); + ASSERT_FALSE(path.empty()); + arimo_ = GetFileContents(path.c_str()); + ASSERT_FALSE(arimo_.empty()); + ASSERT_NE(0u, EPDFFont_RegisterMemFont64("Arimo", /*weight=*/400, + /*italic=*/0, arimo_.data(), + arimo_.size())); + doc_.reset(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc_); + } + + void TearDown() override { + doc_.reset(); + EPDFFont_ClearRegisteredFonts(); + EmbedderTest::TearDown(); + } + + // One paragraph of |text| in |family| at 12 pt, in a box |width| wide. + std::vector Arrange(const WideString& text, + bool rtl, + float width = kBoxWidth, + const char* family = "Arimo", + CPDF_RichTextLayout::Options options = {}) { + CPDF_RichTextParagraphProps props; + props.rtl = rtl; + return Arrange(text, props, width, family, options); + } + + std::vector Arrange(const WideString& text, + const CPDF_RichTextParagraphProps& props, + float width = kBoxWidth, + const char* family = "Arimo", + CPDF_RichTextLayout::Options options = {}) { + CPDF_RichTextDocument document; + document.body.family = WideString::FromASCII(family); + document.body.size = kSize; + CPDF_RichTextParagraph paragraph; + paragraph.props = props; + paragraph.runs.push_back({text, {}}); + document.paragraphs.push_back(std::move(paragraph)); + CPDF_AnnotFontMap fonts(CPDFDocumentFromFPDFDocument(doc_.get()), + /*default_font=*/nullptr, ByteString(), + /*allow_registered_fallbacks=*/true); + CPDF_RichTextLayout layout(&fonts, options); + return layout + .Arrange(document, CFX_FloatRect(0, 0, width, 300), + CPDF_Annot::VerticalAlignment::kTop) + .lines; + } + + std::vector arimo_; + ScopedFPDFDocument doc_; +}; + +// Hebrew and digits in a left-to-right paragraph: the digits take the +// Hebrew's side (they read as right to left between strong characters), and +// the Hebrew with its space and digits reads from the right. +TEST_F(EPDFRichTextLayoutEmbedderTest, LeftToRightParagraphOrdersHebrew) { + const std::vector lines = + Arrange(L"Hello \x05E9\x05DC\x05D5\x05DD 123", /*rtl=*/false); + ASSERT_EQ(1u, lines.size()); + EXPECT_EQ(L"[Hello ]0[123]2[\x05E9\x05DC\x05D5\x05DD ]1", Describe(lines[0])); + // Drawn edge to edge from the left. + float x = 0; + for (const GlyphRun& run : lines[0].runs) { + EXPECT_FLOAT_EQ(x, run.x); + x += run.width; + } + EXPECT_TRUE(lines[0].runs[2].rtl); + EXPECT_TRUE(lines[0].runs[2].needs_actual_text); + EXPECT_FALSE(lines[0].runs[1].rtl); +} + +// A right-to-left paragraph starting with digits: they still read left to +// right, at its right end; the line is right-aligned. +TEST_F(EPDFRichTextLayoutEmbedderTest, RightToLeftParagraphStartingWithDigits) { + const std::vector lines = + Arrange(L"123 \x05E9\x05DC\x05D5\x05DD", /*rtl=*/true); + ASSERT_EQ(1u, lines.size()); + EXPECT_EQ(L"[ \x05E9\x05DC\x05D5\x05DD]1[123]2", Describe(lines[0])); + EXPECT_NEAR(kBoxWidth, lines[0].start_x + lines[0].width, 0.01f); + EXPECT_NEAR(kBoxWidth, lines[0].runs.back().x + lines[0].runs.back().width, + 0.01f); +} + +// A form field's paragraph reads in the direction of its first strong +// character, whatever its dir says. +TEST_F(EPDFRichTextLayoutEmbedderTest, DirectionFromText) { + CPDF_RichTextLayout::Options options; + options.direction_from_text = true; + std::vector lines = Arrange(L"123 \x05E9\x05DC\x05D5\x05DD", + /*rtl=*/false, kBoxWidth, "Arimo", options); + ASSERT_EQ(1u, lines.size()); + EXPECT_EQ(L"[ \x05E9\x05DC\x05D5\x05DD]1[123]2", Describe(lines[0])); + + lines = Arrange(L"abc \x05E9\x05DC\x05D5\x05DD", /*rtl=*/true, kBoxWidth, + "Arimo", options); + ASSERT_EQ(1u, lines.size()); + EXPECT_EQ(L"[abc ]0[\x05E9\x05DC\x05D5\x05DD]1", Describe(lines[0])); +} + +// Breaking a right-to-left paragraph: the space that ends a line goes to the +// paragraph's level, so it trails at the left, past the edge the line is +// aligned to, whether it ran left to right before the break or not. +TEST_F(EPDFRichTextLayoutEmbedderTest, TrailingSpaceHangsAtTheLeft) { + // Two Hebrew words, one per line. + std::vector lines = + Arrange(L"\x05E9\x05DC\x05D5\x05DD \x05E2\x05D5\x05DC\x05DD", + /*rtl=*/true, /*width=*/30); + ASSERT_EQ(2u, lines.size()); + EXPECT_EQ(L"[\x05E9\x05DC\x05D5\x05DD ]1", Describe(lines[0])); + const GlyphRun& hebrew = lines[0].runs[0]; + EXPECT_LT(hebrew.x, lines[0].start_x); + EXPECT_NEAR(30.0f, hebrew.x + hebrew.width, 0.01f); + + // Two Latin words: the space ran at the words' level, 2. + lines = Arrange(L"abc def", /*rtl=*/true, /*width=*/25); + ASSERT_EQ(2u, lines.size()); + EXPECT_EQ(L"[ ]1[abc]2", Describe(lines[0])); + EXPECT_NEAR(lines[0].start_x - lines[0].runs[0].width, lines[0].runs[0].x, + 0.01f); + EXPECT_NEAR(lines[0].start_x, lines[0].runs[1].x, 0.01f); + EXPECT_NEAR(25.0f, lines[0].start_x + lines[0].width, 0.01f); + EXPECT_EQ(L"[def]2", Describe(lines[1])); +} + +// Hebrew points are marks: they stay with their letters, in one run. +TEST_F(EPDFRichTextLayoutEmbedderTest, HebrewPointsStayWithTheirLetters) { + const std::vector lines = + Arrange(L"\x05E9\x05C1\x05B8\x05DC\x05D5\x05B9\x05DD", /*rtl=*/true); + ASSERT_EQ(1u, lines.size()); + ASSERT_EQ(1u, lines[0].runs.size()); + EXPECT_EQ(1, lines[0].runs[0].bidi_level); + EXPECT_FALSE(lines[0].runs[0].degraded); +} + +// Brackets read right to left are mirrored: by HarfBuzz for a font program, +// by the layout for a standard font. +TEST_F(EPDFRichTextLayoutEmbedderTest, RightToLeftBracketsAreMirrored) { + for (const char* family : {"Arimo", "Helvetica"}) { + SCOPED_TRACE(family); + std::vector lines = Arrange(L"()", /*rtl=*/false, kBoxWidth, family); + ASSERT_EQ(1u, lines.size()); + ASSERT_EQ(1u, lines[0].runs.size()); + ASSERT_EQ(2u, lines[0].runs[0].glyphs.size()); + const uint32_t open = lines[0].runs[0].glyphs[0].gid; + const uint32_t close = lines[0].runs[0].glyphs[1].gid; + ASSERT_NE(open, close); + + // "(abc)" in a right-to-left paragraph: the brackets at level 1 around + // the Latin at level 2. Logical ")" is drawn first, as "(". + lines = Arrange(L"(abc)", /*rtl=*/true, kBoxWidth, family); + ASSERT_EQ(1u, lines.size()); + EXPECT_EQ(L"[)]1[abc]2[(]1", Describe(lines[0])); + ASSERT_EQ(1u, lines[0].runs[0].glyphs.size()); + EXPECT_EQ(open, lines[0].runs[0].glyphs[0].gid); + ASSERT_EQ(1u, lines[0].runs[2].glyphs.size()); + EXPECT_EQ(close, lines[0].runs[2].glyphs[0].gid); + } +} + +// The first line's indent is on the side the paragraph starts from. +TEST_F(EPDFRichTextLayoutEmbedderTest, IndentOnTheStartSide) { + CPDF_RichTextParagraphProps props; + props.text_indent = 10; + std::vector lines = Arrange(L"abc", props); + ASSERT_EQ(1u, lines.size()); + EXPECT_NEAR(10.0f, lines[0].start_x, 0.01f); + + props.rtl = true; + lines = Arrange(L"\x05E9\x05DC\x05D5\x05DD", props); + ASSERT_EQ(1u, lines.size()); + EXPECT_NEAR(kBoxWidth - 10.0f, lines[0].start_x + lines[0].width, 0.01f); +} + +// A justified line's ink reaches both edges: the spaces between its words +// stretch, the space it ends with doesn't. +TEST_F(EPDFRichTextLayoutEmbedderTest, JustifiedInkReachesBothEdges) { + CPDF_RichTextParagraphProps props; + props.align = Align::kJustify; + const std::vector lines = Arrange(L"aaa bbb ccc", props, /*width=*/50); + ASSERT_EQ(2u, lines.size()); + ASSERT_EQ(1u, lines[0].runs.size()); + const GlyphRun& run = lines[0].runs[0]; + EXPECT_EQ(L"aaa bbb ", std::wstring(run.text.c_str())); + EXPECT_NEAR(0.0f, run.x, 0.01f); + float x = run.x; + float ink_end = x; + for (const CPDF_RichTextLayout::ShapedGlyph& glyph : run.glyphs) { + x += glyph.x_advance; + if (run.text[glyph.cluster] != L' ') { + ink_end = x; + } + } + EXPECT_NEAR(50.0f, ink_end, 0.01f); +} From 934981c3d95c960b2092b8030d10f93434b5bec2 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Mon, 19 Oct 2026 23:50:13 +0300 Subject: [PATCH 04/16] Match fonts per cluster, in tiers, with a last resort A cluster goes to the first fallback, in the order for its style, that has all of it. Fallbacks are ranked by a tier the caller gives, then style, then the order added (EPDFFont_AddFallbackFont takes the tier). A registered last-resort font (EPDFFont_SetLastResortFont) draws a character no font has as its box; it is embedded like a fallback and the character is still reported. Registered and document fonts keep their coverage as code point ranges. --- core/fpdfdoc/cpdf_annotfontmap.cpp | 50 ++++++-- core/fpdfdoc/cpdf_annotfontmap.h | 13 +- core/fpdfdoc/cpdf_documentfonts.cpp | 30 ++--- core/fpdfdoc/cpdf_documentfonts.h | 11 +- core/fpdfdoc/cpdf_richtextlayout.cpp | 20 ++- core/fpdfdoc/cpdf_richtextlayout.h | 11 +- core/fxge/BUILD.gn | 4 + core/fxge/cfx_codepointrange.cpp | 64 ++++++++++ core/fxge/cfx_codepointrange.h | 29 +++++ core/fxge/cfx_codepointrange_unittest.cpp | 34 +++++ core/fxge/cfx_face.h | 6 +- core/fxge/cfx_fontregistry.cpp | 119 +++++++++--------- core/fxge/cfx_fontregistry.h | 23 +++- fpdfsdk/epdf_annot_embeddertest.cpp | 111 ++++++++++++++++- fpdfsdk/epdf_c_api_test.c | 2 + fpdfsdk/epdf_font.cpp | 29 ++--- fpdfsdk/epdf_font_embeddertest.cpp | 45 ++++++- fpdfsdk/epdf_richtextlayout_embeddertest.cpp | 123 +++++++++++++++++-- public/epdf_font.h | 32 +++-- testing/resources/fonts/last_resort.ttf | Bin 0 -> 1120 bytes 20 files changed, 584 insertions(+), 172 deletions(-) create mode 100644 core/fxge/cfx_codepointrange.cpp create mode 100644 core/fxge/cfx_codepointrange.h create mode 100644 core/fxge/cfx_codepointrange_unittest.cpp create mode 100644 testing/resources/fonts/last_resort.ttf diff --git a/core/fpdfdoc/cpdf_annotfontmap.cpp b/core/fpdfdoc/cpdf_annotfontmap.cpp index c8ddcb9326..cf47875347 100644 --- a/core/fpdfdoc/cpdf_annotfontmap.cpp +++ b/core/fpdfdoc/cpdf_annotfontmap.cpp @@ -294,6 +294,13 @@ std::vector> DocumentFontDicts( return dicts; } +// Joiners and variation selectors draw nothing of their own: a font covers a +// grapheme without them, as the layout skips them. +bool NeedsGlyph(char32_t cp) { + return cp != 0x200D && !(cp >= 0xFE00 && cp <= 0xFE0F) && + !(cp >= 0xE0100 && cp <= 0xE01EF); +} + bool PDFontSupportsUnicode(const RetainPtr& font, uint16_t word) { if (!font) { return false; @@ -909,9 +916,8 @@ bool CPDF_AnnotFontMap::CoversText(int entry, WideStringView text) { // Scalars, not code units: on a 16-bit wchar_t platform a supplementary // character is a surrogate pair, and neither half has a glyph anywhere. for (char32_t cp : pdfium::CodePointView(text)) { - if (cp == 0x200D || (cp >= 0xFE00 && cp <= 0xFE0F) || - (cp >= 0xE0100 && cp <= 0xE01EF)) { - continue; // joiners and variation selectors, as the layout skips them + if (!NeedsGlyph(cp)) { + continue; } if (RichGlyphFor(entry, static_cast(cp)) == 0) { return false; @@ -1003,23 +1009,41 @@ int CPDF_AnnotFontMap::ResolveRichFaceUncached(const WideString& family, return entry; } -int CPDF_AnnotFontMap::FindRichFallback(uint32_t unicode, +int CPDF_AnnotFontMap::FindRichFallback(WideStringView grapheme, int weight, bool italic) { if (!allow_registered_fallbacks_) { return -1; } - std::optional font_id = - CFX_FontRegistry::FindFallbackFont(unicode, weight, italic, - /*for_authoring=*/true); - if (!font_id.has_value()) { - return -1; + for (CFX_FontRegistry::FontId font_id : CFX_FontRegistry::GetFallbackOrder( + weight, italic, /*for_authoring=*/true)) { + const bool covers = std::ranges::all_of( + pdfium::CodePointView(grapheme), [font_id](char32_t cp) { + return !NeedsGlyph(cp) || CFX_FontRegistry::SupportsUnicode( + font_id, static_cast(cp)); + }); + if (covers) { + return EntryForRegisteredFont(font_id); + } } - int32_t existing = FindExistingRegisteredFont(*font_id); - if (existing >= 0) { - return existing; + return -1; +} + +int CPDF_AnnotFontMap::FindLastResort() { + const CFX_FontRegistry::FontId font_id = + CFX_FontRegistry::GetLastResortFont(); + if (!allow_registered_fallbacks_ || + font_id == CFX_FontRegistry::kInvalidFontId || + !CFX_FontRegistry::IsEditingAuthorized(font_id)) { + return -1; } - return AddRegisteredFallbackFont(*font_id); + return EntryForRegisteredFont(font_id); +} + +int CPDF_AnnotFontMap::EntryForRegisteredFont( + CFX_FontRegistry::FontId font_id) { + const int32_t existing = FindExistingRegisteredFont(font_id); + return existing >= 0 ? existing : AddRegisteredFallbackFont(font_id); } CPDF_AnnotFontMap::RichFace CPDF_AnnotFontMap::GetRichFace(int entry) const { diff --git a/core/fpdfdoc/cpdf_annotfontmap.h b/core/fpdfdoc/cpdf_annotfontmap.h index 4b5afe49f8..1df3eb5b27 100644 --- a/core/fpdfdoc/cpdf_annotfontmap.h +++ b/core/fpdfdoc/cpdf_annotfontmap.h @@ -180,9 +180,14 @@ class CPDF_AnnotFontMap final : public IPVT_FontMap { // subset name), so the rich layout keeps naming the /DA alias, exactly as // CPVT always did. A pinned face wins over every resolution step. void PinRichFace(const WideString& family, int weight, bool italic, int entry); - // A registered fallback font, authorised for authoring, covering - // |unicode|; -1 when there is none or fallbacks are not allowed. - int FindRichFallback(uint32_t unicode, int weight, bool italic); + // The first registered fallback font, in the order for the style + // (CFX_FontRegistry::GetFallbackOrder()), that has every character of + // |grapheme|; -1 when none does or fallbacks are not allowed. + int FindRichFallback(WideStringView grapheme, int weight, bool italic); + // The registered last-resort font (CFX_FontRegistry::GetLastResortFont()), + // which draws a grapheme no font has as its box; -1 when there is none or + // fallbacks are not allowed. + int FindLastResort(); RichFace GetRichFace(int entry) const; // The glyph of |entry| for a scalar; 0 when the face lacks it. For a // standard-14 face this is the charcode (one glyph per character). @@ -284,6 +289,8 @@ class CPDF_AnnotFontMap final : public IPVT_FontMap { CFX_FontRegistry::FontId font_id); int32_t FindExistingRegisteredFont(CFX_FontRegistry::FontId font_id) const; int32_t AddRegisteredFallbackFont(CFX_FontRegistry::FontId font_id); + // The entry drawing with a registered font, added when it is the first. + int EntryForRegisteredFont(CFX_FontRegistry::FontId font_id); int ResolveRichFaceUncached(const WideString& family, int weight, bool italic, diff --git a/core/fpdfdoc/cpdf_documentfonts.cpp b/core/fpdfdoc/cpdf_documentfonts.cpp index 5e6fbd0750..c63ec81a5d 100644 --- a/core/fpdfdoc/cpdf_documentfonts.cpp +++ b/core/fpdfdoc/cpdf_documentfonts.cpp @@ -228,19 +228,13 @@ bool IsSfnt(CPDF_DocumentFonts::Format format) { format == CPDF_DocumentFonts::Format::kOpenTypeCFF; } -// |scalars| as [start, start + count) ranges, in order. -std::vector> RangesOf( - std::vector scalars) { +// |scalars| as ranges, in order. +std::vector RangesOf(std::vector scalars) { std::ranges::sort(scalars); - std::vector> ranges; + scalars.erase(std::unique(scalars.begin(), scalars.end()), scalars.end()); + std::vector ranges; for (uint32_t scalar : scalars) { - if (!ranges.empty() && - ranges.back().first + ranges.back().second >= scalar) { - ranges.back().second = - std::max(ranges.back().second, scalar - ranges.back().first + 1); - continue; - } - ranges.emplace_back(scalar, 1); + CFX_AppendCodePoint(&ranges, scalar); } return ranges; } @@ -450,7 +444,7 @@ DataVector CPDF_DocumentFonts::DisplayProgramData(uint32_t objnum) { return {}; } -std::vector> CPDF_DocumentFonts::ProgramCoverage( +std::vector CPDF_DocumentFonts::ProgramCoverage( uint32_t objnum) { Inspected* inspected = FindInspected(objnum); if (!inspected) { @@ -465,7 +459,7 @@ std::vector> CPDF_DocumentFonts::ProgramCoverage( if (inspected->coverage.has_value()) { return *inspected->coverage; } - std::vector scalars; + std::vector ranges; { DataVector bytes = ProgramData(objnum); auto program = std::make_unique(); @@ -473,16 +467,10 @@ std::vector> CPDF_DocumentFonts::ProgramCoverage( program->LoadFaceZeroFromSpan(bytes, /*force_vertical=*/false, /*object_tag=*/0) && program->GetFace()->SelectCharMap(fxge::FontEncoding::kUnicode)) { - for (const CharCodeAndIndex& item : program->GetCharCodesAndIndices( - pdfium::kMaximumSupplementaryCodePoint)) { - if (item.glyph_index != 0) { - scalars.push_back(item.char_code); - } - } + ranges = program->GetFace()->GetMappedCodePointRanges( + pdfium::kMaximumSupplementaryCodePoint); } } - std::vector> ranges = - RangesOf(std::move(scalars)); inspected->coverage = ranges; return ranges; } diff --git a/core/fpdfdoc/cpdf_documentfonts.h b/core/fpdfdoc/cpdf_documentfonts.h index 20d0bc6489..28e91c56e0 100644 --- a/core/fpdfdoc/cpdf_documentfonts.h +++ b/core/fpdfdoc/cpdf_documentfonts.h @@ -22,6 +22,7 @@ #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/unowned_ptr.h" #include "core/fxcrt/widestring.h" +#include "core/fxge/cfx_codepointrange.h" #include "core/fxge/cfx_fontregistry.h" class CPDF_Stream; @@ -97,9 +98,9 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { // empty when it can't draw it or |objnum| isn't one. DataVector DisplayProgramData(uint32_t objnum); // The scalars a browser draws with it, which are the ones the engine writes - // with it, as [start, start + count) ranges in order; empty when it maps - // none or |objnum| isn't one. - std::vector> ProgramCoverage(uint32_t objnum); + // with it, as ranges in order; empty when it maps none or |objnum| isn't + // one. + std::vector ProgramCoverage(uint32_t objnum); // The program stream itself, for the writer; null when |objnum| isn't one. RetainPtr ProgramStream(uint32_t objnum); @@ -132,14 +133,14 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { struct Display { DataVector bytes; DataVector sha256; - std::vector> coverage; + std::vector coverage; }; struct Inspected { RetainPtr stream; // the version inspected size_t raw_size = 0; Program facts; - std::optional>> coverage; + std::optional> coverage; // Built from the first font dictionary found to use the stream. bool display_built = false; std::optional display; diff --git a/core/fpdfdoc/cpdf_richtextlayout.cpp b/core/fpdfdoc/cpdf_richtextlayout.cpp index 0c8f871cf4..a38ef6e9ec 100644 --- a/core/fpdfdoc/cpdf_richtextlayout.cpp +++ b/core/fpdfdoc/cpdf_richtextlayout.cpp @@ -239,21 +239,15 @@ CPDF_RichTextLayout::GraphemeMatch CPDF_RichTextLayout::MatchGrapheme( if (fonts->CoversText(face_entry, grapheme)) { return {face_entry, true}; } - // The grapheme's base scalar picks the fallback font. - uint32_t base = 0; - for (char32_t cp : pdfium::CodePointView(grapheme)) { - if (base == 0 || !IsMark(static_cast(cp))) { - base = static_cast(cp); - if (!IsMark(base)) { - break; - } - } - } - const int fallback = fonts->FindRichFallback(base, weight, italic); - if (fallback >= 0 && fonts->CoversText(fallback, grapheme)) { + const int fallback = fonts->FindRichFallback(grapheme, weight, italic); + if (fallback >= 0) { return {fallback, true}; } - return {face_entry, false}; + // No font has it: the last resort draws it as its box. A grapheme that + // draws nothing stays with the face, which hides it. + const int last_resort = + IsInvisibleGrapheme(grapheme) ? -1 : fonts->FindLastResort(); + return {last_resort >= 0 ? last_resort : face_entry, false}; } // static diff --git a/core/fpdfdoc/cpdf_richtextlayout.h b/core/fpdfdoc/cpdf_richtextlayout.h index cfa8c5acd0..cafa874ca7 100644 --- a/core/fpdfdoc/cpdf_richtextlayout.h +++ b/core/fpdfdoc/cpdf_richtextlayout.h @@ -104,11 +104,12 @@ class CPDF_RichTextLayout { static std::vector GraphemeStarts(const WideString& text); // The font a grapheme draws with when its run's face is |face_entry|: the - // face, when it maps every character of the grapheme; else the fallback - // font its base character picks (the closest style, then the order the - // fallbacks were added in), when that one maps every character. |covered| - // is false when neither does: the grapheme draws as the face's missing - // glyph. Joiners and variation selectors don't need a glyph. + // face, when it maps every character of the grapheme; else the first + // fallback font, in the order for the style, that maps every character. + // |covered| is false when none does: the grapheme draws as the last-resort + // font's box, or as the face's missing glyph when there is no last resort + // or the grapheme draws nothing. Joiners and variation selectors don't + // need a glyph. struct GraphemeMatch { int entry = -1; bool covered = false; diff --git a/core/fxge/BUILD.gn b/core/fxge/BUILD.gn index b94673a5fa..2db9a7a1af 100644 --- a/core/fxge/BUILD.gn +++ b/core/fxge/BUILD.gn @@ -52,6 +52,9 @@ source_set("fxge") { "cfx_glyphbitmap.h", "cfx_glyphcache.cpp", "cfx_glyphcache.h", + # EmbedPDF(fonts): the code points a font maps, as ranges. + "cfx_codepointrange.cpp", + "cfx_codepointrange.h", # EmbedPDF(fonts): a glyph's unscaled outline, and an OpenType font # written from outlines (a document font's display program). "cfx_glyphoutline.cpp", @@ -300,6 +303,7 @@ source_set("test_support") { pdfium_unittest_source_set("unittests") { sources = [ + "cfx_codepointrange_unittest.cpp", "cfx_color_unittest.cpp", "cfx_face_unittest.cpp", "cfx_folderfontinfo_unittest.cpp", diff --git a/core/fxge/cfx_codepointrange.cpp b/core/fxge/cfx_codepointrange.cpp new file mode 100644 index 0000000000..e0d1ea9546 --- /dev/null +++ b/core/fxge/cfx_codepointrange.cpp @@ -0,0 +1,64 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fxge/cfx_codepointrange.h" + +#include + +#include "core/fxge/cfx_face.h" + +#if defined(PDF_ENABLE_FREETYPE) +#include "core/fxge/freetype/fx_freetype.h" +#endif + +#if defined(PDF_ENABLE_FONTATIONS) +#include "core/fxge/cfx_gemodule.h" +#endif + +bool CFX_CodePointRangesContain(pdfium::span ranges, + uint32_t code_point) { + // The last range starting at or below |code_point|. + auto after = std::ranges::upper_bound(ranges, code_point, {}, + &CFX_CodePointRange::first); + if (after == ranges.begin()) { + return false; + } + const CFX_CodePointRange& range = *(after - 1); + return code_point - range.first < range.count; +} + +void CFX_AppendCodePoint(std::vector* ranges, + uint32_t code_point) { + if (!ranges->empty() && + ranges->back().first + ranges->back().count == code_point) { + ++ranges->back().count; + return; + } + ranges->push_back({code_point, 1}); +} + +std::vector CFX_Face::GetMappedCodePointRanges( + char32_t max_char) { + std::vector ranges; +#if defined(PDF_ENABLE_FONTATIONS) + if (CFX_GEModule::IsFontations()) { + for (const CharCodeAndIndex& item : GetCharCodesAndIndices(max_char)) { + if (item.glyph_index != 0) { + CFX_AppendCodePoint(&ranges, item.char_code); + } + } + return ranges; + } +#endif // defined(PDF_ENABLE_FONTATIONS) +#if defined(PDF_ENABLE_FREETYPE) + // Walked one code point at a time, never collected: a font that maps all + // of Unicode is a million of them. + FT_UInt glyph_index = 0; + FT_ULong code_point = FT_Get_First_Char(GetRec(), &glyph_index); + while (glyph_index != 0 && code_point <= max_char) { + CFX_AppendCodePoint(&ranges, static_cast(code_point)); + code_point = FT_Get_Next_Char(GetRec(), code_point, &glyph_index); + } +#endif // defined(PDF_ENABLE_FREETYPE) + return ranges; +} diff --git a/core/fxge/cfx_codepointrange.h b/core/fxge/cfx_codepointrange.h new file mode 100644 index 0000000000..d5cfdcf8da --- /dev/null +++ b/core/fxge/cfx_codepointrange.h @@ -0,0 +1,29 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 +// +// EmbedPDF(fonts): the characters a font maps, as runs of consecutive code +// points. A font that maps every code point is a handful of runs. + +#ifndef CORE_FXGE_CFX_CODEPOINTRANGE_H_ +#define CORE_FXGE_CFX_CODEPOINTRANGE_H_ + +#include + +#include + +#include "core/fxcrt/span.h" + +struct CFX_CodePointRange { + uint32_t first = 0; + uint32_t count = 0; +}; + +// Whether |ranges|, in order and apart, hold |code_point|. +bool CFX_CodePointRangesContain(pdfium::span ranges, + uint32_t code_point); + +// Adds |code_point|, which is above every code point |ranges| holds. +void CFX_AppendCodePoint(std::vector* ranges, + uint32_t code_point); + +#endif // CORE_FXGE_CFX_CODEPOINTRANGE_H_ diff --git a/core/fxge/cfx_codepointrange_unittest.cpp b/core/fxge/cfx_codepointrange_unittest.cpp new file mode 100644 index 0000000000..f9d4024db0 --- /dev/null +++ b/core/fxge/cfx_codepointrange_unittest.cpp @@ -0,0 +1,34 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fxge/cfx_codepointrange.h" + +#include + +#include "testing/gtest/include/gtest/gtest.h" + +TEST(CFXCodePointRangeTest, AppendJoinsNeighbours) { + std::vector ranges; + for (uint32_t code_point : {0x20u, 0x21u, 0x22u, 0x30u, 0x10FFFFu}) { + CFX_AppendCodePoint(&ranges, code_point); + } + ASSERT_EQ(3u, ranges.size()); + EXPECT_EQ(0x20u, ranges[0].first); + EXPECT_EQ(3u, ranges[0].count); + EXPECT_EQ(0x30u, ranges[1].first); + EXPECT_EQ(1u, ranges[1].count); + EXPECT_EQ(0x10FFFFu, ranges[2].first); + EXPECT_EQ(1u, ranges[2].count); +} + +TEST(CFXCodePointRangeTest, ContainEdges) { + const std::vector ranges = {{0x20, 3}, {0x30, 1}}; + EXPECT_FALSE(CFX_CodePointRangesContain(ranges, 0x1F)); + EXPECT_TRUE(CFX_CodePointRangesContain(ranges, 0x20)); + EXPECT_TRUE(CFX_CodePointRangesContain(ranges, 0x22)); + EXPECT_FALSE(CFX_CodePointRangesContain(ranges, 0x23)); + EXPECT_FALSE(CFX_CodePointRangesContain(ranges, 0x2F)); + EXPECT_TRUE(CFX_CodePointRangesContain(ranges, 0x30)); + EXPECT_FALSE(CFX_CodePointRangesContain(ranges, 0x31)); + EXPECT_FALSE(CFX_CodePointRangesContain({}, 0x20)); +} diff --git a/core/fxge/cfx_face.h b/core/fxge/cfx_face.h index e68cb5f826..d82bc50d95 100644 --- a/core/fxge/cfx_face.h +++ b/core/fxge/cfx_face.h @@ -19,7 +19,8 @@ #include "core/fxcrt/observed_ptr.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/span.h" -#include "core/fxge/cfx_glyphoutline.h" // EmbedPDF(fonts) +#include "core/fxge/cfx_codepointrange.h" // EmbedPDF(fonts) +#include "core/fxge/cfx_glyphoutline.h" // EmbedPDF(fonts) #include "core/fxge/fx_font.h" #include "core/fxge/fx_fontencoding.h" @@ -152,6 +153,9 @@ class CFX_Face final : public Retainable, public Observable { FX_RECT GetCharBBox(uint32_t code, int glyph_index); std::vector GetCharCodesAndIndices(char32_t max_char); + // EmbedPDF(fonts): the code points up to |max_char| the current charmap + // maps to a glyph, as ranges (cfx_codepointrange.cpp). + std::vector GetMappedCodePointRanges(char32_t max_char); CharMap GetCurrentCharMap() const; std::optional GetCurrentCharMapEncoding() const; diff --git a/core/fxge/cfx_fontregistry.cpp b/core/fxge/cfx_fontregistry.cpp index 9742730a3e..820364df1b 100644 --- a/core/fxge/cfx_fontregistry.cpp +++ b/core/fxge/cfx_fontregistry.cpp @@ -13,7 +13,6 @@ #include #include "core/fdrm/fx_crypt_sha.h" -#include "core/fxcrt/containers/contains.h" #include "core/fxcrt/data_vector.h" #include "core/fxcrt/epdf_tls.h" #include "core/fxcrt/fx_codepage.h" @@ -35,7 +34,7 @@ struct RegisteredFont { ByteString family_name; // as registered, spaces kept int weight = pdfium::kFontWeightNormal; bool italic = false; - std::vector supported_unicodes; + std::vector coverage; DataVector memory_data; RetainPtr stream; CFX_FontRegistry::EmbeddingPermission permission = @@ -131,10 +130,16 @@ Instancing InstanceVariableFont(pdfium::span data, return outcome; } +struct Fallback { + CFX_FontRegistry::FontId id; + int tier; +}; + struct RegistryState { CFX_FontRegistry::FontId next_font_id = 1; std::vector> fonts; - std::vector fallback_order; + std::vector fallbacks; // in the order they were added + CFX_FontRegistry::FontId last_resort = CFX_FontRegistry::kInvalidFontId; }; EPDF_TLS RegistryState* g_registry = nullptr; @@ -213,26 +218,12 @@ std::unique_ptr LoadFont(pdfium::span data) { return font; } -std::vector CollectSupportedUnicodes(CFX_Font* font) { - if (!font) { +std::vector CollectCoverage(CFX_Font* font) { + RetainPtr face = font ? font->GetFace() : nullptr; + if (!face) { return {}; } - - auto char_codes_and_indices = - font->GetCharCodesAndIndices(pdfium::kMaximumSupplementaryCodePoint); - std::vector supported_unicodes; - supported_unicodes.reserve(char_codes_and_indices.size()); - for (const auto& item : char_codes_and_indices) { - if (item.glyph_index != 0) { - supported_unicodes.push_back(item.char_code); - } - } - - std::ranges::sort(supported_unicodes); - supported_unicodes.erase( - std::unique(supported_unicodes.begin(), supported_unicodes.end()), - supported_unicodes.end()); - return supported_unicodes; + return face->GetMappedCodePointRanges(pdfium::kMaximumSupplementaryCodePoint); } CFX_FontRegistry::FontId RegisterLoadedFontSource( @@ -297,9 +288,8 @@ CFX_FontRegistry::FontId RegisterLoadedFontSource( } } - std::vector supported_unicodes = - CollectSupportedUnicodes(font.get()); - if (supported_unicodes.empty()) { + std::vector coverage = CollectCoverage(font.get()); + if (coverage.empty()) { return CFX_FontRegistry::kInvalidFontId; } @@ -327,7 +317,7 @@ CFX_FontRegistry::FontId RegisterLoadedFontSource( registered_font->family_name.Trim(' '); registered_font->weight = NormalizeWeight(weight, *font); registered_font->italic = NormalizeItalic(italic, *font); - registered_font->supported_unicodes = std::move(supported_unicodes); + registered_font->coverage = std::move(coverage); registered_font->memory_data = std::move(memory_data); registered_font->stream = std::move(stream); @@ -402,7 +392,8 @@ void CFX_FontRegistry::ClearRegisteredFonts() { if (!g_registry) { return; } - g_registry->fallback_order.clear(); + g_registry->fallbacks.clear(); + g_registry->last_resort = kInvalidFontId; g_registry->fonts.clear(); // EmbedPDF: do not reset next_font_id. Documents can keep registered-font // marker resources after ClearRegisteredFonts(); reusing ids could make an @@ -411,16 +402,19 @@ void CFX_FontRegistry::ClearRegisteredFonts() { } // static -bool CFX_FontRegistry::AddFallbackFont(FontId font_id) { +bool CFX_FontRegistry::AddFallbackFont(FontId font_id, int tier) { if (!IsValidFont(font_id)) { return false; } RegistryState* registry = GetRegistry(); - if (pdfium::Contains(registry->fallback_order, font_id)) { + auto existing = + std::ranges::find(registry->fallbacks, font_id, &Fallback::id); + if (existing != registry->fallbacks.end()) { + existing->tier = tier; return true; } - registry->fallback_order.push_back(font_id); + registry->fallbacks.push_back({font_id, tier}); return true; } @@ -429,12 +423,26 @@ void CFX_FontRegistry::ClearFallbackFonts() { if (!g_registry) { return; } - g_registry->fallback_order.clear(); + g_registry->fallbacks.clear(); } // static bool CFX_FontRegistry::HasFallbackFonts() { - return g_registry && !g_registry->fallback_order.empty(); + return g_registry && !g_registry->fallbacks.empty(); +} + +// static +bool CFX_FontRegistry::SetLastResortFont(FontId font_id) { + if (font_id != kInvalidFontId && !IsValidFont(font_id)) { + return false; + } + GetRegistry()->last_resort = font_id; + return true; +} + +// static +CFX_FontRegistry::FontId CFX_FontRegistry::GetLastResortFont() { + return g_registry ? g_registry->last_resort : kInvalidFontId; } // static @@ -512,11 +520,11 @@ CFX_FontRegistry::ProgramFormat CFX_FontRegistry::GetProgramFormat( } // static -pdfium::span CFX_FontRegistry::GetSupportedUnicodes( +pdfium::span CFX_FontRegistry::GetCoverage( FontId font_id) { RegisteredFont* font = GetRegisteredFont(font_id); - return font ? pdfium::span(font->supported_unicodes) - : pdfium::span(); + return font ? pdfium::span(font->coverage) + : pdfium::span(); } // static @@ -599,7 +607,7 @@ bool CFX_FontRegistry::SupportsUnicode(FontId font_id, uint32_t unicode) { if (!font) { return false; } - return std::ranges::binary_search(font->supported_unicodes, unicode); + return CFX_CodePointRangesContain(font->coverage, unicode); } // static @@ -608,28 +616,12 @@ std::optional CFX_FontRegistry::FindFallbackFont( int weight, bool italic, bool for_authoring) { - if (!g_registry) { - return std::nullopt; - } - - std::optional best_font_id; - int best_score = std::numeric_limits::max(); - for (FontId font_id : g_registry->fallback_order) { - RegisteredFont* font = GetRegisteredFont(font_id); - if (!font || !SupportsUnicode(font_id, unicode)) { - continue; - } - if (for_authoring && !font->editing_authorized) { - continue; // preview-and-print: may render, may not write new text - } - - const int score = StyleScore(*font, weight, italic); - if (!best_font_id.has_value() || score < best_score) { - best_font_id = font_id; - best_score = score; + for (FontId font_id : GetFallbackOrder(weight, italic, for_authoring)) { + if (SupportsUnicode(font_id, unicode)) { + return font_id; } } - return best_font_id; + return std::nullopt; } // static @@ -639,21 +631,24 @@ std::vector CFX_FontRegistry::GetFallbackOrder( bool for_authoring) { struct Ranked { FontId id; + int tier; int score; }; std::vector ranked; if (g_registry) { - for (FontId font_id : g_registry->fallback_order) { - RegisteredFont* font = GetRegisteredFont(font_id); + for (const Fallback& fallback : g_registry->fallbacks) { + RegisteredFont* font = GetRegisteredFont(fallback.id); if (!font || (for_authoring && !font->editing_authorized)) { - continue; + continue; // preview-and-print: may render, may not write new text } - ranked.push_back({font_id, StyleScore(*font, weight, italic)}); + ranked.push_back( + {fallback.id, fallback.tier, StyleScore(*font, weight, italic)}); } } - // Stable: among equal scores the font added first stays first, as in - // FindFallbackFont(). - std::ranges::stable_sort(ranked, {}, &Ranked::score); + // Stable: among equal tiers and scores the font added first stays first. + std::ranges::stable_sort(ranked, [](const Ranked& a, const Ranked& b) { + return a.tier != b.tier ? a.tier < b.tier : a.score < b.score; + }); std::vector order; order.reserve(ranked.size()); for (const Ranked& entry : ranked) { diff --git a/core/fxge/cfx_fontregistry.h b/core/fxge/cfx_fontregistry.h index 270ec6d6b8..31e437b2f2 100644 --- a/core/fxge/cfx_fontregistry.h +++ b/core/fxge/cfx_fontregistry.h @@ -17,6 +17,7 @@ #include "core/fxcrt/data_vector.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/span.h" +#include "core/fxge/cfx_codepointrange.h" class CFX_Font; class IFX_SeekableReadStream; @@ -57,10 +58,19 @@ class CFX_FontRegistry { RetainPtr stream); static void ClearRegisteredFonts(); - static bool AddFallbackFont(FontId font_id); + // Adds a registered font to the fallback fonts, in |tier|: lower tiers are + // tried first. Adding a font that is already one changes its tier only. + static bool AddFallbackFont(FontId font_id, int tier); static void ClearFallbackFonts(); static bool HasFallbackFonts(); + // The font new text draws a character with when neither its font nor any + // fallback has it: one box for every code point, so the character shows as + // missing and the text keeps it. kInvalidFontId clears it; false for an + // unknown id. Unlike the fallbacks, it never draws a page's own text. + static bool SetLastResortFont(FontId font_id); + static FontId GetLastResortFont(); + // EmbedPDF: permissions (§3.4 of the rich text plan). static std::optional GetEmbeddingPermission( FontId font_id); @@ -81,9 +91,9 @@ class CFX_FontRegistry { // What the registered program is (registration accepts kTrueType and // kOpenTypeCFF only); kUnsupported for an unknown id. static ProgramFormat GetProgramFormat(FontId font_id); - // The scalars the program maps to a glyph, sorted and unique. Empty for an - // unknown id. - static pdfium::span GetSupportedUnicodes(FontId font_id); + // The code points the program maps to a glyph, as ranges in order. Empty + // for an unknown id. + static pdfium::span GetCoverage(FontId font_id); // True when the registered bytes are a static instance made from a // variable font at registration. A variable font that cannot be instanced // is refused, so every registered program is static. @@ -113,8 +123,9 @@ class CFX_FontRegistry { bool italic, bool for_authoring = false); // The fallback fonts in the order FindFallbackFont() tries them for a - // style: the closest weight and italic first, then the order they were - // added in. The first of them that maps a scalar is the one it returns. + // style: tier by tier, lowest first; within a tier the closest weight and + // italic first, then the order they were added in. The first of them that + // maps a scalar is the one it returns. static std::vector GetFallbackOrder(int weight, bool italic, bool for_authoring); diff --git a/fpdfsdk/epdf_annot_embeddertest.cpp b/fpdfsdk/epdf_annot_embeddertest.cpp index 97d998b185..1361dab373 100644 --- a/fpdfsdk/epdf_annot_embeddertest.cpp +++ b/fpdfsdk/epdf_annot_embeddertest.cpp @@ -180,7 +180,7 @@ EPDF_FONT_ID RegisterDroidSansFallbackFullFont() { EPDF_FONT_ID font_id = EPDFFont_RegisterMemFont64( "DroidSansFallbackFull", /*weight=*/400, /*italic=*/0, font_data.data(), font_data.size()); - if (font_id == 0 || !EPDFFont_AddFallbackFont(font_id)) { + if (font_id == 0 || !EPDFFont_AddFallbackFont(font_id, /*tier=*/0)) { ADD_FAILURE() << "Failed to register DroidSansFallbackFull as fallback"; return 0; } @@ -1353,7 +1353,7 @@ TEST_F(EPDFAnnotEmbedderTest, FreeTextAppearanceFallsBackToRegisteredFont) { EPDFFont_RegisterMemFont64("NotoSansSC", /*weight=*/400, /*italic=*/0, font_data.data(), font_data.size()); ASSERT_NE(0u, font_id); - ASSERT_TRUE(EPDFFont_AddFallbackFont(font_id)); + ASSERT_TRUE(EPDFFont_AddFallbackFont(font_id, /*tier=*/0)); ScopedFPDFDocument doc(FPDF_CreateNewDocument()); ASSERT_TRUE(doc); @@ -2411,7 +2411,7 @@ TEST_F(EPDFAnnotEmbedderTest, "DroidSansFallbackFull", /*weight=*/400, /*italic=*/0, droid_preview.data(), droid_preview.size()); ASSERT_NE(0u, droid_id); - ASSERT_TRUE(EPDFFont_AddFallbackFont(droid_id)); + ASSERT_TRUE(EPDFFont_AddFallbackFont(droid_id, /*tier=*/0)); ScopedFPDFDocument doc(FPDF_CreateNewDocument()); ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); @@ -2969,7 +2969,7 @@ TEST_F(EPDFAnnotEmbedderTest, "NotoSansSC", /*weight=*/400, /*italic=*/0, noto.data(), noto.size()); ASSERT_NE(0u, roboto_id); ASSERT_NE(0u, noto_id); - ASSERT_TRUE(EPDFFont_AddFallbackFont(noto_id)); + ASSERT_TRUE(EPDFFont_AddFallbackFont(noto_id, /*tier=*/0)); ScopedFPDFDocument doc(FPDF_CreateNewDocument()); ASSERT_TRUE(doc); @@ -4279,6 +4279,107 @@ TEST_F(EPDFAnnotEmbedderTest, RichTextRtlRunCarriesActualTextAndExtracts) { << "extracted: " << codepoints; } +// A character no font has is written with the last-resort font, subset into +// the appearance like any fallback, so every viewer shows the same box. Two +// such characters share its one glyph, so the run carries its text as +// ActualText and extracts as typed; the write reports both. +TEST_F(EPDFAnnotEmbedderTest, RichTextLastResortDrawsWhatNoFontHas) { + ScopedRegisteredFonts scoped_fonts; + std::vector roboto = LoadRobotoFontData(); + std::vector box = GetFileContents( + PathService::GetTestFilePath("fonts/last_resort.ttf").c_str()); + ASSERT_FALSE(roboto.empty()); + ASSERT_FALSE(box.empty()); + ASSERT_NE(0u, EPDFFont_RegisterMemFont64("Roboto", /*weight=*/400, + /*italic=*/0, roboto.data(), + roboto.size())); + const EPDF_FONT_ID box_id = EPDFFont_RegisterMemFont64( + "Last Resort", /*weight=*/400, /*italic=*/0, box.data(), box.size()); + ASSERT_NE(0u, box_id); + ASSERT_TRUE(EPDFFont_SetLastResortFont(box_id)); + + std::string saved; + { + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FREETEXT)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 320.0f, 350.0f, 250.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetRichTextJSON( + annot.get(), + "{\"body\":{\"family\":\"Roboto\",\"size\":18}," + "\"paragraphs\":[{\"runs\":[{\"text\":\"A\\u4E2D\\u4E2EB\"}]}]}")); + EXPECT_EQ(2, EPDFDoc_CountMissingCharacters(doc.get())); + RetainPtr font = + GetAppearanceFontDict(annot.get(), RegisteredFontAlias(box_id)); + ASSERT_TRUE(font); + RetainPtr descriptor = + GetType0FontDescriptor(font.Get()); + ASSERT_TRUE(descriptor); + EXPECT_TRUE(descriptor->GetStreamFor("FontFile2")); + EXPECT_THAT(GetNormalAppearance(annot.get()), + HasSubstr(L"/Span <>> BDC")); + + ASSERT_TRUE(FPDFPage_Flatten(page.get(), FLAT_NORMALDISPLAY)); + unsigned long saved_size = 0; + void* saved_buffer = + EPDF_SaveDocumentToOwnedBuffer(doc.get(), /*flags=*/0, &saved_size); + ASSERT_TRUE(saved_buffer); + saved.assign(static_cast(saved_buffer), saved_size); + EPDF_FreeBuffer(saved_buffer); + } + ScopedFPDFDocument doc(FPDF_LoadMemDocument( + saved.data(), static_cast(saved.size()), nullptr)); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); + ASSERT_TRUE(page); + EXPECT_NE(std::wstring::npos, ExtractPageText(page.get()) + .find(L"A\x4E2D\x4E2E" + L"B")); +} + +// A last resort whose licence allows preview and print only writes no new +// text until the app authorizes it, like any other font. +TEST_F(EPDFAnnotEmbedderTest, RichTextPreviewAndPrintLastResortWaits) { + ScopedRegisteredFonts scoped_fonts; + std::vector roboto = LoadRobotoFontData(); + std::vector box = WithFsType( + GetFileContents( + PathService::GetTestFilePath("fonts/last_resort.ttf").c_str()), + 0x0004); + ASSERT_FALSE(roboto.empty()); + ASSERT_FALSE(box.empty()); + ASSERT_NE(0u, EPDFFont_RegisterMemFont64("Roboto", /*weight=*/400, + /*italic=*/0, roboto.data(), + roboto.size())); + const EPDF_FONT_ID box_id = EPDFFont_RegisterMemFont64( + "Last Resort", /*weight=*/400, /*italic=*/0, box.data(), box.size()); + ASSERT_NE(0u, box_id); + ASSERT_TRUE(EPDFFont_SetLastResortFont(box_id)); + + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); + const char kJson[] = + "{\"body\":{\"family\":\"Roboto\",\"size\":18}," + "\"paragraphs\":[{\"runs\":[{\"text\":\"A\\u4E2DB\"}]}]}"; + for (const bool authorized : {false, true}) { + SCOPED_TRACE(authorized); + if (authorized) { + ASSERT_TRUE(EPDFFont_AuthorizeEditing(box_id)); + } + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FREETEXT)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 320.0f, 350.0f, 250.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetRichTextJSON(annot.get(), kJson)); + EXPECT_EQ(authorized, !!GetAppearanceFontDict(annot.get(), + RegisteredFontAlias(box_id))); + } +} + // Rung 2 of plan §3.3 (C note §1.3): a family that is no longer registered // but whose program is already in the document resolves to that program, // through a new Type0 dictionary that references the existing stream. @@ -5073,7 +5174,7 @@ TEST_F(EPDFAnnotEmbedderTest, RegisteredFallbackFontRendersPageMissingGlyph) { EPDFFont_RegisterMemFont64("NotoSansSC", /*weight=*/400, /*italic=*/0, font_data.data(), font_data.size()); ASSERT_NE(0u, font_id); - ASSERT_TRUE(EPDFFont_AddFallbackFont(font_id)); + ASSERT_TRUE(EPDFFont_AddFallbackFont(font_id, /*tier=*/0)); ScopedFPDFBitmap bitmap = render_broken_pdf(broken_pdf); ASSERT_TRUE(bitmap); diff --git a/fpdfsdk/epdf_c_api_test.c b/fpdfsdk/epdf_c_api_test.c index 08eaba4ad7..fc771c565c 100644 --- a/fpdfsdk/epdf_c_api_test.c +++ b/fpdfsdk/epdf_c_api_test.c @@ -318,6 +318,7 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFFont_GetEmbeddingPermission); CHK(EPDFFont_GetFallbackOrder); CHK(EPDFFont_GetFamilyName); + CHK(EPDFFont_GetLastResortFont); CHK(EPDFFont_GetMetrics); CHK(EPDFFont_GetPostScriptName); CHK(EPDFFont_GetProgramData); @@ -330,6 +331,7 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFFont_RegisterFont); CHK(EPDFFont_RegisterMemFont); CHK(EPDFFont_RegisterMemFont64); + CHK(EPDFFont_SetLastResortFont); // epdf_form.h CHK(EPDFForm_AttachWidget); diff --git a/fpdfsdk/epdf_font.cpp b/fpdfsdk/epdf_font.cpp index 0cdd15cb74..a38b817e5f 100644 --- a/fpdfsdk/epdf_font.cpp +++ b/fpdfsdk/epdf_font.cpp @@ -180,14 +180,23 @@ EPDFDoc_GetFontEmbeddingPolicy(FPDF_DOCUMENT document) { } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFFont_AddFallbackFont(EPDF_FONT_ID font_id) { - return CFX_FontRegistry::AddFallbackFont(font_id); +EPDFFont_AddFallbackFont(EPDF_FONT_ID font_id, int tier) { + return CFX_FontRegistry::AddFallbackFont(font_id, tier); } FPDF_EXPORT void FPDF_CALLCONV EPDFFont_ClearFallbackFonts(void) { CFX_FontRegistry::ClearFallbackFonts(); } +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFont_SetLastResortFont(EPDF_FONT_ID font_id) { + return CFX_FontRegistry::SetLastResortFont(font_id); +} + +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV EPDFFont_GetLastResortFont(void) { + return CFX_FontRegistry::GetLastResortFont(); +} + // ---- Registered fonts: programs, coverage, fallback order ------------------- namespace { @@ -207,7 +216,7 @@ unsigned long CopyBytesAndReturnLength(pdfium::span bytes, // Writes (first, count) pairs into the caller's array, up to |capacity| // pairs; returns how many ranges there are either way. unsigned long CopyRangesAndReturnCount( - const std::vector>& ranges, + pdfium::span ranges, uint32_t* out, unsigned long capacity) { if (out) { @@ -217,7 +226,7 @@ unsigned long CopyRangesAndReturnCount( UNSAFE_BUFFERS(pdfium::span(out, count * 2)); for (size_t i = 0; i < count; ++i) { destination[2 * i] = ranges[i].first; - destination[2 * i + 1] = ranges[i].second; + destination[2 * i + 1] = ranges[i].count; } } return pdfium::checked_cast(ranges.size()); @@ -292,16 +301,8 @@ FPDF_EXPORT unsigned long FPDF_CALLCONV EPDFFont_GetCoverage(EPDF_FONT_ID font_id, uint32_t* ranges, unsigned long range_capacity) { - std::vector> result; - for (uint32_t scalar : CFX_FontRegistry::GetSupportedUnicodes(font_id)) { - if (!result.empty() && - result.back().first + result.back().second == scalar) { - ++result.back().second; - } else { - result.emplace_back(scalar, 1); - } - } - return CopyRangesAndReturnCount(result, ranges, range_capacity); + return CopyRangesAndReturnCount(CFX_FontRegistry::GetCoverage(font_id), + ranges, range_capacity); } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFFont_GetMetrics(EPDF_FONT_ID font_id, diff --git a/fpdfsdk/epdf_font_embeddertest.cpp b/fpdfsdk/epdf_font_embeddertest.cpp index 177b8da679..ea4d4431f4 100644 --- a/fpdfsdk/epdf_font_embeddertest.cpp +++ b/fpdfsdk/epdf_font_embeddertest.cpp @@ -283,8 +283,8 @@ TEST_F(EPDFFontEmbedderTest, RegisteredFontProgramCoverageAndFallbackOrder) { EXPECT_EQ(EPDF_FONT_FORMAT_TRUETYPE, EPDFFont_GetProgramFormat(regular)); EXPECT_EQ(-1, EPDFFont_GetProgramFormat(9999)); - ASSERT_TRUE(EPDFFont_AddFallbackFont(regular)); - ASSERT_TRUE(EPDFFont_AddFallbackFont(bold)); + ASSERT_TRUE(EPDFFont_AddFallbackFont(regular, /*tier=*/0)); + ASSERT_TRUE(EPDFFont_AddFallbackFont(bold, /*tier=*/0)); EPDF_FONT_ID order[2] = {}; ASSERT_EQ(2u, EPDFFont_GetFallbackOrder(700, false, order, 2)); EXPECT_EQ(bold, order[0]); @@ -292,6 +292,45 @@ TEST_F(EPDFFontEmbedderTest, RegisteredFontProgramCoverageAndFallbackOrder) { ASSERT_EQ(2u, EPDFFont_GetFallbackOrder(400, false, order, 2)); EXPECT_EQ(regular, order[0]); EXPECT_EQ(bold, order[1]); + + // A lower tier comes first, whatever the style: moved to tier 1, the + // regular font follows the bold one for regular text too. + ASSERT_TRUE(EPDFFont_AddFallbackFont(regular, /*tier=*/1)); + ASSERT_EQ(2u, EPDFFont_GetFallbackOrder(400, false, order, 2)); + EXPECT_EQ(bold, order[0]); + EXPECT_EQ(regular, order[1]); +} + +// The last resort maps every code point but the controls and the surrogates +// to one box: two ranges, however many code points they hold. It isn't a +// fallback, and goes with the registered fonts. +TEST_F(EPDFFontEmbedderTest, LastResortFont) { + ScopedRegisteredFonts scoped_fonts; + std::vector box = GetFileContents( + PathService::GetTestFilePath("fonts/last_resort.ttf").c_str()); + ASSERT_FALSE(box.empty()); + const EPDF_FONT_ID last_resort = EPDFFont_RegisterMemFont64( + "Last Resort", /*weight=*/400, /*italic=*/0, box.data(), box.size()); + ASSERT_NE(0u, last_resort); + uint32_t ranges[4] = {}; + ASSERT_EQ(2u, EPDFFont_GetCoverage(last_resort, ranges, 2)); + EXPECT_EQ(0x20u, ranges[0]); + EXPECT_EQ(0xD800u - 0x20u, ranges[1]); + EXPECT_EQ(0xE000u, ranges[2]); + EXPECT_EQ(0x110000u - 0xE000u, ranges[3]); + + EXPECT_EQ(0u, EPDFFont_GetLastResortFont()); + EXPECT_FALSE(EPDFFont_SetLastResortFont(9999)); + ASSERT_TRUE(EPDFFont_SetLastResortFont(last_resort)); + EXPECT_EQ(last_resort, EPDFFont_GetLastResortFont()); + EPDFFont_ClearFallbackFonts(); + EXPECT_EQ(last_resort, EPDFFont_GetLastResortFont()); + EXPECT_EQ(0u, EPDFFont_GetFallbackOrder(400, false, nullptr, 0)); + ASSERT_TRUE(EPDFFont_SetLastResortFont(0)); + EXPECT_EQ(0u, EPDFFont_GetLastResortFont()); + ASSERT_TRUE(EPDFFont_SetLastResortFont(last_resort)); + EPDFFont_ClearRegisteredFonts(); + EXPECT_EQ(0u, EPDFFont_GetLastResortFont()); } // A whole program the document holds is listed with its facts, and its bytes @@ -462,7 +501,7 @@ TEST_F(EPDFFontEmbedderTest, ResolvesTheFontsAWriteWouldUse) { /*italic=*/0, droid.data(), droid.size()); ASSERT_NE(0u, roboto_id); ASSERT_NE(0u, droid_id); - ASSERT_TRUE(EPDFFont_AddFallbackFont(droid_id)); + ASSERT_TRUE(EPDFFont_AddFallbackFont(droid_id, /*tier=*/0)); CreateEmptyDocument(); ScopedFPDFWideString text0 = GetFPDFWideString(L"Hello \x4E2D"); diff --git a/fpdfsdk/epdf_richtextlayout_embeddertest.cpp b/fpdfsdk/epdf_richtextlayout_embeddertest.cpp index 97bbf1e3c2..f12cf7047c 100644 --- a/fpdfsdk/epdf_richtextlayout_embeddertest.cpp +++ b/fpdfsdk/epdf_richtextlayout_embeddertest.cpp @@ -5,12 +5,14 @@ #include +#include #include #include #include "core/fpdfapi/font/cpdf_font.h" #include "core/fpdfapi/parser/cpdf_document.h" #include "core/fpdfdoc/cpdf_annotfontmap.h" +#include "core/fpdfdoc/cpdf_documentfonts.h" #include "core/fpdfdoc/cpdf_richtext.h" #include "core/fxcrt/fx_coordinates.h" #include "core/fxcrt/widestring.h" @@ -19,6 +21,7 @@ #include "public/epdf_font.h" #include "public/fpdf_edit.h" #include "testing/embedder_test.h" +#include "testing/fx_string_testhelpers.h" #include "testing/gtest/include/gtest/gtest.h" #include "testing/utils/file_util.h" #include "testing/utils/path_service.h" @@ -53,24 +56,40 @@ class EPDFRichTextLayoutEmbedderTest : public EmbedderTest { void SetUp() override { EmbedderTest::SetUp(); EPDFFont_ClearRegisteredFonts(); - const std::string path = PathService::GetThirdPartyFilePath( - "test_fonts/test_fonts/Arimo-Regular.ttf"); - ASSERT_FALSE(path.empty()); - arimo_ = GetFileContents(path.c_str()); - ASSERT_FALSE(arimo_.empty()); - ASSERT_NE(0u, EPDFFont_RegisterMemFont64("Arimo", /*weight=*/400, - /*italic=*/0, arimo_.data(), - arimo_.size())); + arimo_ = Register("Arimo", TestFont("Arimo-Regular.ttf")); + ASSERT_NE(0u, arimo_); doc_.reset(FPDF_CreateNewDocument()); ASSERT_TRUE(doc_); } void TearDown() override { + fonts_.reset(); doc_.reset(); EPDFFont_ClearRegisteredFonts(); EmbedderTest::TearDown(); } + static std::string TestFont(const char* name) { + return PathService::GetThirdPartyFilePath( + std::string("test_fonts/test_fonts/") + name); + } + + static EPDF_FONT_ID Register(const char* family, const std::string& path) { + const std::vector data = GetFileContents(path.c_str()); + EXPECT_FALSE(data.empty()) << path; + return EPDFFont_RegisterMemFont64(family, /*weight=*/400, /*italic=*/0, + data.data(), data.size()); + } + + // The registered font |run| draws with; 0 for any other. + EPDF_FONT_ID FontOf(const GlyphRun& run) const { + return fonts_->DescribeEntry(run.font_entry).registered_font_id; + } + + CPDF_DocumentFonts* DocumentFonts() { + return CPDF_DocumentFonts::For(CPDFDocumentFromFPDFDocument(doc_.get())); + } + // One paragraph of |text| in |family| at 12 pt, in a box |width| wide. std::vector Arrange(const WideString& text, bool rtl, @@ -94,18 +113,21 @@ class EPDFRichTextLayoutEmbedderTest : public EmbedderTest { paragraph.props = props; paragraph.runs.push_back({text, {}}); document.paragraphs.push_back(std::move(paragraph)); - CPDF_AnnotFontMap fonts(CPDFDocumentFromFPDFDocument(doc_.get()), - /*default_font=*/nullptr, ByteString(), - /*allow_registered_fallbacks=*/true); - CPDF_RichTextLayout layout(&fonts, options); + fonts_ = std::make_unique( + CPDFDocumentFromFPDFDocument(doc_.get()), + /*default_font=*/nullptr, ByteString(), + /*allow_registered_fallbacks=*/true); + CPDF_RichTextLayout layout(fonts_.get(), options); return layout .Arrange(document, CFX_FloatRect(0, 0, width, 300), CPDF_Annot::VerticalAlignment::kTop) .lines; } - std::vector arimo_; + EPDF_FONT_ID arimo_ = 0; ScopedFPDFDocument doc_; + // The fonts of the last Arrange(). + std::unique_ptr fonts_; }; // Hebrew and digits in a left-to-right paragraph: the digits take the @@ -250,3 +272,78 @@ TEST_F(EPDFRichTextLayoutEmbedderTest, JustifiedInkReachesBothEdges) { } EXPECT_NEAR(50.0f, ink_end, 0.01f); } + +// A cluster goes to the first fallback, in order, that has all of it. Gelasio +// has "e" but not the combining acute, so "e" + U+0301 goes to Arimo, added +// after it; Helvetica, the face, has neither whole. +TEST_F(EPDFRichTextLayoutEmbedderTest, FallbackHasTheWholeCluster) { + const EPDF_FONT_ID gelasio = + Register("Gelasio", TestFont("Gelasio-Regular.ttf")); + ASSERT_NE(0u, gelasio); + ASSERT_TRUE(EPDFFont_AddFallbackFont(gelasio, /*tier=*/0)); + ASSERT_TRUE(EPDFFont_AddFallbackFont(arimo_, /*tier=*/0)); + const std::vector lines = + Arrange(L"e\x0301", /*rtl=*/false, kBoxWidth, "Helvetica"); + ASSERT_EQ(1u, lines.size()); + ASSERT_EQ(1u, lines[0].runs.size()); + EXPECT_EQ(arimo_, FontOf(lines[0].runs[0])); + EXPECT_TRUE(DocumentFonts()->missing_characters().empty()); +} + +// Fallbacks are tried tier by tier: Tinos, added after Arimo in a lower tier, +// draws the Hebrew both have. +TEST_F(EPDFRichTextLayoutEmbedderTest, FallbackTiers) { + const EPDF_FONT_ID tinos = Register("Tinos", TestFont("Tinos-Regular.ttf")); + ASSERT_NE(0u, tinos); + ASSERT_TRUE(EPDFFont_AddFallbackFont(arimo_, /*tier=*/1)); + ASSERT_TRUE(EPDFFont_AddFallbackFont(tinos, /*tier=*/0)); + const std::vector lines = + Arrange(L"\x05E9\x05DC", /*rtl=*/false, kBoxWidth, "Helvetica"); + ASSERT_EQ(1u, lines.size()); + ASSERT_EQ(1u, lines[0].runs.size()); + EXPECT_EQ(tinos, FontOf(lines[0].runs[0])); +} + +// A character no font has is drawn with the last resort, as its box, and is +// still reported; one that draws nothing stays with the face. +TEST_F(EPDFRichTextLayoutEmbedderTest, LastResortDrawsWhatNoFontHas) { + // Without a last resort, the face draws its missing glyph (a run of its + // own: another script). + std::vector lines = Arrange( + L"A\x4E2D" + L"B", + /*rtl=*/false); + ASSERT_EQ(1u, lines.size()); + ASSERT_EQ(3u, lines[0].runs.size()); + for (const GlyphRun& run : lines[0].runs) { + EXPECT_EQ(arimo_, FontOf(run)); + } + + const EPDF_FONT_ID box = Register( + "Last Resort", PathService::GetTestFilePath("fonts/last_resort.ttf")); + ASSERT_NE(0u, box); + ASSERT_TRUE(EPDFFont_SetLastResortFont(box)); + DocumentFonts()->ClearMissingCharacters(); + lines = Arrange( + L"A\x4E2D" + L"B", + /*rtl=*/false); + ASSERT_EQ(1u, lines.size()); + ASSERT_EQ(3u, lines[0].runs.size()); + EXPECT_EQ(arimo_, FontOf(lines[0].runs[0])); + EXPECT_EQ(box, FontOf(lines[0].runs[1])); + EXPECT_EQ(arimo_, FontOf(lines[0].runs[2])); + ASSERT_EQ(1u, lines[0].runs[1].glyphs.size()); + EXPECT_NE(0u, lines[0].runs[1].glyphs[0].gid); + EXPECT_EQ(std::vector{L"\x4E2D"}, + DocumentFonts()->missing_characters()); + + // An invisible separator Arimo lacks is hidden by it, not boxed. + lines = Arrange( + L"A\x2063" + L"B", + /*rtl=*/false); + ASSERT_EQ(1u, lines.size()); + ASSERT_EQ(1u, lines[0].runs.size()); + EXPECT_EQ(arimo_, FontOf(lines[0].runs[0])); +} diff --git a/public/epdf_font.h b/public/epdf_font.h index e6a7e7c91b..42862d8c53 100644 --- a/public/epdf_font.h +++ b/public/epdf_font.h @@ -71,7 +71,7 @@ EPDFFont_RegisterMemFont64(FPDF_BYTESTRING family_name, size_t size); // Experimental EmbedPDF Extension API. -// Clear all registered fonts and the fallback font order. +// Clear all registered fonts, the fallback fonts and the last resort. // // Existing documents may still contain registered-font DA marker resources // after this call; those markers are invalid until their fonts are registered @@ -143,20 +143,36 @@ FPDF_EXPORT int FPDF_CALLCONV EPDFDoc_GetFontEmbeddingPolicy(FPDF_DOCUMENT document); // Experimental EmbedPDF Extension API. -// Add a registered font to the ordered fallback list used when the selected -// font does not contain a glyph or a PDF page needs a substitute font. +// Add a registered font to the fallback fonts: the fonts that draw a +// character the selected font doesn't have, in new text and where a PDF page +// needs a substitute font. They are tried tier by tier, lowest |tier| first. +// Adding a font that is already one changes its tier only. FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFFont_AddFallbackFont(EPDF_FONT_ID font_id); +EPDFFont_AddFallbackFont(EPDF_FONT_ID font_id, int tier); // Experimental EmbedPDF Extension API. -// Clear the ordered fallback font list without unregistering fonts. +// Clear the fallback fonts without unregistering them. FPDF_EXPORT void FPDF_CALLCONV EPDFFont_ClearFallbackFonts(void); +// Experimental EmbedPDF Extension API. +// Make a registered font the last resort: new text draws a character that +// neither its font nor any fallback has with it, so the character shows as +// that font's box and the text keeps it. Such a character is still reported +// as missing (EPDFDoc_CountMissingCharacters()). A page's own text never uses +// it. Pass 0 to clear it. Returns false for an unknown id. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFont_SetLastResortFont(EPDF_FONT_ID font_id); + +// Experimental EmbedPDF Extension API. +// The last-resort font; 0 when there is none. +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV EPDFFont_GetLastResortFont(void); + // Experimental EmbedPDF Extension API. // The registered fallback fonts in the order new text tries them for a -// style: the closest weight and italic first, then the order they were -// added in; fonts not authorized for editing are left out. The first of -// them that maps a character is the one that draws it. +// style: tier by tier, lowest first; within a tier the closest weight and +// italic first, then the order they were added in. Fonts not authorized for +// editing are left out. The first of them that has a whole character +// (a base character with its marks) is the one that draws it. // // weight, italic - the style. // ids - receives up to |capacity| font ids; may be NULL. diff --git a/testing/resources/fonts/last_resort.ttf b/testing/resources/fonts/last_resort.ttf new file mode 100644 index 0000000000000000000000000000000000000000..37f80e52778a4f2e04849fa3067ad62137d7b550 GIT binary patch literal 1120 zcmbVKO=}ZD7=C7Uw_0hdpm>lT2Jz5?Ni~E}N)DAI1u?XxEyaUW$24XOn-8*EnjVCn zJ^2F!Kh7RJdWfLl!LtZn6r_0Y2PlG2*Joz8+JuVWyUfh?x1&K>Wr`-*&ye8#Wq4gI$Cn7l_`@Tu^gi0|aj$S?TK zsOOzQk^CL`@t|GPm?VPuz-y$bdmVVJ4)K{>wRE$d*;|MJ>GI5W+F=wwL=JH0j2*7* zi~Ed#yeF?yb1iwrI(>eS|5YkW<$1h>iepE`Gk~M;=D=Bem3?9b!CP<-><42QxArM} zvmjpuypQ?P?-Ot;POvod$zA4faE!)OthNlquYsR=M!{6sFQX`_-S4N>@Z#fQc=KfO z8yx40@fwhW%-o&rz!+IQnZb6=$~M#CFllx}E2AlsJZ3WaJBOKNSVqxh|H2)mp}>=1 zfhn>COH5!r#SZcaQtZ;_QHnjB<6aa05&G<|L08y)Ph>LwcS5plaY zT;eGZm6*MaEo@*3Wz17k#VSz+P25I(K(7(n=w<3dMs>*yZZJN8CLfNfQfp!kM`ZpH zV>&ph(i$sunQfDmTkP{PvYf(v|5LTfJT+!)F{7DMafTLOQ$w+1pGe^fH>c^Bu)&y% zv=8@`vpty|M~nF#`B>z literal 0 HcmV?d00001 From 8c214cc13984690ba49bc1c420f53e2000b1b345 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Tue, 20 Oct 2026 00:45:58 +0300 Subject: [PATCH 05/16] Lay out form fields and redaction labels with the shared text engine ArrangeField lays a field's text out with CPVT's geometry (word breaks, line metrics, fitted sizes, alignment, combs) over the shared engine's clusters: a font per cluster with the last resort, shaping, bidi and whole supplementary characters. The forms generator and redaction labels write through it with one annotation font map, saved or on the fly. The map is no longer a CPVT font map, and layout fonts carry no ToUnicode. A test holds the layout to CPVT across the forms parity matrix. --- BUILD.gn | 5 +- BUILD.gn.orig | 532 ++++++++++++++++++ core/fpdfdoc/cpdf_annotfontmap.cpp | 98 ---- core/fpdfdoc/cpdf_annotfontmap.h | 21 +- core/fpdfdoc/cpdf_annotfontsubset.cpp | 28 +- core/fpdfdoc/cpdf_annotfontsubset.h | 10 +- .../cpdf_appearancegenerator_forms.cpp | 371 +++++------- .../cpdf_appearancegenerator_freetext.cpp | 79 ++- .../cpdf_appearancegenerator_internal.h | 9 +- .../cpdf_appearancegenerator_redaction.cpp | 89 ++- .../fpdfdoc/cpdf_appearancegenerator_text.cpp | 9 +- core/fpdfdoc/cpdf_richtextlayout.cpp | 502 ++++++++++++++++- core/fpdfdoc/cpdf_richtextlayout.h | 31 + core/fpdfdoc/cpvt_section.cpp | 13 + core/fpdfdoc/cpvt_section.h | 8 + fpdfsdk/epdf_annot_embeddertest.cpp | 18 +- fpdfsdk/epdf_font_embeddertest.cpp | 136 +++++ fpdfsdk/epdf_richtextlayout_embeddertest.cpp | 173 ++++++ testing/resources/fonts/last_resort.ttf | Bin 1120 -> 1120 bytes 19 files changed, 1685 insertions(+), 447 deletions(-) create mode 100644 BUILD.gn.orig diff --git a/BUILD.gn b/BUILD.gn index 81827f26af..55318ad9ff 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -281,7 +281,7 @@ group("pdfium_public_headers") { ] } -component("pdfium") { +shared_library("pdfium") { output_name = "embedpdf" libs = [] configs += [ ":pdfium_strict_config" ] @@ -319,6 +319,9 @@ component("pdfium") { "gdi32.lib", "user32.lib", ] + sources = [ + "resources.rc", + ] } if (is_mac) { diff --git a/BUILD.gn.orig b/BUILD.gn.orig new file mode 100644 index 0000000000..81827f26af --- /dev/null +++ b/BUILD.gn.orig @@ -0,0 +1,532 @@ +# Copyright 2016 The PDFium Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("//build/config/clang/clang.gni") +import("//build/config/gclient_args.gni") +import("//testing/test.gni") +import("pdfium.gni") + +group("default") { + testonly = true + deps = [ ":pdfium" ] + if (pdf_is_standalone) { + deps += [ ":pdfium_all" ] + } +} + +config("pdfium_common_config") { + cflags = [] + cflags_cc = [] + ldflags = [] + include_dirs = [ "." ] + inputs = [ "unsafe_buffers_paths.txt" ] + defines = [] + + if (!use_system_libopenjpeg2) { + defines += [ "OPJ_STATIC" ] + } + + if (pdf_enable_click_logging) { + defines += [ "PDF_ENABLE_CLICK_LOGGING" ] + } + + if (pdf_enable_fontations) { + defines += [ "PDF_ENABLE_FONTATIONS" ] + } + + if (pdf_enable_freetype) { + defines += [ "PDF_ENABLE_FREETYPE" ] + } + + if (pdf_enable_rust_bmp) { + defines += [ "PDF_ENABLE_RUST_BMP" ] + } + + if (pdf_enable_rust_jpeg) { + defines += [ "PDF_ENABLE_RUST_JPEG" ] + } + + if (pdf_enable_rust_png) { + defines += [ "PDF_ENABLE_RUST_PNG" ] + } + + if (pdf_use_partition_alloc) { + defines += [ "PDF_USE_PARTITION_ALLOC" ] + } + + # EmbedPDF: thread-confined runtime. Translates the embedpdf_thread_local_globals + # GN arg into the EPDF_THREAD_LOCAL_GLOBALS define consumed by EPDF_TLS + # (core/fxcrt/epdf_tls.h). Default off; see pdfium.gni. + if (embedpdf_thread_local_globals) { + defines += [ "EPDF_THREAD_LOCAL_GLOBALS" ] + } + + if (pdf_use_libyuv) { + defines += [ "PDF_USE_LIBYUV" ] + } + + if (is_win) { + # Assume UTF-8 by default to avoid code page dependencies. + cflags += [ "/utf-8" ] + } + + if (is_clang) { + # May flag some issues when converting int to size_t. + cflags += [ "-Wtautological-unsigned-zero-compare" ] + + # Enforce style guide decision regarding CTAD usage: + # https://google.github.io/styleguide/cppguide.html#CTAD + cflags += [ "-Wctad-maybe-unsupported" ] + } + + if (!is_win && !is_clang) { + cflags += [ + # GCC assumes that control can get past an exhaustive switch and then + # warns if there's no return there. + "-Wno-return-type", + ] + } + + if (clang_use_chrome_plugins) { + # Catch misuse of C-style pointers. + # TODO(crbug.com/1320670): enable for non-debug builds once this stops + # interfering with code generation. + # TODO(tsepez): enable for windows, too. + if (is_debug && pdf_use_partition_alloc && !is_win) { + cflags += [ + "-Xclang", + "-plugin-arg-raw-ptr-plugin", + "-Xclang", + "check-raw-ptr-fields", + + "-Xclang", + "-plugin-arg-raw-ptr-plugin", + "-Xclang", + "raw-ptr-exclude-path=public", + + "-Xclang", + "-plugin-arg-raw-ptr-plugin", + "-Xclang", + "raw-ptr-exclude-path=test", + + # TODO(tsepez): enforce raw_ref<> as well. + # "-Xclang", + # "-plugin-arg-raw-ptr-plugin", + # "-Xclang", + # "check-raw-ref-fields", + ] + defines += [ "PDF_ENABLE_UNOWNED_PTR_EXCLUSION" ] + } + + # Catch misuse of cppgc in XFA. + if (pdf_enable_xfa) { + cflags += [ + "-Xclang", + "-add-plugin", + "-Xclang", + "blink-gc-plugin", + ] + + # Disable GC plugin forbidding off-heap collections of GCed: + cflags += [ + "-Xclang", + "-plugin-arg-blink-gc-plugin", + "-Xclang", + "disable-off-heap-collections-of-gced-check", + ] + } + } +} + +config("pdfium_implementation_config") { + defines = [ "FPDF_IMPLEMENTATION" ] + visibility = [ ":pdfium_public_headers" ] +} + +config("pdfium_public_config") { + defines = [] + + if (pdf_enable_brotli) { + defines += [ "PDF_ENABLE_BROTLI" ] + } + + if (pdf_enable_v8) { + defines += [ "PDF_ENABLE_V8" ] + + if (pdf_enable_xfa) { + defines += [ "PDF_ENABLE_XFA" ] + } + } + + if (pdf_use_agg) { + defines += [ "PDF_USE_AGG" ] + } + if (pdf_use_skia) { + defines += [ "PDF_USE_SKIA" ] + } +} + +config("pdfium_core_config") { + cflags = [] + configs = [ + ":pdfium_common_config", + ":pdfium_public_config", + "//build/config/compiler:noshadowing", + ] + defines = [] + if (is_clang) { + cflags += [ + "-Wcovered-switch-default", + "-Wlifetime-safety-permissive", + ] + } +} + +config("pdfium_strict_config") { + configs = [ + ":pdfium_core_config", + "//build/config/compiler:wexit_time_destructors", + "//build/config/compiler:wglobal_constructors", + ] +} + +config("pdfium_noshorten_config") { + cflags = [] + if (is_clang) { + cflags += [ "-Wshorten-64-to-32" ] + } +} + +source_set("pdfium_public_headers_impl") { + sources = [ + "public/cpp/fpdf_deleters.h", + "public/cpp/fpdf_scopers.h", + # EmbedPDF: detached, read-only PDF action models. + "public/epdf_action.h", + # EmbedPDF: the annotation API that extends public/fpdf_annot.h. + "public/epdf_annot.h", + # EmbedPDF: what we add to public/fpdf_attachment.h. + "public/epdf_attachment.h", + # EmbedPDF: what we add to public/fpdf_catalog.h. + "public/epdf_catalog.h", + # EmbedPDF: base documents and layers. + "public/epdf_layer.h", + # EmbedPDF: captures of what a layer write loses, put back by an undo. + "public/epdf_capture.h", + # EmbedPDF: digests of bytes in memory, one implementation for every caller. + "public/epdf_digest.h", + # EmbedPDF: what we add to public/fpdf_doc.h. + "public/epdf_doc.h", + # EmbedPDF: what we add to public/fpdf_edit.h. + "public/epdf_edit.h", + # EmbedPDF: what we add to public/fpdf_flatten.h. + "public/epdf_flatten.h", + # EmbedPDF: public runtime font registration API used by page fallback + # rendering and annotation authoring. + "public/epdf_font.h", + # EmbedPDF: session-free AcroForm model, write transactions, FDF/XFDF. + "public/epdf_form.h", + # EmbedPDF: the catalog's /Names /Pages and /Templates name trees. + "public/epdf_named_pages.h", + # EmbedPDF: the document outline, addressed by object number. + "public/epdf_outline.h", + # EmbedPDF: digital signatures - revisions, the signature model. + "public/epdf_signature.h", + # EmbedPDF: generic namespace-scoped document/page /PieceInfo metadata. + "public/epdf_pieceinfo.h", + # EmbedPDF: PNG encoding of RGBA bitmaps. + "public/epdf_png.h", + # EmbedPDF: what we add to public/fpdf_progressive.h. + "public/epdf_progressive.h", + # EmbedPDF: typed measurement, viewport and caption API. + "public/epdf_measure.h", + "public/epdf_redact.h", + # EmbedPDF: what we add to public/fpdf_save.h. + "public/epdf_save.h", + # EmbedPDF: one-call exact oriented glyph geometry. + "public/epdf_text.h", + # EmbedPDF: what we add to public/fpdfview.h. + "public/epdf_view.h", + "public/fpdf_annot.h", + "public/fpdf_attachment.h", + "public/fpdf_catalog.h", + "public/fpdf_dataavail.h", + "public/fpdf_doc.h", + "public/fpdf_edit.h", + "public/fpdf_ext.h", + "public/fpdf_flatten.h", + "public/fpdf_formfill.h", + "public/fpdf_fwlevent.h", + "public/fpdf_javascript.h", + "public/fpdf_ppo.h", + "public/fpdf_progressive.h", + "public/fpdf_save.h", + "public/fpdf_searchex.h", + "public/fpdf_signature.h", + "public/fpdf_structtree.h", + "public/fpdf_sysfontinfo.h", + "public/fpdf_text.h", + "public/fpdf_transformpage.h", + "public/fpdfview.h", + ] + visibility = [ ":*" ] +} + +group("pdfium_public_headers") { + public_deps = [ ":pdfium_public_headers_impl" ] + public_configs = [ + ":pdfium_public_config", + ":pdfium_implementation_config", + ] +} + +component("pdfium") { + output_name = "embedpdf" + libs = [] + configs += [ ":pdfium_strict_config" ] + public_configs = [ ":pdfium_public_config" ] + + deps = [ + "constants", + "core/fpdfapi/page", + "core/fpdfapi/parser", + "core/fpdfdoc", + "core/fxcodec", + "core/fxcrt", + "core/fxge", + "fpdfsdk", + "fpdfsdk/formfiller", + "fxjs", + ] + + public_deps = [ + ":pdfium_public_headers_impl", + "core/fxcrt", + ] + + if (pdf_enable_xfa) { + deps += [ + "fpdfsdk/fpdfxfa", + "xfa/fxfa", + "xfa/fxfa/parser", + ] + } + + if (is_win) { + libs += [ + "advapi32.lib", + "gdi32.lib", + "user32.lib", + ] + } + + if (is_mac) { + frameworks = [ + "AppKit.framework", + "CoreFoundation.framework", + ] + } + + if (pdf_is_complete_lib) { + static_component_type = "static_library" + complete_static_lib = true + configs -= [ "//build/config/compiler:thin_archive" ] + } +} + +if (pdf_enable_freetype) { + group("freetype_common") { + public_deps = [] + if (pdf_bundle_freetype) { + public_deps += [ "third_party:fx_freetype" ] + } else { + public_deps += [ "//build/config/freetype" ] + } + } +} + +# Targets below this are only visible within this file. +visibility = [ ":*" ] + +group("pdfium_unittest_deps") { + testonly = true + public_deps = [ + "core/fxcrt", + "testing:unit_test_support", + "//testing/gmock", + "//testing/gtest", + ] + visibility += [ + "core/*", + "fpdfsdk/*", + "fxbarcode/*", + "fxjs/*", + "testing/*", + "xfa/*", + ] +} + +test("pdfium_unittests") { + testonly = true + sources = [ "testing/unit_test_main.cpp" ] + deps = [ + "core/fdrm:unittests", + "core/fpdfapi/edit:unittests", + "core/fpdfapi/font:unittests", + "core/fpdfapi/page:unittests", + "core/fpdfapi/parser:unittests", + "core/fpdfapi/render:unittests", + "core/fpdfdoc:unittests", + "core/fpdftext:unittests", + "core/fxcodec:unittests", + "core/fxcrt", + "core/fxcrt:unittests", + "core/fxge:unittests", + "fpdfsdk:unittests", + "testing:test_environments", + "testing:test_support", + "testing:unit_test_support", + "testing:unittests", + "testing/png_codec:unittests", + "//testing/gmock", + "//testing/gtest", + ] + configs += [ ":pdfium_core_config" ] + if (is_android) { + use_raw_android_executable = true + } + if (pdf_enable_v8) { + configs += [ "//v8:external_startup_data" ] + deps += [ + "fxjs:unittests", + "//v8", + ] + if (pdf_enable_xfa) { + deps += [ + "core/fxcrt/css:unittests", + "fxbarcode:unittests", + "xfa/fde:unittests", + "xfa/fgas/crt:unittests", + "xfa/fgas/font:unittests", + "xfa/fgas/layout:unittests", + "xfa/fxfa:unittests", + "xfa/fxfa/formcalc:unittests", + "xfa/fxfa/parser:unittests", + ] + } + } +} + +group("pdfium_embeddertest_deps") { + testonly = true + public_deps = [ + ":pdfium_public_headers", + "core/fxcrt", + "testing:embedder_test_support", + "//testing/gmock", + "//testing/gtest", + ] + visibility += [ + "core/*", + "fpdfsdk/*", + "fxjs/*", + "xfa/*", + ] +} + +test("pdfium_embeddertests") { + testonly = true + sources = [ "testing/embedder_test_main.cpp" ] + deps = [ + ":pdfium_embeddertest_deps", + "core/fpdfapi/edit:embeddertests", + "core/fpdfapi/parser:embeddertests", + "core/fpdfapi/render:embeddertests", + "core/fxcodec:embeddertests", + "core/fxcrt", + "core/fxge:embeddertests", + "fpdfsdk:embeddertests", + "fpdfsdk/formfiller:embeddertests", + "fpdfsdk/pwl:embeddertests", + "testing:test_environments", + "testing:test_support", + "testing/png_codec", + "//testing/gmock", + "//testing/gtest", + ] + include_dirs = [ "testing/gmock/include" ] + configs += [ ":pdfium_core_config" ] + + if (is_android) { + ignore_all_data_deps = true + use_raw_android_executable = true + } + + if (build_with_chromium && pdf_use_skia) { + deps += [ "testing:discardable_memory_allocator" ] + configs += [ "testing:pdfium_in_chromium_config" ] + } + + if (pdf_enable_v8) { + deps += [ + "fxjs:embeddertests", + "//v8", + ] + configs += [ "//v8:external_startup_data" ] + } + + if (pdf_enable_xfa) { + deps += [ + "fpdfsdk/fpdfxfa:embeddertests", + "xfa/fwl:embeddertests", + "xfa/fxfa/layout:embeddertests", + "xfa/fxfa/parser:embeddertests", + ] + } +} + +group("pdfium_all") { + testonly = true + deps = [ + ":pdfium_embeddertests", + ":pdfium_unittests", + "testing:pdfium_diff", + "testing/fuzzers", + "testing/pdfium_test", + "testing/tools:epdf_layer_memory_benchmark", + "testing/tools:epdf_layer_replay_soak", + "testing/tools:epdf_thread_soak", + ] + + if (pdf_is_standalone) { + deps += [ "testing/tools:test_runner_py" ] + } +} + +# Makes additional targets reachable only for "gn check". These are not always +# built by the "all" Ninja target, which uses the "default" group, which in turn +# depends on the "pdfium_all" group. +group("gn_check") { + deps = [] + + # TODO(crbug.com/pdfium/1832): Remove !is_android when //third_party/expat is + # available. + if (defined(checkout_skia) && checkout_skia && !is_android && !is_wasm) { + deps += [ "//skia" ] + } +} + +# GN evaluates each .gn file once per toolchain, so restricting to default +# toolchain will ensure write_file() is called only once. +if (current_toolchain == default_toolchain) { + import("//build/gn_logs.gni") + + # Write debug logs to gn_logs.txt. + _lines = [ + "Generated during 'gn gen' by //BUILD.gn.", + "", + ] + build_gn_logs + + write_file("$root_build_dir/gn_logs.txt", _lines) +} diff --git a/core/fpdfdoc/cpdf_annotfontmap.cpp b/core/fpdfdoc/cpdf_annotfontmap.cpp index cf47875347..d74acb139e 100644 --- a/core/fpdfdoc/cpdf_annotfontmap.cpp +++ b/core/fpdfdoc/cpdf_annotfontmap.cpp @@ -29,10 +29,8 @@ #include "core/fpdfdoc/cpdf_documentfonts.h" #include "core/fpdfdoc/cpdf_fontface.h" #include "core/fpdfdoc/cpdf_interactiveform.h" -#include "core/fpdfdoc/cpdf_richtextlayout.h" #include "core/fxcrt/check.h" #include "core/fxcrt/code_point_view.h" -#include "core/fxcrt/fx_codepage.h" #include "core/fxcrt/fx_safe_types.h" #include "core/fxcrt/numerics/safe_conversions.h" #include "core/fxcrt/stl_util.h" @@ -741,102 +739,6 @@ ByteString CPDF_AnnotFontMap::GetPDFFontAlias(int32_t font_index) { return fonts_[font_index].alias; } -int32_t CPDF_AnnotFontMap::GetWordFontIndex(uint16_t word, - FX_Charset charset, - int32_t font_index) { - if (SupportsWord(font_index, word)) { - return font_index; - } - if (SupportsWord(0, word)) { - return 0; - } - - for (size_t i = 1; i < fonts_.size(); ++i) { - if (SupportsWord(pdfium::checked_cast(i), word)) { - return pdfium::checked_cast(i); - } - } - - const WideString character(static_cast(word)); - if (!allow_registered_fallbacks_ || !fonts_.front().font) { - if (!CPDF_RichTextLayout::IsInvisibleGrapheme(character.AsStringView())) { - NoteMissingCharacter(character); - } - return -1; - } - - const int weight = - fonts_.front().font->GetFontWeight().value_or(pdfium::kFontWeightNormal); - const bool italic = fonts_.front().font->GetItalicAngle() != 0; - std::optional font_id = - CFX_FontRegistry::FindFallbackFont(word, weight, italic, - /*for_authoring=*/true); - if (!font_id.has_value()) { - if (!CPDF_RichTextLayout::IsInvisibleGrapheme(character.AsStringView())) { - NoteMissingCharacter(character); - } - return -1; - } - - int32_t existing_font_index = FindExistingRegisteredFont(*font_id); - if (existing_font_index >= 0) { - return existing_font_index; - } - - return AddRegisteredFallbackFont(*font_id); -} - -int32_t CPDF_AnnotFontMap::CharCodeFromUnicode(int32_t font_index, - uint16_t word) { - RetainPtr font = GetPDFFont(font_index); - if (!font) { - return -1; - } - - uint32_t charcode = font->CharCodeFromUnicode(word); - if (charcode == CPDF_Font::kInvalidCharCode || (charcode == 0 && word != 0)) { - return -1; - } - if (fxcrt::IndexInBounds(fonts_, font_index)) { - FontEntry& entry = fonts_[font_index]; - if (entry.registered_font_id != CFX_FontRegistry::kInvalidFontId || - entry.source == Source::kDocumentProgram) { - entry.glyph_to_unicode.emplace(charcode, word); - } - } - return pdfium::checked_cast(charcode); -} - -FX_Charset CPDF_AnnotFontMap::CharSetFromUnicode(uint16_t word, - FX_Charset old_charset) { - if (word < 0x7F) { - return FX_Charset::kANSI; - } - if (old_charset != FX_Charset::kDefault) { - return old_charset; - } - return CFX_Font::GetCharSetFromUnicode(word); -} - -bool CPDF_AnnotFontMap::SupportsWord(int32_t font_index, uint16_t word) const { - if (!fxcrt::IndexInBounds(fonts_, font_index)) { - return false; - } - const FontEntry& entry = fonts_[font_index]; - if (!entry.font) { - return false; - } - if (entry.registered_font_id != CFX_FontRegistry::kInvalidFontId) { - return CFX_FontRegistry::SupportsUnicode(entry.registered_font_id, word); - } - if (entry.source == Source::kDocumentProgram) { - RetainPtr face = - entry.program ? entry.program->GetFace() : nullptr; - return face && face->GetCharIndex(word) > 0; - } - return PDFontSupportsUnicode(entry.font, word); -} - void CPDF_AnnotFontMap::ReleaseLayoutFonts() { // Rendering caches fonts in the document's page data. Release those cache // references before the scratch streams that their dictionaries reference. diff --git a/core/fpdfdoc/cpdf_annotfontmap.h b/core/fpdfdoc/cpdf_annotfontmap.h index 1df3eb5b27..876a9b148c 100644 --- a/core/fpdfdoc/cpdf_annotfontmap.h +++ b/core/fpdfdoc/cpdf_annotfontmap.h @@ -17,7 +17,6 @@ #include "core/fxcrt/widestring.h" #include "core/fpdfdoc/cpdf_annotfontsubset.h" -#include "core/fpdfdoc/ipvt_fontmap.h" #include "core/fxcrt/bytestring.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/unowned_ptr.h" @@ -31,7 +30,7 @@ class CPDF_IndirectObjectHolder; class CPDF_Stream; class CPDF_StreamAcc; -class CPDF_AnnotFontMap final : public IPVT_FontMap { +class CPDF_AnnotFontMap final { public: // Whose text the appearance is, for the document's embedding policy (§2 of // the Phase C note): under the DEFAULT policy annotation text (FreeText, @@ -52,7 +51,7 @@ class CPDF_AnnotFontMap final : public IPVT_FontMap { CFX_FontRegistry::kInvalidFontId, bool install_dr_entry = false, Owner owner = Owner::kAnnotation); - ~CPDF_AnnotFontMap() override; + ~CPDF_AnnotFontMap(); // Pick the /DR resource name a DA string uses for |font_id|: "ERegF", // suffixed "_N" when a foreign entry already owns that key. Choose reads @@ -242,14 +241,13 @@ class CPDF_AnnotFontMap final : public IPVT_FontMap { // (CPDF_DocumentFonts::NoteMissingCharacter()). void NoteMissingCharacter(const WideString& grapheme); - // IPVT_FontMap: - RetainPtr GetPDFFont(int32_t font_index) override; - ByteString GetPDFFontAlias(int32_t font_index) override; - int32_t GetWordFontIndex(uint16_t word, - FX_Charset charset, - int32_t font_index) override; - int32_t CharCodeFromUnicode(int32_t font_index, uint16_t word) override; - FX_Charset CharSetFromUnicode(uint16_t word, FX_Charset old_charset) override; + // The font |font_index| lays out with (for a registered font or a + // document program, a composite font over its program); null out of + // range. + RetainPtr GetPDFFont(int32_t font_index); + // The resource name the appearance draws |font_index| with. Naming it + // makes its resource part of the appearance. + ByteString GetPDFFontAlias(int32_t font_index); private: struct FontEntry { @@ -278,7 +276,6 @@ class CPDF_AnnotFontMap final : public IPVT_FontMap { std::unique_ptr glyph_identity; }; - bool SupportsWord(int32_t font_index, uint16_t word) const; ByteString AllocateAppearanceAlias(const ByteString& preferred) const; CPDF_AnnotFontSubset::Embedding EmbeddingForRegisteredFonts() const; void InstallDrEntry(const ByteString& alias, diff --git a/core/fpdfdoc/cpdf_annotfontsubset.cpp b/core/fpdfdoc/cpdf_annotfontsubset.cpp index 18183b04a4..4b4f730bfa 100644 --- a/core/fpdfdoc/cpdf_annotfontsubset.cpp +++ b/core/fpdfdoc/cpdf_annotfontsubset.cpp @@ -642,35 +642,29 @@ CPDF_AnnotFontSubset::CreateLayoutFontForProgram( return result; } - auto char_codes_and_indices = - program->GetCharCodesAndIndices(pdfium::kMaximumSupplementaryCodePoint); - if (char_codes_and_indices.empty()) { - return result; - } - // The layout font has the same shape as the resource that will be written, // so the charcodes it hands out (CIDs under Identity-H) are the ones the // appearance bytes carry. For a CID-keyed CFF that is the charset's CID. + // Every glyph has its width, shaping can draw glyphs the cmap doesn't + // reach (ligatures, alternates); there is no ToUnicode, the layout maps + // characters through the program itself. const GlyphIdentity glyphs(program); - std::multimap to_unicode; std::map widths; - for (const auto& item : char_codes_and_indices) { - std::optional cid = glyphs.CidOf(item.glyph_index); - if (!cid.has_value()) { - continue; - } - if (!pdfium::Contains(widths, *cid)) { - widths[*cid] = program->GetGlyphWidth(item.glyph_index); + RetainPtr face = program->GetFace(); + const int glyph_count = face ? face->GetGlyphCount() : 0; + for (int gid = 0; gid < glyph_count; ++gid) { + std::optional cid = glyphs.CidOf(gid); + if (cid.has_value() && !pdfium::Contains(widths, *cid)) { + widths[*cid] = program->GetGlyphWidth(gid); } - to_unicode.emplace(*cid, item.char_code); } - if (widths.empty() || to_unicode.empty()) { + if (widths.empty()) { return result; } StagedFontResource parts; if (!BuildCompositeFontParts(program, base_font_name, program->GetFontSpan(), - identity, widths, to_unicode, &parts)) { + identity, widths, /*to_unicode=*/{}, &parts)) { return result; } // The streams go into a scratch holder of their own: CPDF_Font resolves diff --git a/core/fpdfdoc/cpdf_annotfontsubset.h b/core/fpdfdoc/cpdf_annotfontsubset.h index b6abe9ebcc..021e5a6edb 100644 --- a/core/fpdfdoc/cpdf_annotfontsubset.h +++ b/core/fpdfdoc/cpdf_annotfontsubset.h @@ -68,11 +68,11 @@ class CPDF_AnnotFontSubset final { std::map cid_to_gid_; }; - // A registered font as CPVT lays text out with it. Its program and - // ToUnicode streams live in |scratch|, a holder of its own, never in the - // document: laying out allocates no document object number and leaves - // nothing behind. |scratch| must outlive |font| (it is declared first, so - // it is destroyed last). + // A registered font as the layout measures it: a composite font over its + // program. The program stream lives in |scratch|, a holder of its own, + // never in the document: laying out allocates no document object number + // and leaves nothing behind. |scratch| must outlive |font| (it is declared + // first, so it is destroyed last). struct LayoutFont { LayoutFont(); LayoutFont(LayoutFont&& that) noexcept; diff --git a/core/fpdfdoc/cpdf_appearancegenerator_forms.cpp b/core/fpdfdoc/cpdf_appearancegenerator_forms.cpp index 6198038dee..692668f0c2 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_forms.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_forms.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -20,7 +21,6 @@ #include "constants/appearance.h" #include "constants/form_fields.h" #include "constants/form_flags.h" -#include "core/fxcrt/stream_write_utils.h" #include "core/fpdfapi/font/cpdf_font.h" #include "core/fpdfapi/page/cpdf_docpagedata.h" #include "core/fpdfapi/parser/cpdf_array.h" @@ -37,10 +37,10 @@ #include "core/fpdfdoc/cpdf_defaultappearance.h" #include "core/fpdfdoc/cpdf_formfield.h" #include "core/fpdfdoc/cpdf_interactiveform.h" -#include "core/fpdfdoc/cpvt_fontmap.h" -#include "core/fpdfdoc/cpvt_variabletext.h" +#include "core/fpdfdoc/cpdf_richtextlayout.h" #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/fx_system.h" +#include "core/fxcrt/stream_write_utils.h" #include "core/fxge/cfx_fontregistry.h" #include "core/fxge/cfx_renderdevice.h" @@ -48,17 +48,15 @@ using appearance_generator::AnnotationDimensionsAndColor; using appearance_generator::APGenerationTarget; using appearance_generator::AppearanceCharacteristics; using appearance_generator::BorderStyleInfo; -using appearance_generator::CloneResourcesDictIfMissingFromStream; using appearance_generator::DaFontResolution; using appearance_generator::DefaultAppearanceInfo; +using appearance_generator::EmitFieldTextBody; using appearance_generator::GenerateAPDict; using appearance_generator::GenerateBorderAP; using appearance_generator::GenerateColorAP; using appearance_generator::GenerateDirectFallbackFontDict; -using appearance_generator::GenerateEditAP; using appearance_generator::GenerateEphemeralDefaultAcroFormDict; using appearance_generator::GenerateExtGStateDict; -using appearance_generator::GenerateResourceFontDict; using appearance_generator::GenerateResourcesDict; using appearance_generator::GetAnnotationDimensionsAndColor; using appearance_generator::GetAppearanceCharacteristics; @@ -67,77 +65,81 @@ using appearance_generator::GetDefaultAppearanceInfo; using appearance_generator::PaintOperation; using appearance_generator::ResolveDaFontForEphemeralTarget; using appearance_generator::ResolveDaFontForPersistentTarget; -using appearance_generator::SetVtFontSize; -using appearance_generator::ValidateOrCreateFontResources; namespace { +// A field's text laid out in |plate| (CPDF_RichTextLayout::ArrangeField()) +// and written in the /DA font, which is |map|'s first entry: Td, Tf and a +// string per run. |content_height| receives the height the lines take. +ByteString FieldText(CPDF_AnnotFontMap& map, + const CPDF_RichTextLayout::Options& options, + CPDF_RichTextLayout::Field field, + const CFX_FloatRect& plate, + float* content_height = nullptr) { + const CPDF_AnnotFontMap::EntryFacts face = map.DescribeEntry(0); + field.face_entry = 0; + field.weight = face.weight; + field.italic = face.italic; + CPDF_RichTextLayout layout(&map, options); + const CPDF_RichTextLayout::Result laid = layout.ArrangeField(field, plate); + if (content_height) { + *content_height = laid.content_height; + } + fxcrt::ostringstream body; + EmitFieldTextBody(body, laid, map, plate); + return ByteString(body); +} + +// /Q as an alignment: 0 left, 1 centre, 2 right; anything else is left. +int FieldAlignment(int32_t q) { + return q == 1 || q == 2 ? q : 0; +} + ByteString GenerateTextFieldAP(const CPDF_Dictionary* annot_dict, const CFX_FloatRect& body_rect, float font_size, - CPVT_VariableText& vt, + CPDF_AnnotFontMap& map, + const CPDF_RichTextLayout::Options& options, const WideString* value_override) { RetainPtr v_field = CPDF_FormField::GetFieldAttrForDict(annot_dict, pdfium::form_fields::kV); - WideString value = value_override - ? *value_override - : (v_field ? v_field->GetUnicodeText() : WideString()); RetainPtr q_field = CPDF_FormField::GetFieldAttrForDict(annot_dict, "Q"); - const int32_t align = q_field ? q_field->GetInteger() : 0; RetainPtr ff_field = CPDF_FormField::GetFieldAttrForDict(annot_dict, pdfium::form_fields::kFf); const uint32_t flags = ff_field ? ff_field->GetInteger() : 0; RetainPtr max_len_field = CPDF_FormField::GetFieldAttrForDict(annot_dict, "MaxLen"); const uint32_t max_len = max_len_field ? max_len_field->GetInteger() : 0; - vt.SetPlateRect(body_rect); - vt.SetAlignment(CPVT_VariableText::ToAlignment(align)); - SetVtFontSize(font_size, vt); - - bool is_multi_line = (flags >> 12) & 1; - if (is_multi_line) { - vt.SetMultiLine(true); - vt.SetAutoReturn(true); - } - uint16_t sub_word = 0; - if ((flags >> 13) & 1) { - sub_word = '*'; - vt.SetPasswordChar(sub_word); - } - bool is_char_array = (flags >> 24) & 1; - if (is_char_array) { - vt.SetCharArray(max_len); - } else { - vt.SetLimitChar(max_len); - } - vt.Initialize(); - vt.SetText(value); - vt.RearrangeAll(); - CFX_PointF offset; - if (!is_multi_line) { - offset = CFX_PointF( - 0.0f, (vt.GetContentRect().Height() - body_rect.Height()) / 2.0f); + CPDF_RichTextLayout::Field field; + field.text = value_override + ? *value_override + : (v_field ? v_field->GetUnicodeText() : WideString()); + field.size = font_size; + field.alignment = FieldAlignment(q_field ? q_field->GetInteger() : 0); + field.multiline = (flags >> 12) & 1; + field.password = (flags >> 13) & 1; + if ((flags >> 24) & 1) { + field.cells = max_len; + } else { + field.limit = max_len; } - return GenerateEditAP(vt.GetProvider()->GetFontMap(), vt.GetIterator(), - offset, !is_char_array, sub_word); + field.centered = !field.multiline; + return FieldText(map, options, std::move(field), body_rect); } ByteString GenerateComboBoxAP(const CPDF_Dictionary* annot_dict, const CFX_FloatRect& body_rect, const CFX_Color& text_color, float font_size, - CPVT_VariableText::Provider& provider, + CPDF_AnnotFontMap& map, + const CPDF_RichTextLayout::Options& options, const WideString* value_override) { fxcrt::ostringstream body_stream; RetainPtr v_field = CPDF_FormField::GetFieldAttrForDict(annot_dict, pdfium::form_fields::kV); - WideString value = value_override - ? *value_override - : (v_field ? v_field->GetUnicodeText() : WideString()); - CPVT_VariableText vt(&provider); CFX_FloatRect button_rect = body_rect; button_rect.left = button_rect.right - 13; button_rect.Normalize(); @@ -145,17 +147,14 @@ ByteString GenerateComboBoxAP(const CPDF_Dictionary* annot_dict, edit_rect.right = button_rect.left; edit_rect.Normalize(); edit_rect.Deflate(4.0f, 0); - vt.SetPlateRect(edit_rect); - SetVtFontSize(font_size, vt); - - vt.Initialize(); - vt.SetText(value); - vt.RearrangeAll(); - CFX_FloatRect content_rect = vt.GetContentRect(); - CFX_PointF offset = - CFX_PointF(0.0f, (content_rect.Height() - edit_rect.Height()) / 2.0f); - ByteString edit = - GenerateEditAP(provider.GetFontMap(), vt.GetIterator(), offset, true, 0); + + CPDF_RichTextLayout::Field field; + field.text = value_override + ? *value_override + : (v_field ? v_field->GetUnicodeText() : WideString()); + field.size = font_size; + field.centered = true; + const ByteString edit = FieldText(map, options, std::move(field), edit_rect); if (edit.GetLength() > 0) { body_stream << "/Tx BMC\nq\n"; WriteRect(body_stream, edit_rect) << " re\nW\nn\n"; @@ -184,7 +183,8 @@ ByteString GenerateListBoxAP(const CPDF_Dictionary* annot_dict, const CFX_FloatRect& body_rect, const CFX_Color& text_color, float font_size, - CPVT_VariableText::Provider& provider) { + CPDF_AnnotFontMap& map, + const CPDF_RichTextLayout::Options& options) { RetainPtr opts = ToArray(CPDF_FormField::GetFieldAttrForDict(annot_dict, "Opt")); if (!opts) { @@ -205,13 +205,13 @@ ByteString GenerateListBoxAP(const CPDF_Dictionary* annot_dict, } if (RetainPtr opt = opts->GetDirectObjectAt(i)) { - WideString item; + CPDF_RichTextLayout::Field field; if (opt->IsString()) { - item = opt->GetUnicodeText(); + field.text = opt->GetUnicodeText(); } else if (const CPDF_Array* opt_array = opt->AsArray()) { RetainPtr opt_item = opt_array->GetDirectObjectAt(1); if (opt_item) { - item = opt_item->GetUnicodeText(); + field.text = opt_item->GetUnicodeText(); } } bool is_selected = false; @@ -224,15 +224,12 @@ ByteString GenerateListBoxAP(const CPDF_Dictionary* annot_dict, } } } - CPVT_VariableText vt(&provider); - vt.SetPlateRect( - CFX_FloatRect(body_rect.left, 0.0f, body_rect.right, 0.0f)); - vt.SetFontSize(FXSYS_IsFloatZero(font_size) ? 12.0f : font_size); - vt.Initialize(); - vt.SetText(item); - vt.RearrangeAll(); - - const float item_height = vt.GetContentRect().Height(); + // A row is one line, from the top of what is left. + field.size = FXSYS_IsFloatZero(font_size) ? 12.0f : font_size; + float item_height = 0; + const ByteString row = FieldText( + map, options, std::move(field), + CFX_FloatRect(body_rect.left, fy, body_rect.right, fy), &item_height); if (is_selected) { CFX_FloatRect item_rect = CFX_FloatRect( body_rect.left, fy - item_height, body_rect.right, fy); @@ -244,14 +241,10 @@ ByteString GenerateListBoxAP(const CPDF_Dictionary* annot_dict, body_stream << "BT\n" << GenerateColorAP(CFX_Color::MakeGray(1), PaintOperation::kFill) - << GenerateEditAP(provider.GetFontMap(), vt.GetIterator(), - CFX_PointF(0.0f, fy), true, 0) - << "ET\n"; + << row << "ET\n"; } else { body_stream << "BT\n" - << GenerateColorAP(text_color, PaintOperation::kFill) - << GenerateEditAP(provider.GetFontMap(), vt.GetIterator(), - CFX_PointF(0.0f, fy), true, 0) + << GenerateColorAP(text_color, PaintOperation::kFill) << row << "ET\n"; } fy -= item_height; @@ -264,7 +257,8 @@ void GenerateTextFieldFormAP(fxcrt::ostringstream& app_stream, const CPDF_Dictionary* annot_dict, const CFX_FloatRect& bbox, const DefaultAppearanceInfo& da_info, - CPVT_VariableText::Provider& provider, + CPDF_AnnotFontMap& map, + const CPDF_RichTextLayout::Options& options, const WideString* value_override) { const AppearanceCharacteristics mk = GetAppearanceCharacteristics(annot_dict->GetDictFor("MK")); @@ -311,9 +305,8 @@ void GenerateTextFieldFormAP(fxcrt::ostringstream& app_stream, clip_rect.Deflate(bw, bw); WriteRect(app_stream, clip_rect) << " re W n\n"; - CPVT_VariableText vt(&provider); - ByteString body = GenerateTextFieldAP(annot_dict, body_rect, - da_info.font_size, vt, value_override); + ByteString body = GenerateTextFieldAP( + annot_dict, body_rect, da_info.font_size, map, options, value_override); app_stream << "BT\n"; app_stream << GenerateColorAP(da_info.text_color, PaintOperation::kStroke); @@ -332,7 +325,8 @@ void GenerateComboBoxFormAP(fxcrt::ostringstream& app_stream, const CPDF_Dictionary* annot_dict, const CFX_FloatRect& bbox, const DefaultAppearanceInfo& da_info, - CPVT_VariableText::Provider& provider, + CPDF_AnnotFontMap& map, + const CPDF_RichTextLayout::Options& options, const WideString* value_override) { const AnnotationDimensionsAndColor dims = GetAnnotationDimensionsAndColor(annot_dict); @@ -356,14 +350,16 @@ void GenerateComboBoxFormAP(fxcrt::ostringstream& app_stream, body_rect.Deflate(border_info.width, border_info.width); app_stream << GenerateComboBoxAP(annot_dict, body_rect, da_info.text_color, - da_info.font_size, provider, value_override); + da_info.font_size, map, options, + value_override); } void GenerateListBoxFormAP(fxcrt::ostringstream& app_stream, const CPDF_Dictionary* annot_dict, const CFX_FloatRect& bbox, const DefaultAppearanceInfo& da_info, - CPVT_VariableText::Provider& provider) { + CPDF_AnnotFontMap& map, + const CPDF_RichTextLayout::Options& options) { const AnnotationDimensionsAndColor dims = GetAnnotationDimensionsAndColor(annot_dict); const BorderStyleInfo border_info = @@ -385,8 +381,9 @@ void GenerateListBoxFormAP(fxcrt::ostringstream& app_stream, CFX_FloatRect body_rect = bbox; body_rect.Deflate(border_info.width, border_info.width); - const ByteString body = GenerateListBoxAP( - annot_dict, body_rect, da_info.text_color, da_info.font_size, provider); + const ByteString body = + GenerateListBoxAP(annot_dict, body_rect, da_info.text_color, + da_info.font_size, map, options); if (body.GetLength() > 0) { app_stream << "/Tx BMC\nq\n"; WriteRect(app_stream, body_rect) << " re\nW\nn\n" << body << "Q\nEMC\n"; @@ -479,7 +476,8 @@ void GeneratePushButtonFormAP(fxcrt::ostringstream& app_stream, const CPDF_Dictionary* annot_dict, const CFX_FloatRect& bbox, const DefaultAppearanceInfo& da_info, - CPVT_VariableText::Provider& provider) { + CPDF_AnnotFontMap& map, + const CPDF_RichTextLayout::Options& options) { const AppearanceCharacteristics mk = GetAppearanceCharacteristics(annot_dict->GetDictFor("MK")); const BorderStyleInfo bs = GetBorderStyleInfo(annot_dict->GetDictFor("BS")); @@ -494,17 +492,13 @@ void GeneratePushButtonFormAP(fxcrt::ostringstream& app_stream, CFX_FloatRect body_rect = bbox; body_rect.Deflate(2.0f * bw, bw); - CPVT_VariableText vt(&provider); - vt.SetPlateRect(body_rect); - vt.SetAlignment(CPVT_VariableText::Alignment::kCenter); - SetVtFontSize(da_info.font_size, vt); - vt.Initialize(); - vt.SetText(mk.caption); - vt.RearrangeAll(); - const CFX_PointF offset( - 0.0f, (vt.GetContentRect().Height() - body_rect.Height()) / 2.0f); + CPDF_RichTextLayout::Field field; + field.text = mk.caption; + field.size = da_info.font_size; + field.alignment = 1; + field.centered = true; const ByteString caption = - GenerateEditAP(provider.GetFontMap(), vt.GetIterator(), offset, true, 0); + FieldText(map, options, std::move(field), body_rect); if (caption.IsEmpty()) { return; } @@ -726,7 +720,6 @@ bool GenerateFormAPToTarget(APGenerationTarget* target, } const ByteString& font_name = default_appearance_info.value().font_name; - RetainPtr font_dict; DaFontResolution da_font; if (target->IsPersistent()) { RetainPtr mutable_dr_font_dict; @@ -743,7 +736,6 @@ bool GenerateFormAPToTarget(APGenerationTarget* target, mutable_dr_font_dict = mutable_form_dict->GetOrCreateDictFor("DR")->GetOrCreateDictFor( "Font"); - dr_dict = mutable_form_dict->GetDictFor("DR"); } da_font = ResolveDaFontForPersistentTarget(doc, mutable_dr_font_dict.Get(), font_name); @@ -751,144 +743,89 @@ bool GenerateFormAPToTarget(APGenerationTarget* target, da_font = ResolveDaFontForEphemeralTarget(doc, dr_font_dict.Get(), font_name); } - font_dict = da_font.font_dict; - // A /DA font that is a document program (reserved this session, or the - // composite font the writer installed for one) lays out through the same - // map: CPVT_FontMap can only write the characters its ToUnicode names. - const bool use_registered_font_map = - target->IsPersistent() && - (CFX_FontRegistry::HasFallbackFonts() || - da_font.registered_font_id != CFX_FontRegistry::kInvalidFontId || - da_font.document_program || - CPDF_AnnotFontMap::IsDocumentProgramFont(font_dict.Get())); - if (!font_dict && !use_registered_font_map) { - // Ephemeral render of a widget whose /DA names a registered font that has - // no /DR entry yet: draw with the fallback rather than nothing. + RetainPtr font_dict = da_font.font_dict; + if (!font_dict && + da_font.registered_font_id == CFX_FontRegistry::kInvalidFontId && + !da_font.document_program) { + // A /DA font the form doesn't have: draw with the fallback rather than + // nothing. font_dict = GenerateDirectFallbackFontDict(); } - auto* doc_page_data = CPDF_DocPageData::FromDocument(doc); RetainPtr default_font = - font_dict ? doc_page_data->GetFont(font_dict) : nullptr; - if (!default_font && !use_registered_font_map) { + font_dict ? CPDF_DocPageData::FromDocument(doc)->GetFont(font_dict) + : nullptr; + + // The text lays out in the shared engine, through the annotation font + // map: the /DA font first, then the fallbacks and the last resort, each + // embedded as the field's appearance uses it. + auto owned_map = std::make_unique( + doc, std::move(default_font), font_name, + /*allow_registered_fallbacks=*/true, da_font.registered_font_id, + /*install_dr_entry=*/target->IsPersistent(), + CPDF_AnnotFontMap::Owner::kWidget); + CPDF_AnnotFontMap& map = *owned_map; + da_font.ApplyTo(&map); + if (!map.HasDefaultFont()) { return false; } + CPDF_RichTextLayout::Options options; + options.typographic_features = doc->GetTypographicFeaturesEnabled(); const AnnotationDimensionsAndColor dims = GetAnnotationDimensionsAndColor(annot_dict); - - RetainPtr resources_dict; - RetainPtr normal_stream; - if (target->IsPersistent()) { - RetainPtr ap_dict = - annot_dict->GetOrCreateDictFor(pdfium::annotation::kAP); - normal_stream = ap_dict->GetMutableStreamFor("N"); - if (normal_stream) { - RetainPtr stream_dict = normal_stream->GetMutableDict(); - const bool cloned = - CloneResourcesDictIfMissingFromStream(stream_dict, dr_dict.Get()); - if (!cloned) { - if (!ValidateOrCreateFontResources(doc, stream_dict, font_dict, - font_name)) { - return false; - } - } - resources_dict = stream_dict->GetMutableDictFor("Resources"); - } else { - normal_stream = - doc->NewIndirect(pdfium::MakeRetain()); - ap_dict->SetNewFor("N", doc, normal_stream->GetObjNum()); - } - } else { - auto gs_dict = GenerateExtGStateDict(*annot_dict, "Normal"); - auto resource_font_dict = - GenerateResourceFontDict(doc, font_name, font_dict.Get()); - resources_dict = GenerateResourcesDict(doc, std::move(gs_dict), - std::move(resource_font_dict)); + fxcrt::ostringstream app_stream; + switch (type) { + case CPDF_AppearanceGenerator::kTextField: + GenerateTextFieldFormAP(app_stream, annot_dict, dims.bbox, + default_appearance_info.value(), map, options, + value_override); + break; + case CPDF_AppearanceGenerator::kComboBox: + GenerateComboBoxFormAP(app_stream, annot_dict, dims.bbox, + default_appearance_info.value(), map, options, + value_override); + break; + case CPDF_AppearanceGenerator::kListBox: + GenerateListBoxFormAP(app_stream, annot_dict, dims.bbox, + default_appearance_info.value(), map, options); + break; + case CPDF_AppearanceGenerator::kPushButton: + GeneratePushButtonFormAP(app_stream, annot_dict, dims.bbox, + default_appearance_info.value(), map, options); + break; } - auto generate_form_stream = [&](CPVT_VariableText::Provider& provider, - fxcrt::ostringstream& app_stream) { - switch (type) { - case CPDF_AppearanceGenerator::kTextField: - GenerateTextFieldFormAP(app_stream, annot_dict, dims.bbox, - default_appearance_info.value(), provider, - value_override); - break; - case CPDF_AppearanceGenerator::kComboBox: - GenerateComboBoxFormAP(app_stream, annot_dict, dims.bbox, - default_appearance_info.value(), provider, - value_override); - break; - case CPDF_AppearanceGenerator::kListBox: - GenerateListBoxFormAP(app_stream, annot_dict, dims.bbox, - default_appearance_info.value(), provider); - break; - case CPDF_AppearanceGenerator::kPushButton: - GeneratePushButtonFormAP(app_stream, annot_dict, dims.bbox, - default_appearance_info.value(), provider); - break; - } - }; - - if (use_registered_font_map) { - // EmbedPDF: form widgets need the same registered fallback/subset path as - // FreeText when their value/options contain glyphs outside the DA font. - // Keep the old CPVT_FontMap path unless a registered font is actually - // involved so existing form AP output remains stable by default. - CPDF_AnnotFontMap map( - doc, std::move(default_font), font_name, - /*allow_registered_fallbacks=*/true, da_font.registered_font_id, - /*install_dr_entry=*/true, CPDF_AnnotFontMap::Owner::kWidget); - da_font.ApplyTo(&map); - if (!map.HasDefaultFont()) { + if (!target->IsPersistent()) { + RetainPtr fonts = map.CreateEphemeralFontResourceDict(); + if (!fonts) { return false; } - CPVT_VariableText::Provider provider(&map); - - fxcrt::ostringstream app_stream; - generate_form_stream(provider, app_stream); - - normal_stream->SetDataFromStringstreamAndRemoveFilter(&app_stream); - RetainPtr stream_dict = normal_stream->GetMutableDict(); - stream_dict->SetMatrixFor("Matrix", dims.matrix); - stream_dict->SetRectFor("BBox", dims.bbox); - RetainPtr stream_resources = - stream_dict->GetOrCreateDictFor("Resources"); - RetainPtr resource_font_dict = - map.CreateFontResourceDict(); - if (!resource_font_dict) { - return false; // registered /DA font without a resource: no appearance - } - stream_resources->SetFor("Font", std::move(resource_font_dict)); - return true; - } - - RetainPtr ephemeral_resources_dict = resources_dict; - CPVT_FontMap map(doc, std::move(resources_dict), std::move(default_font), - font_name); - CPVT_VariableText::Provider provider(&map); - - fxcrt::ostringstream app_stream; - generate_form_stream(provider, app_stream); - - if (!target->IsPersistent()) { + auto resources_dict = GenerateResourcesDict( + doc, GenerateExtGStateDict(*annot_dict, "Normal"), std::move(fonts)); + target->ephemeral_fonts = std::move(owned_map); return GenerateAPDict( - target, annot_dict, &app_stream, std::move(ephemeral_resources_dict), + target, annot_dict, &app_stream, std::move(resources_dict), /*is_text_markup_annotation=*/false, dims.matrix, dims.bbox); } + RetainPtr resource_font_dict = map.CreateFontResourceDict(); + if (!resource_font_dict) { + return false; // registered /DA font without a resource: no appearance + } + RetainPtr ap_dict = + annot_dict->GetOrCreateDictFor(pdfium::annotation::kAP); + RetainPtr normal_stream = ap_dict->GetMutableStreamFor("N"); + if (!normal_stream) { + normal_stream = + doc->NewIndirect(pdfium::MakeRetain()); + ap_dict->SetNewFor("N", doc, normal_stream->GetObjNum()); + } normal_stream->SetDataFromStringstreamAndRemoveFilter(&app_stream); RetainPtr stream_dict = normal_stream->GetMutableDict(); stream_dict->SetMatrixFor("Matrix", dims.matrix); stream_dict->SetRectFor("BBox", dims.bbox); - - const bool cloned = - CloneResourcesDictIfMissingFromStream(stream_dict, dr_dict); - if (cloned) { - return true; - } - - ValidateOrCreateFontResources(doc, stream_dict, font_dict, font_name); + stream_dict->GetOrCreateDictFor("Resources") + ->SetFor("Font", std::move(resource_font_dict)); return true; } diff --git a/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp b/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp index ab4400e46e..1f25d1fb30 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp @@ -545,21 +545,20 @@ bool GenerateRichFreeTextAP(APGenerationTarget* target, return PublishRichFreeTextAPToTarget(target, annot_dict, std::move(prepared)); } -} // namespace - -namespace appearance_generator { - -// The text body of a rich FreeText appearance (C note §5): one Tj per -// word, relative Td per word, state operators only when they change, -// ActualText around runs whose glyphs do not spell their text. -void EmitRichTextBody(fxcrt::ostringstream& s, - const CPDF_RichTextLayout::Result& layout, - CPDF_AnnotFontMap& map, - const CFX_FloatRect& area) { +// The runs of |layout|, inside a text object: relative Td per string, state +// operators only when they change, ActualText around runs whose glyphs do +// not spell their text. A FreeText writes a string per word in its runs' +// colours, as Acrobat does, and collects its decorations; a form field +// writes a string per run in the colour set before it, as form fillers do. +void EmitTextRuns(fxcrt::ostringstream& s, + const CPDF_RichTextLayout::Result& layout, + CPDF_AnnotFontMap& map, + const CFX_FloatRect& area, + bool field, + std::vector* decorations) { using GlyphRun = CPDF_RichTextLayout::GlyphRun; using ShapedGlyph = CPDF_RichTextLayout::ShapedGlyph; - s << "BT\n"; int cur_entry = -1; float cur_size = -1; std::optional cur_color; @@ -567,7 +566,6 @@ void EmitRichTextBody(fxcrt::ostringstream& s, float cur_ts = 0; float cur_tz = 100; CFX_PointF origin(0, 0); // the last Td, in the form's space - std::vector decorations; for (const CPDF_RichTextLayout::Line& line : layout.lines) { const float baseline = area.top - line.baseline_y; @@ -586,7 +584,7 @@ void EmitRichTextBody(fxcrt::ostringstream& s, cur_entry = run.font_entry; cur_size = size; } - if (!cur_color.has_value() || *cur_color != run.style.color) { + if (!field && (!cur_color.has_value() || *cur_color != run.style.color)) { s << RichColorOperator(run.style.color, /*fill=*/true); cur_color = run.style.color; } @@ -632,14 +630,15 @@ void EmitRichTextBody(fxcrt::ostringstream& s, return (width * size / 1000.0f + run.style.letter_spacing) * scale; }; - // Words: a space ends the word it follows, as in Acrobat's streams. - // Run positions are relative to the text area's left edge. + // Words: a space ends the word it follows, as in Acrobat's streams; + // a field's run is one string. Run positions are relative to the text + // area's left edge. const float run_left = area.left + run.x; float x = run_left; size_t i = 0; const size_t count = run.glyphs.size(); while (i < count) { - size_t j = i; + size_t j = field ? count : i; while (j < count) { const bool space = is_space(run.glyphs[j]); ++j; @@ -741,14 +740,14 @@ void EmitRichTextBody(fxcrt::ostringstream& s, // baseline, 0.16 · descent thick; line-through 0.39 · ascent above, // 0.04 · ascent thick; `word` skips the spaces. const uint8_t decoration = run.style.decoration; - if (decoration) { + if (decoration && decorations) { const float run_baseline = baseline + run.rise; const float descent = face.descent * size; const float ascent = face.ascent * size; if (decoration & CPDF_RichTextStyle::kUnderline) { - decorations.push_back({run_left, run_left + run.width, - run_baseline - 0.44f * descent, - 0.16f * descent, run.style.color}); + decorations->push_back({run_left, run_left + run.width, + run_baseline - 0.44f * descent, + 0.16f * descent, run.style.color}); } if (decoration & CPDF_RichTextStyle::kWordUnderline) { float wx = run_left; @@ -757,9 +756,9 @@ void EmitRichTextBody(fxcrt::ostringstream& s, for (const ShapedGlyph& glyph : run.glyphs) { if (is_space(glyph)) { if (in_word) { - decorations.push_back({word_start, wx, - run_baseline - 0.44f * descent, - 0.16f * descent, run.style.color}); + decorations->push_back({word_start, wx, + run_baseline - 0.44f * descent, + 0.16f * descent, run.style.color}); in_word = false; } } else if (!in_word) { @@ -769,19 +768,32 @@ void EmitRichTextBody(fxcrt::ostringstream& s, wx += glyph.x_advance; } if (in_word) { - decorations.push_back({word_start, wx, - run_baseline - 0.44f * descent, - 0.16f * descent, run.style.color}); + decorations->push_back({word_start, wx, + run_baseline - 0.44f * descent, + 0.16f * descent, run.style.color}); } } if (decoration & CPDF_RichTextStyle::kLineThrough) { - decorations.push_back({run_left, run_left + run.width, - run_baseline + 0.39f * ascent, 0.04f * ascent, - run.style.color}); + decorations->push_back({run_left, run_left + run.width, + run_baseline + 0.39f * ascent, 0.04f * ascent, + run.style.color}); } } } } +} + +} // namespace + +namespace appearance_generator { + +void EmitRichTextBody(fxcrt::ostringstream& s, + const CPDF_RichTextLayout::Result& layout, + CPDF_AnnotFontMap& map, + const CFX_FloatRect& area) { + std::vector decorations; + s << "BT\n"; + EmitTextRuns(s, layout, map, area, /*field=*/false, &decorations); s << "ET\n"; std::optional stroke_color; for (const DecorationSegment& segment : decorations) { @@ -800,6 +812,13 @@ void EmitRichTextBody(fxcrt::ostringstream& s, } } +void EmitFieldTextBody(fxcrt::ostringstream& s, + const CPDF_RichTextLayout::Result& layout, + CPDF_AnnotFontMap& map, + const CFX_FloatRect& plate) { + EmitTextRuns(s, layout, map, plate, /*field=*/true, /*decorations=*/nullptr); +} + bool GenerateFreeTextAP(APGenerationTarget* target, CPDF_Dictionary* annot_dict, const ByteString& blend_name) { diff --git a/core/fpdfdoc/cpdf_appearancegenerator_internal.h b/core/fpdfdoc/cpdf_appearancegenerator_internal.h index d8038e5631..70f4e0cbf3 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_internal.h +++ b/core/fpdfdoc/cpdf_appearancegenerator_internal.h @@ -201,7 +201,6 @@ ByteString StringFromFontNameAndSize(const ByteString& font_name, float font_size); ByteString DefaultAppearanceFontPart(const ByteString& font_name, float font_size); -void SetVtFontSize(float font_size, CPVT_VariableText& vt); std::optional GetDefaultAppearanceInfo( const CPDF_Dictionary* annot_dict, const CPDF_Dictionary* acroform_dict); @@ -308,10 +307,18 @@ bool GenerateCaretAP(APGenerationTarget* target, const ByteString& blend_name); // cpdf_appearancegenerator_freetext.cpp +// A FreeText's text, or a caption's, in its own text object: a string per +// word, in its runs' colours, with their decorations. void EmitRichTextBody(fxcrt::ostringstream& s, const CPDF_RichTextLayout::Result& layout, CPDF_AnnotFontMap& map, const CFX_FloatRect& area); +// A form field's text (CPDF_RichTextLayout::ArrangeField()), inside the +// caller's text object, in the colour it set: Td, Tf and a string per run. +void EmitFieldTextBody(fxcrt::ostringstream& s, + const CPDF_RichTextLayout::Result& layout, + CPDF_AnnotFontMap& map, + const CFX_FloatRect& plate); bool GenerateFreeTextAP(APGenerationTarget* target, CPDF_Dictionary* annot_dict, const ByteString& blend_name); diff --git a/core/fpdfdoc/cpdf_appearancegenerator_redaction.cpp b/core/fpdfdoc/cpdf_appearancegenerator_redaction.cpp index f43c143a16..5ee9b64f6f 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_redaction.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_redaction.cpp @@ -17,7 +17,6 @@ #include #include "constants/annotation_common.h" -#include "core/fxcrt/stream_write_utils.h" #include "core/fpdfapi/font/cpdf_font.h" #include "core/fpdfapi/page/cpdf_docpagedata.h" #include "core/fpdfapi/parser/cpdf_array.h" @@ -32,16 +31,17 @@ #include "core/fpdfdoc/cpdf_appearancegenerator_internal.h" #include "core/fpdfdoc/cpdf_color_utils.h" #include "core/fpdfdoc/cpdf_interactiveform.h" -#include "core/fpdfdoc/cpvt_variabletext.h" +#include "core/fpdfdoc/cpdf_richtextlayout.h" #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/fx_system.h" +#include "core/fxcrt/stream_write_utils.h" #include "core/fxge/cfx_fontregistry.h" using appearance_generator::APGenerationTarget; using appearance_generator::DaFontResolution; using appearance_generator::DefaultAppearanceInfo; +using appearance_generator::EmitFieldTextBody; using appearance_generator::GenerateColorAP; -using appearance_generator::GenerateEditAP; using appearance_generator::GenerateExtGStateDict; using appearance_generator::GenerateFallbackFontDict; using appearance_generator::GenerateResourcesDict; @@ -55,7 +55,6 @@ using appearance_generator::MarkupQuad; using appearance_generator::PaintOperation; using appearance_generator::ReadMarkupQuad; using appearance_generator::ResolveDaFontForPersistentTarget; -using appearance_generator::SetVtFontSize; namespace { @@ -169,73 +168,71 @@ constexpr float kRedactRepeatFallbackFontSize = 12.0f; constexpr int kRedactMaxRepeatDoublings = 10; // 2^10 = 1024 label instances -// EmbedPDF: lay out the /OverlayText label for one redacted region with the -// same CPVT + annotation-font-map stack as FreeText appearances, so /DA fonts -// (standard, DR-resolved, or registered runtime fonts) shape and subset -// identically. Top-aligned in reading order; ISO 32000-2 prescribes neither -// the vertical placement nor the /Repeat tiling, so /Repeat is expressed as -// "repeat the label, space-joined, wrapped to the region, clipped". +// EmbedPDF: lay out the /OverlayText label for one redacted region as a +// form field's text, in the shared text engine with the annotation font map, +// so /DA fonts (standard, DR-resolved, or registered runtime fonts) shape and +// subset as every appearance's do. Top-aligned in reading order; ISO 32000-2 +// prescribes neither the vertical placement nor the /Repeat tiling, so +// /Repeat is expressed as "repeat the label, space-joined, wrapped to the +// region, clipped". void AppendRedactLabelForRegion(CPDF_AnnotFontMap& map, + const CPDF_RichTextLayout::Options& options, const CPDF_Dictionary* annot_dict, const WideString& overlay_text, float da_font_size, const CFX_Color& label_color, const CFX_FloatRect& region, fxcrt::ostringstream& stream) { - CPVT_VariableText::Provider provider(&map); - CPVT_VariableText vt(&provider); - vt.SetPlateRect(region); - vt.SetAlignment( - CPVT_VariableText::ToAlignment(annot_dict->GetIntegerFor("Q"))); + const CPDF_AnnotFontMap::EntryFacts face = map.DescribeEntry(0); + CPDF_RichTextLayout::Field field; + field.text = overlay_text; + field.face_entry = 0; + field.weight = face.weight; + field.italic = face.italic; + const int q = annot_dict->GetIntegerFor("Q"); + field.alignment = q == 1 || q == 2 ? q : 0; const bool repeat = annot_dict->GetBooleanFor("Repeat", false); - // A label shown once at auto-size (0) fits the region on one line: CPVT - // auto-sizes multi-line text only up to 12pt (the rule for multi-line form + // A label shown once at auto-size (0) fits the region on one line: a + // fitted size of several lines stops at 12pt (the rule for multi-line form // fields), which would leave a large region mostly empty. A label at a set // size, or a repeated one, wraps to the region. const bool fit_one_line = !repeat && FXSYS_IsFloatZero(da_font_size); - vt.SetMultiLine(!fit_one_line); - vt.SetAutoReturn(!fit_one_line); - // CPVT auto-sizing fits ALL text into the plate, so it cannot combine with - // /Repeat (more repetitions would only shrink the font); pin a concrete - // size for the repeat case when /DA asks for auto (size 0). - float font_size = da_font_size; - if (repeat && FXSYS_IsFloatZero(font_size)) { - font_size = kRedactRepeatFallbackFontSize; + field.multiline = !fit_one_line; + // A fitted size fits ALL the text into the region, so it cannot combine + // with /Repeat (more repetitions would only shrink the font); pin a + // concrete size for the repeat case when /DA asks for auto (size 0). + field.size = da_font_size; + if (repeat && FXSYS_IsFloatZero(field.size)) { + field.size = kRedactRepeatFallbackFontSize; } - SetVtFontSize(font_size, vt); - vt.Initialize(); - vt.SetText(overlay_text); - vt.RearrangeAll(); + CPDF_RichTextLayout layout(&map, options); + CPDF_RichTextLayout::Result laid = layout.ArrangeField(field, region); if (repeat) { // Double the space-joined text until the wrapped layout covers the region // vertically; the last row's surplus is removed by the region clip below. // Bounded to keep tiny-font/huge-region combinations sane. - WideString tiled = overlay_text; - for (int i = 0; i < kRedactMaxRepeatDoublings && - vt.GetContentRect().Height() < region.Height(); + for (int i = 0; + i < kRedactMaxRepeatDoublings && laid.content_height < region.Height(); ++i) { - WideString doubled = tiled; + WideString doubled = field.text; doubled += L' '; - doubled += tiled; - tiled = std::move(doubled); - vt.SetText(tiled); - vt.RearrangeAll(); + doubled += field.text; + field.text = std::move(doubled); + laid = layout.ArrangeField(field, region); } } - const ByteString body = - GenerateEditAP(vt.GetProvider()->GetFontMap(), vt.GetIterator(), - CFX_PointF(0.0f, 0.0f), /*continuous=*/true, - /*sub_word=*/0); - if (body.IsEmpty()) { + fxcrt::ostringstream body; + EmitFieldTextBody(body, laid, map, region); + if (body.str().empty()) { return; } stream << "q\n"; WriteRect(stream, region) << " re W n\n"; stream << "BT\n" - << GenerateColorAP(label_color, PaintOperation::kFill) << body + << GenerateColorAP(label_color, PaintOperation::kFill) << body.str() << "ET\nQ\n"; } @@ -346,9 +343,11 @@ bool AppendRedactOverlayOps(CPDF_Document* doc, if (!map.HasDefaultFont()) { return has_fill; } + CPDF_RichTextLayout::Options options; + options.typographic_features = doc->GetTypographicFeaturesEnabled(); for (const RedactOverlayRegion& region : regions) { - AppendRedactLabelForRegion(map, annot_dict, overlay_text, da_font_size, - label_color, region.bbox, stream); + AppendRedactLabelForRegion(map, options, annot_dict, overlay_text, + da_font_size, label_color, region.bbox, stream); } *out_font_resources = map.CreateFontResourceDict(); return !!*out_font_resources; diff --git a/core/fpdfdoc/cpdf_appearancegenerator_text.cpp b/core/fpdfdoc/cpdf_appearancegenerator_text.cpp index a26d26d0cb..7a27ff908f 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_text.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_text.cpp @@ -31,6 +31,7 @@ #include "core/fpdfdoc/cpdf_embed_metadata.h" #include "core/fpdfdoc/cpvt_variabletext.h" #include "core/fpdfdoc/cpvt_word.h" +#include "core/fpdfdoc/ipvt_fontmap.h" #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/fx_system.h" #include "core/fxcrt/stream_write_utils.h" @@ -133,14 +134,6 @@ ByteString DefaultAppearanceFontPart(const ByteString& font_name, return ByteString(font_stream); } -void SetVtFontSize(float font_size, CPVT_VariableText& vt) { - if (FXSYS_IsFloatZero(font_size)) { - vt.SetAutoFontSize(true); - } else { - vt.SetFontSize(font_size); - } -} - std::optional GetDefaultAppearanceInfo( const CPDF_Dictionary* annot_dict, const CPDF_Dictionary* acroform_dict) { diff --git a/core/fpdfdoc/cpdf_richtextlayout.cpp b/core/fpdfdoc/cpdf_richtextlayout.cpp index a38ef6e9ec..8535c0d636 100644 --- a/core/fpdfdoc/cpdf_richtextlayout.cpp +++ b/core/fpdfdoc/cpdf_richtextlayout.cpp @@ -13,11 +13,14 @@ #include "core/fpdfapi/font/cpdf_font.h" #include "core/fpdfdoc/cpdf_annotfontmap.h" +#include "core/fpdfdoc/cpvt_section.h" #include "core/fxcrt/cfx_bidiparagraph.h" #include "core/fxcrt/code_point_view.h" #include "core/fxcrt/fx_linebreak.h" +#include "core/fxcrt/fx_system.h" #include "core/fxcrt/fx_unicode.h" #include "core/fxcrt/numerics/safe_conversions.h" +#include "core/fxcrt/utf16.h" #include "core/fxge/cfx_face.h" #include "core/fxge/cfx_font.h" #include "hb-ot.h" // nogncheck @@ -39,7 +42,8 @@ constexpr float kSubRise = -0.15f; constexpr float kFallbackSize = 12.0f; constexpr float kEpsilon = 0.01f; -// CPVT's auto font size ladder, kept for plain FreeText with a DA size of 0. +// CPVT's auto font size ladder: plain FreeText and form fields with a DA +// size of 0. A field of several lines tries its first quarter. constexpr std::array kAutoSizeSteps = { 4, 6, 8, 9, 10, 12, 14, 18, 20, 25, 30, 35, 40, 45, 50, 55, 60, 70, 80, 90, 100, 110, 120, 130, 144}; @@ -173,6 +177,7 @@ struct ResolvedStyle { float drawn_size = 0; // after sub/superscript scaling float rise = 0; // pt bool has_size_override = false; + int face_entry = -1; // the face, when it isn't resolved from the style }; // One shaping unit: one font entry, one style, one script and one bidi @@ -339,6 +344,14 @@ struct CPDF_RichTextLayout::Impl { std::map> hb_fonts; std::map faces; bool degraded = false; + // A form field: widths in CPVT's arithmetic, so its lines break where + // CPVT's did. + bool field = false; + + // |width| thousandths of an em at |size| pt. + float Scaled(int width, float size) const { + return field ? width * size * 0.001f : width * size / 1000.0f; + } const CPDF_AnnotFontMap::RichFace& FaceOf(int entry) { auto it = faces.find(entry); @@ -390,7 +403,7 @@ struct CPDF_RichTextLayout::Impl { glyph.cluster = pdfium::checked_cast(i); const int width = face.pdf_font && code ? face.pdf_font->GetCharWidth(code) : 0; - glyph.x_advance = (width * size / 1000.0f + letter_spacing) * scale_x; + glyph.x_advance = (Scaled(width, size) + letter_spacing) * scale_x; item->glyphs.push_back(glyph); } if (item->rtl) { @@ -457,7 +470,7 @@ struct CPDF_RichTextLayout::Impl { // it exactly so the appearance needs no TJ adjustment. float advance = positions[i].x_advance * unit; const float pdf_width = - face.program->GetGlyphWidth(glyph.gid) * size / 1000.0f; + Scaled(face.program->GetGlyphWidth(glyph.gid), size); if (std::fabs(advance - pdf_width) * 1000.0f / size < 0.75f) { advance = pdf_width; } @@ -605,11 +618,13 @@ struct CPDF_RichTextLayout::Impl { const CPDF_RichTextStyle& resolved = styles[style].style; const WideStringView grapheme = text.AsStringView().Substr(begin, end - begin); - const GraphemeMatch match = - MatchGrapheme(fonts, - fonts->ResolveRichFace(resolved.family, resolved.weight, - resolved.italic), - grapheme, resolved.weight, resolved.italic); + const int face = + styles[style].face_entry >= 0 + ? styles[style].face_entry + : fonts->ResolveRichFace(resolved.family, resolved.weight, + resolved.italic); + const GraphemeMatch match = MatchGrapheme( + fonts, face, grapheme, resolved.weight, resolved.italic); if (!match.covered && !IsInvisibleGrapheme(grapheme)) { fonts->NoteMissingCharacter(WideString(grapheme)); } @@ -1137,3 +1152,474 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( } return result; } + +// ---- Form fields +// -------------------------------------------------------------- + +namespace { + +// A cluster of a field's text: what CPVT called a word. +struct FieldUnit { + size_t begin = 0; // in its segment's text + size_t end = 0; + float width = 0; + uint16_t code = 0; // its first character, as CPVT's word breaking sees it + float ascent = 0; // its font's at the size, as CPVT measures it + float descent = 0; // negative +}; + +struct FieldLine { + int begin = -1; // units; -1 for an empty section's one line + int end = -1; + float width = 0; + float ascent = 0; + float descent = 0; // negative +}; + +// What a field's text breaks into at line breaks: CPVT's sections. +struct FieldSection { + Segment segment; + std::vector units; + std::vector lines; + std::vector cell_x; // a comb's: where each unit starts + float left = 0; // the box its lines take, y down from the text's top + float top = 0; + float right = 0; + float bottom = 0; +}; + +struct FieldExtent { + float width = 0; // the widest line + float height = 0; // the lines' heights, summed +}; + +// CPVT's character for its word breaking: a code unit, so a supplementary +// character is its high surrogate. +uint16_t BreakCodeOf(wchar_t ch) { + const uint32_t code_point = static_cast(ch); + if (pdfium::IsSupplementary(code_point)) { + return pdfium::SurrogatePair(static_cast(code_point)).high(); + } + return static_cast(code_point); +} + +size_t Utf16LengthOf(WideStringView text) { + size_t length = 0; + for (char32_t code_point : pdfium::CodePointView(text)) { + length += pdfium::IsSupplementary(code_point) ? 2 : 1; + } + return length; +} + +// The text's sections and characters as CPVT's SetText() takes them: line +// breaks (CR, LF or both) start a section in a multi-line field and are +// dropped in any other, a tab is a space, and the limit or the comb's cells +// stop it. Characters (graphemes) are kept whole; a password's are '*'. +std::vector FieldSectionsOf( + const CPDF_RichTextLayout::Field& field) { + const WideString& text = field.text; + const std::vector starts = CPDF_RichTextLayout::GraphemeStarts(text); + std::vector sections(1); + // What CPVT counts: the text taken, line breaks included, and what it + // kept, a break between sections included; the limit in code units, the + // cells in characters. + size_t taken_units = 0; + size_t taken_characters = 0; + size_t kept_units = 0; + size_t kept_characters = 0; + for (size_t begin = 0; begin < text.GetLength();) { + size_t end = begin + 1; + while (end < text.GetLength() && !starts[end]) { + ++end; + } + const WideStringView grapheme = + text.AsStringView().Substr(begin, end - begin); + const size_t units = Utf16LengthOf(grapheme); + if (field.limit > 0 && taken_units + units > field.limit) { + break; + } + if (field.cells > 0 && taken_characters >= field.cells) { + break; + } + const wchar_t first = text[begin]; + if (first == L'\r' || first == L'\n') { + if (field.multiline) { + // CR LF is one break. + const wchar_t pair = first == L'\r' ? L'\n' : L'\r'; + if (end < text.GetLength() && text[end] == pair) { + ++end; + } + sections.emplace_back(); + ++kept_units; + ++kept_characters; + } + } else { + const bool full = (field.limit > 0 && kept_units + units > field.limit) || + (field.cells > 0 && kept_characters >= field.cells); + if (!full) { + if (field.password) { + sections.back() += L'*'; + } else if (first == L'\t') { + sections.back() += L' '; + } else { + sections.back() += WideString(grapheme); + } + kept_units += units; + ++kept_characters; + } + } + taken_units += units; + ++taken_characters; + begin = end; + } + return sections; +} + +// CPVT's SplitLines() over a section's units: the lines they break into +// when |wrap| (before the word that would pass |typeset_width|, or before +// the unit when one word is wider), and the box they take. +FieldExtent SplitFieldLines(const std::vector& units, + bool wrap, + float typeset_width, + float empty_ascent, + float empty_descent, + std::vector* lines) { + if (units.empty()) { + if (lines) { + lines->push_back({-1, -1, 0, empty_ascent, empty_descent}); + } + return {0, empty_ascent - empty_descent}; + } + const int total = pdfium::checked_cast(units.size()); + int head = 0; + float max_x = 0; + float max_y = 0; + float line_width = 0; + float backup_width = 0; + float line_ascent = 0; + float backup_ascent = 0; + float line_descent = 0; + float backup_descent = 0; + int word_start = 0; + bool full_word = false; + int full_words = 0; + int char_index = 0; + bool opened = false; + auto add_line = [&](int tail) { + if (lines) { + lines->push_back({head, tail, line_width, line_ascent, line_descent}); + } + max_y += line_ascent; + max_y -= line_descent; + max_x = std::max(line_width, max_x); + }; + int i = 0; + while (i < total) { + const FieldUnit& unit = units[i]; + const FieldUnit& previous = units[i > 0 ? i - 1 : i]; + line_ascent = std::max(line_ascent, unit.ascent); + line_descent = std::min(line_descent, unit.descent); + if (!opened) { + if (CPVT_IsOpenStylePunctuation(unit.code)) { + opened = true; + full_word = true; + } else if (CPVT_NeedDivision(previous.code, unit.code)) { + full_word = true; + } + } else if (!CPVT_IsSpace(unit.code) && + !CPVT_IsOpenStylePunctuation(unit.code)) { + opened = false; + } + if (full_word) { + full_word = false; + if (char_index > 0) { + ++full_words; + } + word_start = i; + backup_width = line_width; + backup_ascent = line_ascent; + backup_descent = line_descent; + } + ++char_index; + if (wrap && typeset_width > 0 && line_width + unit.width > typeset_width) { + if (full_words > 0) { + i = word_start; + line_width = backup_width; + line_ascent = backup_ascent; + line_descent = backup_descent; + } + if (char_index == 1) { + line_width = unit.width; + ++i; + } + add_line(i - 1); + head = i; + line_width = 0; + line_ascent = 0; + line_descent = 0; + char_index = 0; + full_words = 0; + full_word = false; + } else { + line_width += unit.width; + ++i; + } + } + if (head <= total - 1) { + add_line(total - 1); + } + return {max_x, max_y}; +} + +// Where a line |width| wide starts in |typeset_width|, by /Q. +float FieldAlignmentOffset(int alignment, float typeset_width, float width) { + switch (alignment) { + case 1: + return (typeset_width - width) * 0.5f; + case 2: + return typeset_width - width; + default: + return 0.0f; + } +} + +} // namespace + +CPDF_RichTextLayout::Result CPDF_RichTextLayout::ArrangeField( + const Field& field, + const CFX_FloatRect& plate) { + Result result; + const float plate_width = plate.right - plate.left; + const float plate_height = plate.top - plate.bottom; + const std::vector texts = FieldSectionsOf(field); + // A field reads in the direction of its first strong character. + WideString all_text; + for (const WideString& text : texts) { + all_text += text; + } + const bool rtl = CFX_BidiParagraph::FirstStrongIsRtl(all_text.AsStringView()) + .value_or(false); + const bool comb = field.cells > 0; + + // The fonts' ascent and descent, as CPVT measures a line with them. + std::map> metrics; + auto metrics_of = [&](int entry) { + auto it = metrics.find(entry); + if (it == metrics.end()) { + RetainPtr font = fonts_->GetPDFFont(entry); + it = metrics + .emplace(entry, font ? std::make_pair(font->GetTypeAscent(), + font->GetTypeDescent()) + : std::make_pair(0, 0)) + .first; + } + return it->second; + }; + + // The sections, itemised and shaped at |size|, cut into units. + auto sections_at = [&](Impl* impl, float size) { + ResolvedStyle style; + style.style.size = size; + style.style.weight = field.weight; + style.style.italic = field.italic; + style.drawn_size = size; + style.face_entry = field.face_entry; + const std::vector styles = {style}; + std::vector sections(texts.size()); + size_t source_pos = 0; + for (size_t s = 0; s < texts.size(); ++s) { + Segment& segment = sections[s].segment; + segment.text = texts[s]; + segment.style_of.assign(segment.text.GetLength(), 0); + for (size_t i = 0; i < segment.text.GetLength(); ++i) { + segment.source_pos.push_back(source_pos++); + } + ++source_pos; // the line break + impl->Itemize(&segment, rtl, styles); + const std::vector starts = GraphemeStarts(segment.text); + const size_t length = segment.text.GetLength(); + for (size_t begin = 0; begin < length;) { + size_t end = begin + 1; + while (end < length && + !(starts[end] && Impl::IsClusterBoundary(segment, end))) { + ++end; + } + FieldUnit unit; + unit.begin = begin; + unit.end = end; + unit.width = Impl::RangeWidth(segment, begin, end); + unit.code = BreakCodeOf(segment.text[begin]); + for (const Item& item : segment.items) { + if (begin >= item.begin && begin < item.end) { + const auto [ascent, descent] = metrics_of(item.entry); + unit.ascent = ascent * size * 0.001f; + unit.descent = descent * size * 0.001f; + break; + } + } + sections[s].units.push_back(unit); + begin = end; + } + } + return sections; + }; + auto face_ascent = [&](float size) { + return metrics_of(field.face_entry).first * size * 0.001f; + }; + auto face_descent = [&](float size) { + return metrics_of(field.face_entry).second * size * 0.001f; + }; + const float typeset_width = std::max(plate_width, 0.0f); + + // The size: the biggest step of the ladder the text fits in, searched as + // CPVT's std::lower_bound searches it. + float size = field.size; + if (FXSYS_IsFloatZero(size)) { + size = 0; + if (plate_width > 0) { + const size_t steps = + field.multiline ? kAutoSizeSteps.size() / 4 : kAutoSizeSteps.size(); + auto too_big = [&](float candidate) { + Impl impl(fonts_, options_); + impl.field = true; + float width = 0; + float height = 0; + for (const FieldSection& section : sections_at(&impl, candidate)) { + const FieldExtent extent = SplitFieldLines( + section.units, field.multiline, typeset_width, + face_ascent(candidate), face_descent(candidate), nullptr); + width = std::max(extent.width, width); + height += extent.height; + if (FXSYS_IsFloatBigger(width, plate_width) || + FXSYS_IsFloatBigger(height, plate_height)) { + return true; + } + } + return false; + }; + size_t first = 0; + size_t count = steps; + while (count > 0) { + const size_t step = count / 2; + if (!too_big(kAutoSizeSteps[first + step])) { + first += step + 1; + count -= step + 1; + } else { + count = step; + } + } + size = first == steps ? kAutoSizeSteps[steps - 1] + : kAutoSizeSteps[first == 0 ? 0 : first - 1]; + } + } + + Impl impl(fonts_, options_); + impl.field = true; + std::vector sections = sections_at(&impl, size); + const std::vector styles = [&] { + ResolvedStyle style; + style.style.size = size; + style.drawn_size = size; + style.face_entry = field.face_entry; + return std::vector{style}; + }(); + + // Each section's lines, placed as CPVT's RearrangeTypeset() and + // RearrangeCharArray() place them, the sections one under the other. + float position_y = 0; + float content_height = 0; + for (FieldSection& section : sections) { + if (comb) { + // Each unit in the middle of its cell, the cells filled from the + // left, the middle or the right; every baseline at the face's ascent. + const float node_width = plate_width / static_cast(field.cells); + const int units = pdfium::checked_cast(section.units.size()); + const int cells = pdfium::checked_cast(field.cells); + int start = 0; + if (field.alignment == 1) { + start = (cells - units) / 2; + } else if (field.alignment == 2) { + start = cells - units; + } + const float ascent = face_ascent(size); + float line_descent = face_descent(size); + float x = 0; + for (int i = 0; i < units && i < cells; ++i) { + const FieldUnit& unit = section.units[i]; + // In double, as CPVT computes it. + x = static_cast(node_width * (i + start + 0.5) - + unit.width * 0.5f); + section.cell_x.push_back(x); + x += unit.width; + line_descent = std::min(line_descent, unit.descent); + } + section.lines = { + {units > 0 ? 0 : -1, units - 1, 0, ascent, line_descent}}; + section.top = position_y; + section.right = x; + section.bottom = section.top + (ascent - line_descent); + } else { + const FieldExtent extent = SplitFieldLines( + section.units, field.multiline, typeset_width, face_ascent(size), + face_descent(size), §ion.lines); + section.left = + FieldAlignmentOffset(field.alignment, typeset_width, extent.width); + section.right = section.left + extent.width; + section.top = position_y; + section.bottom = position_y + extent.height; + } + position_y += section.bottom - section.top; + content_height += section.bottom - section.top; + } + + // A one-line field's text has its middle at the plate's middle. + const float offset_y = + field.centered ? (content_height - plate_height) / 2.0f : 0.0f; + + for (FieldSection& section : sections) { + const Segment& segment = section.segment; + float line_top = 0; // the baseline steps down from the section's top + for (const FieldLine& field_line : section.lines) { + Line line; + line.ascent = field_line.ascent; + line.descent = -field_line.descent; + line.width = field_line.width; + line_top += field_line.ascent; + line.baseline_y = (line_top + section.top) - offset_y; + line_top -= field_line.descent; + if (field_line.begin >= 0) { + if (comb) { + // A run per cell. + for (size_t i = 0; i < section.cell_x.size(); ++i) { + const FieldUnit& unit = section.units[i]; + for (GlyphRun& run : + Impl::RunsFor(segment, unit.begin, unit.end, styles)) { + run.x = section.cell_x[i]; + line.runs.push_back(std::move(run)); + } + } + line.start_x = line.runs.empty() ? 0 : line.runs.front().x; + } else { + const size_t begin = section.units[field_line.begin].begin; + const size_t end = section.units[field_line.end].end; + float x = FieldAlignmentOffset(field.alignment, typeset_width, + field_line.width); + line.start_x = (x - section.left) + section.left; + for (GlyphRun& run : Impl::RunsFor(segment, begin, end, styles)) { + run.x = (x - section.left) + section.left; + x += run.width; + line.runs.push_back(std::move(run)); + } + } + for (const GlyphRun& run : line.runs) { + result.degraded = result.degraded || run.degraded; + } + } + result.lines.push_back(std::move(line)); + } + } + result.content_height = content_height; + result.body_size = size; + result.degraded = result.degraded || impl.degraded; + return result; +} diff --git a/core/fpdfdoc/cpdf_richtextlayout.h b/core/fpdfdoc/cpdf_richtextlayout.h index cafa874ca7..adf45f94eb 100644 --- a/core/fpdfdoc/cpdf_richtextlayout.h +++ b/core/fpdfdoc/cpdf_richtextlayout.h @@ -96,6 +96,37 @@ class CPDF_RichTextLayout { const CFX_FloatRect& area, CPDF_Annot::VerticalAlignment vertical_alignment); + // A form field's text, or a redaction label, laid out with the geometry + // PDFium's form filler gives it (CPVT): words break where CPVT breaks them, + // a line is as high as its fonts' ascent and descent with no leading and + // is aligned on its own, a size of 0 is the biggest of CPVT's sizes the + // text fits in (12 pt at most for several lines), and a comb puts one + // character in the middle of each cell. Fonts, shaping and order are this + // layout's: each cluster in the first font that has it, then the last + // resort; the Unicode bidi algorithm with the direction of the first strong + // character. + struct Field { + WideString text; + int face_entry = -1; // the /DA font + int weight = 400; // its style, for the fallbacks + bool italic = false; + float size = 0; // pt; 0 fits the text + int alignment = 0; // /Q: 0 left, 1 centre, 2 right + bool multiline = false; // line breaks start lines; long lines wrap + bool password = false; // each character drawn as '*' + // At most this many UTF-16 code units, line breaks counted (/MaxLen); + // 0 for no limit. A character is kept whole or not at all. + size_t limit = 0; + // A comb of this many cells, one character in each; 0 for none. + size_t cells = 0; + // The text's middle at the plate's middle (a one-line field), else its + // first line at the plate's top. + bool centered = false; + }; + // Line positions come back relative to |plate|'s top-left corner, as + // Arrange()'s do; |Result::body_size| is the size laid out with. + Result ArrangeField(const Field& field, const CFX_FloatRect& plate); + // Pure pieces, exposed for tests. // True at |i| when a line may start with character |i| (never at 0). static std::vector BreakOpportunities(const WideString& text); diff --git a/core/fpdfdoc/cpvt_section.cpp b/core/fpdfdoc/cpvt_section.cpp index 95378e5fe3..fe9551e315 100644 --- a/core/fpdfdoc/cpvt_section.cpp +++ b/core/fpdfdoc/cpvt_section.cpp @@ -175,6 +175,19 @@ bool NeedDivision(uint16_t prevWord, uint16_t curWord) { } // namespace +// EmbedPDF(fonts): CPVT's word breaking for the shared layout. +bool CPVT_NeedDivision(uint16_t previous, uint16_t current) { + return NeedDivision(previous, current); +} + +bool CPVT_IsOpenStylePunctuation(uint16_t word) { + return IsOpenStylePunctuation(word); +} + +bool CPVT_IsSpace(uint16_t word) { + return IsSpace(word); +} + CPVT_Section::Line::Line(const CPVT_LineInfo& lineinfo) : line_info_(lineinfo) {} diff --git a/core/fpdfdoc/cpvt_section.h b/core/fpdfdoc/cpvt_section.h index f35f8b76e9..28ad812bfd 100644 --- a/core/fpdfdoc/cpvt_section.h +++ b/core/fpdfdoc/cpvt_section.h @@ -116,4 +116,12 @@ class CPVT_Section final { UnownedPtr const vt_; }; +// EmbedPDF(fonts): CPVT's word breaking, which the shared layout breaks a +// form field's lines with (CPDF_RichTextLayout::ArrangeField()): whether a +// line may break between two characters, the punctuation that opens a word, +// and the spaces that don't close one. +bool CPVT_NeedDivision(uint16_t previous, uint16_t current); +bool CPVT_IsOpenStylePunctuation(uint16_t word); +bool CPVT_IsSpace(uint16_t word); + #endif // CORE_FPDFDOC_CPVT_SECTION_H_ diff --git a/fpdfsdk/epdf_annot_embeddertest.cpp b/fpdfsdk/epdf_annot_embeddertest.cpp index 1361dab373..271d915e43 100644 --- a/fpdfsdk/epdf_annot_embeddertest.cpp +++ b/fpdfsdk/epdf_annot_embeddertest.cpp @@ -50,6 +50,7 @@ #include "core/fxcrt/fx_safe_types.h" #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/span.h" +#include "core/fxge/cfx_face.h" #include "core/fxge/cfx_font.h" #include "core/fxge/cfx_fontregistry.h" #include "fpdfsdk/cpdfsdk_helpers.h" @@ -3040,9 +3041,11 @@ TEST_F(EPDFAnnotEmbedderTest, RegisteredLayoutFontAllocatesNoDocumentObject) { CPDF_AnnotFontSubset::CreateLayoutFont(cpdf_doc, roboto_id); ASSERT_TRUE(layout.font); ASSERT_TRUE(layout.scratch); - EXPECT_NE(0u, layout.font->CharCodeFromUnicode('A')); - EXPECT_GT(layout.font->GetCharWidth(layout.font->CharCodeFromUnicode('A')), - 0); + // It measures glyphs, found through the program's own cmap: a + // TrueType program's glyph is its CID. + const int glyph = layout.font->GetFont()->GetFace()->GetCharIndex('A'); + ASSERT_GT(glyph, 0); + EXPECT_GT(layout.font->GetCharWidth(static_cast(glyph)), 0); EXPECT_EQ(0u, layout.font->GetFontDict()->GetObjNum()); } EXPECT_EQ(last_obj_before, cpdf_doc->GetLastObjNum()); @@ -4318,7 +4321,11 @@ TEST_F(EPDFAnnotEmbedderTest, RichTextLastResortDrawsWhatNoFontHas) { RetainPtr descriptor = GetType0FontDescriptor(font.Get()); ASSERT_TRUE(descriptor); - EXPECT_TRUE(descriptor->GetStreamFor("FontFile2")); + RetainPtr program = + descriptor->GetStreamFor("FontFile2"); + ASSERT_TRUE(program); + // The box costs what the font does, not a cmap entry per code point. + EXPECT_EQ(box.size(), program->GetRawSize()); EXPECT_THAT(GetNormalAppearance(annot.get()), HasSubstr(L"/Span <>> BDC")); @@ -4345,10 +4352,11 @@ TEST_F(EPDFAnnotEmbedderTest, RichTextLastResortDrawsWhatNoFontHas) { TEST_F(EPDFAnnotEmbedderTest, RichTextPreviewAndPrintLastResortWaits) { ScopedRegisteredFonts scoped_fonts; std::vector roboto = LoadRobotoFontData(); + // Preview and print, and still never subset. std::vector box = WithFsType( GetFileContents( PathService::GetTestFilePath("fonts/last_resort.ttf").c_str()), - 0x0004); + 0x0104); ASSERT_FALSE(roboto.empty()); ASSERT_FALSE(box.empty()); ASSERT_NE(0u, EPDFFont_RegisterMemFont64("Roboto", /*weight=*/400, diff --git a/fpdfsdk/epdf_font_embeddertest.cpp b/fpdfsdk/epdf_font_embeddertest.cpp index ea4d4431f4..13c70ef8be 100644 --- a/fpdfsdk/epdf_font_embeddertest.cpp +++ b/fpdfsdk/epdf_font_embeddertest.cpp @@ -17,6 +17,7 @@ #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_document.h" #include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fpdfapi/parser/cpdf_stream_acc.h" #include "core/fxcrt/bytestring.h" #include "core/fxcrt/widestring.h" #include "core/fxge/cfx_face.h" @@ -161,6 +162,35 @@ std::vector MissingCharacters(FPDF_DOCUMENT document) { return missing; } +// The widget's normal appearance stream, decoded. +std::string NormalAppearanceOf(FPDF_ANNOTATION annot) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + RetainPtr ap = + context->GetAnnotDict()->GetDictFor("AP"); + RetainPtr normal = ap ? ap->GetStreamFor("N") : nullptr; + if (!normal) { + return std::string(); + } + auto access = pdfium::MakeRetain(normal); + access->LoadAllDataFiltered(); + const pdfium::span data = access->GetSpan(); + return std::string(data.begin(), data.end()); +} + +// Whether the widget's appearance names a registered font among its fonts. +bool AppearanceUsesRegisteredFont(FPDF_ANNOTATION annot, EPDF_FONT_ID id) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + RetainPtr ap = + context->GetAnnotDict()->GetDictFor("AP"); + RetainPtr normal = ap ? ap->GetStreamFor("N") : nullptr; + RetainPtr resources = + normal ? normal->GetDict()->GetDictFor("Resources") : nullptr; + RetainPtr fonts = + resources ? resources->GetDictFor("Font") : nullptr; + return fonts && + fonts->KeyExist(ByteString::Format("ERegF%u", id).AsStringView()); +} + ByteString DefaultAppearanceFontName(FPDF_ANNOTATION annot) { CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); const ByteString da = context->GetAnnotDict()->GetByteStringFor("DA"); @@ -957,3 +987,109 @@ TEST_F(EPDFFontEmbedderTest, BuildsADisplayProgramForAType1Font) { FormWithFieldFont(type1, "FontFile", nullptr, "Roboto2-Regular"), EPDF_FONT_FORMAT_TYPE1); } + +// A form field draws each cluster in the first font that has it: Hebrew +// typed into a Helvetica field comes from the fallback, in bidi order, under +// ActualText, and nothing is reported missing. +TEST_F(EPDFFontEmbedderTest, WidgetDrawsEachClusterInAFontThatHasIt) { + ScopedRegisteredFonts scoped_fonts; + std::vector arimo = + LoadThirdPartyFont("test_fonts/test_fonts/Arimo-Regular.ttf"); + ASSERT_FALSE(arimo.empty()); + const EPDF_FONT_ID arimo_id = EPDFFont_RegisterMemFont64( + "Arimo", /*weight=*/400, /*italic=*/0, arimo.data(), arimo.size()); + ASSERT_NE(0u, arimo_id); + ASSERT_TRUE(EPDFFont_AddFallbackFont(arimo_id, /*tier=*/0)); + + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_WIDGET)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 300.0f, 350.0f, 270.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), FPDF_FONT_HELVETICA, + 12.0f, 0, 0, 0)); + EPDFDoc_ClearMissingCharacters(document()); + AttachAndFill(annot.get(), L"hebrew", L"Hello \x05E9\x05DC\x05D5\x05DD 123"); + + EXPECT_TRUE(AppearanceUsesRegisteredFont(annot.get(), arimo_id)); + const std::string stream = NormalAppearanceOf(annot.get()); + EXPECT_NE(std::string::npos, + stream.find("/Span <>> BDC")) + << stream; + // Left to right: the Latin, the digits, then the Hebrew. + const size_t hello = stream.find("(Hello ) Tj"); + const size_t digits = stream.find("(123) Tj"); + const size_t hebrew = stream.find("/ActualText"); + ASSERT_NE(std::string::npos, hello) << stream; + ASSERT_NE(std::string::npos, digits) << stream; + EXPECT_LT(hello, digits); + EXPECT_LT(digits, hebrew); + EXPECT_EQ(0, EPDFDoc_CountMissingCharacters(document())); +} + +// A character no font has is drawn with the last resort, as its box, and +// reported; a supplementary character stays whole. +TEST_F(EPDFFontEmbedderTest, WidgetDrawsWhatNoFontHasWithTheLastResort) { + ScopedRegisteredFonts scoped_fonts; + std::vector box = GetFileContents( + PathService::GetTestFilePath("fonts/last_resort.ttf").c_str()); + ASSERT_FALSE(box.empty()); + const EPDF_FONT_ID box_id = EPDFFont_RegisterMemFont64( + "Last Resort", /*weight=*/400, /*italic=*/0, box.data(), box.size()); + ASSERT_NE(0u, box_id); + ASSERT_TRUE(EPDFFont_SetLastResortFont(box_id)); + + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_WIDGET)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 300.0f, 350.0f, 270.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), FPDF_FONT_HELVETICA, + 12.0f, 0, 0, 0)); + EPDFDoc_ClearMissingCharacters(document()); + // As UTF-16 code units: the test's string helper writes one per wchar_t. + AttachAndFill(annot.get(), L"boxes", + L"A\x4E2D" + L"B\xD83D\xDE00"); + + EXPECT_TRUE(AppearanceUsesRegisteredFont(annot.get(), box_id)); + const std::string stream = NormalAppearanceOf(annot.get()); + EXPECT_NE(std::string::npos, stream.find("(A) Tj")) << stream; + EXPECT_NE(std::string::npos, stream.find("(B) Tj")) << stream; + EXPECT_NE(std::string::npos, stream.find("")) << stream; + // UTF-16, as the report hands them out. + EXPECT_EQ((std::vector{L"\x4E2D", L"\xD83D\xDE00"}), + MissingCharacters(document())); +} + +// A password field writes stars and nothing else: not the value, and no +// ActualText that would carry it. +TEST_F(EPDFFontEmbedderTest, PasswordFieldWritesOnlyStars) { + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_WIDGET)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 300.0f, 350.0f, 270.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), FPDF_FONT_HELVETICA, + 12.0f, 0, 0, 0)); + ScopedFPDFWideString name = GetFPDFWideString(L"secret"); + const uint32_t field = EPDFForm_CreateField( + document(), 4 /* EPDF_FORMFIELD_FAMILY_TEXT */, name.get(), 0); + ASSERT_GT(field, 0u); + ASSERT_TRUE(EPDFForm_AttachWidget( + document(), field, EPDFAnnot_GetObjectNumber(annot.get()), nullptr, 0)); + ASSERT_TRUE(EPDFForm_SetFieldFlags(document(), field, 1u << 13, 0)); + ASSERT_TRUE(embedpdf_test::FillTextField(document(), field, L"hunter2")); + ASSERT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot.get())); + + const std::string stream = NormalAppearanceOf(annot.get()); + EXPECT_NE(std::string::npos, stream.find("(*******) Tj")) << stream; + EXPECT_EQ(std::string::npos, stream.find("hunter")) << stream; + EXPECT_EQ(std::string::npos, stream.find("ActualText")) << stream; +} diff --git a/fpdfsdk/epdf_richtextlayout_embeddertest.cpp b/fpdfsdk/epdf_richtextlayout_embeddertest.cpp index f12cf7047c..47e6b98463 100644 --- a/fpdfsdk/epdf_richtextlayout_embeddertest.cpp +++ b/fpdfsdk/epdf_richtextlayout_embeddertest.cpp @@ -14,6 +14,9 @@ #include "core/fpdfdoc/cpdf_annotfontmap.h" #include "core/fpdfdoc/cpdf_documentfonts.h" #include "core/fpdfdoc/cpdf_richtext.h" +#include "core/fpdfdoc/cpvt_fontmap.h" +#include "core/fpdfdoc/cpvt_variabletext.h" +#include "core/fpdfdoc/cpvt_word.h" #include "core/fxcrt/fx_coordinates.h" #include "core/fxcrt/widestring.h" #include "fpdfsdk/cpdfsdk_helpers.h" @@ -347,3 +350,173 @@ TEST_F(EPDFRichTextLayoutEmbedderTest, LastResortDrawsWhatNoFontHas) { ASSERT_EQ(1u, lines[0].runs.size()); EXPECT_EQ(arimo_, FontOf(lines[0].runs[0])); } + +namespace { + +// A character a field draws, where its baseline starts. +struct PlacedCharacter { + wchar_t ch; + float x; + float y; +}; + +} // namespace + +// The field layout keeps the geometry of PDFium's form filler (CPVT) where +// CPVT drew correctly: the same characters at the same places, for the +// standard fonts, the sizes, fitted sizes, alignments, several lines, long +// words, passwords, limits and combs the forms parity gate draws. +TEST_F(EPDFRichTextLayoutEmbedderTest, FieldMatchesCpvt) { + struct Case { + const char* font; + const wchar_t* text; + float size; + int alignment; + bool multiline; + bool password; + size_t limit; + size_t cells; + CFX_FloatRect plate; + }; + const CFX_FloatRect line(20, 20, 180, 44); + const CFX_FloatRect block(20, 20, 170, 110); + const wchar_t* const kParagraphs = + L"The quick brown fox jumps over the lazy dog, again and again.\n" + L"Second paragraph here\n\nAfter a blank line"; + std::vector cases; + for (const char* font : {"Helvetica", "Helvetica-Bold", "Times-Roman", + "Times-BoldItalic", "Courier-BoldOblique"}) { + for (float size : {0.0f, 8.0f, 11.0f, 14.0f}) { + cases.push_back({font, + L"H\x00E9g, quick \x20AC" + L"12.50 \x201C" + L"fox\x201D", + size, 0, false, false, 0, 0, line}); + } + } + for (int alignment : {0, 1, 2}) { + for (const wchar_t* text : {L"Wide WWW text", L"i", L""}) { + cases.push_back( + {"Helvetica", text, 10, alignment, false, false, 0, 0, line}); + } + for (float size : {0.0f, 9.0f}) { + cases.push_back({"Helvetica", kParagraphs, size, alignment, true, false, + 0, 0, block}); + } + for (size_t cells : {6u, 10u}) { + cases.push_back({"Courier", L"AiW1\x00E9", 12, alignment, false, false, 0, + cells, line}); + } + } + cases.push_back({"Helvetica", + L"A long value that will not fit in this box at all", 0, 0, + false, false, 0, 0, line}); + cases.push_back({"Times-Roman", L"Tall", 0, 0, false, false, 0, 0, + CFX_FloatRect(20, 20, 140, 90)}); + cases.push_back({"Times-Roman", L"Supercalifragilisticexpialidocious word", + 12, 0, true, false, 0, 0, CFX_FloatRect(20, 20, 100, 80)}); + cases.push_back({"Helvetica", + L"Words (in brackets) and [more] text {here} to (wrap)", 11, + 0, true, false, 0, 0, CFX_FloatRect(20, 20, 95, 110)}); + cases.push_back({"Helvetica", L"secret", 10, 0, false, true, 0, 0, line}); + cases.push_back( + {"Helvetica", L"truncate me please", 10, 0, false, false, 8, 0, line}); + cases.push_back({"Helvetica", L"AiWm.", 12, 0, false, false, 0, 7, line}); + cases.push_back({"Helvetica", L"1234", 0, 0, false, false, 0, 4, line}); + + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(doc_.get()); + for (const Case& c : cases) { + SCOPED_TRACE(testing::Message() + << c.font << " " << c.size << " q" << c.alignment + << (c.multiline ? " multiline" : "") + << (c.password ? " password" : "") << " limit " << c.limit + << " cells " << c.cells << " \"" << WideString(c.text).ToUTF8() + << "\""); + RetainPtr font = CPDF_Font::GetStockFont(doc, c.font); + ASSERT_TRUE(font); + const bool centered = !c.multiline; + + // CPVT, as the form generator drove it. + std::vector expected; + { + CPVT_FontMap cpvt_fonts(doc, /*resources=*/nullptr, font, "F"); + CPVT_VariableText::Provider provider(&cpvt_fonts); + CPVT_VariableText vt(&provider); + vt.SetPlateRect(c.plate); + vt.SetAlignment(CPVT_VariableText::ToAlignment(c.alignment)); + if (c.size == 0) { + vt.SetAutoFontSize(true); + } else { + vt.SetFontSize(c.size); + } + if (c.multiline) { + vt.SetMultiLine(true); + vt.SetAutoReturn(true); + } + if (c.password) { + vt.SetPasswordChar('*'); + } + if (c.cells > 0) { + vt.SetCharArray(static_cast(c.cells)); + } else { + vt.SetLimitChar(static_cast(c.limit)); + } + vt.Initialize(); + vt.SetText(c.text); + vt.RearrangeAll(); + const CFX_PointF offset( + 0, centered ? (vt.GetContentRect().Height() - c.plate.Height()) / 2.0f + : 0); + CPVT_VariableText::Iterator* it = vt.GetIterator(); + it->SetAt(0); + while (it->NextWord()) { + CPVT_Word word; + if (it->GetWord(word)) { + const CFX_PointF at = word.location() + offset; + expected.push_back( + {c.password ? L'*' : static_cast(word.word()), at.x, + at.y}); + } + } + } + + // The field layout. + std::vector actual; + { + CPDF_AnnotFontMap map(doc, font, "F", + /*allow_registered_fallbacks=*/false); + ASSERT_TRUE(map.HasDefaultFont()); + CPDF_RichTextLayout::Field field; + field.text = c.text; + field.face_entry = 0; + field.size = c.size; + field.alignment = c.alignment; + field.multiline = c.multiline; + field.password = c.password; + field.limit = c.limit; + field.cells = c.cells; + field.centered = centered; + CPDF_RichTextLayout layout(&map, {}); + const CPDF_RichTextLayout::Result laid = + layout.ArrangeField(field, c.plate); + for (const Line& laid_line : laid.lines) { + const float y = c.plate.top - laid_line.baseline_y; + for (const GlyphRun& run : laid_line.runs) { + float x = c.plate.left + run.x; + for (const CPDF_RichTextLayout::ShapedGlyph& glyph : run.glyphs) { + actual.push_back({run.text[glyph.cluster], x, y}); + x += glyph.x_advance; + } + } + } + } + + ASSERT_EQ(expected.size(), actual.size()); + for (size_t i = 0; i < expected.size(); ++i) { + SCOPED_TRACE(i); + EXPECT_EQ(expected[i].ch, actual[i].ch); + EXPECT_NEAR(expected[i].x, actual[i].x, 0.001f); + EXPECT_NEAR(expected[i].y, actual[i].y, 0.001f); + } + } +} diff --git a/testing/resources/fonts/last_resort.ttf b/testing/resources/fonts/last_resort.ttf index 37f80e52778a4f2e04849fa3067ad62137d7b550..189b5bca0f28c23859b36aeaaf70273b66aa101e 100644 GIT binary patch delta 37 tcmaFB@qj~+fsuiMfs3Jmftf+UKUm)=bfVy5M#hO-Pc!b`+{k#J2>_fn3K9SS delta 37 tcmaFB@qj~+fsuiMfs3Jmftf+UKUm)=WTN0=Muv%7Pc!b_+{k#J2>_fY3K9SS From b31c2959bc8f5c54bddcd008b550066b57450f26 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Tue, 20 Oct 2026 11:35:58 +0300 Subject: [PATCH 06/16] Box /ActualText characters in page space inside form XObjects ProcessMarkedContent() boxed and placed /ActualText characters with the text object's rect and position, which are in the space of the form that draws it. Text in a form drawn anywhere but the page's origin, such as a flattened form field, was selected in the wrong place. The rect is now split in the object's space and each character's slice, and its origin, taken through the form's matrix. --- core/fpdftext/cpdf_textpage.cpp | 14 ++++-- fpdfsdk/epdf_edit_test_util.cpp | 8 ++- fpdfsdk/epdf_edit_test_util.h | 4 ++ fpdfsdk/epdf_text_embeddertest.cpp | 81 ++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 6 deletions(-) diff --git a/core/fpdftext/cpdf_textpage.cpp b/core/fpdftext/cpdf_textpage.cpp index f34a289d13..63b1d34e9f 100644 --- a/core/fpdftext/cpdf_textpage.cpp +++ b/core/fpdftext/cpdf_textpage.cpp @@ -1194,7 +1194,13 @@ bool CPDF_TextPage::ProcessMarkedContent(const TransformedTextObject& obj) { (prev_text_obj_ && IsRightToLeft(*prev_text_obj_) && CFX_BidiString(actual_text, /*auto_order=*/true).OverallDirection() == CFX_BidiChar::Direction::kRight); - CFX_Matrix matrix = text_obj->GetTextMatrix() * obj.form_matrix_; + // EmbedPDF(bug-fix): the characters are boxed and placed in page space, as + // every other character is. The text object's rect and position are in the + // space of the form XObject that draws it; upstream used them as they are, + // so /ActualText in a form drawn anywhere but the page's origin, such as a + // flattened field, was boxed in the wrong place. + const CFX_Matrix& form_matrix = obj.form_matrix_; + CFX_Matrix matrix = text_obj->GetTextMatrix() * form_matrix; CFX_FloatRect rect = text_obj->GetRect(); float step = 0; @@ -1220,9 +1226,9 @@ bool CPDF_TextPage::ProcessMarkedContent(const TransformedTextObject& obj) { char_box.Translate(k * step, 0); temp_text_buf_.AppendChar(wc); // EmbedPDF(text-geometry): not a real glyph, so no local origin. - temp_char_list_.push_back( - CharInfo::Synthetic(CharType::kActualText, wc, text_obj->GetPos(), - char_box, matrix, text_obj)); + temp_char_list_.push_back(CharInfo::Synthetic( + CharType::kActualText, wc, form_matrix.Transform(text_obj->GetPos()), + form_matrix.TransformRect(char_box), matrix, text_obj)); } return is_rtl; } diff --git a/fpdfsdk/epdf_edit_test_util.cpp b/fpdfsdk/epdf_edit_test_util.cpp index 4f0edf7590..610e1fc539 100644 --- a/fpdfsdk/epdf_edit_test_util.cpp +++ b/fpdfsdk/epdf_edit_test_util.cpp @@ -103,8 +103,12 @@ std::string MakePdf(const std::vector& objects) { } std::string Stream(const std::string& data) { - return "<< /Length " + std::to_string(data.size()) + " >>\nstream\n" + data + - "\nendstream"; + return Stream("", data); +} + +std::string Stream(const std::string& entries, const std::string& data) { + return "<< " + entries + (entries.empty() ? "" : " ") + "/Length " + + std::to_string(data.size()) + " >>\nstream\n" + data + "\nendstream"; } std::map ObjectGraph(CPDF_Document* doc) { diff --git a/fpdfsdk/epdf_edit_test_util.h b/fpdfsdk/epdf_edit_test_util.h index 2c5178e402..75ef0b1d72 100644 --- a/fpdfsdk/epdf_edit_test_util.h +++ b/fpdfsdk/epdf_edit_test_util.h @@ -45,6 +45,10 @@ std::string MakePdf(const std::vector& objects); // A stream object body holding `data`. std::string Stream(const std::string& data); +// A stream object body holding `data`, with `entries` in its dictionary +// before /Length. +std::string Stream(const std::string& entries, const std::string& data); + // Every indirect object, by number, from 1 to the last, as text: streams with // their raw bytes, so two graphs compare by value. std::map ObjectGraph(CPDF_Document* doc); diff --git a/fpdfsdk/epdf_text_embeddertest.cpp b/fpdfsdk/epdf_text_embeddertest.cpp index b8c64a5815..89e6d07ea3 100644 --- a/fpdfsdk/epdf_text_embeddertest.cpp +++ b/fpdfsdk/epdf_text_embeddertest.cpp @@ -6,11 +6,14 @@ #include "public/epdf_text.h" #include +#include +#include #include "core/fpdfapi/parser/cpdf_document.h" #include "core/fpdfapi/parser/cpdf_read_only_graph_guard.h" #include "core/fxcrt/notreached.h" #include "fpdfsdk/cpdfsdk_helpers.h" +#include "fpdfsdk/epdf_edit_test_util.h" #include "public/cpp/fpdf_scopers.h" #include "public/fpdf_text.h" #include "testing/embedder_test.h" @@ -161,6 +164,84 @@ TEST_F(EPDFTextEmbedderTest, ChromeType3TextLooseBoxesShareTheLine) { } } +// /ActualText is boxed and placed in page space wherever it is drawn. The same +// span drawn on the page, and through a form that doubles its size and moves +// it 200 points down, has the same boxes and origins, 200 points lower. +TEST_F(EPDFTextEmbedderTest, ActualTextInAFormIsInPageSpace) { + static constexpr char kSpan[] = + "/Span << /ActualText >> BDC (abcd) Tj EMC"; + const std::string pdf = MakePdf({ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 400 400] /Contents 4 0 R " + "/Resources << /Font << /F1 5 0 R >> /XObject << /Fm 6 0 R >> >> >>", + Stream(std::string("BT /F1 24 Tf 40 300 Td ") + kSpan + + " ET q 1 0 0 1 20 50 cm /Fm Do Q"), + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", + Stream("/Type /XObject /Subtype /Form /BBox [0 0 200 100] " + "/Matrix [2 0 0 2 0 0] /Resources << /Font << /F1 5 0 R >> >>", + std::string("BT /F1 12 Tf 10 25 Td ") + kSpan + " ET"), + }); + ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); + ASSERT_TRUE(page); + ScopedFPDFTextPage text_page(FPDFText_LoadPage(page.get())); + ASSERT_TRUE(text_page); + + // The span's letters, on the page and then in the form. + std::vector letters; + for (int i = 0; i < FPDFText_CountChars(text_page.get()); ++i) { + if (FPDFText_GetUnicode(text_page.get(), i) >= 0x05D0) { + letters.push_back(i); + } + } + ASSERT_EQ(8u, letters.size()); + for (size_t k = 0; k < 4; ++k) { + SCOPED_TRACE(k); + const int on_page = letters[k]; + const int in_form = letters[k + 4]; + EXPECT_EQ(FPDFText_GetUnicode(text_page.get(), on_page), + FPDFText_GetUnicode(text_page.get(), in_form)); + + double page_box[4]; + double form_box[4]; + ASSERT_TRUE(FPDFText_GetCharBox(text_page.get(), on_page, &page_box[0], + &page_box[1], &page_box[2], &page_box[3])); + ASSERT_TRUE(FPDFText_GetCharBox(text_page.get(), in_form, &form_box[0], + &form_box[1], &form_box[2], &form_box[3])); + // Left, right, bottom, top. + EXPECT_NEAR(page_box[0], form_box[0], 0.01); + EXPECT_NEAR(page_box[1], form_box[1], 0.01); + EXPECT_NEAR(page_box[2] - 200, form_box[2], 0.01); + EXPECT_NEAR(page_box[3] - 200, form_box[3], 0.01); + + FS_RECTF page_loose; + FS_RECTF form_loose; + ASSERT_TRUE( + FPDFText_GetLooseCharBox(text_page.get(), on_page, &page_loose)); + ASSERT_TRUE( + FPDFText_GetLooseCharBox(text_page.get(), in_form, &form_loose)); + EXPECT_NEAR(page_loose.left, form_loose.left, 0.01); + EXPECT_NEAR(page_loose.right, form_loose.right, 0.01); + EXPECT_NEAR(page_loose.top - 200, form_loose.top, 0.01); + EXPECT_NEAR(page_loose.bottom - 200, form_loose.bottom, 0.01); + + double page_x; + double page_y; + double form_x; + double form_y; + ASSERT_TRUE( + FPDFText_GetCharOrigin(text_page.get(), on_page, &page_x, &page_y)); + ASSERT_TRUE( + FPDFText_GetCharOrigin(text_page.get(), in_form, &form_x, &form_y)); + EXPECT_NEAR(40, form_x, 0.01); + EXPECT_NEAR(100, form_y, 0.01); + EXPECT_NEAR(page_x, form_x, 0.01); + EXPECT_NEAR(page_y - 200, form_y, 0.01); + } +} + TEST_F(EPDFTextEmbedderTest, GetRotatedCharGeometry) { ASSERT_TRUE(OpenDocument("rotated_text.pdf")); ScopedPage page = LoadScopedPage(0); From 2e4c50e6e4fddcf6c3f5ba69296ab7fdbb3416ae Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Tue, 20 Oct 2026 11:53:24 +0300 Subject: [PATCH 07/16] Read /ActualText as written when it holds marks or inner spaces CloseTempLine() cut a line's bidi segments at every mark and space, also inside an /ActualText span, and gave a right-to-left line's segments in visual order, each kept forward: pointed Hebrew and Arabic with harakat came out scrambled, and a right-to-left span of two words had them swapped. Inside one span, a mark now takes the direction of the character before it and characters between two of the same direction take theirs (UAX #9 W1 and N1), so the span is one segment; its leading and trailing neutrals keep their own. --- core/fpdftext/cpdf_textpage.cpp | 77 +++++++++++++++++++++++++++++- fpdfsdk/epdf_text_embeddertest.cpp | 45 +++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/core/fpdftext/cpdf_textpage.cpp b/core/fpdftext/cpdf_textpage.cpp index 63b1d34e9f..2f054c5e0a 100644 --- a/core/fpdftext/cpdf_textpage.cpp +++ b/core/fpdftext/cpdf_textpage.cpp @@ -449,6 +449,78 @@ bool FollowsLiteralRightToLeftText( return false; } +// EmbedPDF(bug-fix): the text a line's bidi segments are cut from. An +// /ActualText span is logical text, so inside one, a mark takes the direction +// of the character before it, and characters with no strong direction between +// two with the same one take theirs (UAX #9 W1 and N1); the span's leading and +// trailing ones keep their own. Cut at its marks and inner spaces, a span came +// out as its segments in visual order, each kept forward: a pointed Hebrew +// word read back scrambled, and a right-to-left span of two words with the +// words swapped. +WideString BidiTextOf(const WideString& str, + pdfium::span chars) { + enum class Strong { kNone, kLeft, kRight }; + auto strong_of = [](wchar_t c) { + switch (pdfium::unicode::GetBidiClass(c)) { + case BidiClass::kL: + return Strong::kLeft; + case BidiClass::kR: + case BidiClass::kAL: + return Strong::kRight; + default: + return Strong::kNone; + } + }; + + WideString text = str; + const size_t count = std::min(str.GetLength(), chars.size()); + size_t begin = 0; + while (begin < count) { + const CPDF_TextPage::CharInfo& first = chars[begin]; + size_t end = begin + 1; + if (first.char_type() != CPDF_TextPage::CharType::kActualText) { + begin = end; + continue; + } + while (end < count && + chars[end].char_type() == CPDF_TextPage::CharType::kActualText && + chars[end].text_object() == first.text_object()) { + ++end; + } + + std::vector resolved; + for (size_t i = begin; i < end; ++i) { + Strong direction = strong_of(str[i]); + if (direction == Strong::kNone && i > begin && + pdfium::unicode::GetBidiClass(str[i]) == BidiClass::kNSM) { + direction = resolved.back(); + } + resolved.push_back(direction); + } + std::optional last_strong; + for (size_t i = 0; i < resolved.size(); ++i) { + if (resolved[i] == Strong::kNone) { + continue; + } + if (last_strong && resolved[*last_strong] == resolved[i]) { + std::fill(resolved.begin() + *last_strong + 1, resolved.begin() + i, + resolved[i]); + } + last_strong = i; + } + + // Stand-ins of the resolved direction: Hebrew alef, Latin a. + for (size_t i = begin; i < end; ++i) { + const Strong direction = resolved[i - begin]; + if (direction != Strong::kNone && strong_of(str[i]) == Strong::kNone) { + text.SetAt(i, direction == Strong::kRight ? 0x05D0 : L'a'); + } + } + begin = end; + } + return text; +} + } // namespace CPDF_TextPage::TransformedTextObject::TransformedTextObject() = default; @@ -1007,7 +1079,10 @@ void CPDF_TextPage::CloseTempLine() { } prev_char_is_space = true; } - CFX_BidiString bidi(str, /*auto_order=*/true); + // EmbedPDF(bug-fix): /ActualText spans aren't cut at their marks and inner + // spaces (BidiTextOf); the characters are still those of `str`. + const WideString bidi_text = BidiTextOf(str, temp_char_list_); + CFX_BidiString bidi(bidi_text, /*auto_order=*/true); if (rtl_) { bidi.SetOverallDirectionRight(); } diff --git a/fpdfsdk/epdf_text_embeddertest.cpp b/fpdfsdk/epdf_text_embeddertest.cpp index 89e6d07ea3..86da683286 100644 --- a/fpdfsdk/epdf_text_embeddertest.cpp +++ b/fpdfsdk/epdf_text_embeddertest.cpp @@ -17,6 +17,7 @@ #include "public/cpp/fpdf_scopers.h" #include "public/fpdf_text.h" #include "testing/embedder_test.h" +#include "testing/fx_string_testhelpers.h" #include "testing/gmock/include/gmock/gmock.h" using ::testing::ElementsAreArray; @@ -242,6 +243,50 @@ TEST_F(EPDFTextEmbedderTest, ActualTextInAFormIsInPageSpace) { } } +// /ActualText is logical text: a span reads back as written, whatever marks +// and spaces it holds, on a line of its own or after left-to-right text. +TEST_F(EPDFTextEmbedderTest, ActualTextReadsAsWritten) { + // Pointed Hebrew, two Hebrew words with digits, Arabic with harakat, an + // Arabic word ending in one, and pointed Hebrew after Latin text, each span + // over Latin glyphs. + static constexpr char kPointed[] = ""; + const std::string content = + std::string("BT /F1 12 Tf 20 350 Td /Span << /ActualText ") + kPointed + + " >> BDC (abcd) Tj EMC 0 -40 Td /Span << /ActualText " + " >> BDC " + "(abcd efg hij) Tj EMC 0 -40 Td /Span << /ActualText " + " >> BDC (abcd) Tj EMC " + "0 -40 Td /Span << /ActualText >> BDC " + "(abcd) Tj EMC 0 -40 Td (Hello ) Tj /Span << /ActualText " + + kPointed + " >> BDC (abcd) Tj EMC ET"; + const std::string pdf = MakePdf({ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 400 400] /Contents 4 0 R " + "/Resources << /Font << /F1 5 0 R >> >> >>", + Stream(content), + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", + }); + ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); + ASSERT_TRUE(page); + ScopedFPDFTextPage text_page(FPDFText_LoadPage(page.get())); + ASSERT_TRUE(text_page); + + unsigned short buffer[128] = {}; + ASSERT_GT(FPDFText_GetText(text_page.get(), 0, + FPDFText_CountChars(text_page.get()), buffer), + 0); + EXPECT_EQ( + L"\x05e9\x05b8\x05c1\x05dc\x05d5\x05b9\x05dd\r\n" + L"\x05e9\x05dc\x05d5\x05dd 123 \x05e2\x05d5\x05dc\x05dd\r\n" + L"\x0645\x064e\x0631\x0652\x062d\x064e\x0628\x064b\x0627\r\n" + L"\x0643\x064e\x062a\x064e\x0628\x064e\r\n" + L"Hello \x05e9\x05b8\x05c1\x05dc\x05d5\x05b9\x05dd", + GetPlatformWString(reinterpret_cast(buffer))); +} + TEST_F(EPDFTextEmbedderTest, GetRotatedCharGeometry) { ASSERT_TRUE(OpenDocument("rotated_text.pdf")); ScopedPage page = LoadScopedPage(0); From dcd60817bbd7938f2fdf111b0e0f785d9c619b3b Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Tue, 20 Oct 2026 15:30:20 +0300 Subject: [PATCH 08/16] Realign misaligned sfnt tables for browsers Add shared sfnt writing and alignment helpers. When an embedded Unicode-mapped font has misaligned tables, preserve its original program while providing browsers with a realigned display program and hash. --- core/fpdfdoc/cpdf_documentfonts.cpp | 58 ++++++--- core/fpdfdoc/cpdf_documentfonts.h | 12 +- core/fxge/BUILD.gn | 4 + core/fxge/cfx_opentypewriter.cpp | 53 +------- core/fxge/cfx_sfnt.cpp | 185 ++++++++++++++++++++++++++++ core/fxge/cfx_sfnt.h | 36 ++++++ fpdfsdk/epdf_font_embeddertest.cpp | 135 ++++++++++++++++++++ public/epdf_font.h | 12 +- 8 files changed, 420 insertions(+), 75 deletions(-) create mode 100644 core/fxge/cfx_sfnt.cpp create mode 100644 core/fxge/cfx_sfnt.h diff --git a/core/fpdfdoc/cpdf_documentfonts.cpp b/core/fpdfdoc/cpdf_documentfonts.cpp index c63ec81a5d..3cfc32677a 100644 --- a/core/fpdfdoc/cpdf_documentfonts.cpp +++ b/core/fpdfdoc/cpdf_documentfonts.cpp @@ -19,6 +19,7 @@ #include "core/fxcrt/utf16.h" #include "core/fxge/cfx_face.h" #include "core/fxge/cfx_font.h" +#include "core/fxge/cfx_sfnt.h" #include "core/fxge/fx_fontencoding.h" namespace { @@ -228,6 +229,13 @@ bool IsSfnt(CPDF_DocumentFonts::Format format) { format == CPDF_DocumentFonts::Format::kOpenTypeCFF; } +// Whether a browser loads |program| as it is: an sfnt that maps characters +// through a Unicode cmap, its tables where the font format puts them. +bool LoadsAsIs(const CPDF_DocumentFonts::Program& program) { + return IsSfnt(program.format) && program.maps_unicode && + program.tables_aligned; +} + // |scalars| as ranges, in order. std::vector RangesOf(std::vector scalars) { std::ranges::sort(scalars); @@ -335,6 +343,7 @@ CPDF_DocumentFonts::Program CPDF_DocumentFonts::Inspect( facts.permission = CFX_FontRegistry::ClassifyFsType(face->GetFsTypeFlags()); facts.maps_unicode = face->SelectCharMap(fxge::FontEncoding::kUnicode); + facts.tables_aligned = SfntTablesAligned(data->GetSpan()); } } } @@ -380,9 +389,13 @@ DataVector CPDF_DocumentFonts::ProgramData(uint32_t objnum) { if (!inspected || inspected->facts.sha256.empty()) { return {}; } + return BytesOf(*inspected); +} + +DataVector CPDF_DocumentFonts::BytesOf(const Inspected& inspected) { CPDF_DocPageData* page_data = CPDF_DocPageData::FromDocument(doc_); RetainPtr data = - page_data->GetFontFileStreamAcc(inspected->stream); + page_data->GetFontFileStreamAcc(inspected.stream); if (!data) { return {}; } @@ -400,8 +413,7 @@ DataVector CPDF_DocumentFonts::DisplayHashOf( if (program.sha256.empty()) { return {}; } - // A browser loads it as it is. - if (IsSfnt(program.format) && program.maps_unicode) { + if (LoadsAsIs(program)) { return program.sha256; } // An inline program can't be addressed, so nothing could fetch it. @@ -415,15 +427,27 @@ DataVector CPDF_DocumentFonts::DisplayHashOf( Inspected& inspected = it->second; if (!inspected.display_built) { inspected.display_built = true; - std::optional built = - BuildDisplayProgram(doc_, font_dict, program.postscript_name, - CPDF_FontFace{family, weight, italic}); - if (built.has_value()) { - Display display; - display.sha256 = CryptSha256Generate(built->bytes); - display.coverage = RangesOf(std::move(built->characters)); - display.bytes = std::move(built->bytes); - inspected.display = std::move(display); + if (IsSfnt(program.format) && program.maps_unicode) { + // Only its tables are out of place: the same program, realigned. Its + // coverage is its own cmap's (ProgramCoverage). + DataVector aligned = AlignSfntTables(BytesOf(inspected)); + if (!aligned.empty()) { + Display display; + display.sha256 = CryptSha256Generate(aligned); + display.bytes = std::move(aligned); + inspected.display = std::move(display); + } + } else { + std::optional built = + BuildDisplayProgram(doc_, font_dict, program.postscript_name, + CPDF_FontFace{family, weight, italic}); + if (built.has_value()) { + Display display; + display.sha256 = CryptSha256Generate(built->bytes); + display.coverage = RangesOf(std::move(built->characters)); + display.bytes = std::move(built->bytes); + inspected.display = std::move(display); + } } } return inspected.display.has_value() ? inspected.display->sha256 @@ -438,7 +462,7 @@ DataVector CPDF_DocumentFonts::DisplayProgramData(uint32_t objnum) { if (inspected->display.has_value()) { return inspected->display->bytes; } - if (IsSfnt(inspected->facts.format) && inspected->facts.maps_unicode) { + if (LoadsAsIs(inspected->facts)) { return ProgramData(objnum); } return {}; @@ -450,11 +474,11 @@ std::vector CPDF_DocumentFonts::ProgramCoverage( if (!inspected) { return {}; } - if (inspected->display.has_value()) { - return inspected->display->coverage; - } + // A program that maps Unicode is drawn through its own cmap, its tables + // realigned or not; any other through its display program's. if (!inspected->facts.maps_unicode) { - return {}; + return inspected->display.has_value() ? inspected->display->coverage + : std::vector(); } if (inspected->coverage.has_value()) { return *inspected->coverage; diff --git a/core/fpdfdoc/cpdf_documentfonts.h b/core/fpdfdoc/cpdf_documentfonts.h index 28e91c56e0..f019ffc71c 100644 --- a/core/fpdfdoc/cpdf_documentfonts.h +++ b/core/fpdfdoc/cpdf_documentfonts.h @@ -54,11 +54,16 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { // The program loads, has glyphs and maps characters through a Unicode // cmap: the layout can write any character it maps. bool maps_unicode = false; + // An sfnt whose tables each start on a four-byte boundary, as the font + // format puts them. FreeType reads one whose tables don't; a browser + // refuses it. + bool tables_aligned = false; // SHA-256 of the decoded program; empty without one. DataVector sha256; // SHA-256 of what a browser loads for it: the program itself when it is - // an sfnt that maps characters through a Unicode cmap, else its display - // program (cpdf_displayprogram.h). Empty when a browser can't draw it. + // an sfnt that maps characters through a Unicode cmap, with its tables + // realigned when they aren't (cfx_sfnt.h); else its display program + // (cpdf_displayprogram.h). Empty when a browser can't draw it. DataVector display_sha256; // Line metrics as the layout reads them (em, descent positive); 0 when // the program doesn't load. @@ -130,6 +135,7 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { void ClearMissingCharacters() { missing_characters_.clear(); } private: + // What a browser loads in place of a program it can't load as it is. struct Display { DataVector bytes; DataVector sha256; @@ -149,6 +155,8 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { // The facts about |stream|, inspected once per version. Program Inspect(RetainPtr stream, Format format); Inspected* FindInspected(uint32_t objnum); + // The decoded bytes of the stream |inspected| is. + DataVector BytesOf(const Inspected& inspected); // |program|'s display hash, its display program built through // |font_dict| once per stream version when it needs one. DataVector DisplayHashOf(const Program& program, diff --git a/core/fxge/BUILD.gn b/core/fxge/BUILD.gn index 2db9a7a1af..456423f545 100644 --- a/core/fxge/BUILD.gn +++ b/core/fxge/BUILD.gn @@ -69,6 +69,10 @@ source_set("fxge") { "cfx_path.h", "cfx_renderdevice.cpp", "cfx_renderdevice.h", + # EmbedPDF(fonts): the sfnt container: writing one, and its tables + # realigned for a browser. + "cfx_sfnt.cpp", + "cfx_sfnt.h", "cfx_standardfont.cpp", "cfx_standardfont.h", "cfx_substfont.cpp", diff --git a/core/fxge/cfx_opentypewriter.cpp b/core/fxge/cfx_opentypewriter.cpp index ed578b3894..c4ebf36eb3 100644 --- a/core/fxge/cfx_opentypewriter.cpp +++ b/core/fxge/cfx_opentypewriter.cpp @@ -11,6 +11,7 @@ #include #include "core/fxcrt/numerics/safe_conversions.h" +#include "core/fxge/cfx_sfnt.h" CFX_OpenTypeFont::CFX_OpenTypeFont() = default; CFX_OpenTypeFont::CFX_OpenTypeFont(CFX_OpenTypeFont&&) noexcept = default; @@ -443,56 +444,6 @@ DataVector NameTable( return table.Take(); } -uint32_t Checksum(const DataVector& bytes) { - uint32_t sum = 0; - for (size_t i = 0; i < bytes.size(); i += 4) { - uint32_t word = 0; - for (size_t j = 0; j < 4; ++j) { - word = (word << 8) | (i + j < bytes.size() ? bytes[i + j] : 0); - } - sum += word; - } - return sum; -} - -// An sfnt from its tables, in tag order: the table directory, each table -// padded to four bytes, then head's checksum adjustment. -DataVector Sfnt( - uint32_t version, - const std::map>& tables) { - const size_t count = tables.size(); - size_t entry_selector = 0; - while ((size_t{2} << entry_selector) <= count) { - ++entry_selector; - } - const size_t search_range = (size_t{1} << entry_selector) * 16; - Writer font; - font.U32(version) - .U16(count) - .U16(search_range) - .U16(entry_selector) - .U16(count * 16 - search_range); - size_t offset = 12 + count * 16; - size_t head_offset = 0; - for (const auto& [tag, table] : tables) { - if (tag == "head") { - head_offset = offset; - } - font.Tag(tag.c_str()).U32(Checksum(table)).U32(offset).U32(table.size()); - offset += (table.size() + 3) / 4 * 4; - } - for (const auto& [tag, table] : tables) { - font.Bytes(table).Zeros((4 - table.size() % 4) % 4); - } - DataVector bytes = font.Take(); - const uint32_t adjustment = 0xb1b0afba - Checksum(bytes); - for (int i = 0; i < 4; ++i) { - bytes[head_offset + 8 + i] = - static_cast(adjustment >> (24 - 8 * i)); - } - return bytes; -} - ByteString StyleName(const CFX_OpenTypeFont& font) { const bool bold = font.weight >= 600; if (bold && font.italic) { @@ -657,5 +608,5 @@ DataVector WriteOpenTypeCFF(const CFX_OpenTypeFont& font) { {4, full_name}, {6, font.postscript_name}}); tables["cmap"] = CmapTable(font.cmap); - return Sfnt(0x4f54544f, tables); // 'OTTO' + return WriteSfnt(0x4f54544f, tables); // 'OTTO' } diff --git a/core/fxge/cfx_sfnt.cpp b/core/fxge/cfx_sfnt.cpp new file mode 100644 index 0000000000..77ab8fa9f1 --- /dev/null +++ b/core/fxge/cfx_sfnt.cpp @@ -0,0 +1,185 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fxge/cfx_sfnt.h" + +#include +#include + +#include "core/fxcrt/byteorder.h" + +namespace { + +// The versions a TrueType or an OpenType font starts with: 1.0, Apple's +// 'true', and 'OTTO' for CFF outlines. +constexpr uint32_t kTrueTypeVersion = 0x00010000; +constexpr uint32_t kAppleTrueTypeVersion = 0x74727565; // 'true' +constexpr uint32_t kOpenTypeCffVersion = 0x4f54544f; // 'OTTO' + +// What head's checkSumAdjustment makes the whole font's checksum. +constexpr uint32_t kFontChecksum = 0xb1b0afba; +// Where head holds checkSumAdjustment. +constexpr size_t kChecksumAdjustmentOffset = 8; + +constexpr size_t kHeaderSize = 12; +constexpr size_t kTableRecordSize = 16; + +struct TableRecord { + ByteString tag; + size_t offset = 0; + size_t length = 0; +}; + +struct Directory { + uint32_t version = 0; + std::vector tables; +}; + +uint32_t U32At(pdfium::span bytes, size_t offset) { + return fxcrt::GetUInt32MSBFirst(bytes.subspan(offset).first<4u>()); +} + +// |bytes|' table directory; nullopt when it isn't an sfnt or names a table +// that doesn't lie inside it. +std::optional ReadDirectory(pdfium::span bytes) { + if (bytes.size() < kHeaderSize) { + return std::nullopt; + } + Directory directory; + directory.version = U32At(bytes, 0); + if (directory.version != kTrueTypeVersion && + directory.version != kAppleTrueTypeVersion && + directory.version != kOpenTypeCffVersion) { + return std::nullopt; + } + const size_t count = + fxcrt::GetUInt16MSBFirst(bytes.subspan<4u>().first<2u>()); + if (count == 0 || bytes.size() < kHeaderSize + count * kTableRecordSize) { + return std::nullopt; + } + for (size_t i = 0; i < count; ++i) { + const size_t record = kHeaderSize + i * kTableRecordSize; + TableRecord table; + table.tag = ByteString(ByteStringView(bytes.subspan(record, 4u))); + table.offset = U32At(bytes, record + 8); + table.length = U32At(bytes, record + 12); + if (table.offset > bytes.size() || + table.length > bytes.size() - table.offset) { + return std::nullopt; + } + directory.tables.push_back(std::move(table)); + } + return directory; +} + +// Each writes the low bytes of |value|, big-endian: the caller has checked it +// fits. +void PutU16(DataVector* out, uint64_t value) { + out->push_back(static_cast(value >> 8)); + out->push_back(static_cast(value)); +} + +void PutU32(DataVector* out, uint64_t value) { + PutU16(out, value >> 16); + PutU16(out, value); +} + +uint32_t Checksum(pdfium::span bytes) { + uint32_t sum = 0; + for (size_t i = 0; i < bytes.size(); i += 4) { + uint32_t word = 0; + for (size_t j = 0; j < 4; ++j) { + word = (word << 8) | (i + j < bytes.size() ? bytes[i + j] : 0); + } + sum += word; + } + return sum; +} + +} // namespace + +DataVector WriteSfnt( + uint32_t version, + const std::map>& tables) { + const size_t count = tables.size(); + size_t entry_selector = 0; + while ((size_t{2} << entry_selector) <= count) { + ++entry_selector; + } + const size_t search_range = (size_t{1} << entry_selector) * 16; + DataVector font; + PutU32(&font, version); + PutU16(&font, count); + PutU16(&font, search_range); + PutU16(&font, entry_selector); + PutU16(&font, count * 16 - search_range); + + // head's checksum, and the font's, are taken with checkSumAdjustment 0. + std::map> written = tables; + auto head = written.find("head"); + const bool adjusts = head != written.end() && + head->second.size() >= kChecksumAdjustmentOffset + 4; + if (adjusts) { + fxcrt::PutUInt32MSBFirst(0, pdfium::span(head->second) + .subspan(kChecksumAdjustmentOffset) + .first<4u>()); + } + size_t offset = kHeaderSize + count * kTableRecordSize; + size_t head_offset = 0; + for (const auto& [tag, table] : written) { + if (tag == "head") { + head_offset = offset; + } + for (char c : tag) { + font.push_back(static_cast(c)); + } + PutU32(&font, Checksum(table)); + PutU32(&font, offset); + PutU32(&font, table.size()); + offset += (table.size() + 3) / 4 * 4; + } + for (const auto& [tag, table] : written) { + font.insert(font.end(), table.begin(), table.end()); + font.insert(font.end(), (4 - table.size() % 4) % 4, 0); + } + if (adjusts) { + fxcrt::PutUInt32MSBFirst( + kFontChecksum - Checksum(font), + pdfium::span(font) + .subspan(head_offset + kChecksumAdjustmentOffset) + .first<4u>()); + } + return font; +} + +bool SfntTablesAligned(pdfium::span bytes) { + std::optional directory = ReadDirectory(bytes); + if (!directory.has_value()) { + return false; + } + for (const TableRecord& table : directory->tables) { + if (table.offset % 4 != 0) { + return false; + } + } + return true; +} + +DataVector AlignSfntTables(pdfium::span bytes) { + std::optional directory = ReadDirectory(bytes); + if (!directory.has_value()) { + return {}; + } + std::map> tables; + for (const TableRecord& table : directory->tables) { + pdfium::span data = + bytes.subspan(table.offset, table.length); + // A tag named twice: which table is meant can't be told. + if (!tables + .emplace(table.tag, DataVector(data.begin(), data.end())) + .second) { + return {}; + } + } + return WriteSfnt(directory->version, tables); +} diff --git a/core/fxge/cfx_sfnt.h b/core/fxge/cfx_sfnt.h new file mode 100644 index 0000000000..cec1559e4b --- /dev/null +++ b/core/fxge/cfx_sfnt.h @@ -0,0 +1,36 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 +// +// EmbedPDF(fonts): the container TrueType and OpenType fonts share, an sfnt: +// a table directory, then the tables it names. The font format starts each +// table on a four-byte boundary. FreeType reads a font whose tables don't; a +// browser's font sanitizer refuses it whole. + +#ifndef CORE_FXGE_CFX_SFNT_H_ +#define CORE_FXGE_CFX_SFNT_H_ + +#include + +#include + +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/data_vector.h" +#include "core/fxcrt/span.h" + +// An sfnt of |tables|, by tag: the table directory in tag order with each +// table's checksum, each table padded to four bytes, and head's +// checkSumAdjustment set for the whole font (whatever head held there). +DataVector WriteSfnt( + uint32_t version, + const std::map>& tables); + +// Whether |bytes| is an sfnt whose tables each start on a four-byte +// boundary; false for anything else. +bool SfntTablesAligned(pdfium::span bytes); + +// |bytes|' tables written again (WriteSfnt): the same tables, each on a +// four-byte boundary. Empty when |bytes| isn't an sfnt whose tables lie +// inside it. +DataVector AlignSfntTables(pdfium::span bytes); + +#endif // CORE_FXGE_CFX_SFNT_H_ diff --git a/fpdfsdk/epdf_font_embeddertest.cpp b/fpdfsdk/epdf_font_embeddertest.cpp index 13c70ef8be..bf5ae7fbc4 100644 --- a/fpdfsdk/epdf_font_embeddertest.cpp +++ b/fpdfsdk/epdf_font_embeddertest.cpp @@ -5,6 +5,8 @@ #include +#include +#include #include #include #include @@ -23,6 +25,7 @@ #include "core/fxge/cfx_face.h" #include "core/fxge/cfx_font.h" #include "core/fxge/cfx_glyphoutline.h" +#include "core/fxge/cfx_sfnt.h" #include "core/fxge/fx_fontencoding.h" #include "fpdfsdk/cpdfsdk_helpers.h" #include "fpdfsdk/epdf_edit_test_util.h" @@ -59,6 +62,53 @@ std::vector LoadRoboto() { return LoadThirdPartyFont("harfbuzz/src/perf/fonts/Roboto-Regular.ttf"); } +// |font| with every table one byte further on, as a font whose tables were +// written back to back is: FreeType reads it, a browser refuses it. +std::vector WithTablesOffByOne(const std::vector& font) { + const size_t count = (size_t{font[4]} << 8) | font[5]; + const size_t tables_start = 12 + count * 16; + std::vector moved(font.begin(), font.begin() + tables_start); + for (size_t i = 0; i < count; ++i) { + const size_t offset_at = 12 + i * 16 + 8; + uint32_t offset = 0; + for (size_t j = 0; j < 4; ++j) { + offset = (offset << 8) | moved[offset_at + j]; + } + ++offset; + for (size_t j = 0; j < 4; ++j) { + moved[offset_at + j] = static_cast(offset >> (24 - 8 * j)); + } + } + moved.push_back(0); + moved.insert(moved.end(), font.begin() + tables_start, font.end()); + return moved; +} + +// The tables of an sfnt by tag, as written. +std::map> TablesOf( + const std::vector& font) { + auto u32 = [&](size_t at) { + return (uint32_t{font[at]} << 24) | (uint32_t{font[at + 1]} << 16) | + (uint32_t{font[at + 2]} << 8) | font[at + 3]; + }; + std::map> tables; + const size_t count = (size_t{font[4]} << 8) | font[5]; + for (size_t i = 0; i < count; ++i) { + const size_t record = 12 + i * 16; + const std::string tag(font.begin() + record, font.begin() + record + 4); + const uint32_t offset = u32(record + 8); + const uint32_t length = u32(record + 12); + std::vector table(font.begin() + offset, + font.begin() + offset + length); + // checkSumAdjustment covers the whole font: it moves with the layout. + if (tag == "head") { + std::fill(table.begin() + 8, table.begin() + 12, 0); + } + tables[tag] = std::move(table); + } + return tables; +} + std::vector LoadDroidSansFallback() { std::string font_path = PathService::GetTestFilePath("fonts/DroidSansFallbackFull.ttf"); @@ -431,6 +481,91 @@ TEST_F(EPDFFontEmbedderTest, ListsADocumentProgramAndHandsOutItsBytes) { EXPECT_EQ(0u, EPDFDoc_GetFontProgramData(document(), 1, nullptr, 0)); } +// A program whose tables don't start on four-byte boundaries goes to a +// browser realigned: the same tables, where the font format puts them, under +// a hash of their own. The engine writes with the program as it is. +TEST_F(EPDFFontEmbedderTest, RealignsAProgramABrowserWouldRefuse) { + ScopedRegisteredFonts scoped_fonts; + std::vector roboto = LoadRoboto(); + ASSERT_FALSE(roboto.empty()); + ASSERT_TRUE(SfntTablesAligned(roboto)); + const std::vector misaligned = WithTablesOffByOne(roboto); + ASSERT_FALSE(SfntTablesAligned(misaligned)); + const EPDF_FONT_ID font_id = + EPDFFont_RegisterMemFont64("Roboto", /*weight=*/400, /*italic=*/0, + misaligned.data(), misaligned.size()); + ASSERT_NE(0u, font_id); + + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ASSERT_TRUE(page); + AddRegisteredFontField(page.get(), font_id, L"name", 320.0f, L"hello"); + + EPDF_FONT_FACES faces = EPDFFontFaces_Load(document()); + ASSERT_TRUE(faces); + std::optional found = FindProgram(faces, L"Roboto"); + ASSERT_TRUE(found.has_value()); + const unsigned long objnum = + EPDFFontFaces_GetProgramObjNum(faces, found->face, found->program); + ASSERT_NE(0u, objnum); + EXPECT_TRUE( + EPDFFontFaces_ProgramMapsUnicode(faces, found->face, found->program)); + unsigned char hash[32] = {}; + unsigned char display_hash[32] = {}; + ASSERT_TRUE( + EPDFFontFaces_GetProgramHash(faces, found->face, found->program, hash)); + ASSERT_TRUE(EPDFFontFaces_GetProgramDisplayHash( + faces, found->face, found->program, display_hash)); + EPDFFontFaces_Close(faces); + + // The document holds the program as it was registered. + std::vector program(misaligned.size()); + ASSERT_EQ(misaligned.size(), + EPDFDoc_GetFontProgramData(document(), objnum, program.data(), + program.size())); + EXPECT_EQ(misaligned, program); + + const unsigned long length = + EPDFDoc_GetFontDisplayProgramData(document(), objnum, nullptr, 0); + ASSERT_GT(length, 0u); + std::vector display(length); + ASSERT_EQ(length, EPDFDoc_GetFontDisplayProgramData( + document(), objnum, display.data(), display.size())); + EXPECT_TRUE(SfntTablesAligned(display)); + EXPECT_EQ(TablesOf(misaligned), TablesOf(display)); + // Named by its own bytes, not the program's. + const DataVector display_sha256 = CryptSha256Generate(display); + EXPECT_EQ(std::vector(display_sha256.begin(), display_sha256.end()), + std::vector(display_hash, display_hash + 32)); + EXPECT_NE(std::vector(hash, hash + 32), + std::vector(display_hash, display_hash + 32)); + // The whole font, padded to whole words, sums to the constant head's + // adjustment makes it. + ASSERT_EQ(0u, display.size() % 4); + uint32_t sum = 0; + for (size_t i = 0; i < display.size(); i += 4) { + sum += (uint32_t{display[i]} << 24) | (uint32_t{display[i + 1]} << 16) | + (uint32_t{display[i + 2]} << 8) | display[i + 3]; + } + EXPECT_EQ(0xb1b0afbau, sum); + // A browser draws the characters the program's own cmap maps. + const unsigned long range_count = + EPDFDoc_GetFontProgramCoverage(document(), objnum, nullptr, 0); + std::vector ranges(range_count * 2); + EPDFDoc_GetFontProgramCoverage(document(), objnum, ranges.data(), + range_count); + auto covers = [&](uint32_t scalar) { + for (size_t i = 0; i + 1 < ranges.size(); i += 2) { + if (scalar >= ranges[i] && scalar < ranges[i] + ranges[i + 1]) { + return true; + } + } + return false; + }; + EXPECT_TRUE(covers('A')); + EXPECT_TRUE(covers(0xe9)); +} + // A widget writes with a document program by its object number: /DA names a // session alias, the appearance embeds that very program, characters it // lacks are reported, and the box's font reads back as that program. diff --git a/public/epdf_font.h b/public/epdf_font.h index 42862d8c53..63bd1f8170 100644 --- a/public/epdf_font.h +++ b/public/epdf_font.h @@ -324,10 +324,10 @@ EPDFFontFaces_GetProgramHash(EPDF_FONT_FACES faces, unsigned char* sha256); // The SHA-256 of what a browser loads for the program into |sha256| (32 // bytes): the program itself when it is TrueType or OpenType with a Unicode -// cmap, else the display program built for it -// (EPDFDoc_GetFontDisplayProgramData()). Returns false when a browser can't -// draw it (Type 3, nothing embedded, a program that doesn't load) or for an -// index out of range. +// cmap and its tables where the font format puts them, else the display +// program built for it (EPDFDoc_GetFontDisplayProgramData()). Returns false +// when a browser can't draw it (Type 3, nothing embedded, a program that +// doesn't load) or for an index out of range. FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFFontFaces_GetProgramDisplayHash(EPDF_FONT_FACES faces, int face, @@ -355,7 +355,9 @@ EPDFDoc_GetFontProgramData(FPDF_DOCUMENT document, // Experimental EmbedPDF Extension API. // What a browser loads for such a program: the program itself when it is -// TrueType or OpenType with a Unicode cmap. Otherwise (Type 1, bare CFF, a +// TrueType or OpenType with a Unicode cmap. When such a program's tables +// don't each start on a four-byte boundary (a browser refuses it, FreeType +// doesn't), the same tables realigned. Otherwise (Type 1, bare CFF, a // program without a Unicode cmap) an OpenType font built from it: its // glyphs, a Unicode cmap holding exactly the characters the engine writes // with the font (each through the PDF's encoding to the glyph it draws), and From bbab36d8195e260de617f61ce38d07508eac345d Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Tue, 20 Oct 2026 16:13:05 +0300 Subject: [PATCH 09/16] Write a simple field font's new text through its program's cmap A /DA font over a program that can author text (an sfnt with a Unicode cmap whose licence allows editing) lays new text out with that program's own cmap, as composite /DA fonts already did and as Acrobat does: a simple font's 8-bit encoding no longer sends characters its program has (Hebrew in a Mac Roman Adobe Hebrew) to a fallback. --- core/fpdfdoc/cpdf_annotfontmap.cpp | 18 +++++--------- core/fpdfdoc/cpdf_annotfontmap.h | 5 ---- fpdfsdk/epdf_font_embeddertest.cpp | 40 +++++++++++++++++++++++++----- 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/core/fpdfdoc/cpdf_annotfontmap.cpp b/core/fpdfdoc/cpdf_annotfontmap.cpp index d74acb139e..1094153423 100644 --- a/core/fpdfdoc/cpdf_annotfontmap.cpp +++ b/core/fpdfdoc/cpdf_annotfontmap.cpp @@ -383,12 +383,13 @@ CPDF_AnnotFontMap::CPDF_AnnotFontMap( ProgramLineMetrics(entry.font->GetFont(), &entry.ascent, &entry.descent); } } - // A composite /DA font over a program that can author text (what the - // writer installs for a document program) lays new text out with that - // program's own cmap: its ToUnicode only names the characters drawn so far. + // A /DA font over a program that can author text lays new text out with + // that program's own cmap, as Acrobat does: a composite font's ToUnicode + // only names the characters drawn so far (what the writer installs for a + // document program), and a simple font's encoding reaches 256 codes at + // most (no Hebrew in a Mac Roman font whose program has it). std::optional candidate; - if (entry.source == Source::kDocumentFontDict && entry.font && - entry.font->GetFontDict()->GetNameFor("Subtype") == "Type0") { + if (entry.source == Source::kDocumentFontDict && entry.font) { candidate = CandidateFromFontDict(entry.font->GetFontDict().Get()); } fonts_.push_back(std::move(entry)); @@ -490,13 +491,6 @@ ByteString CPDF_AnnotFontMap::AllocateAppearanceAlias( return candidate; } -// static -bool CPDF_AnnotFontMap::IsDocumentProgramFont( - const CPDF_Dictionary* font_dict) { - return font_dict && font_dict->GetNameFor("Subtype") == "Type0" && - CandidateFromFontDict(font_dict).has_value(); -} - bool CPDF_AnnotFontMap::HasDefaultFont() const { return !fonts_.empty() && fonts_.front().font; } diff --git a/core/fpdfdoc/cpdf_annotfontmap.h b/core/fpdfdoc/cpdf_annotfontmap.h index 876a9b148c..2f19af8e32 100644 --- a/core/fpdfdoc/cpdf_annotfontmap.h +++ b/core/fpdfdoc/cpdf_annotfontmap.h @@ -73,11 +73,6 @@ class CPDF_AnnotFontMap final { const CPDF_Document* doc, const ByteString& alias); - // Whether a /DA font is a composite font over a program the layout can - // write any character of: what the writer installs for a document - // program, which the map then lays out with that program's own cmap. - static bool IsDocumentProgramFont(const CPDF_Dictionary* font_dict); - bool HasDefaultFont() const; // The /DA font is one of the standard 14 (a non-embedded base-14 Type1): // its metrics are PDFium's, not Acrobat's, so the rich layout resolves a diff --git a/fpdfsdk/epdf_font_embeddertest.cpp b/fpdfsdk/epdf_font_embeddertest.cpp index bf5ae7fbc4..90321a75e9 100644 --- a/fpdfsdk/epdf_font_embeddertest.cpp +++ b/fpdfsdk/epdf_font_embeddertest.cpp @@ -903,13 +903,15 @@ std::vector LoadTestFont(const char* path) { return GetFileContents(font_path.c_str()); } -// A form whose field writes with a simple font over |program| (embedded -// under |font_file|, with |file_subtype| as the stream's /Subtype when it has -// one), WinAnsi-encoded, every character 600 wide but "D" 700. +// A form whose field writes with a simple font (a |font_subtype| font) over +// |program| (embedded under |font_file|, with |file_subtype| as the stream's +// /Subtype when it has one), WinAnsi-encoded, every character 600 wide but +// "D" 700. std::string FormWithFieldFont(const std::vector& program, const char* font_file, const char* file_subtype, - const char* base_font) { + const char* base_font, + const char* font_subtype = "Type1") { std::string widths; for (int code = 32; code <= 126; ++code) { widths += code == 'D' ? "700 " : "600 "; @@ -930,8 +932,8 @@ std::string FormWithFieldFont(const std::vector& program, file, "<< /Type /Annot /Subtype /Widget /FT /Tx /T (kept) /P 3 0 R /Rect [20 " "200 280 230] /DA (/FKept 12 Tf 0 g) /V (AB) >>", - std::string("<< /Type /Font /Subtype /Type1 /BaseFont /") + base_font + - " /FirstChar 32 /LastChar 126 /Widths [" + widths + + std::string("<< /Type /Font /Subtype /") + font_subtype + " /BaseFont /" + + base_font + " /FirstChar 32 /LastChar 126 /Widths [" + widths + "] /Encoding /WinAnsiEncoding /FontDescriptor 7 0 R >>", std::string("<< /Type /FontDescriptor /FontName /") + base_font + " /Flags 32 /FontBBox [0 -250 1000 900] /ItalicAngle 0 /Ascent 900 " @@ -1103,6 +1105,32 @@ void ExpectDisplayProgramDrawsTheKeptFont(const std::string& pdf, } // namespace +// A field whose /DA font is a simple font over a program that can author +// text writes new text with that program, through its own cmap: the font's +// WinAnsi encoding can't reach "Ω", the program has it. +TEST_F(EPDFFontEmbedderTest, SimpleFieldFontWritesThroughItsProgram) { + std::vector roboto = LoadRoboto(); + ASSERT_FALSE(roboto.empty()); + const std::string pdf = FormWithFieldFont(roboto, "FontFile2", nullptr, + "Roboto-Regular", "TrueType"); + ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(annot); + const uint32_t field = EPDFAnnot_GetObjectNumber(annot.get()); + ASSERT_EQ(5u, field); + + EPDFDoc_ClearMissingCharacters(doc.get()); + ASSERT_TRUE(embedpdf_test::FillTextField(doc.get(), field, L"A\u03A9B")); + ASSERT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot.get())); + EXPECT_EQ(0, EPDFDoc_CountMissingCharacters(doc.get())); + // The appearance draws with the field's own program, the one /DR embeds. + EXPECT_EQ(4u, AppearanceFontProgram(annot.get(), + DefaultAppearanceFontName(annot.get()))); +} + // A form's field font that is bare CFF, which a browser can't load, gets a // display program it can. TEST_F(EPDFFontEmbedderTest, BuildsADisplayProgramForABareCffFont) { From 10b46b07a5707e46c8efea6cefe796c663674bfa Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Wed, 21 Oct 2026 11:23:00 +0300 Subject: [PATCH 10/16] Mark new field appearances as form XObject --- .../cpdf_appearancegenerator_forms.cpp | 4 +++ fpdfsdk/epdf_form_embeddertest.cpp | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/core/fpdfdoc/cpdf_appearancegenerator_forms.cpp b/core/fpdfdoc/cpdf_appearancegenerator_forms.cpp index 692668f0c2..f7e051cbd0 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_forms.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_forms.cpp @@ -822,6 +822,10 @@ bool GenerateFormAPToTarget(APGenerationTarget* target, } normal_stream->SetDataFromStringstreamAndRemoveFilter(&app_stream); RetainPtr stream_dict = normal_stream->GetMutableDict(); + // A form XObject, said so: a stream made here has no type yet, and + // readers look for /Subtype /Form. + stream_dict->SetNewFor("Type", "XObject"); + stream_dict->SetNewFor("Subtype", "Form"); stream_dict->SetMatrixFor("Matrix", dims.matrix); stream_dict->SetRectFor("BBox", dims.bbox); stream_dict->GetOrCreateDictFor("Resources") diff --git a/fpdfsdk/epdf_form_embeddertest.cpp b/fpdfsdk/epdf_form_embeddertest.cpp index 7de04007c5..177a7a53c2 100644 --- a/fpdfsdk/epdf_form_embeddertest.cpp +++ b/fpdfsdk/epdf_form_embeddertest.cpp @@ -2167,6 +2167,37 @@ TEST_F(EPDFFormEmbedderTest, CalculationOrderSetReplaceAndRemove) { // A push button: created, given a widget and a caption, and drawn with it. // Its value can't be written. +// A field's appearance written where there was none is a form XObject that +// says so: readers look for /Subtype /Form. +TEST_F(EPDFFormEmbedderTest, NewFieldAppearanceIsAFormXObject) { + ASSERT_TRUE(OpenDocument("hello_world.pdf")); + FPDF_PAGE page = LoadPage(0); + ASSERT_TRUE(page); + const uint32_t field = + EPDFForm_CreateField(document(), EPDF_FORMFIELD_FAMILY_TEXT, + GetFPDFWideString(L"name").get(), 0); + ASSERT_GT(field, 0u); + const uint32_t widget = CreateWidgetAnnot(page, 20, 200, 180, 230); + ASSERT_GT(widget, 0u); + ASSERT_TRUE(EPDFForm_AttachWidget(document(), field, widget, nullptr, 0)); + ASSERT_TRUE(SetOneValue(document(), field, L"Hello")); + unsigned long changed_count = 0; + ASSERT_TRUE(EPDFForm_RedrawFieldWidgets(document(), field, nullptr, 0, + &changed_count)); + + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document()); + RetainPtr annot = + ToDictionary(doc->GetIndirectObject(widget)); + ASSERT_TRUE(annot); + RetainPtr ap = annot->GetDictFor("AP"); + ASSERT_TRUE(ap); + RetainPtr normal = ap->GetStreamFor("N"); + ASSERT_TRUE(normal); + EXPECT_EQ("XObject", normal->GetDict()->GetNameFor("Type")); + EXPECT_EQ("Form", normal->GetDict()->GetNameFor("Subtype")); + UnloadPage(page); +} + TEST_F(EPDFFormEmbedderTest, PushButtonIsAuthorableAndShowsItsCaption) { ASSERT_TRUE(OpenDocument("hello_world.pdf")); FPDF_PAGE page = LoadPage(0); From 88d0ece8b93d128a90550baf4715fb7cf25f5dae Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Wed, 21 Oct 2026 20:35:01 +0300 Subject: [PATCH 11/16] Cache font walks and expose fingerprints Add an API to fingerprint document fonts and cache per-place font walks for layered documents. Invalidate affected walks when transactions end, and cover incremental updates, aborts, and plain documents with tests. --- core/fpdfapi/parser/cpdf_document.h | 6 + core/fpdfapi/parser/cpdf_layer_document.cpp | 7 + core/fpdfapi/parser/cpdf_layer_document.h | 5 + core/fpdfapi/parser/cpdf_layer_transaction.h | 4 + core/fpdfdoc/cpdf_documentfonts.cpp | 275 +++++++++++++++---- core/fpdfdoc/cpdf_documentfonts.h | 44 +++ fpdfsdk/epdf_c_api_test.c | 1 + fpdfsdk/epdf_font.cpp | 10 + fpdfsdk/epdf_font_embeddertest.cpp | 159 +++++++++++ public/epdf_font.h | 10 + 10 files changed, 470 insertions(+), 51 deletions(-) diff --git a/core/fpdfapi/parser/cpdf_document.h b/core/fpdfapi/parser/cpdf_document.h index 9e39ee4f52..a228e9d73b 100644 --- a/core/fpdfapi/parser/cpdf_document.h +++ b/core/fpdfapi/parser/cpdf_document.h @@ -145,6 +145,12 @@ class CPDF_Document : public Observable, class FontIndex { public: virtual ~FontIndex() = default; + // A layer's transaction ended, committed or aborted: |objnums| are every + // object number it wrote, created or hid, and the overlay epoch was + // |epoch_at_begin| when it began and is |epoch| now. + virtual void TransactionEnded(const std::set& objnums, + uint64_t epoch_at_begin, + uint64_t epoch) {} }; FontIndex* font_index() const { return font_index_.get(); } void SetFontIndex(std::unique_ptr index) { diff --git a/core/fpdfapi/parser/cpdf_layer_document.cpp b/core/fpdfapi/parser/cpdf_layer_document.cpp index 255eaf8f6c..e9d4f5710c 100644 --- a/core/fpdfapi/parser/cpdf_layer_document.cpp +++ b/core/fpdfapi/parser/cpdf_layer_document.cpp @@ -504,6 +504,7 @@ bool CPDF_LayerDocument::BeginTransaction() { CPDF_WriteGeneration::SetCurrent(transaction_->generation); transaction_->root_before = GetCachedRootDict(); transaction_->info_before = GetCachedInfoDict(); + transaction_->epoch_at_begin = overlay_epoch_; return true; } @@ -541,6 +542,9 @@ bool CPDF_LayerDocument::CommitTransaction() { } last_transaction_stats_ = tx->stats; ++overlay_epoch_; + if (FontIndex* index = font_index()) { + index->TransactionEnded(tx->touched, tx->epoch_at_begin, overlay_epoch_); + } return true; } @@ -569,6 +573,9 @@ bool CPDF_LayerDocument::AbortTransaction() { ForgetDerivedDataOf(unreachable); last_transaction_stats_ = tx->stats; ++overlay_epoch_; + if (FontIndex* index = font_index()) { + index->TransactionEnded(tx->touched, tx->epoch_at_begin, overlay_epoch_); + } // tx->written and tx->hidden are dropped with tx. Nothing is undone. return true; } diff --git a/core/fpdfapi/parser/cpdf_layer_document.h b/core/fpdfapi/parser/cpdf_layer_document.h index d58e6a86ac..99a8056629 100644 --- a/core/fpdfapi/parser/cpdf_layer_document.h +++ b/core/fpdfapi/parser/cpdf_layer_document.h @@ -52,6 +52,11 @@ class CPDF_LayerDocument final : public CPDF_Document { bool CommitTransaction(); bool AbortTransaction(); bool InTransaction() const { return !!transaction_; } + // EmbedPDF(fonts): the open transaction, for a derived cache that reads + // mid-write (what it has touched so far); null outside one. + const CPDF_LayerTransaction* open_transaction() const { + return transaction_.get(); + } // What the last ended (or the open) transaction cost. const CPDF_LayerTransactionStats& GetTransactionStats() const { return transaction_ ? transaction_->stats : last_transaction_stats_; diff --git a/core/fpdfapi/parser/cpdf_layer_transaction.h b/core/fpdfapi/parser/cpdf_layer_transaction.h index f1b1500c16..04e61f32f5 100644 --- a/core/fpdfapi/parser/cpdf_layer_transaction.h +++ b/core/fpdfapi/parser/cpdf_layer_transaction.h @@ -75,6 +75,10 @@ struct CPDF_LayerTransaction { RetainPtr root_before; RetainPtr info_before; + // EmbedPDF(fonts): the overlay epoch at begin, for the font index to tell + // what changed outside any transaction. + uint64_t epoch_at_begin = 0; + CPDF_LayerTransactionStats stats; }; diff --git a/core/fpdfdoc/cpdf_documentfonts.cpp b/core/fpdfdoc/cpdf_documentfonts.cpp index 3cfc32677a..a02a3a9241 100644 --- a/core/fpdfdoc/cpdf_documentfonts.cpp +++ b/core/fpdfdoc/cpdf_documentfonts.cpp @@ -4,6 +4,7 @@ #include "core/fpdfdoc/cpdf_documentfonts.h" #include +#include #include #include #include @@ -12,6 +13,7 @@ #include "core/fpdfapi/page/cpdf_docpagedata.h" #include "core/fpdfapi/parser/cpdf_array.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_layer_document.h" #include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fpdfapi/parser/cpdf_stream_acc.h" #include "core/fpdfdoc/cpdf_displayprogram.h" @@ -28,126 +30,151 @@ namespace { // walked set misses (a direct object repeated) stops here. constexpr int kMaxDepth = 64; -// Every font dictionary the document's drawings can use, each once. +// The font dictionaries the drawings of one place can use, each once: the +// AcroForm's, a page's, or the whole document's. Notes the object number of +// every indirect object it reads, so a change to any of them can be told. class FontWalk { public: explicit FontWalk(CPDF_Document* doc) : doc_(doc) {} - std::vector> Run() { - const CPDF_Dictionary* root = doc_->GetRoot(); + void Form() { + RetainPtr root = + Noted(pdfium::WrapRetain(doc_->GetRoot())); RetainPtr acroform = - root ? root->GetDictFor("AcroForm") : nullptr; + root ? Noted(root->GetDictFor("AcroForm")) : nullptr; if (acroform) { - Resources(acroform->GetDictFor("DR").Get(), 0); - Fields(acroform->GetArrayFor("Fields").Get(), 0); + Resources(acroform->GetDictFor("DR"), 0); + Fields(acroform->GetArrayFor("Fields"), 0); } + } + + void Page(RetainPtr page) { + if (!Noted(page)) { + return; + } + Resources(InheritedResources(page), 0); + RetainPtr annots = Noted(page->GetArrayFor("Annots")); + for (size_t j = 0; annots && j < annots->size(); ++j) { + Appearance(annots->GetDictAt(j), 0); + } + } + + void Document() { + Form(); const int page_count = doc_->GetPageCount(); for (int i = 0; i < page_count; ++i) { - RetainPtr page = doc_->GetPageDictionary(i); - if (!page) { - continue; - } - Resources(InheritedResources(page.Get()).Get(), 0); - RetainPtr annots = page->GetArrayFor("Annots"); - for (size_t j = 0; annots && j < annots->size(); ++j) { - Appearance(annots->GetDictAt(j).Get(), 0); - } + Page(doc_->GetPageDictionary(i)); } + } + + std::vector> TakeFonts() { return std::move(fonts_); } + // Sorted. + std::vector TakeReads() { + return std::vector(reads_.begin(), reads_.end()); + } private: + template + RetainPtr Noted(RetainPtr object) { + if (object && object->GetObjNum() != 0) { + reads_.insert(object->GetObjNum()); + } + return object; + } + // A page's /Resources, or the nearest one up its /Parent chain. - static RetainPtr InheritedResources( - const CPDF_Dictionary* page) { + RetainPtr InheritedResources( + RetainPtr page) { std::set seen; - RetainPtr node(page); + RetainPtr node = std::move(page); for (int i = 0; node && i < kMaxDepth && seen.insert(node.Get()).second; ++i) { if (RetainPtr resources = node->GetDictFor("Resources")) { return resources; } - node = node->GetDictFor("Parent"); + node = Noted(node->GetDictFor("Parent")); } return nullptr; } - void Resources(const CPDF_Dictionary* resources, int depth) { - if (!resources || depth > kMaxDepth || !walked_.insert(resources).second) { + void Resources(RetainPtr resources, int depth) { + if (!Noted(resources) || depth > kMaxDepth || + !walked_.insert(resources.Get()).second) { return; } if (RetainPtr fonts = - resources->GetDictFor("Font")) { + Noted(resources->GetDictFor("Font"))) { CPDF_DictionaryLocker locker(fonts); for (const auto& [key, value] : locker) { Font(value ? ToDictionary(value->GetDirect()) : nullptr, depth); } } if (RetainPtr xobjects = - resources->GetDictFor("XObject")) { + Noted(resources->GetDictFor("XObject"))) { CPDF_DictionaryLocker locker(xobjects); for (const auto& [key, value] : locker) { RetainPtr stream = value ? ToStream(value->GetDirect()) : nullptr; if (stream && stream->GetDict()->GetNameFor("Subtype") == "Form") { - FormStream(stream.Get(), depth); + FormStream(std::move(stream), depth); } } } // Tiling patterns are streams with their own resources; shading // patterns are dictionaries and draw no text. if (RetainPtr patterns = - resources->GetDictFor("Pattern")) { + Noted(resources->GetDictFor("Pattern"))) { CPDF_DictionaryLocker locker(patterns); for (const auto& [key, value] : locker) { - RetainPtr stream = - value ? ToStream(value->GetDirect()) : nullptr; - FormStream(stream.Get(), depth); + FormStream(value ? ToStream(value->GetDirect()) : nullptr, depth); } } if (RetainPtr states = - resources->GetDictFor("ExtGState")) { + Noted(resources->GetDictFor("ExtGState"))) { CPDF_DictionaryLocker locker(states); for (const auto& [key, value] : locker) { RetainPtr state = - value ? ToDictionary(value->GetDirect()) : nullptr; + Noted(value ? ToDictionary(value->GetDirect()) : nullptr); RetainPtr mask = - state ? state->GetDictFor("SMask") : nullptr; - FormStream(mask ? mask->GetStreamFor("G").Get() : nullptr, depth); + state ? Noted(state->GetDictFor("SMask")) : nullptr; + FormStream(mask ? mask->GetStreamFor("G") : nullptr, depth); } } } - void FormStream(const CPDF_Stream* stream, int depth) { - if (!stream || depth > kMaxDepth || !walked_.insert(stream).second) { + void FormStream(RetainPtr stream, int depth) { + if (!Noted(stream) || depth > kMaxDepth || + !walked_.insert(stream.Get()).second) { return; } - Resources(stream->GetDict()->GetDictFor("Resources").Get(), depth + 1); + Resources(stream->GetDict()->GetDictFor("Resources"), depth + 1); } void Font(RetainPtr font, int depth) { - if (!font || !font_set_.insert(font.Get()).second) { + if (!Noted(font) || !font_set_.insert(font.Get()).second) { return; } if (font->GetNameFor("Subtype") == "Type3") { // A Type 3 glyph is a drawing, and may draw text in other fonts. - Resources(font->GetDictFor("Resources").Get(), depth + 1); + Resources(font->GetDictFor("Resources"), depth + 1); } fonts_.push_back(std::move(font)); } // Every state of the normal, rollover and down appearances. - void Appearance(const CPDF_Dictionary* annot, int depth) { + void Appearance(RetainPtr annot, int depth) { RetainPtr ap = - annot ? annot->GetDictFor("AP") : nullptr; + Noted(annot) ? Noted(annot->GetDictFor("AP")) : nullptr; if (!ap) { return; } for (const char* key : {"N", "R", "D"}) { - RetainPtr entry = ap->GetDirectObjectFor(key); + RetainPtr entry = Noted(ap->GetDirectObjectFor(key)); if (const CPDF_Stream* stream = entry ? entry->AsStream() : nullptr) { - FormStream(stream, depth); + FormStream(pdfium::WrapRetain(stream), depth); continue; } const CPDF_Dictionary* states = entry ? entry->AsDictionary() : nullptr; @@ -156,23 +183,23 @@ class FontWalk { } CPDF_DictionaryLocker locker(states); for (const auto& [state, value] : locker) { - RetainPtr stream = - value ? ToStream(value->GetDirect()) : nullptr; - FormStream(stream.Get(), depth); + FormStream(value ? ToStream(value->GetDirect()) : nullptr, depth); } } } // Fields' own /DR, down their /Kids. - void Fields(const CPDF_Array* fields, int depth) { - for (size_t i = 0; fields && depth <= kMaxDepth && i < fields->size(); - ++i) { - RetainPtr field = fields->GetDictAt(i); + void Fields(RetainPtr fields, int depth) { + if (!Noted(fields)) { + return; + } + for (size_t i = 0; depth <= kMaxDepth && i < fields->size(); ++i) { + RetainPtr field = Noted(fields->GetDictAt(i)); if (!field || !walked_.insert(field.Get()).second) { continue; } - Resources(field->GetDictFor("DR").Get(), depth + 1); - Fields(field->GetArrayFor("Kids").Get(), depth + 1); + Resources(field->GetDictFor("DR"), depth + 1); + Fields(field->GetArrayFor("Kids"), depth + 1); } } @@ -180,6 +207,7 @@ class FontWalk { std::set walked_; std::set font_set_; std::vector> fonts_; + std::set reads_; }; // The program stream a font dictionary carries, and what kind it says it is. @@ -269,7 +297,7 @@ CPDF_DocumentFonts* CPDF_DocumentFonts::Find(const CPDF_Document* doc) { std::vector CPDF_DocumentFonts::CollectFaces() { std::vector faces; std::map face_index; // family key|weight|italic - for (const RetainPtr& font : FontWalk(doc_).Run()) { + for (const RetainPtr& font : FontDicts()) { const CPDF_FontFace face = FaceOfFontDict(font.Get()); const ByteString key = ByteString::Format("%s|%d|%d", FontFamilyKey(face.family).c_str(), @@ -309,6 +337,151 @@ std::vector CPDF_DocumentFonts::CollectFaces() { return faces; } +DataVector CPDF_DocumentFonts::FacesFingerprint() { + // Every field fixed-width or length-prefixed: two different lists never + // serialize alike. + DataVector bytes; + auto put = [&](uint32_t value) { + for (int shift = 24; shift >= 0; shift -= 8) { + bytes.push_back(static_cast(value >> shift)); + } + }; + auto put_bytes = [&](pdfium::span data) { + put(static_cast(data.size())); + bytes.insert(bytes.end(), data.begin(), data.end()); + }; + const std::vector faces = CollectFaces(); + put(static_cast(faces.size())); + for (const Face& face : faces) { + put_bytes(face.family.ToUTF8().unsigned_span()); + put(static_cast(face.weight)); + put(face.italic); + put(static_cast(face.programs.size())); + for (const Program& program : face.programs) { + put(program.objnum); + put(static_cast(program.format)); + put(static_cast(program.embedded)); + put_bytes(program.postscript_name.unsigned_span()); + put(program.permission.has_value() + ? 1 + static_cast(*program.permission) + : 0); + put(program.maps_unicode); + put(program.tables_aligned); + put_bytes(program.sha256); + put_bytes(program.display_sha256); + put(std::bit_cast(program.ascent)); + put(std::bit_cast(program.descent)); + } + } + return CryptSha256Generate(bytes); +} + +std::vector> CPDF_DocumentFonts::FontDicts() { + CPDF_LayerDocument* layer = CPDF_LayerDocument::FromDocument(doc_.get()); + if (!layer) { + FontWalk walk(doc_); + walk.Document(); + ++places_walked_; + return walk.TakeFonts(); + } + if (const CPDF_LayerTransaction* write = layer->open_transaction()) { + // Mid-write: what the write touched so far is read as it is now. The + // places walked here are forgotten again when the write ends. + if (write->epoch_at_begin != known_epoch_) { + ForgetPlaces(); + known_epoch_ = write->epoch_at_begin; + } + Forget(write->touched); + } else if (layer->GetOverlayEpoch() != known_epoch_) { + // Something changed outside any transaction this index was told of. + ForgetPlaces(); + known_epoch_ = layer->GetOverlayEpoch(); + } + auto walk_place = [&](auto&& walk_it) { + FontWalk walk(doc_); + walk_it(walk); + ++places_walked_; + return Place{walk.TakeFonts(), walk.TakeReads()}; + }; + std::vector> fonts; + std::set seen; + auto take = [&](const Place& place) { + for (const RetainPtr& font : place.fonts) { + if (seen.insert(font.Get()).second) { + fonts.push_back(font); + } + } + }; + if (!form_place_.has_value()) { + form_place_ = walk_place([](FontWalk& walk) { walk.Form(); }); + } + take(*form_place_); + // Pages a write removed drop out; pages it added are walked. + std::map pages; + const int page_count = doc_->GetPageCount(); + for (int i = 0; i < page_count; ++i) { + RetainPtr page = doc_->GetPageDictionary(i); + if (!page) { + continue; + } + const uint32_t objnum = page->GetObjNum(); + auto kept = page_places_.find(objnum); + if (objnum != 0 && kept != page_places_.end()) { + take(kept->second); + pages.emplace(objnum, std::move(kept->second)); + page_places_.erase(kept); + continue; + } + if (objnum != 0 && pages.contains(objnum)) { + take(pages[objnum]); + continue; + } + Place place = walk_place([&](FontWalk& walk) { walk.Page(page); }); + take(place); + if (objnum != 0) { + pages.emplace(objnum, std::move(place)); + } + } + page_places_ = std::move(pages); + return fonts; +} + +void CPDF_DocumentFonts::TransactionEnded(const std::set& objnums, + uint64_t epoch_at_begin, + uint64_t epoch) { + if (epoch_at_begin != known_epoch_) { + // Something changed before the transaction began that this index wasn't + // told of. + ForgetPlaces(); + } + Forget(objnums); + known_epoch_ = epoch; +} + +void CPDF_DocumentFonts::Forget(const std::set& objnums) { + if (objnums.empty()) { + return; + } + auto reads_any = [&](const Place& place) { + return std::ranges::any_of(objnums, [&](uint32_t objnum) { + return std::ranges::binary_search(place.reads, objnum); + }); + }; + if (form_place_.has_value() && reads_any(*form_place_)) { + form_place_.reset(); + } + std::erase_if(page_places_, + [&](const auto& entry) { return reads_any(entry.second); }); + for (uint32_t objnum : objnums) { + inspected_.erase(objnum); + } +} + +void CPDF_DocumentFonts::ForgetPlaces() { + form_place_.reset(); + page_places_.clear(); +} + CPDF_DocumentFonts::Program CPDF_DocumentFonts::Inspect( RetainPtr stream, Format format) { diff --git a/core/fpdfdoc/cpdf_documentfonts.h b/core/fpdfdoc/cpdf_documentfonts.h index f019ffc71c..9af5458af7 100644 --- a/core/fpdfdoc/cpdf_documentfonts.h +++ b/core/fpdfdoc/cpdf_documentfonts.h @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -94,8 +95,28 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { // glyph resources and every state of every annotation appearance. Each // dictionary is walked once; cycles stop. Programs are inspected once per // stream version. + // + // On a layer, what each place (the AcroForm, each page) reaches is kept + // and walked again only after a transaction touches something it read + // (TransactionEnded), so a list after an edit walks the pages the edit + // changed. A plain document is walked whole every time: nothing says what + // changed. std::vector CollectFaces(); + // SHA-256 of the faces CollectFaces() returns, every fact of each in + // order: equal fingerprints, the same faces. Read before and after a write, + // it says whether the write changed the document's fonts. + DataVector FacesFingerprint(); + + // How many places (the AcroForm, a page, or the whole of a plain + // document) have been walked: what an edit costs, for tests. + size_t places_walked() const { return places_walked_; } + + // CPDF_Document::FontIndex: + void TransactionEnded(const std::set& objnums, + uint64_t epoch_at_begin, + uint64_t epoch) override; + // A program found by the walk, by its stream's object number: its decoded // bytes, the program as the layout reads it. Empty for any other number. DataVector ProgramData(uint32_t objnum); @@ -135,6 +156,14 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { void ClearMissingCharacters() { missing_characters_.clear(); } private: + // What the walk of one place found. + struct Place { + std::vector> fonts; + // Every indirect object the walk read, sorted: a transaction that + // touches one of them makes the place's walk stale. + std::vector reads; + }; + // What a browser loads in place of a program it can't load as it is. struct Display { DataVector bytes; @@ -152,6 +181,14 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { std::optional display; }; + // Every font dictionary the document's drawings can use, each once, in + // the order the walk meets them. + std::vector> FontDicts(); + // Forgets the places that read any of |objnums|, and the inspections of + // programs among them. + void Forget(const std::set& objnums); + void ForgetPlaces(); + // The facts about |stream|, inspected once per version. Program Inspect(RetainPtr stream, Format format); Inspected* FindInspected(uint32_t objnum); @@ -166,6 +203,13 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { bool italic); UnownedPtr const doc_; + // A layer's places, kept between lists: the AcroForm's, and each page's by + // its dictionary's object number. + std::optional form_place_; + std::map page_places_; + // The layer's overlay epoch the places are known to be current at. + uint64_t known_epoch_ = 0; + size_t places_walked_ = 0; std::map inspected_; // by program stream objnum std::map program_aliases_; std::vector missing_characters_; diff --git a/fpdfsdk/epdf_c_api_test.c b/fpdfsdk/epdf_c_api_test.c index fc771c565c..819a3f034c 100644 --- a/fpdfsdk/epdf_c_api_test.c +++ b/fpdfsdk/epdf_c_api_test.c @@ -279,6 +279,7 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFDoc_GetFontEmbeddingPolicy); CHK(EPDFDoc_GetFontProgramCoverage); CHK(EPDFDoc_GetFontProgramData); + CHK(EPDFDoc_GetFontsFingerprint); CHK(EPDFDoc_GetMissingCharacter); CHK(EPDFDoc_ResolveFonts); CHK(EPDFDoc_SetFontEmbeddingPolicy); diff --git a/fpdfsdk/epdf_font.cpp b/fpdfsdk/epdf_font.cpp index a38b817e5f..4ac4da4d7c 100644 --- a/fpdfsdk/epdf_font.cpp +++ b/fpdfsdk/epdf_font.cpp @@ -404,6 +404,16 @@ FPDF_EXPORT void FPDF_CALLCONV EPDFFontFaces_Close(EPDF_FONT_FACES faces) { delete faces; } +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_GetFontsFingerprint(FPDF_DOCUMENT document, unsigned char* sha256) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + if (!doc) { + return false; + } + return CopySha256(CPDF_DocumentFonts::For(doc)->FacesFingerprint(), sha256); +} + FPDF_EXPORT int FPDF_CALLCONV EPDFFontFaces_Count(EPDF_FONT_FACES faces) { return faces ? fxcrt::CollectionSize(faces->faces) : -1; } diff --git a/fpdfsdk/epdf_font_embeddertest.cpp b/fpdfsdk/epdf_font_embeddertest.cpp index 90321a75e9..d5279f6672 100644 --- a/fpdfsdk/epdf_font_embeddertest.cpp +++ b/fpdfsdk/epdf_font_embeddertest.cpp @@ -18,8 +18,11 @@ #include "core/fpdfapi/parser/cpdf_array.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_name.h" +#include "core/fpdfapi/parser/cpdf_reference.h" #include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fpdfapi/parser/cpdf_stream_acc.h" +#include "core/fpdfdoc/cpdf_documentfonts.h" #include "core/fxcrt/bytestring.h" #include "core/fxcrt/widestring.h" #include "core/fxge/cfx_face.h" @@ -32,6 +35,7 @@ #include "public/cpp/fpdf_scopers.h" #include "public/epdf_annot.h" #include "public/epdf_form.h" +#include "public/epdf_layer.h" #include "public/fpdf_annot.h" #include "public/fpdf_edit.h" #include "testing/embedder_test.h" @@ -822,6 +826,161 @@ TEST_F(EPDFFontEmbedderTest, FindsFontsWhereverADrawingUsesThem) { EPDFFontFaces_Close(faces); } +namespace { + +// Three pages: the first two share a resource dictionary with Helvetica, the +// third has its own with Times. +std::string ThreePagesOfFonts() { + return MakePdf({ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources 6 0 R " + ">>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources 6 0 R " + ">>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << " + "/Font << /F1 8 0 R >> >> >>", + "<< /Font << /F1 7 0 R >> >>", + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", + "<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman >>", + }); +} + +// A fresh layer over |pdf|, which must outlive it. +ScopedFPDFDocument OpenLayerOver(const std::string& pdf) { + EPDF_BASE_DOCUMENT base = + EPDF_LoadMemBaseDocument(pdf.data(), pdf.size(), nullptr); + EXPECT_TRUE(base); + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, nullptr)); + EPDF_ReleaseBaseDocument(base); + return layer; +} + +std::vector FontsFingerprint(FPDF_DOCUMENT document) { + std::vector fingerprint(32); + EXPECT_TRUE(EPDFDoc_GetFontsFingerprint(document, fingerprint.data())); + return fingerprint; +} + +bool ListsFace(FPDF_DOCUMENT document, const std::wstring& family) { + EPDF_FONT_FACES faces = EPDFFontFaces_Load(document); + const bool listed = HasFace(faces, family); + EPDFFontFaces_Close(faces); + return listed; +} + +// Gives page |page_objnum|'s resources a Courier font, as a write that adds +// text in it would. +void AddCourierTo(FPDF_DOCUMENT document, uint32_t page_objnum) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + auto font = doc->NewIndirect(); + font->SetNewFor("Type", "Font"); + font->SetNewFor("Subtype", "Type1"); + font->SetNewFor("BaseFont", "Courier"); + RetainPtr page = + ToDictionary(doc->GetMutableIndirectObject(page_objnum)); + ASSERT_TRUE(page); + page->GetMutableDictFor("Resources") + ->GetOrCreateDictFor("Font") + ->SetNewFor("F9", doc, font->GetObjNum()); +} + +} // namespace + +// On a layer, a list after a write walks only the places the write touched: +// a page, or every page that reads a dictionary it changed. +TEST_F(EPDFFontEmbedderTest, AListAfterAWriteWalksOnlyWhatItTouched) { + const std::string pdf = ThreePagesOfFonts(); + ScopedFPDFDocument layer = OpenLayerOver(pdf); + ASSERT_TRUE(layer); + const CPDF_DocumentFonts* index = + CPDF_DocumentFonts::For(CPDFDocumentFromFPDFDocument(layer.get())); + + const std::vector before = FontsFingerprint(layer.get()); + EXPECT_EQ(4u, index->places_walked()); // the AcroForm and three pages + EXPECT_EQ(before, FontsFingerprint(layer.get())); + EXPECT_EQ(4u, index->places_walked()); + + // Courier on the third page: that page is walked again, nothing else. + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + AddCourierTo(layer.get(), 5); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + const std::vector with_courier = FontsFingerprint(layer.get()); + EXPECT_NE(before, with_courier); + EXPECT_EQ(5u, index->places_walked()); + EXPECT_TRUE(ListsFace(layer.get(), L"Courier")); + + // A write that changes no font keeps the fingerprint. + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + { + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), 0)); + FPDFPage_SetRotation(page.get(), 1); + } + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_EQ(with_courier, FontsFingerprint(layer.get())); + EXPECT_EQ(6u, index->places_walked()); + + // The shared resource dictionary changed: both pages that read it. + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(layer.get()); + RetainPtr shared = + ToDictionary(doc->GetMutableIndirectObject(6)); + ASSERT_TRUE(shared); + shared->GetMutableDictFor("Font")->RemoveFor("F1"); + } + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_NE(with_courier, FontsFingerprint(layer.get())); + EXPECT_EQ(8u, index->places_walked()); + EXPECT_FALSE(ListsFace(layer.get(), L"Helvetica")); +} + +// A write rolled back leaves the fonts as they were, though a list during it +// saw its font. +TEST_F(EPDFFontEmbedderTest, AWriteRolledBackChangesNoFonts) { + const std::string pdf = ThreePagesOfFonts(); + ScopedFPDFDocument layer = OpenLayerOver(pdf); + ASSERT_TRUE(layer); + const std::vector before = FontsFingerprint(layer.get()); + + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + AddCourierTo(layer.get(), 3); + EXPECT_NE(before, FontsFingerprint(layer.get())); + EXPECT_TRUE(ListsFace(layer.get(), L"Courier")); + ASSERT_TRUE(EPDFLayer_AbortTransaction(layer.get())); + + EXPECT_EQ(before, FontsFingerprint(layer.get())); + EXPECT_FALSE(ListsFace(layer.get(), L"Courier")); +} + +// A change made outside any transaction is seen: everything is walked again. +TEST_F(EPDFFontEmbedderTest, AChangeOutsideATransactionIsSeen) { + const std::string pdf = ThreePagesOfFonts(); + ScopedFPDFDocument layer = OpenLayerOver(pdf); + ASSERT_TRUE(layer); + const std::vector before = FontsFingerprint(layer.get()); + + AddCourierTo(layer.get(), 4); + EXPECT_NE(before, FontsFingerprint(layer.get())); + EXPECT_TRUE(ListsFace(layer.get(), L"Courier")); +} + +// A plain document says nothing of what changed: it is walked whole each +// time. +TEST_F(EPDFFontEmbedderTest, APlainDocumentIsWalkedWholeEachTime) { + const std::string pdf = ThreePagesOfFonts(); + ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(doc); + const CPDF_DocumentFonts* index = + CPDF_DocumentFonts::For(CPDFDocumentFromFPDFDocument(doc.get())); + const std::vector before = FontsFingerprint(doc.get()); + EXPECT_EQ(before, FontsFingerprint(doc.get())); + EXPECT_EQ(2u, index->places_walked()); + AddCourierTo(doc.get(), 5); + EXPECT_NE(before, FontsFingerprint(doc.get())); +} + // A free text drawn with no font for a character reports it once; marks, // line breaks and zero width characters are never reported. TEST_F(EPDFFontEmbedderTest, FreeTextReportsCharactersNoFontHas) { diff --git a/public/epdf_font.h b/public/epdf_font.h index 63bd1f8170..0569df271d 100644 --- a/public/epdf_font.h +++ b/public/epdf_font.h @@ -262,6 +262,16 @@ FPDF_EXPORT EPDF_FONT_FACES FPDF_CALLCONV EPDFFontFaces_Load(FPDF_DOCUMENT document); FPDF_EXPORT void FPDF_CALLCONV EPDFFontFaces_Close(EPDF_FONT_FACES faces); +// Experimental EmbedPDF Extension API. +// The SHA-256 of every fact EPDFFontFaces_Load() lists for |document|, in +// order, into |sha256| (32 bytes): equal fingerprints, the same faces. Read +// it before and after a write to learn whether the write changed the +// document's fonts. It lists them to tell; on a layer, a list after a +// transaction walks only the places it touched. Returns false for a NULL +// document. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_GetFontsFingerprint(FPDF_DOCUMENT document, unsigned char* sha256); + // The number of faces; -1 for a NULL snapshot. FPDF_EXPORT int FPDF_CALLCONV EPDFFontFaces_Count(EPDF_FONT_FACES faces); From 3fb86383fe7e18db43662a0a4549cfa59b10ff26 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 1 Nov 2026 12:33:42 +0200 Subject: [PATCH 12/16] Let a rich text style name its exact document program A style's program (its stream's object number) joins the face request, so the rich text writer and EPDFDoc_ResolveFonts draw with that program even when another describes itself alike. The body's program becomes the /DA font; a run that restates the body's face keeps it, another face is found by its family. --- core/fpdfdoc/cpdf_annotfontmap.cpp | 50 +++++- core/fpdfdoc/cpdf_annotfontmap.h | 18 +- .../cpdf_appearancegenerator_freetext.cpp | 4 +- core/fpdfdoc/cpdf_documentfonts.cpp | 30 ++-- core/fpdfdoc/cpdf_documentfonts.h | 3 + core/fpdfdoc/cpdf_fontresolver.cpp | 14 +- core/fpdfdoc/cpdf_fontresolver.h | 3 + core/fpdfdoc/cpdf_richtext.h | 10 ++ core/fpdfdoc/cpdf_richtextjson.cpp | 8 + core/fpdfdoc/cpdf_richtextlayout.cpp | 16 +- core/fpdfdoc/cpdf_richtextparser.cpp | 10 +- core/fpdfdoc/cpdf_richtextparser_unittest.cpp | 28 ++++ fpdfsdk/epdf_font.cpp | 10 +- fpdfsdk/epdf_font_embeddertest.cpp | 157 +++++++++++++++++- public/epdf_font.h | 7 +- 15 files changed, 323 insertions(+), 45 deletions(-) diff --git a/core/fpdfdoc/cpdf_annotfontmap.cpp b/core/fpdfdoc/cpdf_annotfontmap.cpp index 1094153423..8d66345b1d 100644 --- a/core/fpdfdoc/cpdf_annotfontmap.cpp +++ b/core/fpdfdoc/cpdf_annotfontmap.cpp @@ -800,9 +800,11 @@ void CPDF_AnnotFontMap::PinRichFace(const WideString& family, // static ByteString CPDF_AnnotFontMap::FaceRequestKey(const WideString& family, int weight, - bool italic) { - return ByteString::Format("%s|%d|%d", FontFamilyKey(family).c_str(), - std::clamp(weight, 100, 900), italic ? 1 : 0); + bool italic, + uint32_t program) { + return ByteString::Format("%s|%d|%d|%u", FontFamilyKey(family).c_str(), + std::clamp(weight, 100, 900), italic ? 1 : 0, + program); } bool CPDF_AnnotFontMap::CoversText(int entry, WideStringView text) { @@ -825,13 +827,15 @@ bool CPDF_AnnotFontMap::CoversText(int entry, WideStringView text) { int CPDF_AnnotFontMap::ResolveRichFace(const WideString& family, int weight, bool italic, - WideStringView text) { - const ByteString request = FaceRequestKey(family, weight, italic); + WideStringView text, + uint32_t program) { + const ByteString request = FaceRequestKey(family, weight, italic, program); auto cached = resolved_faces_.find(request); if (cached != resolved_faces_.end()) { return cached->second; } - const int entry = ResolveRichFaceUncached(family, weight, italic, text); + const int entry = + ResolveRichFaceUncached(family, weight, italic, text, program); if (entry >= 0) { resolved_faces_[request] = entry; } @@ -841,10 +845,19 @@ int CPDF_AnnotFontMap::ResolveRichFace(const WideString& family, int CPDF_AnnotFontMap::ResolveRichFaceUncached(const WideString& family, int weight, bool italic, - WideStringView text) { + WideStringView text, + uint32_t program) { weight = std::clamp(weight, 100, 900); bool degraded = false; + // A program the writer named is the face, whatever the family resolves to. + if (program != 0) { + const int entry = EntryForDocumentProgram(program); + if (entry >= 0) { + return entry; + } + } + // 0. A face the caller pinned to an entry (the /DA font of a regenerated // plain box), if it can draw the text; a box's own font that lacks a // glyph the edit needs is not the edit's face. @@ -1161,6 +1174,29 @@ int CPDF_AnnotFontMap::FindDocumentProgram(const WideString& family, return -1; } +int CPDF_AnnotFontMap::EntryForDocumentProgram(uint32_t objnum) { + for (size_t i = 0; i < fonts_.size(); ++i) { + if (fonts_[i].source == Source::kDocumentProgram && + fonts_[i].program_stream && + fonts_[i].program_stream->GetObjNum() == objnum) { + return static_cast(i); + } + } + if (!doc_) { + return -1; + } + CPDF_DocumentFonts* document_fonts = CPDF_DocumentFonts::For(doc_); + std::optional described = + document_fonts->DescribeProgram(objnum); + RetainPtr stream = document_fonts->ProgramStream(objnum); + if (!described.has_value() || !stream) { + return -1; + } + return AddDocumentProgram( + std::move(stream), described->base_font_name, + {described->family.ToUTF8(), described->weight, described->italic}); +} + int CPDF_AnnotFontMap::AddDocumentProgram( RetainPtr stream, const ByteString& base_font_name, diff --git a/core/fpdfdoc/cpdf_annotfontmap.h b/core/fpdfdoc/cpdf_annotfontmap.h index 2f19af8e32..77f6478cb5 100644 --- a/core/fpdfdoc/cpdf_annotfontmap.h +++ b/core/fpdfdoc/cpdf_annotfontmap.h @@ -157,14 +157,22 @@ class CPDF_AnnotFontMap final { // next rung takes over for the whole request, so a subset never draws // glyph 0 for the characters it lacks. Registered and standard faces keep // the layout's per-glyph fallback (rung 4) for their own gaps. + // + // A |program| (a document program's object number) is the face exactly, + // before every rung: the writer chose it by the name the font list gives + // it, which the family can't tell from another program that describes + // itself alike. Like a registered face it keeps the per-glyph fallback for + // its gaps. A program that can't author text leaves the family to decide. int ResolveRichFace(const WideString& family, int weight, bool italic, - WideStringView text = WideStringView()); + WideStringView text = WideStringView(), + uint32_t program = 0); // The key ResolveRichFace caches by: what the layout groups text with. static ByteString FaceRequestKey(const WideString& family, int weight, - bool italic); + bool italic, + uint32_t program = 0); // Does |entry| map every base character of |text|? Joiners and variation // selectors are exempt, as in the layout. Empty text is covered. bool CoversText(int entry, WideStringView text); @@ -286,7 +294,11 @@ class CPDF_AnnotFontMap final { int ResolveRichFaceUncached(const WideString& family, int weight, bool italic, - WideStringView text); + WideStringView text, + uint32_t program); + // The entry drawing with the document program |objnum|, added when it is + // the first; -1 when the walk didn't find it or it can't author text. + int EntryForDocumentProgram(uint32_t objnum); // The best-styled document program under |family| that is eligible and // covers |text|; -1 when none is. int FindDocumentProgram(const WideString& family, diff --git a/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp b/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp index 1f25d1fb30..3c65f94bf3 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp @@ -462,8 +462,8 @@ PrepareRichFreeTextAPInternal(CPDF_Document* doc, // request resolves here without text. Aliases are settled before the // content names them. const CPDF_RichTextStyle& body = in.document->body; - const int body_entry = - map.ResolveRichFace(body.family, body.weight, body.italic); + const int body_entry = map.ResolveRichFace( + body.family, body.weight, body.italic, WideStringView(), body.program); if (body_entry < 0) { return nullptr; } diff --git a/core/fpdfdoc/cpdf_documentfonts.cpp b/core/fpdfdoc/cpdf_documentfonts.cpp index a02a3a9241..53a76f58dd 100644 --- a/core/fpdfdoc/cpdf_documentfonts.cpp +++ b/core/fpdfdoc/cpdf_documentfonts.cpp @@ -691,19 +691,7 @@ bool CPDF_DocumentFonts::ReserveProgramAlias(uint32_t objnum, return true; } } - std::optional found; - for (const Face& face : CollectFaces()) { - for (const Program& program : face.programs) { - if (program.objnum == objnum) { - found = ReservedProgram{objnum, program.postscript_name, face.family, - face.weight, face.italic}; - break; - } - } - if (found.has_value()) { - break; - } - } + std::optional found = DescribeProgram(objnum); if (!found.has_value()) { return false; } @@ -727,6 +715,22 @@ bool CPDF_DocumentFonts::ReserveProgramAlias(uint32_t objnum, return true; } +std::optional +CPDF_DocumentFonts::DescribeProgram(uint32_t objnum) { + if (objnum == 0) { + return std::nullopt; + } + for (const Face& face : CollectFaces()) { + for (const Program& program : face.programs) { + if (program.objnum == objnum) { + return ReservedProgram{objnum, program.postscript_name, face.family, + face.weight, face.italic}; + } + } + } + return std::nullopt; +} + std::optional CPDF_DocumentFonts::ProgramForAlias(const ByteString& alias) const { auto it = program_aliases_.find(alias); diff --git a/core/fpdfdoc/cpdf_documentfonts.h b/core/fpdfdoc/cpdf_documentfonts.h index 9af5458af7..bc172c4645 100644 --- a/core/fpdfdoc/cpdf_documentfonts.h +++ b/core/fpdfdoc/cpdf_documentfonts.h @@ -143,6 +143,9 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { }; bool ReserveProgramAlias(uint32_t objnum, ByteString* alias); std::optional ProgramForAlias(const ByteString& alias) const; + // A program the walk found, as its font dictionary describes it (the first + // that names it); nullopt for any other number. + std::optional DescribeProgram(uint32_t objnum); // Characters an appearance drew with no font that has them (each a // grapheme, drawn as its face's missing glyph), first seen first, at most diff --git a/core/fpdfdoc/cpdf_fontresolver.cpp b/core/fpdfdoc/cpdf_fontresolver.cpp index 2e7e6ba5ff..97f886fc30 100644 --- a/core/fpdfdoc/cpdf_fontresolver.cpp +++ b/core/fpdfdoc/cpdf_fontresolver.cpp @@ -70,20 +70,22 @@ CPDF_FontResolver::Result CPDF_FontResolver::Resolve( // Each face resolves once over every character asked of it, as the // layout primes it, so two runs of one face get one program. + auto key_of = [](const Run& run) { + return CPDF_AnnotFontMap::FaceRequestKey(run.family, run.weight, run.italic, + run.program); + }; std::map text_of; for (const Run& run : runs) { - text_of[CPDF_AnnotFontMap::FaceRequestKey(run.family, run.weight, - run.italic)] += run.text; + text_of[key_of(run)] += run.text; } for (const Run& run : runs) { map.ResolveRichFace(run.family, run.weight, run.italic, - text_of[CPDF_AnnotFontMap::FaceRequestKey( - run.family, run.weight, run.italic)] - .AsStringView()); + text_of[key_of(run)].AsStringView(), run.program); } for (const Run& run : runs) { - const int entry = map.ResolveRichFace(run.family, run.weight, run.italic); + const int entry = map.ResolveRichFace(run.family, run.weight, run.italic, + WideStringView(), run.program); ResolvedRun resolved; resolved.face = map.DescribeEntry(entry); resolved.fallbacks = CFX_FontRegistry::GetFallbackOrder( diff --git a/core/fpdfdoc/cpdf_fontresolver.h b/core/fpdfdoc/cpdf_fontresolver.h index 90373ec2b9..2df78f7429 100644 --- a/core/fpdfdoc/cpdf_fontresolver.h +++ b/core/fpdfdoc/cpdf_fontresolver.h @@ -26,6 +26,9 @@ class CPDF_FontResolver { WideString family; // a family name, or a registered font's family int weight = 400; bool italic = false; + // The document program the write would name (its stream's object + // number), the face exactly; 0 when the family decides. + uint32_t program = 0; }; struct ResolvedRun { diff --git a/core/fpdfdoc/cpdf_richtext.h b/core/fpdfdoc/cpdf_richtext.h index 2dc082126f..a209f45f40 100644 --- a/core/fpdfdoc/cpdf_richtext.h +++ b/core/fpdfdoc/cpdf_richtext.h @@ -36,6 +36,12 @@ struct CPDF_RichTextStyle { Script script = Script::kNormal; float letter_spacing = 0.0f; // pt float horz_scale = 1.0f; // xfa-font-horizontal-scale / 100 + // The document program (its stream's object number) the writer chose for + // this face, which the family alone can't tell from another program that + // describes itself the same way; 0 when the family decides. Never written + // to /RC or /DS, which name families only: the body's reaches the file as + // the /DA font. + uint32_t program = 0; }; // What a run overrides. Unknown declarations are kept verbatim so a later @@ -50,11 +56,15 @@ struct CPDF_RichTextStyleDelta { std::optional script; std::optional letter_spacing; std::optional horz_scale; + std::optional program; WideString unknown_declarations; // "kerning-mode:pair;..." as written bool IsEmpty() const; }; +// |base| with |delta| over it. A program names one face: a delta that asks +// for another family, weight or italic without naming a program of its own +// leaves the base's. CPDF_RichTextStyle ApplyRichTextStyleDelta( const CPDF_RichTextStyle& base, const CPDF_RichTextStyleDelta& delta); diff --git a/core/fpdfdoc/cpdf_richtextjson.cpp b/core/fpdfdoc/cpdf_richtextjson.cpp index 9312964292..3fceb7a5c7 100644 --- a/core/fpdfdoc/cpdf_richtextjson.cpp +++ b/core/fpdfdoc/cpdf_richtextjson.cpp @@ -463,6 +463,14 @@ bool ReadStyleDelta(const JsonValue* object, CPDF_RichTextStyleDelta* delta) { if (!delta->horz_scale || *delta->horz_scale <= 0) { return false; } + } else if (key == "program") { + // A document program's object number: the exact font for the face. + if (value.type != JsonValue::Type::kNumber || value.number < 1 || + value.number > 0x7FFFFFFF || + value.number != static_cast(value.number)) { + return false; + } + delta->program = static_cast(value.number); } else if (key == "unknown") { std::optional unknown = String(&value); if (!unknown) { diff --git a/core/fpdfdoc/cpdf_richtextlayout.cpp b/core/fpdfdoc/cpdf_richtextlayout.cpp index 8535c0d636..8aed12e47b 100644 --- a/core/fpdfdoc/cpdf_richtextlayout.cpp +++ b/core/fpdfdoc/cpdf_richtextlayout.cpp @@ -622,7 +622,8 @@ struct CPDF_RichTextLayout::Impl { styles[style].face_entry >= 0 ? styles[style].face_entry : fonts->ResolveRichFace(resolved.family, resolved.weight, - resolved.italic); + resolved.italic, WideStringView(), + resolved.program); const GraphemeMatch match = MatchGrapheme( fonts, face, grapheme, resolved.weight, resolved.italic); if (!match.covered && !IsInvisibleGrapheme(grapheme)) { @@ -801,7 +802,7 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( std::vector requests; auto note = [&](const CPDF_RichTextStyle& style, const WideString& text) { const ByteString key = CPDF_AnnotFontMap::FaceRequestKey( - style.family, style.weight, style.italic); + style.family, style.weight, style.italic, style.program); auto it = std::ranges::find_if( requests, [&](const RequestText& r) { return r.key == key; }); if (it == requests.end()) { @@ -822,8 +823,8 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( } for (const RequestText& request : requests) { fonts_->ResolveRichFace(request.style.family, request.style.weight, - request.style.italic, - request.text.AsStringView()); + request.style.italic, request.text.AsStringView(), + request.style.program); } } @@ -982,9 +983,10 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( // 4. The line box (C note §4.4). if (line.runs.empty()) { const ResolvedStyle& at_break = styles[segment.break_style]; - int entry = fonts_->ResolveRichFace(at_break.style.family, - at_break.style.weight, - at_break.style.italic); + int entry = fonts_->ResolveRichFace( + at_break.style.family, at_break.style.weight, + at_break.style.italic, WideStringView(), + at_break.style.program); const CPDF_AnnotFontMap::RichFace face = impl.FaceOf(entry); const float size = at_break.style.size; // parent size line.ascent = face.ascent * size; diff --git a/core/fpdfdoc/cpdf_richtextparser.cpp b/core/fpdfdoc/cpdf_richtextparser.cpp index eeaccabd9e..bca8d00b81 100644 --- a/core/fpdfdoc/cpdf_richtextparser.cpp +++ b/core/fpdfdoc/cpdf_richtextparser.cpp @@ -1099,7 +1099,7 @@ ByteString JsonParagraphProps(const CPDF_RichTextParagraphProps& props, bool CPDF_RichTextStyleDelta::IsEmpty() const { return !family && !weight && !italic && !size && !color && !decoration && - !script && !letter_spacing && !horz_scale && + !script && !letter_spacing && !horz_scale && !program && unknown_declarations.IsEmpty(); } @@ -1134,6 +1134,14 @@ CPDF_RichTextStyle ApplyRichTextStyleDelta( if (delta.horz_scale) { out.horz_scale = *delta.horz_scale; } + // A program draws the base's face only: a run that restates that face + // keeps it, one in another face finds that face by its family. + if (delta.program) { + out.program = *delta.program; + } else if (out.weight != base.weight || out.italic != base.italic || + FontFamilyKey(out.family) != FontFamilyKey(base.family)) { + out.program = 0; + } return out; } diff --git a/core/fpdfdoc/cpdf_richtextparser_unittest.cpp b/core/fpdfdoc/cpdf_richtextparser_unittest.cpp index 8b1ed422cc..99137c8f98 100644 --- a/core/fpdfdoc/cpdf_richtextparser_unittest.cpp +++ b/core/fpdfdoc/cpdf_richtextparser_unittest.cpp @@ -318,3 +318,31 @@ TEST(CPDF_RichTextParserTest, JsonShape) { EXPECT_TRUE(CPDF_RichTextParser::ToJSON(plain).Contains( "{\"text\":\"a \\\"quoted\\\" \\\\ line\\ttab\"}")); } + +// A program draws the base's face only: a run keeps it while it restates that +// face (the family as it compares), and finds another face by its family. +TEST(CPDF_RichTextParserTest, AProgramFollowsItsFaceOnly) { + CPDF_RichTextStyle base = Defaults(); + base.family = L"Mono"; + base.program = 8; + + CPDF_RichTextStyleDelta same; + same.family = L"mono"; + same.size = 14.0f; + EXPECT_EQ(8u, ApplyRichTextStyleDelta(base, same).program); + EXPECT_EQ(8u, + ApplyRichTextStyleDelta(base, CPDF_RichTextStyleDelta()).program); + + CPDF_RichTextStyleDelta bold; + bold.weight = 700; + EXPECT_EQ(0u, ApplyRichTextStyleDelta(base, bold).program); + CPDF_RichTextStyleDelta other; + other.family = L"Helvetica"; + EXPECT_EQ(0u, ApplyRichTextStyleDelta(base, other).program); + + CPDF_RichTextStyleDelta named; + named.weight = 700; + named.program = 5; + EXPECT_EQ(5u, ApplyRichTextStyleDelta(base, named).program); + EXPECT_FALSE(named.IsEmpty()); +} diff --git a/fpdfsdk/epdf_font.cpp b/fpdfsdk/epdf_font.cpp index 4ac4da4d7c..a2febceb3e 100644 --- a/fpdfsdk/epdf_font.cpp +++ b/fpdfsdk/epdf_font.cpp @@ -606,7 +606,8 @@ EPDFDoc_ResolveFonts(FPDF_DOCUMENT document, const FPDF_WIDESTRING* texts, const FPDF_WIDESTRING* families, const int* weights, - const FPDF_BOOL* italics) { + const FPDF_BOOL* italics, + const int* programs) { ScopedFPDFDocumentView document_view(document); CPDF_Document* doc = document_view.Get(); if (!doc || run_count < 0 || @@ -626,6 +627,10 @@ EPDFDoc_ResolveFonts(FPDF_DOCUMENT document, auto family_span = UNSAFE_BUFFERS(pdfium::span(families, count)); auto weight_span = UNSAFE_BUFFERS(pdfium::span(weights, count)); auto italic_span = UNSAFE_BUFFERS(pdfium::span(italics, count)); + pdfium::span program_span; + if (programs) { + program_span = UNSAFE_BUFFERS(pdfium::span(programs, count)); + } for (size_t i = 0; i < count; ++i) { CPDF_FontResolver::Run run; run.text = text_span[i] ? WideStringFromFPDFWideString(text_span[i]) @@ -634,6 +639,9 @@ EPDFDoc_ResolveFonts(FPDF_DOCUMENT document, : WideString(); run.weight = weight_span[i]; run.italic = !!italic_span[i]; + if (!program_span.empty() && program_span[i] > 0) { + run.program = static_cast(program_span[i]); + } runs.push_back(std::move(run)); } auto resolution = std::make_unique(); diff --git a/fpdfsdk/epdf_font_embeddertest.cpp b/fpdfsdk/epdf_font_embeddertest.cpp index d5279f6672..fa9709f37d 100644 --- a/fpdfsdk/epdf_font_embeddertest.cpp +++ b/fpdfsdk/epdf_font_embeddertest.cpp @@ -685,7 +685,7 @@ TEST_F(EPDFFontEmbedderTest, ResolvesTheFontsAWriteWouldUse) { const FPDF_BOOL italics[] = {false, false, false}; EPDF_FONT_RESOLUTION resolution = EPDFDoc_ResolveFonts( - document(), nullptr, 3, texts, families, weights, italics); + document(), nullptr, 3, texts, families, weights, italics, nullptr); ASSERT_TRUE(resolution); EXPECT_EQ(EPDF_FONT_SOURCE_STANDARD, EPDFFontResolution_GetSource(resolution, 0)); @@ -712,7 +712,7 @@ TEST_F(EPDFFontEmbedderTest, ResolvesTheFontsAWriteWouldUse) { // Without the fallback, the CJK character has no font. EPDFFont_ClearFallbackFonts(); resolution = EPDFDoc_ResolveFonts(document(), nullptr, 1, texts, families, - weights, italics); + weights, italics, nullptr); ASSERT_TRUE(resolution); EXPECT_EQ(0u, EPDFFontResolution_GetFallbacks(resolution, 0, nullptr, 0)); ASSERT_EQ(1, EPDFFontResolution_CountMissingCharacters(resolution)); @@ -752,7 +752,7 @@ TEST_F(EPDFFontEmbedderTest, ResolvesADocumentProgram) { const int weights[] = {400}; const FPDF_BOOL italics[] = {false}; EPDF_FONT_RESOLUTION resolution = EPDFDoc_ResolveFonts( - document(), nullptr, 1, texts, families, weights, italics); + document(), nullptr, 1, texts, families, weights, italics, nullptr); ASSERT_TRUE(resolution); EXPECT_EQ(EPDF_FONT_SOURCE_DOCUMENT_PROGRAM, EPDFFontResolution_GetSource(resolution, 0)); @@ -1044,7 +1044,7 @@ TEST_F(EPDFFontEmbedderTest, UnauthorizedRegisteredFontDoesNotTakeOverABox) { const int weights[] = {400}; const FPDF_BOOL italics[] = {false}; EPDF_FONT_RESOLUTION resolution = EPDFDoc_ResolveFonts( - document(), annot.get(), 1, texts, families, weights, italics); + document(), annot.get(), 1, texts, families, weights, italics, nullptr); ASSERT_TRUE(resolution); EXPECT_EQ(EPDF_FONT_SOURCE_DOCUMENT_FONT, EPDFFontResolution_GetSource(resolution, 0)); @@ -1415,3 +1415,152 @@ TEST_F(EPDFFontEmbedderTest, PasswordFieldWritesOnlyStars) { EXPECT_EQ(std::string::npos, stream.find("hunter")) << stream; EXPECT_EQ(std::string::npos, stream.find("ActualText")) << stream; } + +namespace { + +// One page holding two whole programs that describe themselves alike: neither +// font dictionary has /FontFamily or /FontWeight, and "Thin" isn't a weight +// the name is read for, so both are Mono, 400, upright. Object 5 is +// Mono-Regular's program (Roboto), object 8 Mono-Thin's (Arimo). +constexpr uint32_t kMonoRegular = 5; +constexpr uint32_t kMonoThin = 8; + +std::string TwoProgramsDescribedAlike() { + const std::vector regular = LoadRoboto(); + const std::vector thin = + LoadThirdPartyFont("test_fonts/test_fonts/Arimo-Regular.ttf"); + auto program = [](const std::vector& bytes) { + return Stream("/Length1 " + std::to_string(bytes.size()), + std::string(bytes.begin(), bytes.end())); + }; + auto descriptor = [](const std::string& name, uint32_t file) { + return "<< /Type /FontDescriptor /FontName /" + name + + " /Flags 32 /FontBBox [0 -300 1000 900] /ItalicAngle 0 /Ascent 900 " + "/Descent -300 /CapHeight 700 /StemV 80 /FontFile2 " + + std::to_string(file) + " 0 R >>"; + }; + auto font = [](const std::string& name, uint32_t descriptor) { + return "<< /Type /Font /Subtype /TrueType /BaseFont /" + name + + " /FirstChar 32 /LastChar 32 /Widths [250] /Encoding " + "/WinAnsiEncoding /FontDescriptor " + + std::to_string(descriptor) + " 0 R >>"; + }; + return MakePdf({ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 400 400] /Contents 4 0 R " + "/Resources << /Font << /F1 7 0 R /F2 10 0 R >> >> >>", + Stream("BT /F1 12 Tf 40 300 Td ( ) Tj /F2 12 Tf ( ) Tj ET"), + program(regular), + descriptor("Mono-Regular", kMonoRegular), + font("Mono-Regular", 6), + program(thin), + descriptor("Mono-Thin", kMonoThin), + font("Mono-Thin", 9), + }); +} + +// The programs of the Type 0 fonts |annot|'s appearance draws with. +std::vector AppearanceFontPrograms(FPDF_ANNOTATION annot) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + RetainPtr ap = + context->GetAnnotDict()->GetDictFor("AP"); + RetainPtr normal = ap ? ap->GetStreamFor("N") : nullptr; + RetainPtr resources = + normal ? normal->GetDict()->GetDictFor("Resources") : nullptr; + RetainPtr fonts = + resources ? resources->GetDictFor("Font") : nullptr; + std::vector programs; + if (!fonts) { + return programs; + } + CPDF_DictionaryLocker locker(fonts); + for (const auto& [alias, value] : locker) { + const uint32_t program = AppearanceFontProgram(annot, alias); + if (program != 0) { + programs.push_back(program); + } + } + std::ranges::sort(programs); + return programs; +} + +} // namespace + +// A run can name the program a write would use: two programs that describe +// themselves alike are one face to their family, never to their program. +TEST_F(EPDFFontEmbedderTest, ResolvesTheProgramARunNames) { + ScopedRegisteredFonts scoped_fonts; + const std::string pdf = TwoProgramsDescribedAlike(); + ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(doc); + ScopedFPDFWideString text = GetFPDFWideString(L"Hello"); + ScopedFPDFWideString family = GetFPDFWideString(L"Mono"); + FPDF_WIDESTRING texts[] = {text.get(), text.get()}; + FPDF_WIDESTRING families[] = {family.get(), family.get()}; + const int weights[] = {400, 400}; + const FPDF_BOOL italics[] = {false, false}; + const int programs[] = {0, static_cast(kMonoThin)}; + + EPDF_FONT_RESOLUTION resolution = EPDFDoc_ResolveFonts( + doc.get(), nullptr, 2, texts, families, weights, italics, programs); + ASSERT_TRUE(resolution); + // By its family, the first of the two the document holds. + EXPECT_EQ(EPDF_FONT_SOURCE_DOCUMENT_PROGRAM, + EPDFFontResolution_GetSource(resolution, 0)); + EXPECT_EQ(kMonoRegular, EPDFFontResolution_GetProgramObjNum(resolution, 0)); + // By its program, the one named. + EXPECT_EQ(EPDF_FONT_SOURCE_DOCUMENT_PROGRAM, + EPDFFontResolution_GetSource(resolution, 1)); + EXPECT_EQ(kMonoThin, EPDFFontResolution_GetProgramObjNum(resolution, 1)); + EPDFFontResolution_Close(resolution); +} + +// A rich text body that names its program draws with it and writes it as the +// /DA font; a run that restates the body's face keeps it, and a run in +// another face finds that face by its family. +TEST_F(EPDFFontEmbedderTest, RichTextDrawsTheProgramItsStyleNames) { + ScopedRegisteredFonts scoped_fonts; + const std::string pdf = TwoProgramsDescribedAlike(); + ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); + ASSERT_TRUE(page); + auto box = [&](const std::string& json) { + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FREETEXT)); + const FS_RECTF rect{50.0f, 320.0f, 350.0f, 250.0f}; + EXPECT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + EXPECT_TRUE(EPDFAnnot_SetRichTextJSON(annot.get(), json.c_str())); + return annot; + }; + const std::string mono = + "\"family\":\"Mono\",\"weight\":400,\"italic\":false"; + + // First, while the document holds only its own two: by the family, the + // first of them (a write adds its /DR entry, which a later one finds first). + ScopedFPDFAnnotation by_family = + box("{\"body\":{" + mono + + "},\"paragraphs\":[{\"runs\":[{\"text\":\"Hello\"}]}]}"); + EXPECT_EQ(kMonoRegular, + AppearanceFontProgram(by_family.get(), + DefaultAppearanceFontName(by_family.get()))); + + ScopedFPDFAnnotation named = box( + "{\"body\":{" + mono + + ",\"program\":8},\"paragraphs\":[{\"runs\":[{\"text\":\"Hello\"}]}]}"); + EXPECT_EQ(kMonoThin, + AppearanceFontProgram(named.get(), + DefaultAppearanceFontName(named.get()))); + + ScopedFPDFAnnotation runs = + box("{\"body\":{" + mono + + ",\"program\":8},\"paragraphs\":[{\"runs\":[{\"text\":\"A\"}," + "{\"text\":\"B\",\"style\":{\"family\":\"Mono\"}}," + "{\"text\":\"C\",\"style\":{\"family\":\"Helvetica\"}}]}]}"); + // Only Mono-Thin among the programs: "B" restates the body's face, and + // "C" draws in Helvetica. + EXPECT_EQ(std::vector{kMonoThin}, + AppearanceFontPrograms(runs.get())); + EXPECT_NE(std::string::npos, NormalAppearanceOf(runs.get()).find("/Helv")); +} diff --git a/public/epdf_font.h b/public/epdf_font.h index 0569df271d..44adb9aa09 100644 --- a/public/epdf_font.h +++ b/public/epdf_font.h @@ -417,6 +417,10 @@ typedef struct epdf_font_resolution_t__* EPDF_FONT_RESOLUTION; // families - each run's family (a family name or a registered font's). // weights - each run's weight, 100 to 900. // italics - whether each run is italic. +// programs - each run's document program, the object number of the font +// program stream a write would name for it (the face exactly, +// whatever its family says), or 0 to resolve the family; NULL +// when no run names one. // // Returns NULL for a NULL document or arrays. FPDF_EXPORT EPDF_FONT_RESOLUTION FPDF_CALLCONV @@ -426,7 +430,8 @@ EPDFDoc_ResolveFonts(FPDF_DOCUMENT document, const FPDF_WIDESTRING* texts, const FPDF_WIDESTRING* families, const int* weights, - const FPDF_BOOL* italics); + const FPDF_BOOL* italics, + const int* programs); FPDF_EXPORT void FPDF_CALLCONV EPDFFontResolution_Close(EPDF_FONT_RESOLUTION resolution); From ae9df0c7f831f544046dc00117ebf7decc582932 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 1 Nov 2026 12:36:48 +0200 Subject: [PATCH 13/16] Remove build patch leftovers from BUILD.gn BUILD.gn and BUILD.gn.orig were committed while shared-library.patch was applied: restore BUILD.gn as the base has it and drop the patch backup. --- BUILD.gn | 5 +- BUILD.gn.orig | 532 -------------------------------------------------- 2 files changed, 1 insertion(+), 536 deletions(-) delete mode 100644 BUILD.gn.orig diff --git a/BUILD.gn b/BUILD.gn index 55318ad9ff..81827f26af 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -281,7 +281,7 @@ group("pdfium_public_headers") { ] } -shared_library("pdfium") { +component("pdfium") { output_name = "embedpdf" libs = [] configs += [ ":pdfium_strict_config" ] @@ -319,9 +319,6 @@ shared_library("pdfium") { "gdi32.lib", "user32.lib", ] - sources = [ - "resources.rc", - ] } if (is_mac) { diff --git a/BUILD.gn.orig b/BUILD.gn.orig deleted file mode 100644 index 81827f26af..0000000000 --- a/BUILD.gn.orig +++ /dev/null @@ -1,532 +0,0 @@ -# Copyright 2016 The PDFium Authors -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -import("//build/config/clang/clang.gni") -import("//build/config/gclient_args.gni") -import("//testing/test.gni") -import("pdfium.gni") - -group("default") { - testonly = true - deps = [ ":pdfium" ] - if (pdf_is_standalone) { - deps += [ ":pdfium_all" ] - } -} - -config("pdfium_common_config") { - cflags = [] - cflags_cc = [] - ldflags = [] - include_dirs = [ "." ] - inputs = [ "unsafe_buffers_paths.txt" ] - defines = [] - - if (!use_system_libopenjpeg2) { - defines += [ "OPJ_STATIC" ] - } - - if (pdf_enable_click_logging) { - defines += [ "PDF_ENABLE_CLICK_LOGGING" ] - } - - if (pdf_enable_fontations) { - defines += [ "PDF_ENABLE_FONTATIONS" ] - } - - if (pdf_enable_freetype) { - defines += [ "PDF_ENABLE_FREETYPE" ] - } - - if (pdf_enable_rust_bmp) { - defines += [ "PDF_ENABLE_RUST_BMP" ] - } - - if (pdf_enable_rust_jpeg) { - defines += [ "PDF_ENABLE_RUST_JPEG" ] - } - - if (pdf_enable_rust_png) { - defines += [ "PDF_ENABLE_RUST_PNG" ] - } - - if (pdf_use_partition_alloc) { - defines += [ "PDF_USE_PARTITION_ALLOC" ] - } - - # EmbedPDF: thread-confined runtime. Translates the embedpdf_thread_local_globals - # GN arg into the EPDF_THREAD_LOCAL_GLOBALS define consumed by EPDF_TLS - # (core/fxcrt/epdf_tls.h). Default off; see pdfium.gni. - if (embedpdf_thread_local_globals) { - defines += [ "EPDF_THREAD_LOCAL_GLOBALS" ] - } - - if (pdf_use_libyuv) { - defines += [ "PDF_USE_LIBYUV" ] - } - - if (is_win) { - # Assume UTF-8 by default to avoid code page dependencies. - cflags += [ "/utf-8" ] - } - - if (is_clang) { - # May flag some issues when converting int to size_t. - cflags += [ "-Wtautological-unsigned-zero-compare" ] - - # Enforce style guide decision regarding CTAD usage: - # https://google.github.io/styleguide/cppguide.html#CTAD - cflags += [ "-Wctad-maybe-unsupported" ] - } - - if (!is_win && !is_clang) { - cflags += [ - # GCC assumes that control can get past an exhaustive switch and then - # warns if there's no return there. - "-Wno-return-type", - ] - } - - if (clang_use_chrome_plugins) { - # Catch misuse of C-style pointers. - # TODO(crbug.com/1320670): enable for non-debug builds once this stops - # interfering with code generation. - # TODO(tsepez): enable for windows, too. - if (is_debug && pdf_use_partition_alloc && !is_win) { - cflags += [ - "-Xclang", - "-plugin-arg-raw-ptr-plugin", - "-Xclang", - "check-raw-ptr-fields", - - "-Xclang", - "-plugin-arg-raw-ptr-plugin", - "-Xclang", - "raw-ptr-exclude-path=public", - - "-Xclang", - "-plugin-arg-raw-ptr-plugin", - "-Xclang", - "raw-ptr-exclude-path=test", - - # TODO(tsepez): enforce raw_ref<> as well. - # "-Xclang", - # "-plugin-arg-raw-ptr-plugin", - # "-Xclang", - # "check-raw-ref-fields", - ] - defines += [ "PDF_ENABLE_UNOWNED_PTR_EXCLUSION" ] - } - - # Catch misuse of cppgc in XFA. - if (pdf_enable_xfa) { - cflags += [ - "-Xclang", - "-add-plugin", - "-Xclang", - "blink-gc-plugin", - ] - - # Disable GC plugin forbidding off-heap collections of GCed: - cflags += [ - "-Xclang", - "-plugin-arg-blink-gc-plugin", - "-Xclang", - "disable-off-heap-collections-of-gced-check", - ] - } - } -} - -config("pdfium_implementation_config") { - defines = [ "FPDF_IMPLEMENTATION" ] - visibility = [ ":pdfium_public_headers" ] -} - -config("pdfium_public_config") { - defines = [] - - if (pdf_enable_brotli) { - defines += [ "PDF_ENABLE_BROTLI" ] - } - - if (pdf_enable_v8) { - defines += [ "PDF_ENABLE_V8" ] - - if (pdf_enable_xfa) { - defines += [ "PDF_ENABLE_XFA" ] - } - } - - if (pdf_use_agg) { - defines += [ "PDF_USE_AGG" ] - } - if (pdf_use_skia) { - defines += [ "PDF_USE_SKIA" ] - } -} - -config("pdfium_core_config") { - cflags = [] - configs = [ - ":pdfium_common_config", - ":pdfium_public_config", - "//build/config/compiler:noshadowing", - ] - defines = [] - if (is_clang) { - cflags += [ - "-Wcovered-switch-default", - "-Wlifetime-safety-permissive", - ] - } -} - -config("pdfium_strict_config") { - configs = [ - ":pdfium_core_config", - "//build/config/compiler:wexit_time_destructors", - "//build/config/compiler:wglobal_constructors", - ] -} - -config("pdfium_noshorten_config") { - cflags = [] - if (is_clang) { - cflags += [ "-Wshorten-64-to-32" ] - } -} - -source_set("pdfium_public_headers_impl") { - sources = [ - "public/cpp/fpdf_deleters.h", - "public/cpp/fpdf_scopers.h", - # EmbedPDF: detached, read-only PDF action models. - "public/epdf_action.h", - # EmbedPDF: the annotation API that extends public/fpdf_annot.h. - "public/epdf_annot.h", - # EmbedPDF: what we add to public/fpdf_attachment.h. - "public/epdf_attachment.h", - # EmbedPDF: what we add to public/fpdf_catalog.h. - "public/epdf_catalog.h", - # EmbedPDF: base documents and layers. - "public/epdf_layer.h", - # EmbedPDF: captures of what a layer write loses, put back by an undo. - "public/epdf_capture.h", - # EmbedPDF: digests of bytes in memory, one implementation for every caller. - "public/epdf_digest.h", - # EmbedPDF: what we add to public/fpdf_doc.h. - "public/epdf_doc.h", - # EmbedPDF: what we add to public/fpdf_edit.h. - "public/epdf_edit.h", - # EmbedPDF: what we add to public/fpdf_flatten.h. - "public/epdf_flatten.h", - # EmbedPDF: public runtime font registration API used by page fallback - # rendering and annotation authoring. - "public/epdf_font.h", - # EmbedPDF: session-free AcroForm model, write transactions, FDF/XFDF. - "public/epdf_form.h", - # EmbedPDF: the catalog's /Names /Pages and /Templates name trees. - "public/epdf_named_pages.h", - # EmbedPDF: the document outline, addressed by object number. - "public/epdf_outline.h", - # EmbedPDF: digital signatures - revisions, the signature model. - "public/epdf_signature.h", - # EmbedPDF: generic namespace-scoped document/page /PieceInfo metadata. - "public/epdf_pieceinfo.h", - # EmbedPDF: PNG encoding of RGBA bitmaps. - "public/epdf_png.h", - # EmbedPDF: what we add to public/fpdf_progressive.h. - "public/epdf_progressive.h", - # EmbedPDF: typed measurement, viewport and caption API. - "public/epdf_measure.h", - "public/epdf_redact.h", - # EmbedPDF: what we add to public/fpdf_save.h. - "public/epdf_save.h", - # EmbedPDF: one-call exact oriented glyph geometry. - "public/epdf_text.h", - # EmbedPDF: what we add to public/fpdfview.h. - "public/epdf_view.h", - "public/fpdf_annot.h", - "public/fpdf_attachment.h", - "public/fpdf_catalog.h", - "public/fpdf_dataavail.h", - "public/fpdf_doc.h", - "public/fpdf_edit.h", - "public/fpdf_ext.h", - "public/fpdf_flatten.h", - "public/fpdf_formfill.h", - "public/fpdf_fwlevent.h", - "public/fpdf_javascript.h", - "public/fpdf_ppo.h", - "public/fpdf_progressive.h", - "public/fpdf_save.h", - "public/fpdf_searchex.h", - "public/fpdf_signature.h", - "public/fpdf_structtree.h", - "public/fpdf_sysfontinfo.h", - "public/fpdf_text.h", - "public/fpdf_transformpage.h", - "public/fpdfview.h", - ] - visibility = [ ":*" ] -} - -group("pdfium_public_headers") { - public_deps = [ ":pdfium_public_headers_impl" ] - public_configs = [ - ":pdfium_public_config", - ":pdfium_implementation_config", - ] -} - -component("pdfium") { - output_name = "embedpdf" - libs = [] - configs += [ ":pdfium_strict_config" ] - public_configs = [ ":pdfium_public_config" ] - - deps = [ - "constants", - "core/fpdfapi/page", - "core/fpdfapi/parser", - "core/fpdfdoc", - "core/fxcodec", - "core/fxcrt", - "core/fxge", - "fpdfsdk", - "fpdfsdk/formfiller", - "fxjs", - ] - - public_deps = [ - ":pdfium_public_headers_impl", - "core/fxcrt", - ] - - if (pdf_enable_xfa) { - deps += [ - "fpdfsdk/fpdfxfa", - "xfa/fxfa", - "xfa/fxfa/parser", - ] - } - - if (is_win) { - libs += [ - "advapi32.lib", - "gdi32.lib", - "user32.lib", - ] - } - - if (is_mac) { - frameworks = [ - "AppKit.framework", - "CoreFoundation.framework", - ] - } - - if (pdf_is_complete_lib) { - static_component_type = "static_library" - complete_static_lib = true - configs -= [ "//build/config/compiler:thin_archive" ] - } -} - -if (pdf_enable_freetype) { - group("freetype_common") { - public_deps = [] - if (pdf_bundle_freetype) { - public_deps += [ "third_party:fx_freetype" ] - } else { - public_deps += [ "//build/config/freetype" ] - } - } -} - -# Targets below this are only visible within this file. -visibility = [ ":*" ] - -group("pdfium_unittest_deps") { - testonly = true - public_deps = [ - "core/fxcrt", - "testing:unit_test_support", - "//testing/gmock", - "//testing/gtest", - ] - visibility += [ - "core/*", - "fpdfsdk/*", - "fxbarcode/*", - "fxjs/*", - "testing/*", - "xfa/*", - ] -} - -test("pdfium_unittests") { - testonly = true - sources = [ "testing/unit_test_main.cpp" ] - deps = [ - "core/fdrm:unittests", - "core/fpdfapi/edit:unittests", - "core/fpdfapi/font:unittests", - "core/fpdfapi/page:unittests", - "core/fpdfapi/parser:unittests", - "core/fpdfapi/render:unittests", - "core/fpdfdoc:unittests", - "core/fpdftext:unittests", - "core/fxcodec:unittests", - "core/fxcrt", - "core/fxcrt:unittests", - "core/fxge:unittests", - "fpdfsdk:unittests", - "testing:test_environments", - "testing:test_support", - "testing:unit_test_support", - "testing:unittests", - "testing/png_codec:unittests", - "//testing/gmock", - "//testing/gtest", - ] - configs += [ ":pdfium_core_config" ] - if (is_android) { - use_raw_android_executable = true - } - if (pdf_enable_v8) { - configs += [ "//v8:external_startup_data" ] - deps += [ - "fxjs:unittests", - "//v8", - ] - if (pdf_enable_xfa) { - deps += [ - "core/fxcrt/css:unittests", - "fxbarcode:unittests", - "xfa/fde:unittests", - "xfa/fgas/crt:unittests", - "xfa/fgas/font:unittests", - "xfa/fgas/layout:unittests", - "xfa/fxfa:unittests", - "xfa/fxfa/formcalc:unittests", - "xfa/fxfa/parser:unittests", - ] - } - } -} - -group("pdfium_embeddertest_deps") { - testonly = true - public_deps = [ - ":pdfium_public_headers", - "core/fxcrt", - "testing:embedder_test_support", - "//testing/gmock", - "//testing/gtest", - ] - visibility += [ - "core/*", - "fpdfsdk/*", - "fxjs/*", - "xfa/*", - ] -} - -test("pdfium_embeddertests") { - testonly = true - sources = [ "testing/embedder_test_main.cpp" ] - deps = [ - ":pdfium_embeddertest_deps", - "core/fpdfapi/edit:embeddertests", - "core/fpdfapi/parser:embeddertests", - "core/fpdfapi/render:embeddertests", - "core/fxcodec:embeddertests", - "core/fxcrt", - "core/fxge:embeddertests", - "fpdfsdk:embeddertests", - "fpdfsdk/formfiller:embeddertests", - "fpdfsdk/pwl:embeddertests", - "testing:test_environments", - "testing:test_support", - "testing/png_codec", - "//testing/gmock", - "//testing/gtest", - ] - include_dirs = [ "testing/gmock/include" ] - configs += [ ":pdfium_core_config" ] - - if (is_android) { - ignore_all_data_deps = true - use_raw_android_executable = true - } - - if (build_with_chromium && pdf_use_skia) { - deps += [ "testing:discardable_memory_allocator" ] - configs += [ "testing:pdfium_in_chromium_config" ] - } - - if (pdf_enable_v8) { - deps += [ - "fxjs:embeddertests", - "//v8", - ] - configs += [ "//v8:external_startup_data" ] - } - - if (pdf_enable_xfa) { - deps += [ - "fpdfsdk/fpdfxfa:embeddertests", - "xfa/fwl:embeddertests", - "xfa/fxfa/layout:embeddertests", - "xfa/fxfa/parser:embeddertests", - ] - } -} - -group("pdfium_all") { - testonly = true - deps = [ - ":pdfium_embeddertests", - ":pdfium_unittests", - "testing:pdfium_diff", - "testing/fuzzers", - "testing/pdfium_test", - "testing/tools:epdf_layer_memory_benchmark", - "testing/tools:epdf_layer_replay_soak", - "testing/tools:epdf_thread_soak", - ] - - if (pdf_is_standalone) { - deps += [ "testing/tools:test_runner_py" ] - } -} - -# Makes additional targets reachable only for "gn check". These are not always -# built by the "all" Ninja target, which uses the "default" group, which in turn -# depends on the "pdfium_all" group. -group("gn_check") { - deps = [] - - # TODO(crbug.com/pdfium/1832): Remove !is_android when //third_party/expat is - # available. - if (defined(checkout_skia) && checkout_skia && !is_android && !is_wasm) { - deps += [ "//skia" ] - } -} - -# GN evaluates each .gn file once per toolchain, so restricting to default -# toolchain will ensure write_file() is called only once. -if (current_toolchain == default_toolchain) { - import("//build/gn_logs.gni") - - # Write debug logs to gn_logs.txt. - _lines = [ - "Generated during 'gn gen' by //BUILD.gn.", - "", - ] + build_gn_logs - - write_file("$root_build_dir/gn_logs.txt", _lines) -} From 336b3121c0469703f6527482008582a889398875 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 1 Nov 2026 13:45:50 +0200 Subject: [PATCH 14/16] Give each reading of a font program its own display program A display program depends on how a font dictionary reads its program (encoding, /Widths, flags), so two dictionaries sharing one Type 1 or bare CFF stream got one hash and the first one's advances. Display programs are now kept per stream and dictionary, by SHA-256, and EPDFDoc_GetFontDisplayProgramData and EPDFDoc_GetFontDisplayProgramCoverage (was EPDFDoc_GetFontProgramCoverage) take the display hash instead of the stream's object number. --- core/fpdfdoc/cpdf_documentfonts.cpp | 136 ++++++++++++++++++++-------- core/fpdfdoc/cpdf_documentfonts.h | 44 ++++++--- fpdfsdk/epdf_c_api_test.c | 2 +- fpdfsdk/epdf_font.cpp | 25 ++--- fpdfsdk/epdf_font_embeddertest.cpp | 135 +++++++++++++++++++++++---- public/epdf_font.h | 33 ++++--- 6 files changed, 279 insertions(+), 96 deletions(-) diff --git a/core/fpdfdoc/cpdf_documentfonts.cpp b/core/fpdfdoc/cpdf_documentfonts.cpp index 53a76f58dd..425b5d2304 100644 --- a/core/fpdfdoc/cpdf_documentfonts.cpp +++ b/core/fpdfdoc/cpdf_documentfonts.cpp @@ -326,7 +326,8 @@ std::vector CPDF_DocumentFonts::CollectFaces() { const bool known = std::ranges::any_of(entry.programs, [&](const Program& other) { return program.objnum != 0 - ? other.objnum == program.objnum + ? other.objnum == program.objnum && + other.display_sha256 == program.display_sha256 : other.objnum == 0 && other.format == program.format && other.sha256 == program.sha256; }); @@ -475,6 +476,11 @@ void CPDF_DocumentFonts::Forget(const std::set& objnums) { for (uint32_t objnum : objnums) { inspected_.erase(objnum); } + for (auto& [objnum, inspected] : inspected_) { + std::erase_if(inspected.display_of_dict, [&](const auto& entry) { + return objnums.contains(entry.first); + }); + } } void CPDF_DocumentFonts::ForgetPlaces() { @@ -598,67 +604,119 @@ DataVector CPDF_DocumentFonts::DisplayHashOf( return {}; } Inspected& inspected = it->second; - if (!inspected.display_built) { - inspected.display_built = true; - if (IsSfnt(program.format) && program.maps_unicode) { - // Only its tables are out of place: the same program, realigned. Its - // coverage is its own cmap's (ProgramCoverage). + if (IsSfnt(program.format) && program.maps_unicode) { + // Only its tables are out of place: the same program, realigned, whatever + // reads it. Its coverage is its own cmap's. + if (!inspected.aligned_built) { + inspected.aligned_built = true; DataVector aligned = AlignSfntTables(BytesOf(inspected)); if (!aligned.empty()) { Display display; display.sha256 = CryptSha256Generate(aligned); display.bytes = std::move(aligned); - inspected.display = std::move(display); + inspected.aligned = std::move(display); } - } else { - std::optional built = - BuildDisplayProgram(doc_, font_dict, program.postscript_name, - CPDF_FontFace{family, weight, italic}); - if (built.has_value()) { - Display display; - display.sha256 = CryptSha256Generate(built->bytes); - display.coverage = RangesOf(std::move(built->characters)); - display.bytes = std::move(built->bytes); - inspected.display = std::move(display); + } + return inspected.aligned.has_value() ? inspected.aligned->sha256 + : DataVector(); + } + // Any other program is drawn as this dictionary reads it: its encoding, + // widths and flags go into the display program, so two dictionaries that + // share a stream can need two. + const uint32_t dict_objnum = font_dict->GetObjNum(); + if (dict_objnum != 0) { + auto known = inspected.display_of_dict.find(dict_objnum); + if (known != inspected.display_of_dict.end()) { + return known->second.value_or(DataVector()); + } + } + std::optional> sha256; + std::optional built = + BuildDisplayProgram(doc_, font_dict, program.postscript_name, + CPDF_FontFace{family, weight, italic}); + if (built.has_value()) { + Display display; + display.sha256 = CryptSha256Generate(built->bytes); + display.coverage = RangesOf(std::move(built->characters)); + display.bytes = std::move(built->bytes); + sha256 = display.sha256; + inspected.displays.try_emplace(display.sha256, std::move(display)); + } + if (dict_objnum != 0) { + inspected.display_of_dict[dict_objnum] = sha256; + } + return sha256.value_or(DataVector()); +} + +CPDF_DocumentFonts::Inspected* CPDF_DocumentFonts::FindDisplay( + pdfium::span sha256, + const Display** display) { + const DataVector wanted(sha256.begin(), sha256.end()); + auto found = [&]() -> Inspected* { + for (auto& [objnum, inspected] : inspected_) { + // Only a version the document still has. + if (doc_->GetIndirectObject(objnum) != inspected.stream || + inspected.stream->GetRawSize() != inspected.raw_size) { + continue; + } + if (LoadsAsIs(inspected.facts) && inspected.facts.sha256 == wanted) { + *display = nullptr; + return &inspected; + } + if (inspected.aligned.has_value() && + inspected.aligned->sha256 == wanted) { + *display = &*inspected.aligned; + return &inspected; + } + auto built = inspected.displays.find(wanted); + if (built != inspected.displays.end()) { + *display = &built->second; + return &inspected; } } + return nullptr; + }; + if (Inspected* inspected = found()) { + return inspected; } - return inspected.display.has_value() ? inspected.display->sha256 - : DataVector(); + // Not found by a walk yet, or changed since: walk again. + CollectFaces(); + return found(); } -DataVector CPDF_DocumentFonts::DisplayProgramData(uint32_t objnum) { - Inspected* inspected = FindInspected(objnum); +DataVector CPDF_DocumentFonts::DisplayProgramData( + pdfium::span sha256) { + const Display* display = nullptr; + Inspected* inspected = FindDisplay(sha256, &display); if (!inspected) { return {}; } - if (inspected->display.has_value()) { - return inspected->display->bytes; - } - if (LoadsAsIs(inspected->facts)) { - return ProgramData(objnum); - } - return {}; + return display ? display->bytes : BytesOf(*inspected); } -std::vector CPDF_DocumentFonts::ProgramCoverage( - uint32_t objnum) { - Inspected* inspected = FindInspected(objnum); +std::vector CPDF_DocumentFonts::DisplayProgramCoverage( + pdfium::span sha256) { + const Display* display = nullptr; + Inspected* inspected = FindDisplay(sha256, &display); if (!inspected) { return {}; } // A program that maps Unicode is drawn through its own cmap, its tables // realigned or not; any other through its display program's. - if (!inspected->facts.maps_unicode) { - return inspected->display.has_value() ? inspected->display->coverage - : std::vector(); + if (inspected->facts.maps_unicode) { + return CmapCoverage(*inspected); } - if (inspected->coverage.has_value()) { - return *inspected->coverage; + return display ? display->coverage : std::vector(); +} + +const std::vector& CPDF_DocumentFonts::CmapCoverage( + Inspected& inspected) { + if (inspected.coverage.has_value()) { + return *inspected.coverage; } std::vector ranges; { - DataVector bytes = ProgramData(objnum); + DataVector bytes = BytesOf(inspected); auto program = std::make_unique(); if (!bytes.empty() && program->LoadFaceZeroFromSpan(bytes, /*force_vertical=*/false, @@ -668,8 +726,8 @@ std::vector CPDF_DocumentFonts::ProgramCoverage( pdfium::kMaximumSupplementaryCodePoint); } } - inspected->coverage = ranges; - return ranges; + inspected.coverage = std::move(ranges); + return *inspected.coverage; } RetainPtr CPDF_DocumentFonts::ProgramStream(uint32_t objnum) { diff --git a/core/fpdfdoc/cpdf_documentfonts.h b/core/fpdfdoc/cpdf_documentfonts.h index bc172c4645..cde70d32e8 100644 --- a/core/fpdfdoc/cpdf_documentfonts.h +++ b/core/fpdfdoc/cpdf_documentfonts.h @@ -21,6 +21,7 @@ #include "core/fxcrt/bytestring.h" #include "core/fxcrt/data_vector.h" #include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/span.h" #include "core/fxcrt/unowned_ptr.h" #include "core/fxcrt/widestring.h" #include "core/fxge/cfx_codepointrange.h" @@ -76,8 +77,10 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { WideString family; int weight = 400; bool italic = false; - // One per program stream, in the order the walk found them; fonts - // without a program (not embedded, Type 3) add one entry per format. + // One per program stream and display program, in the order the walk + // found them (a stream two dictionaries read differently is two); + // fonts without a program (not embedded, Type 3) add one entry per + // format. std::vector programs; }; @@ -120,13 +123,14 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { // A program found by the walk, by its stream's object number: its decoded // bytes, the program as the layout reads it. Empty for any other number. DataVector ProgramData(uint32_t objnum); - // What a browser loads for such a program (see Program::display_sha256); - // empty when it can't draw it or |objnum| isn't one. - DataVector DisplayProgramData(uint32_t objnum); + // What a browser loads, by its SHA-256 (a Program::display_sha256 the walk + // found); empty for any other hash. + DataVector DisplayProgramData(pdfium::span sha256); // The scalars a browser draws with it, which are the ones the engine writes - // with it, as ranges in order; empty when it maps none or |objnum| isn't - // one. - std::vector ProgramCoverage(uint32_t objnum); + // with it, as ranges in order; empty when it maps none or |sha256| isn't + // one the walk found. + std::vector DisplayProgramCoverage( + pdfium::span sha256); // The program stream itself, for the writer; null when |objnum| isn't one. RetainPtr ProgramStream(uint32_t objnum); @@ -178,10 +182,21 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { RetainPtr stream; // the version inspected size_t raw_size = 0; Program facts; + // The scalars its own cmap maps. std::optional> coverage; - // Built from the first font dictionary found to use the stream. - bool display_built = false; - std::optional display; + // A Unicode sfnt a browser would refuse, its tables realigned: the same + // whichever dictionary reads it. + bool aligned_built = false; + std::optional aligned; + // Display programs by their SHA-256. Any other program is read through + // each font dictionary's encoding, widths and flags, so one stream can + // be drawn as more than one. + std::map, Display> displays; + // The display each font dictionary (by object number) reads the stream + // as; nullopt when it builds none. Forgotten when the dictionary + // changes; one that isn't an indirect object builds its display again + // each time. + std::map>> display_of_dict; }; // Every font dictionary the document's drawings can use, each once, in @@ -197,6 +212,13 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { Inspected* FindInspected(uint32_t objnum); // The decoded bytes of the stream |inspected| is. DataVector BytesOf(const Inspected& inspected); + // The scalars |inspected|'s own cmap maps, read once. + const std::vector& CmapCoverage(Inspected& inspected); + // The program a browser loads by |sha256|: its inspection, with |display| + // the display program it is, or null when it is the program as it is. + // Walks again when no current inspection has it. + Inspected* FindDisplay(pdfium::span sha256, + const Display** display); // |program|'s display hash, its display program built through // |font_dict| once per stream version when it needs one. DataVector DisplayHashOf(const Program& program, diff --git a/fpdfsdk/epdf_c_api_test.c b/fpdfsdk/epdf_c_api_test.c index 7f0f9141ad..62af38e030 100644 --- a/fpdfsdk/epdf_c_api_test.c +++ b/fpdfsdk/epdf_c_api_test.c @@ -284,9 +284,9 @@ int CheckEmbedPDFCApi(void) { // epdf_font.h CHK(EPDFDoc_ClearMissingCharacters); CHK(EPDFDoc_CountMissingCharacters); + CHK(EPDFDoc_GetFontDisplayProgramCoverage); CHK(EPDFDoc_GetFontDisplayProgramData); CHK(EPDFDoc_GetFontEmbeddingPolicy); - CHK(EPDFDoc_GetFontProgramCoverage); CHK(EPDFDoc_GetFontProgramData); CHK(EPDFDoc_GetFontsFingerprint); CHK(EPDFDoc_GetMissingCharacter); diff --git a/fpdfsdk/epdf_font.cpp b/fpdfsdk/epdf_font.cpp index a2febceb3e..930a278d1c 100644 --- a/fpdfsdk/epdf_font.cpp +++ b/fpdfsdk/epdf_font.cpp @@ -551,33 +551,36 @@ EPDFDoc_GetFontProgramData(FPDF_DOCUMENT document, FPDF_EXPORT unsigned long FPDF_CALLCONV EPDFDoc_GetFontDisplayProgramData(FPDF_DOCUMENT document, - unsigned long program_objnum, + const unsigned char* display_sha256, void* buffer, unsigned long buflen) { ScopedFPDFDocumentView document_view(document); CPDF_Document* doc = document_view.Get(); - if (!doc || program_objnum == 0) { + if (!doc || !display_sha256) { return 0; } + // SAFETY: required from caller: |display_sha256| holds 32 bytes. const DataVector data = CPDF_DocumentFonts::For(doc)->DisplayProgramData( - static_cast(program_objnum)); + UNSAFE_BUFFERS(pdfium::span(display_sha256, 32u))); return CopyBytesAndReturnLength(data, buffer, buflen); } FPDF_EXPORT unsigned long FPDF_CALLCONV -EPDFDoc_GetFontProgramCoverage(FPDF_DOCUMENT document, - unsigned long program_objnum, - uint32_t* ranges, - unsigned long range_capacity) { +EPDFDoc_GetFontDisplayProgramCoverage(FPDF_DOCUMENT document, + const unsigned char* display_sha256, + uint32_t* ranges, + unsigned long range_capacity) { ScopedFPDFDocumentView document_view(document); CPDF_Document* doc = document_view.Get(); - if (!doc || program_objnum == 0) { + if (!doc || !display_sha256) { return 0; } - return CopyRangesAndReturnCount(CPDF_DocumentFonts::For(doc)->ProgramCoverage( - static_cast(program_objnum)), - ranges, range_capacity); + // SAFETY: required from caller: |display_sha256| holds 32 bytes. + return CopyRangesAndReturnCount( + CPDF_DocumentFonts::For(doc)->DisplayProgramCoverage( + UNSAFE_BUFFERS(pdfium::span(display_sha256, 32u))), + ranges, range_capacity); } // ---- The fonts a write would use diff --git a/fpdfsdk/epdf_font_embeddertest.cpp b/fpdfsdk/epdf_font_embeddertest.cpp index fa9709f37d..c38353cef8 100644 --- a/fpdfsdk/epdf_font_embeddertest.cpp +++ b/fpdfsdk/epdf_font_embeddertest.cpp @@ -477,10 +477,13 @@ TEST_F(EPDFFontEmbedderTest, ListsADocumentProgramAndHandsOutItsBytes) { ASSERT_EQ(roboto.size(), EPDFDoc_GetFontProgramData( document(), objnum, bytes.data(), bytes.size())); EXPECT_EQ(roboto, bytes); - ASSERT_EQ(roboto.size(), EPDFDoc_GetFontDisplayProgramData( - document(), objnum, bytes.data(), bytes.size())); + ASSERT_EQ(roboto.size(), + EPDFDoc_GetFontDisplayProgramData(document(), display_hash, + bytes.data(), bytes.size())); EXPECT_EQ(roboto, bytes); - EXPECT_GT(EPDFDoc_GetFontProgramCoverage(document(), objnum, nullptr, 0), 0u); + EXPECT_GT(EPDFDoc_GetFontDisplayProgramCoverage(document(), display_hash, + nullptr, 0), + 0u); // The catalog is an object, not a font program. EXPECT_EQ(0u, EPDFDoc_GetFontProgramData(document(), 1, nullptr, 0)); } @@ -530,11 +533,12 @@ TEST_F(EPDFFontEmbedderTest, RealignsAProgramABrowserWouldRefuse) { EXPECT_EQ(misaligned, program); const unsigned long length = - EPDFDoc_GetFontDisplayProgramData(document(), objnum, nullptr, 0); + EPDFDoc_GetFontDisplayProgramData(document(), display_hash, nullptr, 0); ASSERT_GT(length, 0u); std::vector display(length); - ASSERT_EQ(length, EPDFDoc_GetFontDisplayProgramData( - document(), objnum, display.data(), display.size())); + ASSERT_EQ(length, + EPDFDoc_GetFontDisplayProgramData(document(), display_hash, + display.data(), display.size())); EXPECT_TRUE(SfntTablesAligned(display)); EXPECT_EQ(TablesOf(misaligned), TablesOf(display)); // Named by its own bytes, not the program's. @@ -553,11 +557,11 @@ TEST_F(EPDFFontEmbedderTest, RealignsAProgramABrowserWouldRefuse) { } EXPECT_EQ(0xb1b0afbau, sum); // A browser draws the characters the program's own cmap maps. - const unsigned long range_count = - EPDFDoc_GetFontProgramCoverage(document(), objnum, nullptr, 0); + const unsigned long range_count = EPDFDoc_GetFontDisplayProgramCoverage( + document(), display_hash, nullptr, 0); std::vector ranges(range_count * 2); - EPDFDoc_GetFontProgramCoverage(document(), objnum, ranges.data(), - range_count); + EPDFDoc_GetFontDisplayProgramCoverage(document(), display_hash, ranges.data(), + range_count); auto covers = [&](uint32_t scalar) { for (size_t i = 0; i + 1 < ranges.size(); i += 2) { if (scalar >= ranges[i] && scalar < ranges[i] + ranges[i + 1]) { @@ -1176,15 +1180,13 @@ void ExpectDisplayProgramDrawsTheKeptFont(const std::string& pdf, faces, found->face, found->program, display_hash)); EXPECT_NE(std::vector(source_hash, source_hash + 32), std::vector(display_hash, display_hash + 32)); - const unsigned long objnum = - EPDFFontFaces_GetProgramObjNum(faces, found->face, found->program); EPDFFontFaces_Close(faces); const unsigned long length = - EPDFDoc_GetFontDisplayProgramData(doc.get(), objnum, nullptr, 0); + EPDFDoc_GetFontDisplayProgramData(doc.get(), display_hash, nullptr, 0); ASSERT_GT(length, 0u); std::vector display(length); - ASSERT_EQ(length, EPDFDoc_GetFontDisplayProgramData(doc.get(), objnum, + ASSERT_EQ(length, EPDFDoc_GetFontDisplayProgramData(doc.get(), display_hash, display.data(), length)); const DataVector hashed = CryptSha256Generate(display); EXPECT_EQ(std::vector(display_hash, display_hash + 32), @@ -1237,11 +1239,12 @@ void ExpectDisplayProgramDrawsTheKeptFont(const std::string& pdf, EXPECT_GE(compared, 5); // The characters it draws are exactly the ones the engine writes. - const unsigned long range_count = - EPDFDoc_GetFontProgramCoverage(doc.get(), objnum, nullptr, 0); + const unsigned long range_count = EPDFDoc_GetFontDisplayProgramCoverage( + doc.get(), display_hash, nullptr, 0); ASSERT_GT(range_count, 0u); std::vector ranges(range_count * 2); - EPDFDoc_GetFontProgramCoverage(doc.get(), objnum, ranges.data(), range_count); + EPDFDoc_GetFontDisplayProgramCoverage(doc.get(), display_hash, ranges.data(), + range_count); for (uint32_t scalar = 1; scalar < 0x3000; ++scalar) { const uint32_t code = font->CharCodeFromUnicode(static_cast(scalar)); @@ -1252,12 +1255,13 @@ void ExpectDisplayProgramDrawsTheKeptFont(const std::string& pdf, EXPECT_EQ(written, display_face->GetCharIndex(scalar) > 0) << scalar; } - // The same bytes from another opening of the same file. + // The same bytes from another opening of the same file, by the hash alone: + // the document is walked to find it. ScopedFPDFDocument again( FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); ASSERT_TRUE(again); std::vector rebuilt(length); - ASSERT_EQ(length, EPDFDoc_GetFontDisplayProgramData(again.get(), objnum, + ASSERT_EQ(length, EPDFDoc_GetFontDisplayProgramData(again.get(), display_hash, rebuilt.data(), length)); EXPECT_EQ(display, rebuilt); } @@ -1310,6 +1314,99 @@ TEST_F(EPDFFontEmbedderTest, BuildsADisplayProgramForAType1Font) { EPDF_FONT_FORMAT_TYPE1); } +// Two font dictionaries that share one bare CFF program but give it other +// widths are two display programs: each draws at its own dictionary's +// advances, under a hash of its own. The program is still one stream. +TEST_F(EPDFFontEmbedderTest, + ADisplayProgramIsTheProgramAsItsDictionaryReadsIt) { + std::vector cff = LoadTestFont("fonts/foxit_fixed.cff"); + ASSERT_FALSE(cff.empty()); + auto font = [](int width, int descriptor) { + std::string widths; + for (int code = 32; code <= 126; ++code) { + widths += std::to_string(width) + " "; + } + return "<< /Type /Font /Subtype /Type1 /BaseFont /FoxitFixed /FirstChar 32 " + "/LastChar 126 /Widths [" + + widths + "] /Encoding /WinAnsiEncoding /FontDescriptor " + + std::to_string(descriptor) + " 0 R >>"; + }; + const char kDescriptor[] = + "<< /Type /FontDescriptor /FontName /FoxitFixed /Flags 32 /FontBBox [0 " + "-250 1000 900] /ItalicAngle 0 /Ascent 900 /Descent -250 /CapHeight 700 " + "/StemV 80 /FontFile3 4 0 R >>"; + const std::string pdf = MakePdf({ + "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [] /DR << /Font << " + "/FNarrow 5 0 R /FWide 6 0 R >> >> >> >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] >>", + Stream("/Subtype /Type1C", std::string(cff.begin(), cff.end())), + font(600, 7), + font(900, 8), + kDescriptor, + kDescriptor, + }); + ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(doc); + + EPDF_FONT_FACES faces = EPDFFontFaces_Load(doc.get()); + ASSERT_TRUE(faces); + std::vector> display_hashes; + std::vector> source_hashes; + for (int face = 0; face < EPDFFontFaces_Count(faces); ++face) { + for (int program = 0; program < EPDFFontFaces_CountPrograms(faces, face); + ++program) { + if (EPDFFontFaces_GetProgramFormat(faces, face, program) != + EPDF_FONT_FORMAT_CFF) { + continue; + } + EXPECT_EQ(4u, EPDFFontFaces_GetProgramObjNum(faces, face, program)); + unsigned char hash[32] = {}; + ASSERT_TRUE(EPDFFontFaces_GetProgramHash(faces, face, program, hash)); + source_hashes.emplace_back(hash, hash + 32); + ASSERT_TRUE( + EPDFFontFaces_GetProgramDisplayHash(faces, face, program, hash)); + display_hashes.emplace_back(hash, hash + 32); + } + } + EPDFFontFaces_Close(faces); + ASSERT_EQ(2u, display_hashes.size()); + EXPECT_EQ(source_hashes[0], source_hashes[1]); + EXPECT_NE(display_hashes[0], display_hashes[1]); + + // Each draws "A" at the advance its dictionary gives it, in walk order. + const int expected_advance[] = {600, 900}; + for (size_t i = 0; i < 2; ++i) { + const unsigned long length = EPDFDoc_GetFontDisplayProgramData( + doc.get(), display_hashes[i].data(), nullptr, 0); + ASSERT_GT(length, 0u); + std::vector display(length); + ASSERT_EQ(length, + EPDFDoc_GetFontDisplayProgramData( + doc.get(), display_hashes[i].data(), display.data(), length)); + const DataVector hashed = CryptSha256Generate(display); + EXPECT_EQ(display_hashes[i], + std::vector(hashed.begin(), hashed.end())); + auto display_font = std::make_unique(); + ASSERT_TRUE(display_font->LoadFaceZeroFromSpan( + display, /*force_vertical=*/false, /*object_tag=*/0)); + RetainPtr display_face = display_font->GetFace(); + ASSERT_TRUE(display_face->SelectCharMap(fxge::FontEncoding::kUnicode)); + const int glyph = display_face->GetCharIndex('A'); + ASSERT_GT(glyph, 0); + ASSERT_EQ(0, display_face->LoadGlyph(glyph, /*scale=*/false)); + EXPECT_EQ(expected_advance[i], display_face->GetGlyphTTWidth(glyph)) << i; + EXPECT_GT(EPDFDoc_GetFontDisplayProgramCoverage( + doc.get(), display_hashes[i].data(), nullptr, 0), + 0u); + } + + // A hash no program has finds nothing. + const unsigned char unknown[32] = {}; + EXPECT_EQ(0u, + EPDFDoc_GetFontDisplayProgramData(doc.get(), unknown, nullptr, 0)); +} + // A form field draws each cluster in the first font that has it: Hebrew // typed into a Helvetica field comes from the fallback, in bidi order, under // ActualText, and nothing is reported missing. diff --git a/public/epdf_font.h b/public/epdf_font.h index 44adb9aa09..1b162c0f03 100644 --- a/public/epdf_font.h +++ b/public/epdf_font.h @@ -364,34 +364,37 @@ EPDFDoc_GetFontProgramData(FPDF_DOCUMENT document, unsigned long buflen); // Experimental EmbedPDF Extension API. -// What a browser loads for such a program: the program itself when it is +// What a browser loads, by the SHA-256 EPDFFontFaces_GetProgramDisplayHash() +// gives (|display_sha256|, 32 bytes): the program itself when it is // TrueType or OpenType with a Unicode cmap. When such a program's tables // don't each start on a four-byte boundary (a browser refuses it, FreeType // doesn't), the same tables realigned. Otherwise (Type 1, bare CFF, a -// program without a Unicode cmap) an OpenType font built from it: its -// glyphs, a Unicode cmap holding exactly the characters the engine writes -// with the font (each through the PDF's encoding to the glyph it draws), and -// the advances the engine lays them out with (/Widths). The same bytes every -// time. Returns the length, 0 when a browser can't draw it or -// |program_objnum| isn't such a program; pass NULL/0 to query it. Nothing is +// program without a Unicode cmap) an OpenType font built from the program as +// one font dictionary reads it: its glyphs, a Unicode cmap holding exactly +// the characters the engine writes with that font (each through its +// encoding to the glyph it draws), and the advances the engine lays them out +// with (its /Widths). Two dictionaries that read one stream differently have +// two. The same bytes every time. Returns the length, 0 when no program the +// document's fonts use has that hash; pass NULL/0 to query it. Nothing is // copied when |buflen| is too small. FPDF_EXPORT unsigned long FPDF_CALLCONV EPDFDoc_GetFontDisplayProgramData(FPDF_DOCUMENT document, - unsigned long program_objnum, + const unsigned char* display_sha256, void* buffer, unsigned long buflen); // Experimental EmbedPDF Extension API. -// The characters a browser draws with such a program, which are the ones the +// The characters a browser draws with that program (by |display_sha256|, as +// EPDFDoc_GetFontDisplayProgramData() takes it), which are the ones the // engine writes with it, as ranges: |ranges| receives up to // |range_capacity| pairs (first code point, count). Returns the number of -// ranges, 0 when it maps none or isn't such a program; pass NULL/0 to query -// it. +// ranges, 0 when it maps none or no program has that hash; pass NULL/0 to +// query it. FPDF_EXPORT unsigned long FPDF_CALLCONV -EPDFDoc_GetFontProgramCoverage(FPDF_DOCUMENT document, - unsigned long program_objnum, - uint32_t* ranges, - unsigned long range_capacity); +EPDFDoc_GetFontDisplayProgramCoverage(FPDF_DOCUMENT document, + const unsigned char* display_sha256, + uint32_t* ranges, + unsigned long range_capacity); // ---- The fonts a write would use -------------------------------------------- From 2d07bda0aaeaa8964e7bffa3516a344a8d35e088 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 1 Nov 2026 14:44:31 +0200 Subject: [PATCH 15/16] Report the display program each resolved run draws with EPDFFontResolution_GetProgramDisplayHash gives a resolved run's program by its display hash, as the run's own font dictionary reads it. The object number can't tell apart two dictionaries that read one stream differently, so two fields over a shared CFF program both previewed with one dictionary's widths. --- core/fpdfdoc/cpdf_annotfontmap.cpp | 1 + core/fpdfdoc/cpdf_annotfontmap.h | 2 + core/fpdfdoc/cpdf_documentfonts.cpp | 67 ++++++++++++++++++++--------- core/fpdfdoc/cpdf_documentfonts.h | 20 +++++++-- core/fpdfdoc/cpdf_fontresolver.cpp | 21 +++++++++ core/fpdfdoc/cpdf_fontresolver.h | 5 +++ fpdfsdk/epdf_c_api_test.c | 1 + fpdfsdk/epdf_font.cpp | 8 ++++ fpdfsdk/epdf_font_embeddertest.cpp | 56 +++++++++++++++++++++--- public/epdf_font.h | 10 +++++ 10 files changed, 161 insertions(+), 30 deletions(-) diff --git a/core/fpdfdoc/cpdf_annotfontmap.cpp b/core/fpdfdoc/cpdf_annotfontmap.cpp index 8d66345b1d..653ff8dc6c 100644 --- a/core/fpdfdoc/cpdf_annotfontmap.cpp +++ b/core/fpdfdoc/cpdf_annotfontmap.cpp @@ -1336,6 +1336,7 @@ CPDF_AnnotFontMap::EntryFacts CPDF_AnnotFontMap::DescribeEntry( facts.weight = face.weight; facts.italic = face.italic; facts.base_font_name = font_dict->GetNameFor("BaseFont"); + facts.font_dict = font_dict; RetainPtr descriptor = FontDescriptorOf(font_dict.Get()); for (const char* key : {"FontFile2", "FontFile3", "FontFile"}) { diff --git a/core/fpdfdoc/cpdf_annotfontmap.h b/core/fpdfdoc/cpdf_annotfontmap.h index 77f6478cb5..7384cc48d6 100644 --- a/core/fpdfdoc/cpdf_annotfontmap.h +++ b/core/fpdfdoc/cpdf_annotfontmap.h @@ -230,6 +230,8 @@ class CPDF_AnnotFontMap final { CFX_FontRegistry::kInvalidFontId; // kDocumentProgram's stream, and kDocumentFontDict's when it embeds one. uint32_t program_objnum = 0; + // kDocumentFontDict's dictionary, which says how its program is read. + RetainPtr font_dict; // kStandard14: the standard name; else the font's /BaseFont. ByteString base_font_name; // The face it draws: its family, weight and italic. diff --git a/core/fpdfdoc/cpdf_documentfonts.cpp b/core/fpdfdoc/cpdf_documentfonts.cpp index 425b5d2304..10fd372d68 100644 --- a/core/fpdfdoc/cpdf_documentfonts.cpp +++ b/core/fpdfdoc/cpdf_documentfonts.cpp @@ -308,21 +308,7 @@ std::vector CPDF_DocumentFonts::CollectFaces() { } Face& entry = faces[found->second]; - auto [stream, format] = ProgramOf(font.Get()); - const ByteString base_font = ProgramBaseFont(font.Get()); - Program program; - if (stream) { - program = Inspect(stream, format); - program.embedded = - HasFontSubsetTag(base_font) ? Embedded::kSubset : Embedded::kWhole; - } else { - program.format = format; - } - program.postscript_name = BaseFontNameWithoutSubsetTag(base_font); - if (stream) { - program.display_sha256 = DisplayHashOf(program, font.Get(), face.family, - face.weight, face.italic); - } + Program program = ReadProgram(font.Get(), face); const bool known = std::ranges::any_of(entry.programs, [&](const Program& other) { return program.objnum != 0 @@ -536,6 +522,26 @@ CPDF_DocumentFonts::Program CPDF_DocumentFonts::Inspect( return facts; } +CPDF_DocumentFonts::Program CPDF_DocumentFonts::ReadProgram( + const CPDF_Dictionary* font, + const CPDF_FontFace& face) { + auto [stream, format] = ProgramOf(font); + const ByteString base_font = ProgramBaseFont(font); + Program program; + if (stream) { + program = Inspect(stream, format); + program.embedded = + HasFontSubsetTag(base_font) ? Embedded::kSubset : Embedded::kWhole; + } else { + program.format = format; + } + program.postscript_name = BaseFontNameWithoutSubsetTag(base_font); + if (stream) { + program.display_sha256 = DisplayHashOf(program, font, face); + } + return program; +} + CPDF_DocumentFonts::Inspected* CPDF_DocumentFonts::FindInspected( uint32_t objnum) { if (objnum == 0) { @@ -586,9 +592,7 @@ DataVector CPDF_DocumentFonts::BytesOf(const Inspected& inspected) { DataVector CPDF_DocumentFonts::DisplayHashOf( const Program& program, const CPDF_Dictionary* font_dict, - const WideString& family, - int weight, - bool italic) { + const CPDF_FontFace& face) { if (program.sha256.empty()) { return {}; } @@ -620,9 +624,12 @@ DataVector CPDF_DocumentFonts::DisplayHashOf( return inspected.aligned.has_value() ? inspected.aligned->sha256 : DataVector(); } - // Any other program is drawn as this dictionary reads it: its encoding, + // Any other program is drawn as a dictionary reads it: its encoding, // widths and flags go into the display program, so two dictionaries that // share a stream can need two. + if (!font_dict) { + return {}; + } const uint32_t dict_objnum = font_dict->GetObjNum(); if (dict_objnum != 0) { auto known = inspected.display_of_dict.find(dict_objnum); @@ -632,8 +639,7 @@ DataVector CPDF_DocumentFonts::DisplayHashOf( } std::optional> sha256; std::optional built = - BuildDisplayProgram(doc_, font_dict, program.postscript_name, - CPDF_FontFace{family, weight, italic}); + BuildDisplayProgram(doc_, font_dict, program.postscript_name, face); if (built.has_value()) { Display display; display.sha256 = CryptSha256Generate(built->bytes); @@ -738,6 +744,25 @@ RetainPtr CPDF_DocumentFonts::ProgramStream(uint32_t objnum) { return pdfium::WrapRetain(const_cast(inspected->stream.Get())); } +DataVector CPDF_DocumentFonts::DisplayHashOfFont( + const CPDF_Dictionary* font_dict) { + if (!font_dict) { + return {}; + } + return ReadProgram(font_dict, FaceOfFontDict(font_dict)).display_sha256; +} + +DataVector CPDF_DocumentFonts::DisplayHashOfProgram(uint32_t objnum) { + Inspected* inspected = FindInspected(objnum); + if (!inspected) { + return {}; + } + // No dictionary to read it through: only a program every dictionary reads + // alike has one. + return DisplayHashOf(inspected->facts, /*font_dict=*/nullptr, + CPDF_FontFace()); +} + bool CPDF_DocumentFonts::ReserveProgramAlias(uint32_t objnum, ByteString* alias) { if (objnum == 0) { diff --git a/core/fpdfdoc/cpdf_documentfonts.h b/core/fpdfdoc/cpdf_documentfonts.h index cde70d32e8..5c31dee4f5 100644 --- a/core/fpdfdoc/cpdf_documentfonts.h +++ b/core/fpdfdoc/cpdf_documentfonts.h @@ -27,7 +27,9 @@ #include "core/fxge/cfx_codepointrange.h" #include "core/fxge/cfx_fontregistry.h" +class CPDF_Dictionary; class CPDF_Stream; +struct CPDF_FontFace; class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { public: @@ -133,6 +135,14 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { pdfium::span sha256); // The program stream itself, for the writer; null when |objnum| isn't one. RetainPtr ProgramStream(uint32_t objnum); + // What a browser loads for the program |font_dict| embeds, as that + // dictionary reads it: the display hash CollectFaces() lists for it. Empty + // when it embeds none or a browser can't draw it. + DataVector DisplayHashOfFont(const CPDF_Dictionary* font_dict); + // What a browser loads for program stream |objnum| when every dictionary + // reads it alike (an sfnt with a Unicode cmap, as every document program + // the layout writes with is); empty for any other program. + DataVector DisplayHashOfProgram(uint32_t objnum); // A /DA font naming a document program: like a registered font's, the // alias is this session's until an appearance installs the /DR entry. @@ -209,6 +219,9 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { // The facts about |stream|, inspected once per version. Program Inspect(RetainPtr stream, Format format); + // The program |font| embeds as |font| (whose face is |face|) reads it, its + // display hash too; only its format when it embeds none. + Program ReadProgram(const CPDF_Dictionary* font, const CPDF_FontFace& face); Inspected* FindInspected(uint32_t objnum); // The decoded bytes of the stream |inspected| is. DataVector BytesOf(const Inspected& inspected); @@ -220,12 +233,11 @@ class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { Inspected* FindDisplay(pdfium::span sha256, const Display** display); // |program|'s display hash, its display program built through - // |font_dict| once per stream version when it needs one. + // |font_dict| (whose face is |face|) once per stream version when it needs + // one; empty for such a program without |font_dict|. DataVector DisplayHashOf(const Program& program, const CPDF_Dictionary* font_dict, - const WideString& family, - int weight, - bool italic); + const CPDF_FontFace& face); UnownedPtr const doc_; // A layer's places, kept between lists: the AcroForm's, and each page's by diff --git a/core/fpdfdoc/cpdf_fontresolver.cpp b/core/fpdfdoc/cpdf_fontresolver.cpp index 97f886fc30..139798297a 100644 --- a/core/fpdfdoc/cpdf_fontresolver.cpp +++ b/core/fpdfdoc/cpdf_fontresolver.cpp @@ -13,9 +13,29 @@ #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_document.h" #include "core/fpdfdoc/cpdf_appearancegenerator_internal.h" +#include "core/fpdfdoc/cpdf_documentfonts.h" #include "core/fpdfdoc/cpdf_fontface.h" #include "core/fpdfdoc/cpdf_richtextlayout.h" +namespace { + +// What a browser loads for the program |face| draws with: a document font's +// program as its dictionary reads it, a document program as it is. +DataVector DisplayHashOf(CPDF_Document* doc, + const CPDF_AnnotFontMap::EntryFacts& face) { + if (face.source == CPDF_AnnotFontMap::Source::kDocumentFontDict) { + return CPDF_DocumentFonts::For(doc)->DisplayHashOfFont( + face.font_dict.Get()); + } + if (face.source == CPDF_AnnotFontMap::Source::kDocumentProgram) { + return CPDF_DocumentFonts::For(doc)->DisplayHashOfProgram( + face.program_objnum); + } + return {}; +} + +} // namespace + // static CPDF_FontResolver::Result CPDF_FontResolver::Resolve( CPDF_Document* doc, @@ -88,6 +108,7 @@ CPDF_FontResolver::Result CPDF_FontResolver::Resolve( WideStringView(), run.program); ResolvedRun resolved; resolved.face = map.DescribeEntry(entry); + resolved.display_sha256 = DisplayHashOf(doc, resolved.face); resolved.fallbacks = CFX_FontRegistry::GetFallbackOrder( run.weight, run.italic, /*for_authoring=*/true); const std::vector starts = diff --git a/core/fpdfdoc/cpdf_fontresolver.h b/core/fpdfdoc/cpdf_fontresolver.h index 2df78f7429..c264d21fcf 100644 --- a/core/fpdfdoc/cpdf_fontresolver.h +++ b/core/fpdfdoc/cpdf_fontresolver.h @@ -12,6 +12,7 @@ #include #include "core/fpdfdoc/cpdf_annotfontmap.h" +#include "core/fxcrt/data_vector.h" #include "core/fxcrt/widestring.h" #include "core/fxge/cfx_fontregistry.h" @@ -35,6 +36,10 @@ class CPDF_FontResolver { // The entry the run's face resolves to: where its program comes from, // which one, the face it draws, and whether it substitutes the request. CPDF_AnnotFontMap::EntryFacts face; + // SHA-256 of what a browser loads for a document program or document + // font's program, as the dictionary the run draws with reads it (what + // CPDF_DocumentFonts::CollectFaces() lists for it); empty otherwise. + DataVector display_sha256; // The registered fallback fonts in the order the layout tries them for // the run's style (authorized for editing only). std::vector fallbacks; diff --git a/fpdfsdk/epdf_c_api_test.c b/fpdfsdk/epdf_c_api_test.c index 62af38e030..3eb6633079 100644 --- a/fpdfsdk/epdf_c_api_test.c +++ b/fpdfsdk/epdf_c_api_test.c @@ -314,6 +314,7 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFFontResolution_GetFallbacks); CHK(EPDFFontResolution_GetFamily); CHK(EPDFFontResolution_GetMissingCharacter); + CHK(EPDFFontResolution_GetProgramDisplayHash); CHK(EPDFFontResolution_GetProgramObjNum); CHK(EPDFFontResolution_GetRegisteredFont); CHK(EPDFFontResolution_GetSource); diff --git a/fpdfsdk/epdf_font.cpp b/fpdfsdk/epdf_font.cpp index 930a278d1c..f24533575f 100644 --- a/fpdfsdk/epdf_font.cpp +++ b/fpdfsdk/epdf_font.cpp @@ -688,6 +688,14 @@ EPDFFontResolution_GetProgramObjNum(EPDF_FONT_RESOLUTION resolution, int run) { return entry ? entry->face.program_objnum : 0; } +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontResolution_GetProgramDisplayHash(EPDF_FONT_RESOLUTION resolution, + int run, + unsigned char* sha256) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + return entry && CopySha256(entry->display_sha256, sha256); +} + FPDF_EXPORT unsigned long FPDF_CALLCONV EPDFFontResolution_GetBaseFontName(EPDF_FONT_RESOLUTION resolution, int run, diff --git a/fpdfsdk/epdf_font_embeddertest.cpp b/fpdfsdk/epdf_font_embeddertest.cpp index c38353cef8..6dd7910be6 100644 --- a/fpdfsdk/epdf_font_embeddertest.cpp +++ b/fpdfsdk/epdf_font_embeddertest.cpp @@ -729,7 +729,8 @@ TEST_F(EPDFFontEmbedderTest, ResolvesTheFontsAWriteWouldUse) { } // A family the document holds as a whole program, and nobody registered, -// resolves to that program. +// resolves to that program, which a browser loads under the hash the faces +// list for it. TEST_F(EPDFFontEmbedderTest, ResolvesADocumentProgram) { ScopedRegisteredFonts scoped_fonts; std::vector roboto = LoadRoboto(); @@ -746,6 +747,9 @@ TEST_F(EPDFFontEmbedderTest, ResolvesADocumentProgram) { ASSERT_TRUE(found.has_value()); const unsigned long objnum = EPDFFontFaces_GetProgramObjNum(faces, found->face, found->program); + unsigned char listed_hash[32] = {}; + ASSERT_TRUE(EPDFFontFaces_GetProgramDisplayHash(faces, found->face, + found->program, listed_hash)); EPDFFontFaces_Close(faces); EPDFFont_ClearRegisteredFonts(); @@ -761,6 +765,10 @@ TEST_F(EPDFFontEmbedderTest, ResolvesADocumentProgram) { EXPECT_EQ(EPDF_FONT_SOURCE_DOCUMENT_PROGRAM, EPDFFontResolution_GetSource(resolution, 0)); EXPECT_EQ(objnum, EPDFFontResolution_GetProgramObjNum(resolution, 0)); + unsigned char hash[32] = {}; + ASSERT_TRUE(EPDFFontResolution_GetProgramDisplayHash(resolution, 0, hash)); + EXPECT_EQ(std::vector(listed_hash, listed_hash + 32), + std::vector(hash, hash + 32)); EPDFFontResolution_Close(resolution); } @@ -1316,7 +1324,8 @@ TEST_F(EPDFFontEmbedderTest, BuildsADisplayProgramForAType1Font) { // Two font dictionaries that share one bare CFF program but give it other // widths are two display programs: each draws at its own dictionary's -// advances, under a hash of its own. The program is still one stream. +// advances, under a hash of its own. The program is still one stream. A field +// in either font resolves to the display program its own dictionary reads. TEST_F(EPDFFontEmbedderTest, ADisplayProgramIsTheProgramAsItsDictionaryReadsIt) { std::vector cff = LoadTestFont("fonts/foxit_fixed.cff"); @@ -1335,16 +1344,25 @@ TEST_F(EPDFFontEmbedderTest, "<< /Type /FontDescriptor /FontName /FoxitFixed /Flags 32 /FontBBox [0 " "-250 1000 900] /ItalicAngle 0 /Ascent 900 /Descent -250 /CapHeight 700 " "/StemV 80 /FontFile3 4 0 R >>"; + auto field = [](const char* name, const char* font_alias, int bottom) { + return std::string("<< /Type /Annot /Subtype /Widget /FT /Tx /T (") + name + + ") /Rect [10 " + std::to_string(bottom) + " 200 " + + std::to_string(bottom + 30) + "] /DA (/" + font_alias + + " 12 Tf 0 g) /P 3 0 R >>"; + }; const std::string pdf = MakePdf({ - "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [] /DR << /Font << " - "/FNarrow 5 0 R /FWide 6 0 R >> >> >> >>", + "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [9 0 R 10 0 R] " + "/DR << /Font << /FNarrow 5 0 R /FWide 6 0 R >> >> >> >>", "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", - "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Annots [9 0 R 10 " + "0 R] >>", Stream("/Subtype /Type1C", std::string(cff.begin(), cff.end())), font(600, 7), font(900, 8), kDescriptor, kDescriptor, + field("narrow", "FNarrow", 200), + field("wide", "FWide", 100), }); ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); ASSERT_TRUE(doc); @@ -1405,6 +1423,34 @@ TEST_F(EPDFFontEmbedderTest, const unsigned char unknown[32] = {}; EXPECT_EQ(0u, EPDFDoc_GetFontDisplayProgramData(doc.get(), unknown, nullptr, 0)); + + // Each field draws its text in its own font: the same stream, read by its + // own dictionary. + ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); + ASSERT_TRUE(page); + ScopedFPDFWideString text = GetFPDFWideString(L"Hello"); + ScopedFPDFWideString family = GetFPDFWideString(L"FoxitFixed"); + FPDF_WIDESTRING texts[] = {text.get()}; + FPDF_WIDESTRING families[] = {family.get()}; + const int weights[] = {400}; + const FPDF_BOOL italics[] = {false}; + const int programs[] = {0}; + for (int i = 0; i < 2; ++i) { + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), i)); + ASSERT_TRUE(annot); + EPDF_FONT_RESOLUTION resolution = EPDFDoc_ResolveFonts( + doc.get(), annot.get(), 1, texts, families, weights, italics, programs); + ASSERT_TRUE(resolution); + EXPECT_EQ(EPDF_FONT_SOURCE_DOCUMENT_FONT, + EPDFFontResolution_GetSource(resolution, 0)) + << i; + EXPECT_EQ(4u, EPDFFontResolution_GetProgramObjNum(resolution, 0)) << i; + unsigned char hash[32] = {}; + ASSERT_TRUE(EPDFFontResolution_GetProgramDisplayHash(resolution, 0, hash)) + << i; + EXPECT_EQ(display_hashes[i], std::vector(hash, hash + 32)) << i; + EPDFFontResolution_Close(resolution); + } } // A form field draws each cluster in the first font that has it: Hebrew diff --git a/public/epdf_font.h b/public/epdf_font.h index 1b162c0f03..745116ddde 100644 --- a/public/epdf_font.h +++ b/public/epdf_font.h @@ -448,6 +448,16 @@ EPDFFontResolution_GetRegisteredFont(EPDF_FONT_RESOLUTION resolution, int run); // and for EPDF_FONT_SOURCE_DOCUMENT_FONT when it embeds one; 0 otherwise. FPDF_EXPORT unsigned long FPDF_CALLCONV EPDFFontResolution_GetProgramObjNum(EPDF_FONT_RESOLUTION resolution, int run); +// The SHA-256 of what a browser loads for that program into |sha256| (32 +// bytes), as the font dictionary the run draws with reads it: the hash +// EPDFFontFaces_GetProgramDisplayHash() lists for it, which +// EPDFDoc_GetFontDisplayProgramData() takes. Two dictionaries that read one +// program stream differently give two. Returns false when the run draws with +// no document program or a browser can't draw it. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontResolution_GetProgramDisplayHash(EPDF_FONT_RESOLUTION resolution, + int run, + unsigned char* sha256); // The font's /BaseFont: the standard name for EPDF_FONT_SOURCE_STANDARD // ("Helvetica-Bold"). Bytes and a trailing NUL; returns the length // including the NUL, 0 when there is none; pass NULL/0 to query it. From 9e5f69546af733a019a401a686094cdba716887d Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 1 Nov 2026 15:41:42 +0200 Subject: [PATCH 16/16] Clarify font fork ownership and text geometry Document the font code's upstream touchpoints, test coverage, and CPVT line-breaking dependency in the fork ledger. Clarify the coordinate space used for synthetic ActualText and distinguish glyph outlines from generated OpenType fonts. --- core/fpdftext/cpdf_textpage.cpp | 4 +++- core/fxcrt/BUILD.gn | 2 ++ core/fxge/BUILD.gn | 5 +++-- docs/embedpdf/fork-ledger.md | 15 +++++++++++++-- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/core/fpdftext/cpdf_textpage.cpp b/core/fpdftext/cpdf_textpage.cpp index 894e07c711..97dd8a251c 100644 --- a/core/fpdftext/cpdf_textpage.cpp +++ b/core/fpdftext/cpdf_textpage.cpp @@ -1310,7 +1310,9 @@ bool CPDF_TextPage::ProcessMarkedContent(const TransformedTextObject& obj) { CFX_FloatRect char_box(rect); char_box.Translate(k * step, 0); temp_text_buf_.AppendChar(wc); - // EmbedPDF(text-geometry): not a real glyph, so no local origin. + // EmbedPDF(text-geometry, bug-fix): not a real glyph, so no local origin. + // The position and box are taken through the form's matrix into page + // space (see `form_matrix` above). temp_char_list_.push_back(CharInfo::Synthetic( CharType::kActualText, wc, form_matrix.Transform(text_obj->GetPos()), form_matrix.TransformRect(char_box), matrix, text_obj)); diff --git a/core/fxcrt/BUILD.gn b/core/fxcrt/BUILD.gn index d67f2f2396..6b47d26965 100644 --- a/core/fxcrt/BUILD.gn +++ b/core/fxcrt/BUILD.gn @@ -33,6 +33,8 @@ source_set("fxcrt") { "bytestring_pool.h", "cfx_bidi_resolver.cpp", "cfx_bidi_resolver.h", + # EmbedPDF(fonts): the Unicode bidirectional algorithm over one paragraph, + # through ICU's ubidi; the rich text layout orders a line's runs with it. "cfx_bidiparagraph.cpp", "cfx_bidiparagraph.h", "cfx_bitstream.cpp", diff --git a/core/fxge/BUILD.gn b/core/fxge/BUILD.gn index 51354a5c92..4e139163dd 100644 --- a/core/fxge/BUILD.gn +++ b/core/fxge/BUILD.gn @@ -55,14 +55,15 @@ source_set("fxge") { # EmbedPDF(fonts): the code points a font maps, as ranges. "cfx_codepointrange.cpp", "cfx_codepointrange.h", - # EmbedPDF(fonts): a glyph's unscaled outline, and an OpenType font - # written from outlines (a document font's display program). + # EmbedPDF(fonts): a glyph's outline in font units, unscaled and unhinted. "cfx_glyphoutline.cpp", "cfx_glyphoutline.h", "cfx_graphstate.cpp", "cfx_graphstate.h", "cfx_graphstatedata.cpp", "cfx_graphstatedata.h", + # EmbedPDF(fonts): an OpenType font written from glyph outlines, the + # display program of a document font a browser can't load as it is. "cfx_opentypewriter.cpp", "cfx_opentypewriter.h", "cfx_path.cpp", diff --git a/docs/embedpdf/fork-ledger.md b/docs/embedpdf/fork-ledger.md index c26688d705..e89de2b696 100644 --- a/docs/embedpdf/fork-ledger.md +++ b/docs/embedpdf/fork-ledger.md @@ -407,13 +407,24 @@ the same in every engine, WebAssembly (which has no system fonts) included. **Where.** `cfx_fontregistry` (ours), `cpdf_font*`, `cfx_fontmapper.cpp`. Upstream's embedder tests expect standard fonts from the test fonts, so the embedder test environment turns on `CFX_FontMapper::SetStandardFontsFromSystemForTesting()`; our tests of the rule turn it off. +A document's fonts, their display programs and the resolver are ours (`cpdf_documentfonts`, +`cpdf_displayprogram`, `cpdf_fontresolver`, `cfx_sfnt`, `cfx_opentypewriter`, `cfx_glyphoutline`, +`cfx_codepointrange`, `cfx_bidiparagraph`). They reach into upstream files at three places: +`CPDF_Document::FontIndex`, the document's hold on its font index; `CPVT_NeedDivision`, +`CPVT_IsOpenStylePunctuation` and `CPVT_IsSpace` in `cpvt_section`, CPVT's own word breaking, +which the rich text layout breaks a field's lines with; and `CFX_Face::LoadUnscaledGlyphOutline` +and `GetMappedCodePointRanges`, defined in our `cfx_glyphoutline.cpp` and `cfx_codepointrange.cpp`. **Guarded by.** The font embedder tests; `cfx_fontmapper_unittest.cpp` and -`EPDFViewEmbedderTest.StandardFontsDrawWithBuiltInFaces` for standard fonts. +`EPDFViewEmbedderTest.StandardFontsDrawWithBuiltInFaces` for standard fonts; +`epdf_font_embeddertest.cpp` and `epdf_richtextlayout_embeddertest.cpp` (`FieldMatchesCpvt` for +the line breaks) for a document's fonts and the layout. **When upstream touches this.** Font loading and mapping. A new upstream test that expects the test fonts for a standard font passes as it is; one of ours that expects the built-in faces turns the switch off. Our font code calls FreeType directly (`CFX_Face::GetFsTypeFlags()`, CID-keyed CFF, the registry), so our builds keep `pdf_enable_freetype` on; a Fontations-only build would need -Skrifa versions of them. +Skrifa versions of them. When upstream changes CPVT's word breaking, the layout follows on purpose: +a field breaks its lines where upstream's would. Keep `font_index_` declared after +`epdf_attachment_` in `CPDF_Document`, so the index goes before the attachment and the parser. ### measurement **Rule.** Measurement dictionaries and dimension appearances.