diff --git a/core/fpdfapi/font/cpdf_font.cpp b/core/fpdfapi/font/cpdf_font.cpp index c0de42a8de..16923148ea 100644 --- a/core/fpdfapi/font/cpdf_font.cpp +++ b/core/fpdfapi/font/cpdf_font.cpp @@ -30,8 +30,10 @@ #include "core/fxcrt/check.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" #include "core/fxge/cfx_fontmapper.h" +#include "core/fxge/cfx_fontregistry.h" #include "core/fxge/cfx_substfont.h" #include "core/fxge/fx_font.h" #include "core/fxge/fx_fontencoding.h" @@ -395,14 +397,38 @@ const char* CPDF_Font::GetAdobeCharName( } uint32_t CPDF_Font::FallbackFontFromCharcode(uint32_t charcode) { + // EmbedPDF: prefer fonts registered through EPDFFont_* before falling back to + // PDFium's hard-coded Arial substitute. This lets broken PDFs with missing + // glyph coverage render through the same runtime fallback registry used by + // annotation authoring, without repairing or mutating the source PDF. + WideString str = UnicodeFromCharCode(charcode); + uint32_t unicode = !str.IsEmpty() ? str[0] : charcode; + for (size_t i = 0; i < font_fallbacks_.size(); ++i) { + if (font_fallbacks_[i]->GetFace() && + font_fallbacks_[i]->GetFace()->GetCharIndex(unicode) != 0) { + return pdfium::checked_cast(i); + } + } + + FX_SAFE_INT32 safe_weight = stem_v_; + safe_weight *= 5; + const int weight = safe_weight.ValueOrDefault(pdfium::kFontWeightNormal); + if (auto font_id = CFX_FontRegistry::FindFallbackFont(unicode, weight, + italic_angle_ != 0)) { + std::unique_ptr fallback_font = + CFX_FontRegistry::CreateFont(*font_id); + if (fallback_font) { + font_fallbacks_.push_back(std::move(fallback_font)); + return pdfium::checked_cast(font_fallbacks_.size() - 1); + } + } + if (font_fallbacks_.empty()) { - font_fallbacks_.push_back(std::make_unique()); - FX_SAFE_INT32 safe_weight = stem_v_; - safe_weight *= 5; - font_fallbacks_[0]->LoadSubst( - "Arial", IsTrueTypeFont(), flags_, - safe_weight.ValueOrDefault(pdfium::kFontWeightNormal), italic_angle_, - FX_CodePage::kDefANSI, IsVertWriting()); + auto fallback_font = std::make_unique(); + fallback_font->LoadSubst("Arial", IsTrueTypeFont(), flags_, weight, + italic_angle_, FX_CodePage::kDefANSI, + IsVertWriting()); + font_fallbacks_.push_back(std::move(fallback_font)); } return 0; } diff --git a/core/fpdfdoc/BUILD.gn b/core/fpdfdoc/BUILD.gn index f258efae31..912e46daba 100644 --- a/core/fpdfdoc/BUILD.gn +++ b/core/fpdfdoc/BUILD.gn @@ -13,6 +13,12 @@ source_set("fpdfdoc") { "cpdf_action.h", "cpdf_annot.cpp", "cpdf_annot.h", + # EmbedPDF: registered FreeText annotation fonts and per-layer subset + # embedding. Keep these fork-owned files when rebasing from upstream PDFium. + "cpdf_annotfontmap.cpp", + "cpdf_annotfontmap.h", + "cpdf_annotfontsubset.cpp", + "cpdf_annotfontsubset.h", "cpdf_annotlist.cpp", "cpdf_annotlist.h", "cpdf_apsettings.cpp", @@ -92,6 +98,9 @@ source_set("fpdfdoc") { "../fpdfapi/render", "../fxcrt", "../fxge", + # EmbedPDF: CPDF_AnnotFontSubset uses HarfBuzz subsetting to embed only the + # glyphs used by each saved annotation/layer. + "../../third_party/harfbuzz-ng", ] visibility = [ "../../*" ] } diff --git a/core/fpdfdoc/cpdf_annotfontmap.cpp b/core/fpdfdoc/cpdf_annotfontmap.cpp new file mode 100644 index 0000000000..4a9c88b270 --- /dev/null +++ b/core/fpdfdoc/cpdf_annotfontmap.cpp @@ -0,0 +1,308 @@ +// Copyright 2026 The EmbedPDF Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// EmbedPDF: annotation font map for registered runtime fonts. This lets +// FreeText appearance generation pick per-glyph fallback fonts and later embed +// only the glyph subset used by the annotation/layer. + +#include "core/fpdfdoc/cpdf_annotfontmap.h" + +#include +#include +#include + +#include "core/fpdfapi/font/cpdf_font.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_reference.h" +#include "core/fpdfdoc/cpdf_annotfontsubset.h" +#include "core/fpdfdoc/cpdf_interactiveform.h" +#include "core/fxcrt/check.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" +#include "core/fxge/cfx_font.h" + +namespace { + +constexpr char kRegisteredFontResourcePrefix[] = "ERegF"; + +ByteString ResourceKeyForRegisteredFont(CFX_FontRegistry::FontId font_id) { + return ByteString::Format("%s%u", kRegisteredFontResourcePrefix, font_id); +} + +bool PDFontSupportsUnicode(const RetainPtr& font, uint16_t word) { + if (!font) { + return false; + } + + uint32_t charcode = font->CharCodeFromUnicode(word); + if (charcode == CPDF_Font::kInvalidCharCode || (charcode == 0 && word != 0)) { + return false; + } + + bool vert_glyph = false; + return font->GlyphFromCharCode(charcode, &vert_glyph) > 0; +} + +} // namespace + +CPDF_AnnotFontMap::CPDF_AnnotFontMap(CPDF_Document* doc, + RetainPtr default_font, + const ByteString& default_font_alias, + bool allow_registered_fallbacks) + : doc_(doc), allow_registered_fallbacks_(allow_registered_fallbacks) { + FontEntry entry; + entry.font = std::move(default_font); + entry.alias = default_font_alias; + RetainPtr default_font_dict = + entry.font ? entry.font->GetFontDict() : nullptr; + if (auto font_id = + CPDF_AnnotFontSubset::GetRegisteredFontIdFromMarkerFontDict( + default_font_dict.Get())) { + RetainPtr registered_font = CreateRegisteredLayoutFont(*font_id); + if (registered_font) { + entry.font = std::move(registered_font); + entry.registered_font_id = *font_id; + } + } + fonts_.push_back(std::move(entry)); +} + +CPDF_AnnotFontMap::~CPDF_AnnotFontMap() { + DeleteTemporaryLayoutObjects(); +} + +// static +bool CPDF_AnnotFontMap::EnsureRegisteredFontMarkerInDocument( + CPDF_Document* doc, + CFX_FontRegistry::FontId font_id, + ByteString* resource_key) { + if (!doc || !resource_key || !CFX_FontRegistry::IsValidFont(font_id)) { + return false; + } + + RetainPtr root_dict = doc->GetMutableRoot(); + if (!root_dict) { + return false; + } + + RetainPtr acroform_dict = + root_dict->GetMutableDictFor("AcroForm"); + if (!acroform_dict) { + acroform_dict = CPDF_InteractiveForm::InitAcroFormDict(doc); + CHECK(acroform_dict); + } + + RetainPtr font_res = + acroform_dict->GetOrCreateDictFor("DR")->GetOrCreateDictFor("Font"); + + ByteString key = ResourceKeyForRegisteredFont(font_id); + if (RetainPtr existing_font_dict = + font_res->GetMutableDictFor(key.AsStringView())) { + // EmbedPDF: registered-font identity is stored in the marker dictionary, + // not inferred from the resource alias. This survives alias collisions, + // suffixes, and resource renaming during save/merge. + if (CPDF_AnnotFontSubset::GetRegisteredFontIdFromMarkerFontDict( + existing_font_dict.Get()) == font_id) { + *resource_key = key; + return true; + } + } + + const ByteString base_key = key; + for (int suffix = 1; font_res->KeyExist(key.AsStringView()); ++suffix) { + key = ByteString::Format("%s_%d", base_key.c_str(), suffix); + } + + RetainPtr marker_font_dict = + CPDF_AnnotFontSubset::CreateMarkerFontDict(doc, font_id); + if (!marker_font_dict) { + return false; + } + + font_res->SetNewFor(key, doc, marker_font_dict->GetObjNum()); + *resource_key = key; + return true; +} + +RetainPtr CPDF_AnnotFontMap::CreateFontResourceDict() { + if (!doc_) { + return nullptr; + } + + auto resource_font_dict = doc_->New(); + for (FontEntry& entry : fonts_) { + if (!entry.font || entry.alias.IsEmpty()) { + continue; + } + + if (entry.registered_font_id != CFX_FontRegistry::kInvalidFontId) { + RetainPtr subset_font_dict = + CPDF_AnnotFontSubset::CreateSubsetFontDict( + doc_, entry.registered_font_id, entry.glyph_to_unicode); + if (subset_font_dict) { + resource_font_dict->SetNewFor( + entry.alias, doc_, subset_font_dict->GetObjNum()); + } + continue; + } + + RetainPtr font_dict = entry.font->GetFontDict(); + if (!font_dict) { + continue; + } + + const uint32_t font_obj_num = font_dict->GetObjNum(); + if (font_obj_num != 0) { + resource_font_dict->SetNewFor(entry.alias, doc_, + font_obj_num); + } else { + resource_font_dict->SetFor(entry.alias, font_dict->Clone()); + } + } + return resource_font_dict; +} + +RetainPtr CPDF_AnnotFontMap::GetPDFFont(int32_t font_index) { + return fxcrt::IndexInBounds(fonts_, font_index) ? fonts_[font_index].font + : nullptr; +} + +ByteString CPDF_AnnotFontMap::GetPDFFontAlias(int32_t font_index) { + return fxcrt::IndexInBounds(fonts_, font_index) ? fonts_[font_index].alias + : ByteString(); +} + +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); + } + } + + if (!allow_registered_fallbacks_ || !fonts_.front().font) { + 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); + if (!font_id.has_value()) { + 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.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); + } + return PDFontSupportsUnicode(entry.font, word); +} + +void CPDF_AnnotFontMap::DeleteTemporaryLayoutObjects() { + fonts_.clear(); + if (!doc_) { + temporary_layout_object_numbers_.clear(); + return; + } + + for (uint32_t obj_num : temporary_layout_object_numbers_) { + doc_->DeleteIndirectObject(obj_num); + } + temporary_layout_object_numbers_.clear(); +} + +RetainPtr CPDF_AnnotFontMap::CreateRegisteredLayoutFont( + CFX_FontRegistry::FontId font_id) { + CPDF_AnnotFontSubset::LayoutFont layout_font = + CPDF_AnnotFontSubset::CreateLayoutFont(doc_, font_id); + temporary_layout_object_numbers_.insert( + temporary_layout_object_numbers_.end(), + layout_font.temporary_object_numbers.begin(), + layout_font.temporary_object_numbers.end()); + return std::move(layout_font.font); +} + +int32_t CPDF_AnnotFontMap::FindExistingRegisteredFont( + CFX_FontRegistry::FontId font_id) const { + for (size_t i = 0; i < fonts_.size(); ++i) { + if (fonts_[i].registered_font_id == font_id) { + return pdfium::checked_cast(i); + } + } + return -1; +} + +int32_t CPDF_AnnotFontMap::AddRegisteredFallbackFont( + CFX_FontRegistry::FontId font_id) { + RetainPtr font = CreateRegisteredLayoutFont(font_id); + if (!font) { + return -1; + } + + FontEntry entry; + entry.font = std::move(font); + entry.alias = ResourceKeyForRegisteredFont(font_id); + entry.registered_font_id = font_id; + fonts_.push_back(std::move(entry)); + return pdfium::checked_cast(fonts_.size() - 1); +} diff --git a/core/fpdfdoc/cpdf_annotfontmap.h b/core/fpdfdoc/cpdf_annotfontmap.h new file mode 100644 index 0000000000..19193dea09 --- /dev/null +++ b/core/fpdfdoc/cpdf_annotfontmap.h @@ -0,0 +1,72 @@ +// Copyright 2026 The EmbedPDF Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// EmbedPDF: annotation font map for registered runtime fonts. This file is +// fork-owned and supports FreeText fallback font routing/subsetting. + +#ifndef CORE_FPDFDOC_CPDF_ANNOTFONTMAP_H_ +#define CORE_FPDFDOC_CPDF_ANNOTFONTMAP_H_ + +#include + +#include +#include + +#include "core/fpdfdoc/ipvt_fontmap.h" +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/unowned_ptr.h" +#include "core/fxge/cfx_fontregistry.h" + +class CPDF_Dictionary; +class CPDF_Document; +class CPDF_Font; + +class CPDF_AnnotFontMap final : public IPVT_FontMap { + public: + CPDF_AnnotFontMap(CPDF_Document* doc, + RetainPtr default_font, + const ByteString& default_font_alias, + bool allow_registered_fallbacks); + ~CPDF_AnnotFontMap() override; + + static bool EnsureRegisteredFontMarkerInDocument( + CPDF_Document* doc, + CFX_FontRegistry::FontId font_id, + ByteString* resource_key); + + RetainPtr CreateFontResourceDict(); + + // 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; + + private: + struct FontEntry { + RetainPtr font; + ByteString alias; + CFX_FontRegistry::FontId registered_font_id = + CFX_FontRegistry::kInvalidFontId; + std::map glyph_to_unicode; + }; + + bool SupportsWord(int32_t font_index, uint16_t word) const; + void DeleteTemporaryLayoutObjects(); + RetainPtr CreateRegisteredLayoutFont( + CFX_FontRegistry::FontId font_id); + int32_t FindExistingRegisteredFont(CFX_FontRegistry::FontId font_id) const; + int32_t AddRegisteredFallbackFont(CFX_FontRegistry::FontId font_id); + + UnownedPtr const doc_; + const bool allow_registered_fallbacks_; + std::vector fonts_; + std::vector temporary_layout_object_numbers_; +}; + +#endif // CORE_FPDFDOC_CPDF_ANNOTFONTMAP_H_ diff --git a/core/fpdfdoc/cpdf_annotfontsubset.cpp b/core/fpdfdoc/cpdf_annotfontsubset.cpp new file mode 100644 index 0000000000..bec45b0be7 --- /dev/null +++ b/core/fpdfdoc/cpdf_annotfontsubset.cpp @@ -0,0 +1,708 @@ +// Copyright 2026 The EmbedPDF Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// EmbedPDF: builds PDF font dictionaries for registered annotation fonts, +// including per-annotation/layer subsets so large fallback fonts are not fully +// embedded into saved PDFs. + +#include "core/fpdfdoc/cpdf_annotfontsubset.h" + +#include +#include +#include +#include +#include + +#include "constants/font_encodings.h" +#include "core/fpdfapi/font/cpdf_font.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_name.h" +#include "core/fpdfapi/parser/cpdf_number.h" +#include "core/fpdfapi/parser/cpdf_reference.h" +#include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fpdfapi/parser/cpdf_string.h" +#include "core/fxcrt/check.h" +#include "core/fxcrt/check_op.h" +#include "core/fxcrt/containers/contains.h" +#include "core/fxcrt/data_vector.h" +#include "core/fxcrt/fx_extension.h" +#include "core/fxcrt/fx_safe_types.h" +#include "core/fxcrt/fx_string.h" +#include "core/fxcrt/numerics/safe_conversions.h" +#include "core/fxcrt/span.h" +#include "core/fxcrt/utf16.h" +#include "core/fxge/cfx_font.h" +#include "hb-subset.h" // nogncheck + +namespace { + +constexpr char kRegisteredFontIdKey[] = "EmbedPDFRegisteredFontId"; +constexpr uint32_t kMaxBfCharBfRangeEntries = 100; +constexpr uint32_t kMaxPdfCid = 0xffff; + +enum class ObjectStorage { + kDirect, + kIndirect, +}; + +ByteString NormalizeBaseFontName(ByteString name) { + name.Remove(' '); + return name.IsEmpty() ? ByteString(CFX_Font::kUntitledFontName) : name; +} + +ByteString BaseFontNameForRegisteredFont(CFX_FontRegistry::FontId font_id, + const CFX_Font* font) { + ByteString name = CFX_FontRegistry::GetBaseFontName(font_id); + if (name.IsEmpty() && font) { + name = font->GetBaseFontName(); + } + return NormalizeBaseFontName(std::move(name)); +} + +RetainPtr NewDictionary(CPDF_Document* doc, + ObjectStorage storage) { + return storage == ObjectStorage::kIndirect + ? doc->NewIndirect() + : pdfium::MakeRetain(); +} + +RetainPtr NewArray(CPDF_Document* doc, ObjectStorage storage) { + return storage == ObjectStorage::kIndirect ? doc->NewIndirect() + : pdfium::MakeRetain(); +} + +RetainPtr NewStream(CPDF_Document* doc, + pdfium::span data, + ObjectStorage storage) { + return storage == ObjectStorage::kIndirect + ? doc->NewIndirect(data) + : pdfium::MakeRetain(data); +} + +template +void SetReferenceOrDirect(CPDF_Dictionary* dict, + const ByteString& key, + CPDF_Document* doc, + RetainPtr object) { + if (!object) { + return; + } + + const uint32_t obj_num = object->GetObjNum(); + if (obj_num != 0) { + dict->SetNewFor(key, doc, obj_num); + return; + } + + dict->SetFor(key, RetainPtr(std::move(object))); +} + +template +void AppendReferenceOrDirect(CPDF_Array* array, + CPDF_Document* doc, + RetainPtr object) { + if (!object) { + return; + } + + const uint32_t obj_num = object->GetObjNum(); + if (obj_num != 0) { + array->AppendNew(doc, obj_num); + return; + } + + array->Append(RetainPtr(std::move(object))); +} + +ByteString MakeSubsetBaseFontName( + const ByteString& base_font_name, + const CPDF_AnnotFontSubset::GlyphUnicodeMap& glyph_to_unicode) { + // EmbedPDF: a deterministic six-letter PDF subset tag is enough to keep + // subsets distinguishable for readers/debugging. A theoretical hash collision + // is harmless because each AP resource dictionary still points at its own + // embedded subset font object. + uint32_t hash = 2166136261u; + auto mix = [&hash](uint32_t value) { + for (int i = 0; i < 4; ++i) { + hash ^= (value >> (i * 8)) & 0xff; + hash *= 16777619u; + } + }; + + for (const auto& [glyph_id, unicode] : glyph_to_unicode) { + mix(glyph_id); + mix(unicode); + } + + char prefix[7] = {}; + for (int i = 0; i < 6; ++i) { + prefix[i] = static_cast('A' + (hash % 26)); + hash = hash / 26 + 1; + } + return ByteString(prefix) + "+" + base_font_name; +} + +RetainPtr CreateCompositeFontDict(CPDF_Document* doc, + const ByteString& name, + ObjectStorage storage) { + auto font_dict = NewDictionary(doc, storage); + font_dict->SetNewFor("Type", "Font"); + font_dict->SetNewFor("Subtype", "Type0"); + font_dict->SetNewFor("Encoding", "Identity-H"); + font_dict->SetNewFor("BaseFont", name); + return font_dict; +} + +RetainPtr CreateCidFontDict(CPDF_Document* doc, + const ByteString& name, + ObjectStorage storage) { + auto cid_font_dict = NewDictionary(doc, storage); + cid_font_dict->SetNewFor("Type", "Font"); + cid_font_dict->SetNewFor("Subtype", "CIDFontType2"); + cid_font_dict->SetNewFor("BaseFont", name); + cid_font_dict->SetNewFor("CIDToGIDMap", "Identity"); + + auto cid_system_info_dict = pdfium::MakeRetain(); + cid_system_info_dict->SetNewFor("Registry", "Adobe"); + cid_system_info_dict->SetNewFor("Ordering", "Identity"); + cid_system_info_dict->SetNewFor("Supplement", 0); + cid_font_dict->SetFor("CIDSystemInfo", std::move(cid_system_info_dict)); + return cid_font_dict; +} + +RetainPtr LoadFontDesc( + CPDF_Document* doc, + const ByteString& font_name, + CFX_Font* font, + pdfium::span font_data, + ObjectStorage storage, + std::vector* temporary_object_numbers) { + auto font_descriptor_dict = NewDictionary(doc, storage); + font_descriptor_dict->SetNewFor("Type", "FontDescriptor"); + font_descriptor_dict->SetNewFor("FontName", font_name); + + int flags = pdfium::kFontStyleNonSymbolic; + if (font->IsFixedWidth()) { + flags |= pdfium::kFontStyleFixedPitch; + } + if (font_name.Contains("Serif")) { + flags |= pdfium::kFontStyleSerif; + } + if (font->IsItalic()) { + flags |= pdfium::kFontStyleItalic; + } + if (font->IsBold()) { + flags |= pdfium::kFontStyleForceBold; + } + font_descriptor_dict->SetNewFor("Flags", flags); + + FX_RECT bbox = font->GetBBox().value_or(FX_RECT()); + font_descriptor_dict->SetRectFor("FontBBox", CFX_FloatRect(bbox)); + font_descriptor_dict->SetNewFor("ItalicAngle", + font->IsItalic() ? -12 : 0); + font_descriptor_dict->SetNewFor("Ascent", font->GetAscent()); + font_descriptor_dict->SetNewFor("Descent", font->GetDescent()); + font_descriptor_dict->SetNewFor("CapHeight", font->GetAscent()); + font_descriptor_dict->SetNewFor("StemV", + font->IsBold() ? 120 : 70); + + RetainPtr stream = + storage == ObjectStorage::kDirect + ? doc->NewIndirect(font_data) + : NewStream(doc, font_data, ObjectStorage::kIndirect); + stream->GetMutableDict()->SetNewFor( + "Length1", pdfium::checked_cast(font_data.size())); + if (temporary_object_numbers && storage == ObjectStorage::kDirect) { + temporary_object_numbers->push_back(stream->GetObjNum()); + } + font_descriptor_dict->SetNewFor("FontFile2", doc, + stream->GetObjNum()); + return font_descriptor_dict; +} + +RetainPtr CreateWidthsArray( + CPDF_Document* doc, + const std::map& widths, + ObjectStorage storage) { + auto widths_array = NewArray(doc, storage); + for (auto it = widths.begin(); it != widths.end(); ++it) { + auto next_it = std::next(it); + + if (next_it != widths.end() && next_it->first == it->first + 1 && + next_it->second == it->second) { + widths_array->AppendNew(static_cast(it->first)); + + while (next_it != widths.end() && next_it->first == it->first + 1 && + next_it->second == it->second) { + it = next_it; + next_it = std::next(it); + } + widths_array->AppendNew(static_cast(it->first)); + widths_array->AppendNew(static_cast(it->second)); + continue; + } + + widths_array->AppendNew(static_cast(it->first)); + auto current_width_array = pdfium::MakeRetain(); + current_width_array->AppendNew(static_cast(it->second)); + + while (next_it != widths.end() && next_it->first == it->first + 1) { + it = next_it; + next_it = std::next(it); + current_width_array->AppendNew(static_cast(it->second)); + } + widths_array->Append(std::move(current_width_array)); + } + return widths_array; +} + +const char kToUnicodeStart[] = + "/CIDInit /ProcSet findresource begin\n" + "12 dict begin\n" + "begincmap\n" + "/CIDSystemInfo\n" + "<> def\n" + "/CMapName /Adobe-Identity-H def\n" + "/CMapType 2 def\n" + "1 begincodespacerange\n" + "<0000> \n" + "endcodespacerange\n"; + +const char kToUnicodeEnd[] = + "endcmap\n" + "CMapName currentdict /CMap defineresource pop\n" + "end\n" + "end\n"; + +void AddCharcode(fxcrt::ostringstream& buffer, uint32_t number) { + CHECK_LE(number, kMaxPdfCid); + buffer << "<"; + char ans[4]; + FXSYS_IntToFourHexChars(number, ans); + for (char c : ans) { + buffer << c; + } + buffer << ">"; +} + +void AddUnicode(fxcrt::ostringstream& buffer, uint32_t unicode) { + if (pdfium::IsHighSurrogate(unicode) || pdfium::IsLowSurrogate(unicode)) { + unicode = 0; + } + + char unicode_buf[8]; + pdfium::span unicode_span = FXSYS_ToUTF16BE(unicode, unicode_buf); + CHECK(!unicode_span.empty()); + buffer << "<"; + for (char c : unicode_span) { + buffer << c; + } + buffer << ">"; +} + +RetainPtr LoadUnicode( + CPDF_Document* doc, + const std::multimap& to_unicode, + ObjectStorage storage, + std::vector* temporary_object_numbers) { + std::map char_to_unicode_map; + std::map, std::vector> + char_range_to_unicodes_map; + std::map, uint32_t> + char_range_to_consecutive_unicodes_map; + + for (auto it = to_unicode.begin(); it != to_unicode.end(); ++it) { + uint32_t first_charcode = it->first; + uint32_t first_unicode = it->second; + { + auto next_it = std::next(it); + if (next_it == to_unicode.end() || first_charcode + 1 != next_it->first) { + char_to_unicode_map[first_charcode] = first_unicode; + continue; + } + } + + ++it; + uint32_t current_charcode = it->first; + uint32_t current_unicode = it->second; + if (current_charcode % 256 == 0) { + char_to_unicode_map[first_charcode] = first_unicode; + char_to_unicode_map[current_charcode] = current_unicode; + continue; + } + + const size_t max_extra = 255 - (current_charcode % 256); + auto next_it = std::next(it); + if (first_unicode + 1 != current_unicode) { + std::vector unicodes = {first_unicode, current_unicode}; + for (size_t i = 0; i < max_extra; ++i) { + if (next_it == to_unicode.end() || + current_charcode + 1 != next_it->first) { + break; + } + ++it; + ++current_charcode; + unicodes.push_back(it->second); + next_it = std::next(it); + } + CHECK_EQ(it->first - first_charcode + 1, unicodes.size()); + char_range_to_unicodes_map[std::make_pair(first_charcode, it->first)] = + std::move(unicodes); + continue; + } + + for (size_t i = 0; i < max_extra; ++i) { + if (next_it == to_unicode.end() || + current_charcode + 1 != next_it->first || + current_unicode + 1 != next_it->second) { + break; + } + ++it; + ++current_charcode; + ++current_unicode; + next_it = std::next(it); + } + char_range_to_consecutive_unicodes_map[std::make_pair( + first_charcode, current_charcode)] = first_unicode; + } + + fxcrt::ostringstream buffer; + buffer << kToUnicodeStart; + + uint32_t to_process = + pdfium::checked_cast(char_to_unicode_map.size()); + auto char_it = char_to_unicode_map.begin(); + while (to_process) { + const uint32_t count = std::min(to_process, kMaxBfCharBfRangeEntries); + buffer << count << " beginbfchar\n"; + for (uint32_t i = 0; i < count; ++i) { + CHECK(char_it != char_to_unicode_map.end()); + AddCharcode(buffer, char_it->first); + buffer << " "; + AddUnicode(buffer, char_it->second); + buffer << "\n"; + ++char_it; + } + buffer << "endbfchar\n"; + to_process -= count; + } + + to_process = + pdfium::checked_cast(char_range_to_unicodes_map.size()); + auto range_it = char_range_to_unicodes_map.begin(); + while (to_process) { + const uint32_t count = std::min(to_process, kMaxBfCharBfRangeEntries); + buffer << count << " beginbfrange\n"; + for (uint32_t i = 0; i < count; ++i) { + CHECK(range_it != char_range_to_unicodes_map.end()); + AddCharcode(buffer, range_it->first.first); + buffer << " "; + AddCharcode(buffer, range_it->first.second); + buffer << " ["; + auto unicodes = pdfium::span(range_it->second); + AddUnicode(buffer, unicodes[0]); + for (uint32_t code : unicodes.subspan(1u)) { + buffer << " "; + AddUnicode(buffer, code); + } + buffer << "]\n"; + ++range_it; + } + buffer << "endbfrange\n"; + to_process -= count; + } + + to_process = pdfium::checked_cast( + char_range_to_consecutive_unicodes_map.size()); + auto consecutive_it = char_range_to_consecutive_unicodes_map.begin(); + while (to_process) { + const uint32_t count = std::min(to_process, kMaxBfCharBfRangeEntries); + buffer << count << " beginbfrange\n"; + for (uint32_t i = 0; i < count; ++i) { + CHECK(consecutive_it != char_range_to_consecutive_unicodes_map.end()); + AddCharcode(buffer, consecutive_it->first.first); + buffer << " "; + AddCharcode(buffer, consecutive_it->first.second); + buffer << " "; + AddUnicode(buffer, consecutive_it->second); + buffer << "\n"; + ++consecutive_it; + } + buffer << "endbfrange\n"; + to_process -= count; + } + + buffer << kToUnicodeEnd; + RetainPtr stream = doc->NewIndirect(&buffer); + if (temporary_object_numbers && storage == ObjectStorage::kDirect) { + temporary_object_numbers->push_back(stream->GetObjNum()); + } + return stream; +} + +void CreateDescendantFontsArray(CPDF_Document* doc, + CPDF_Dictionary* font_dict, + RetainPtr cid_font_dict) { + auto descendant_fonts_array = + font_dict->SetNewFor("DescendantFonts"); + AppendReferenceOrDirect(descendant_fonts_array.Get(), doc, + std::move(cid_font_dict)); +} + +DataVector SubsetFontDataRetainGids( + pdfium::span font_data, + const CPDF_AnnotFontSubset::GlyphUnicodeMap& glyph_to_unicode) { + if (font_data.empty() || glyph_to_unicode.empty()) { + return DataVector(); + } + + hb_blob_t* source_blob = + hb_blob_create(reinterpret_cast(font_data.data()), + pdfium::checked_cast(font_data.size()), + HB_MEMORY_MODE_READONLY, nullptr, nullptr); + if (!source_blob) { + return DataVector(); + } + + hb_face_t* source_face = hb_face_create(source_blob, 0); + hb_blob_destroy(source_blob); + if (!source_face) { + return DataVector(); + } + + hb_subset_input_t* input = hb_subset_input_create_or_fail(); + if (!input) { + hb_face_destroy(source_face); + return DataVector(); + } + + hb_set_t* glyph_set = hb_subset_input_glyph_set(input); + hb_set_add(glyph_set, 0); + for (const auto& [glyph_id, unicode] : glyph_to_unicode) { + if (glyph_id <= kMaxPdfCid) { + hb_set_add(glyph_set, glyph_id); + } + } + + hb_subset_input_set_flags( + input, HB_SUBSET_FLAGS_RETAIN_GIDS | HB_SUBSET_FLAGS_NO_HINTING); + + hb_face_t* subset_face = hb_subset_or_fail(source_face, input); + hb_subset_input_destroy(input); + hb_face_destroy(source_face); + if (!subset_face) { + return DataVector(); + } + + hb_blob_t* subset_blob = hb_face_reference_blob(subset_face); + hb_face_destroy(subset_face); + if (!subset_blob) { + return DataVector(); + } + + unsigned int subset_length = 0; + const char* subset_data = hb_blob_get_data(subset_blob, &subset_length); + DataVector result; + if (subset_data && subset_length > 0) { + result = DataVector( + reinterpret_cast(subset_data), + reinterpret_cast(subset_data) + subset_length); + } + hb_blob_destroy(subset_blob); + return result; +} + +RetainPtr BuildCompositeFont( + CPDF_Document* doc, + CFX_Font* font, + const ByteString& base_font_name, + pdfium::span font_data, + const std::map& widths, + const std::multimap& to_unicode, + ObjectStorage storage, + std::vector* temporary_object_numbers) { + if (!doc || !font || widths.empty() || to_unicode.empty()) { + return nullptr; + } + + RetainPtr font_dict = + CreateCompositeFontDict(doc, base_font_name, storage); + RetainPtr cid_font_dict = + CreateCidFontDict(doc, base_font_name, storage); + + RetainPtr font_descriptor_dict = LoadFontDesc( + doc, base_font_name, font, font_data, storage, temporary_object_numbers); + SetReferenceOrDirect(cid_font_dict.Get(), "FontDescriptor", doc, + std::move(font_descriptor_dict)); + + RetainPtr widths_array = CreateWidthsArray(doc, widths, storage); + SetReferenceOrDirect(cid_font_dict.Get(), "W", doc, std::move(widths_array)); + + CreateDescendantFontsArray(doc, font_dict.Get(), std::move(cid_font_dict)); + + RetainPtr to_unicode_stream = + LoadUnicode(doc, to_unicode, storage, temporary_object_numbers); + SetReferenceOrDirect(font_dict.Get(), "ToUnicode", doc, + std::move(to_unicode_stream)); + return font_dict; +} + +} // namespace + +CPDF_AnnotFontSubset::LayoutFont::LayoutFont() = default; + +CPDF_AnnotFontSubset::LayoutFont::LayoutFont(LayoutFont&& that) noexcept = + default; + +CPDF_AnnotFontSubset::LayoutFont& CPDF_AnnotFontSubset::LayoutFont::operator=( + LayoutFont&& that) noexcept = default; + +CPDF_AnnotFontSubset::LayoutFont::~LayoutFont() = default; + +// static +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; + } + + std::unique_ptr font = CFX_FontRegistry::CreateFont(font_id); + if (!font || !font->HasAnyGlyphs()) { + return result; + } + + auto char_codes_and_indices = + font->GetCharCodesAndIndices(pdfium::kMaximumSupplementaryCodePoint); + if (char_codes_and_indices.empty()) { + return result; + } + + std::multimap to_unicode; + std::map widths; + for (const auto& item : char_codes_and_indices) { + if (item.glyph_index > kMaxPdfCid) { + continue; + } + if (!pdfium::Contains(widths, item.glyph_index)) { + widths[item.glyph_index] = font->GetGlyphWidth(item.glyph_index); + } + to_unicode.emplace(item.glyph_index, item.char_code); + } + if (widths.empty() || to_unicode.empty()) { + return result; + } + + const ByteString base_font_name = + BaseFontNameForRegisteredFont(font_id, font.get()); + RetainPtr font_dict = BuildCompositeFont( + doc, font.get(), base_font_name, font->GetFontSpan(), widths, to_unicode, + ObjectStorage::kDirect, &result.temporary_object_numbers); + result.font = CPDF_Font::Create(doc, std::move(font_dict), nullptr); + return result; +} + +// static +RetainPtr CPDF_AnnotFontSubset::CreateSubsetFontDict( + CPDF_Document* doc, + CFX_FontRegistry::FontId font_id, + const GlyphUnicodeMap& glyph_to_unicode) { + if (!doc || glyph_to_unicode.empty() || + !CFX_FontRegistry::IsValidFont(font_id)) { + return nullptr; + } + + std::unique_ptr font = CFX_FontRegistry::CreateFont(font_id); + if (!font || !font->HasAnyGlyphs()) { + return nullptr; + } + + GlyphUnicodeMap filtered_glyph_to_unicode; + std::map widths; + std::multimap to_unicode; + for (const auto& [glyph_id, unicode] : glyph_to_unicode) { + if (glyph_id == 0 || glyph_id > kMaxPdfCid) { + continue; + } + filtered_glyph_to_unicode.emplace(glyph_id, unicode); + widths[glyph_id] = font->GetGlyphWidth(glyph_id); + to_unicode.emplace(glyph_id, unicode); + } + if (filtered_glyph_to_unicode.empty()) { + return nullptr; + } + + DataVector subset_font_data = + SubsetFontDataRetainGids(font->GetFontSpan(), filtered_glyph_to_unicode); + pdfium::span font_data = subset_font_data.empty() + ? font->GetFontSpan() + : pdfium::span(subset_font_data); + + const ByteString base_font_name = + BaseFontNameForRegisteredFont(font_id, font.get()); + const ByteString subset_font_name = + MakeSubsetBaseFontName(base_font_name, filtered_glyph_to_unicode); + return BuildCompositeFont(doc, font.get(), subset_font_name, font_data, + widths, to_unicode, ObjectStorage::kIndirect, + /*temporary_object_numbers=*/nullptr); +} + +// static +RetainPtr CPDF_AnnotFontSubset::CreateMarkerFontDict( + CPDF_Document* doc, + CFX_FontRegistry::FontId font_id) { + if (!doc || !CFX_FontRegistry::IsValidFont(font_id)) { + return nullptr; + } + + auto font_dict = doc->NewIndirect(); + font_dict->SetNewFor("Type", "Font"); + font_dict->SetNewFor("Subtype", "Type1"); + font_dict->SetNewFor( + "BaseFont", BaseFontNameForRegisteredFont(font_id, nullptr)); + font_dict->SetNewFor("Encoding", + pdfium::font_encodings::kWinAnsiEncoding); + font_dict->SetNewFor(kRegisteredFontIdKey, + ByteString::Format("%u", font_id)); + return font_dict; +} + +// static +std::optional +CPDF_AnnotFontSubset::GetRegisteredFontIdFromMarkerFontDict( + const CPDF_Dictionary* font_dict) { + if (!font_dict) { + return std::nullopt; + } + + ByteString id_string = font_dict->GetByteStringFor(kRegisteredFontIdKey); + if (id_string.IsEmpty()) { + return std::nullopt; + } + + uint32_t font_id = 0; + for (char ch : id_string.AsStringView()) { + if (ch < '0' || ch > '9') { + return std::nullopt; + } + FX_SAFE_UINT32 safe_font_id = font_id; + safe_font_id *= 10; + safe_font_id += ch - '0'; + if (!safe_font_id.IsValid()) { + return std::nullopt; + } + font_id = safe_font_id.ValueOrDie(); + } + + if (!CFX_FontRegistry::IsValidFont(font_id)) { + return std::nullopt; + } + return font_id; +} diff --git a/core/fpdfdoc/cpdf_annotfontsubset.h b/core/fpdfdoc/cpdf_annotfontsubset.h new file mode 100644 index 0000000000..a1118be63d --- /dev/null +++ b/core/fpdfdoc/cpdf_annotfontsubset.h @@ -0,0 +1,57 @@ +// Copyright 2026 The EmbedPDF Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// EmbedPDF: fork-owned helper for registered annotation font layout and +// per-annotation/layer subset embedding. + +#ifndef CORE_FPDFDOC_CPDF_ANNOTFONTSUBSET_H_ +#define CORE_FPDFDOC_CPDF_ANNOTFONTSUBSET_H_ + +#include + +#include +#include +#include + +#include "core/fxcrt/retain_ptr.h" +#include "core/fxge/cfx_fontregistry.h" + +class CPDF_Dictionary; +class CPDF_Document; +class CPDF_Font; + +class CPDF_AnnotFontSubset final { + public: + using GlyphUnicodeMap = std::map; + + struct LayoutFont { + LayoutFont(); + LayoutFont(LayoutFont&& that) noexcept; + LayoutFont& operator=(LayoutFont&& that) noexcept; + ~LayoutFont(); + + LayoutFont(const LayoutFont&) = delete; + LayoutFont& operator=(const LayoutFont&) = delete; + + RetainPtr font; + std::vector temporary_object_numbers; + }; + + static LayoutFont CreateLayoutFont(CPDF_Document* doc, + CFX_FontRegistry::FontId font_id); + + static RetainPtr CreateSubsetFontDict( + CPDF_Document* doc, + CFX_FontRegistry::FontId font_id, + const GlyphUnicodeMap& glyph_to_unicode); + + static RetainPtr CreateMarkerFontDict( + CPDF_Document* doc, + CFX_FontRegistry::FontId font_id); + + static std::optional + GetRegisteredFontIdFromMarkerFontDict(const CPDF_Dictionary* font_dict); +}; + +#endif // CORE_FPDFDOC_CPDF_ANNOTFONTSUBSET_H_ diff --git a/core/fpdfdoc/cpdf_generateap.cpp b/core/fpdfdoc/cpdf_generateap.cpp index e399eb0635..b60948a594 100644 --- a/core/fpdfdoc/cpdf_generateap.cpp +++ b/core/fpdfdoc/cpdf_generateap.cpp @@ -34,6 +34,8 @@ #include "core/fpdfapi/parser/fpdf_parser_decode.h" #include "core/fpdfapi/parser/fpdf_parser_utility.h" #include "core/fpdfdoc/cpdf_annot.h" +#include "core/fpdfdoc/cpdf_annotfontmap.h" +#include "core/fpdfdoc/cpdf_annotfontsubset.h" #include "core/fpdfdoc/cpdf_cloudy_border.h" #include "core/fpdfdoc/cpdf_color_utils.h" #include "core/fpdfdoc/cpdf_defaultappearance.h" @@ -45,6 +47,7 @@ #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/fx_system.h" #include "core/fxcrt/notreached.h" +#include "core/fxge/cfx_fontregistry.h" #include "core/fxge/cfx_renderdevice.h" namespace { @@ -262,9 +265,12 @@ ByteString GetPDFWordString(IPVT_FontMap* font_map, } ByteString word_string; - uint32_t char_code = pdf_font->CharCodeFromUnicode(word); - if (char_code != CPDF_Font::kInvalidCharCode) { - pdf_font->AppendChar(&word_string, char_code); + // EmbedPDF: route unicode-to-charcode mapping through IPVT_FontMap so + // CPDF_AnnotFontMap can use registered fallback fonts and subset-local glyph + // ids when generating FreeText appearance streams. + int32_t char_code = font_map->CharCodeFromUnicode(font_index, word); + if (char_code >= 0) { + pdf_font->AppendChar(&word_string, static_cast(char_code)); } return word_string; } @@ -1839,7 +1845,11 @@ bool GenerateFreeTextAP(APGenerationTarget* target, actual_text_color = fpdfdoc::CFXColorFromArray(*tc); } - CPVT_FontMap map(doc, nullptr, std::move(default_font), font_name); + // EmbedPDF: use the annotation font map instead of CPVT_FontMap so + // FreeText AP generation can fall back to registered fonts and produce + // persistent, per-annotation subsets when saving. + CPDF_AnnotFontMap map(doc, std::move(default_font), font_name, + target->IsPersistent()); CPVT_VariableText::Provider provider(&map); CPVT_VariableText vt(&provider); @@ -1882,8 +1892,9 @@ bool GenerateFreeTextAP(APGenerationTarget* target, // Finalize AP dict. auto graphics_state_dict = GenerateExtGStateDict(*annot_dict, blend_name); - auto resource_font_dict = - GenerateResourceFontDict(doc, font_name, font_dict.Get()); + // EmbedPDF: collect both the original DA font and any registered fallback + // fonts actually used by this annotation into the AP resource dictionary. + auto resource_font_dict = map.CreateFontResourceDict(); auto resource_dict = GenerateResourcesDict( doc, std::move(graphics_state_dict), std::move(resource_font_dict)); GenerateAndSetAPDict(target, annot_dict, &appearance_stream, @@ -1916,7 +1927,9 @@ bool GenerateFreeTextAP(APGenerationTarget* target, appearance_stream << "q\n" << border_stream << "Q\n"; } - CPVT_FontMap map(doc, nullptr, std::move(default_font), font_name); + // EmbedPDF: same registered-font/subset path as the callout branch above. + CPDF_AnnotFontMap map(doc, std::move(default_font), font_name, + target->IsPersistent()); CPVT_VariableText::Provider provider(&map); CPVT_VariableText vt(&provider); @@ -1961,8 +1974,9 @@ bool GenerateFreeTextAP(APGenerationTarget* target, } auto graphics_state_dict = GenerateExtGStateDict(*annot_dict, blend_name); - auto resource_font_dict = - GenerateResourceFontDict(doc, font_name, font_dict.Get()); + // EmbedPDF: include registered fallback subset fonts used by this FreeText + // appearance, scoped to this annotation/layer. + auto resource_font_dict = map.CreateFontResourceDict(); auto resource_dict = GenerateResourcesDict( doc, std::move(graphics_state_dict), std::move(resource_font_dict)); if (rot_info.is_rotated) { @@ -2984,6 +2998,12 @@ bool GenerateFormAPToTarget(APGenerationTarget* target, if (!default_font) { return false; } + const bool use_registered_font_map = + target->IsPersistent() && + (CFX_FontRegistry::HasFallbackFonts() || + CPDF_AnnotFontSubset::GetRegisteredFontIdFromMarkerFontDict( + font_dict.Get()) + .has_value()); const AnnotationDimensionsAndColor dims = GetAnnotationDimensionsAndColor(annot_dict); @@ -3018,26 +3038,53 @@ bool GenerateFormAPToTarget(APGenerationTarget* target, std::move(resource_font_dict)); } + auto generate_form_stream = [&](CPVT_VariableText::Provider& provider, + fxcrt::ostringstream& app_stream) { + switch (type) { + case CPDF_GenerateAP::kTextField: + GenerateTextFieldFormAP(app_stream, annot_dict, dims.bbox, + default_appearance_info.value(), provider); + break; + case CPDF_GenerateAP::kComboBox: + GenerateComboBoxFormAP(app_stream, annot_dict, dims.bbox, + default_appearance_info.value(), provider); + break; + case CPDF_GenerateAP::kListBox: + GenerateListBoxFormAP(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); + 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"); + stream_resources->SetFor("Font", map.CreateFontResourceDict()); + 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; - switch (type) { - case CPDF_GenerateAP::kTextField: - GenerateTextFieldFormAP(app_stream, annot_dict, dims.bbox, - default_appearance_info.value(), provider); - break; - case CPDF_GenerateAP::kComboBox: - GenerateComboBoxFormAP(app_stream, annot_dict, dims.bbox, - default_appearance_info.value(), provider); - break; - case CPDF_GenerateAP::kListBox: - GenerateListBoxFormAP(app_stream, annot_dict, dims.bbox, - default_appearance_info.value(), provider); - break; - } + generate_form_stream(provider, app_stream); if (!target->IsPersistent()) { return GenerateAPDict( @@ -3597,3 +3644,32 @@ bool CPDF_GenerateAP::UpdateDefaultAppearance(CPDF_Document* doc, annot_dict->SetNewFor("DA", da_color_part + " " + da_font_part); return true; } + +bool CPDF_GenerateAP::UpdateDefaultAppearanceRegisteredFont( + CPDF_Document* doc, + CPDF_Dictionary* annot_dict, + CFX_FontRegistry::FontId font_id, + float font_size, + const CFX_Color& color) { + // EmbedPDF: allow FreeText DA to reference a registered runtime font. The DA + // stores a lightweight marker resource; actual subset embedding happens when + // AP generation knows the characters used by this annotation/layer. + if (!doc || !annot_dict || !CFX_FontRegistry::IsValidFont(font_id)) { + return false; + } + + ByteString resource_key; + if (!CPDF_AnnotFontMap::EnsureRegisteredFontMarkerInDocument(doc, font_id, + &resource_key) || + resource_key.IsEmpty()) { + return false; + } + + ByteString da_font_part = StringFromFontNameAndSize(resource_key, font_size); + ByteString da_color_part = GenerateColorAP(color, PaintOperation::kFill); + da_color_part.TrimBack('\n'); + da_font_part.TrimBack('\n'); + + annot_dict->SetNewFor("DA", da_color_part + " " + da_font_part); + return true; +} diff --git a/core/fpdfdoc/cpdf_generateap.h b/core/fpdfdoc/cpdf_generateap.h index c0b4cc1f4c..0b31f2d25f 100644 --- a/core/fpdfdoc/cpdf_generateap.h +++ b/core/fpdfdoc/cpdf_generateap.h @@ -10,6 +10,7 @@ #include #include "core/fpdfdoc/cpdf_annot.h" +#include "core/fxge/cfx_fontregistry.h" class CPDF_Dictionary; class CPDF_Document; @@ -73,6 +74,15 @@ class CPDF_GenerateAP { float font_size, const CFX_Color& color); + // EmbedPDF: Set FreeText DA to a registered runtime font. The actual AP path + // later embeds a subset for only the characters used by the annotation/layer. + static bool UpdateDefaultAppearanceRegisteredFont( + CPDF_Document* doc, + CPDF_Dictionary* annot_dict, + CFX_FontRegistry::FontId font_id, + float font_size, + const CFX_Color& color); + CPDF_GenerateAP() = delete; CPDF_GenerateAP(const CPDF_GenerateAP&) = delete; CPDF_GenerateAP& operator=(const CPDF_GenerateAP&) = delete; diff --git a/core/fpdfdoc/cpvt_fontmap.cpp b/core/fpdfdoc/cpvt_fontmap.cpp index 7f3ef705cc..cb44b89b34 100644 --- a/core/fpdfdoc/cpvt_fontmap.cpp +++ b/core/fpdfdoc/cpvt_fontmap.cpp @@ -16,7 +16,7 @@ #include "core/fpdfdoc/cpdf_interactiveform.h" #include "core/fxcrt/check.h" #include "core/fxcrt/fx_codepage.h" -#include "core/fxcrt/notreached.h" +#include "core/fxcrt/numerics/safe_conversions.h" CPVT_FontMap::CPVT_FontMap(CPDF_Document* doc, RetainPtr pResDict, @@ -81,14 +81,48 @@ ByteString CPVT_FontMap::GetPDFFontAlias(int32_t nFontIndex) { int32_t CPVT_FontMap::GetWordFontIndex(uint16_t word, FX_Charset charset, int32_t nFontIndex) { - NOTREACHED(); + // EmbedPDF: preserve upstream shared AP behavior for the default form/popup + // font map: choose the DA font first, then PDFium's native annotation font, + // based only on CharCodeFromUnicode(). Stricter glyph checks live in + // CPDF_AnnotFontMap, which is used only by registered FreeText fonts. + if (RetainPtr pDefFont = GetPDFFont(0)) { + if (pDefFont->CharCodeFromUnicode(word) != CPDF_Font::kInvalidCharCode) { + return 0; + } + } + if (RetainPtr pSysFont = GetPDFFont(1)) { + if (pSysFont->CharCodeFromUnicode(word) != CPDF_Font::kInvalidCharCode) { + return 1; + } + } + return -1; } int32_t CPVT_FontMap::CharCodeFromUnicode(int32_t nFontIndex, uint16_t word) { - NOTREACHED(); + // EmbedPDF: default implementation is equivalent to the old shared AP path's + // direct pdf_font->CharCodeFromUnicode() call. The hook exists so specialized + // font maps can return registered-font subset glyph ids. + RetainPtr font = GetPDFFont(nFontIndex); + if (!font) { + return -1; + } + uint32_t charcode = font->CharCodeFromUnicode(word); + if (charcode == CPDF_Font::kInvalidCharCode || + !pdfium::IsValueInRangeForNumericType(charcode)) { + return -1; + } + return static_cast(charcode); } FX_Charset CPVT_FontMap::CharSetFromUnicode(uint16_t word, FX_Charset nOldCharset) { - NOTREACHED(); + // EmbedPDF: provide a conservative default implementation for the PVT + // provider hook; registered annotation maps may override as needed. + if (word < 0x7F) { + return FX_Charset::kANSI; + } + if (nOldCharset != FX_Charset::kDefault) { + return nOldCharset; + } + return CFX_Font::GetCharSetFromUnicode(word); } diff --git a/core/fpdfdoc/cpvt_variabletext.cpp b/core/fpdfdoc/cpvt_variabletext.cpp index 60cb93a41d..63b0be70f3 100644 --- a/core/fpdfdoc/cpvt_variabletext.cpp +++ b/core/fpdfdoc/cpvt_variabletext.cpp @@ -68,17 +68,9 @@ int32_t CPVT_VariableText::Provider::GetTypeDescent(int32_t nFontIndex) { int32_t CPVT_VariableText::Provider::GetWordFontIndex(uint16_t word, FX_Charset charset, int32_t nFontIndex) { - if (RetainPtr pDefFont = font_map_->GetPDFFont(0)) { - if (pDefFont->CharCodeFromUnicode(word) != CPDF_Font::kInvalidCharCode) { - return 0; - } - } - if (RetainPtr pSysFont = font_map_->GetPDFFont(1)) { - if (pSysFont->CharCodeFromUnicode(word) != CPDF_Font::kInvalidCharCode) { - return 1; - } - } - return -1; + // EmbedPDF: delegate font choice to IPVT_FontMap so CPDF_AnnotFontMap can + // route individual FreeText glyphs to registered fallback fonts. + return font_map_->GetWordFontIndex(word, charset, nFontIndex); } int32_t CPVT_VariableText::Provider::GetDefaultFontIndex() { diff --git a/core/fxge/BUILD.gn b/core/fxge/BUILD.gn index 9604143bd5..4763ea406f 100644 --- a/core/fxge/BUILD.gn +++ b/core/fxge/BUILD.gn @@ -33,6 +33,10 @@ source_set("fxge") { "cfx_font.h", "cfx_fontmapper.cpp", "cfx_fontmapper.h", + # EmbedPDF: runtime font registry shared by page fallback rendering and + # annotation authoring/subset embedding. + "cfx_fontregistry.cpp", + "cfx_fontregistry.h", "cfx_fontmgr.cpp", "cfx_fontmgr.h", "cfx_gemodule.cpp", diff --git a/core/fxge/cfx_fontregistry.cpp b/core/fxge/cfx_fontregistry.cpp new file mode 100644 index 0000000000..36185b0f7b --- /dev/null +++ b/core/fxge/cfx_fontregistry.cpp @@ -0,0 +1,326 @@ +// Copyright 2026 The EmbedPDF Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// EmbedPDF: process/thread-local registry for runtime fonts. Registered fonts +// are used both for page-rendering fallback and for annotation authoring. + +#include "core/fxge/cfx_fontregistry.h" + +#include +#include +#include +#include +#include + +#include "core/fxcrt/containers/contains.h" +#include "core/fxcrt/data_vector.h" +#include "core/fxcrt/epdf_tls.h" +#include "core/fxcrt/fx_codepage.h" +#include "core/fxcrt/fx_stream.h" +#include "core/fxcrt/numerics/safe_conversions.h" +#include "core/fxcrt/stl_util.h" +#include "core/fxcrt/utf16.h" +#include "core/fxge/cfx_face.h" +#include "core/fxge/cfx_font.h" + +namespace { + +struct RegisteredFont { + CFX_FontRegistry::FontId id = CFX_FontRegistry::kInvalidFontId; + ByteString base_font_name; + int weight = pdfium::kFontWeightNormal; + bool italic = false; + std::vector supported_unicodes; + DataVector memory_data; + RetainPtr stream; +}; + +struct RegistryState { + CFX_FontRegistry::FontId next_font_id = 1; + std::vector> fonts; + std::vector fallback_order; +}; + +EPDF_TLS RegistryState* g_registry = nullptr; + +RegistryState* GetRegistry() { + if (!g_registry) { + g_registry = new RegistryState(); + } + return g_registry; +} + +RegisteredFont* GetRegisteredFont(CFX_FontRegistry::FontId font_id) { + if (font_id == CFX_FontRegistry::kInvalidFontId || !g_registry) { + return nullptr; + } + + for (const auto& font : g_registry->fonts) { + if (font && font->id == font_id) { + return font.get(); + } + } + return nullptr; +} + +ByteString NormalizeBaseFontName(ByteString name) { + name.Remove(' '); + return name.IsEmpty() ? ByteString(CFX_Font::kUntitledFontName) : name; +} + +int NormalizeWeight(int weight, const CFX_Font& font) { + if (weight >= 100 && weight <= 900) { + return weight; + } + return font.IsBold() ? pdfium::kFontWeightBold : pdfium::kFontWeightNormal; +} + +bool NormalizeItalic(int italic, const CFX_Font& font) { + if (italic == 0 || italic == 1) { + return italic == 1; + } + return font.IsItalic(); +} + +DataVector ReadStreamToData(IFX_SeekableReadStream* stream) { + if (!stream || stream->GetSize() <= 0 || + !pdfium::IsValueInRangeForNumericType(stream->GetSize())) { + return {}; + } + + DataVector data(pdfium::checked_cast(stream->GetSize())); + if (!stream->ReadBlockAtOffset(pdfium::span(data), /*offset=*/0)) { + return {}; + } + return data; +} + +std::unique_ptr LoadFont(pdfium::span data) { + if (data.empty()) { + return nullptr; + } + + auto font = std::make_unique(); + if (!font->LoadEmbedded(data, /*force_vertical=*/false, /*object_tag=*/0)) { + return nullptr; + } + return font; +} + +std::vector CollectSupportedUnicodes(CFX_Font* font) { + if (!font) { + 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; +} + +CFX_FontRegistry::FontId RegisterLoadedFontSource( + const ByteString& family_name, + int weight, + int italic, + pdfium::span data, + DataVector memory_data, + RetainPtr stream) { + if (data.empty()) { + return CFX_FontRegistry::kInvalidFontId; + } + + RegistryState* registry = GetRegistry(); + if (registry->next_font_id == + std::numeric_limits::max()) { + return CFX_FontRegistry::kInvalidFontId; + } + + std::unique_ptr font = LoadFont(data); + if (!font || !font->HasAnyGlyphs()) { + return CFX_FontRegistry::kInvalidFontId; + } + + std::vector supported_unicodes = + CollectSupportedUnicodes(font.get()); + if (supported_unicodes.empty()) { + return CFX_FontRegistry::kInvalidFontId; + } + + auto registered_font = std::make_unique(); + registered_font->id = registry->next_font_id++; + registered_font->base_font_name = NormalizeBaseFontName( + family_name.IsEmpty() ? font->GetBaseFontName() : family_name); + registered_font->weight = NormalizeWeight(weight, *font); + registered_font->italic = NormalizeItalic(italic, *font); + registered_font->supported_unicodes = std::move(supported_unicodes); + registered_font->memory_data = std::move(memory_data); + registered_font->stream = std::move(stream); + + const CFX_FontRegistry::FontId id = registered_font->id; + registry->fonts.push_back(std::move(registered_font)); + return id; +} + +int StyleScore(const RegisteredFont& font, int weight, bool italic) { + const int weight_score = std::abs(font.weight - weight); + const int italic_score = font.italic == italic ? 0 : 1000; + return weight_score + italic_score; +} + +} // namespace + +// static +CFX_FontRegistry::FontId CFX_FontRegistry::RegisterMemoryFont( + const ByteString& family_name, + int weight, + int italic, + pdfium::span data) { + if (data.empty()) { + return kInvalidFontId; + } + + DataVector memory_data(data.begin(), data.end()); + pdfium::span font_data(memory_data); + return RegisterLoadedFontSource(family_name, weight, italic, font_data, + std::move(memory_data), nullptr); +} + +// static +CFX_FontRegistry::FontId CFX_FontRegistry::RegisterFont( + const ByteString& family_name, + int weight, + int italic, + RetainPtr stream) { + DataVector data = ReadStreamToData(stream.Get()); + return RegisterLoadedFontSource(family_name, weight, italic, + pdfium::span(data), {}, std::move(stream)); +} + +// static +void CFX_FontRegistry::ClearRegisteredFonts() { + if (!g_registry) { + return; + } + g_registry->fallback_order.clear(); + 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 + // old marker resolve to a different font registered later in the same + // runtime/thread. +} + +// static +bool CFX_FontRegistry::AddFallbackFont(FontId font_id) { + if (!IsValidFont(font_id)) { + return false; + } + + RegistryState* registry = GetRegistry(); + if (pdfium::Contains(registry->fallback_order, font_id)) { + return true; + } + registry->fallback_order.push_back(font_id); + return true; +} + +// static +void CFX_FontRegistry::ClearFallbackFonts() { + if (!g_registry) { + return; + } + g_registry->fallback_order.clear(); +} + +// static +bool CFX_FontRegistry::HasFallbackFonts() { + return g_registry && !g_registry->fallback_order.empty(); +} + +// static +bool CFX_FontRegistry::IsValidFont(FontId font_id) { + return GetRegisteredFont(font_id) != nullptr; +} + +// static +ByteString CFX_FontRegistry::GetBaseFontName(FontId font_id) { + RegisteredFont* font = GetRegisteredFont(font_id); + return font ? font->base_font_name : ByteString(); +} + +// static +int CFX_FontRegistry::GetStyleWeight(FontId font_id) { + RegisteredFont* font = GetRegisteredFont(font_id); + return font ? font->weight : pdfium::kFontWeightNormal; +} + +// static +bool CFX_FontRegistry::IsStyleItalic(FontId font_id) { + RegisteredFont* font = GetRegisteredFont(font_id); + return font && font->italic; +} + +// static +bool CFX_FontRegistry::SupportsUnicode(FontId font_id, uint32_t unicode) { + RegisteredFont* font = GetRegisteredFont(font_id); + if (!font) { + return false; + } + return std::ranges::binary_search(font->supported_unicodes, unicode); +} + +// static +std::optional +CFX_FontRegistry::FindFallbackFont(uint32_t unicode, int weight, bool italic) { + 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; + } + + const int score = StyleScore(*font, weight, italic); + if (!best_font_id.has_value() || score < best_score) { + best_font_id = font_id; + best_score = score; + } + } + return best_font_id; +} + +// static +std::unique_ptr CFX_FontRegistry::CreateFont(FontId font_id) { + RegisteredFont* registered_font = GetRegisteredFont(font_id); + if (!registered_font) { + return nullptr; + } + + if (!registered_font->memory_data.empty()) { + return LoadFont(pdfium::span(registered_font->memory_data)); + } + + DataVector data = ReadStreamToData(registered_font->stream.Get()); + return LoadFont(pdfium::span(data)); +} + +// static +void CFX_FontRegistry::DestroyGlobals() { + delete g_registry; + g_registry = nullptr; +} diff --git a/core/fxge/cfx_fontregistry.h b/core/fxge/cfx_fontregistry.h new file mode 100644 index 0000000000..736934d662 --- /dev/null +++ b/core/fxge/cfx_fontregistry.h @@ -0,0 +1,56 @@ +// Copyright 2026 The EmbedPDF Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// EmbedPDF: fork-owned runtime font registry shared by page fallback rendering +// and annotation appearance/subset embedding. + +#ifndef CORE_FXGE_CFX_FONTREGISTRY_H_ +#define CORE_FXGE_CFX_FONTREGISTRY_H_ + +#include + +#include +#include + +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/span.h" + +class CFX_Font; +class IFX_SeekableReadStream; + +class CFX_FontRegistry { + public: + using FontId = uint32_t; + + static constexpr FontId kInvalidFontId = 0; + + static FontId RegisterMemoryFont(const ByteString& family_name, + int weight, + int italic, + pdfium::span data); + static FontId RegisterFont(const ByteString& family_name, + int weight, + int italic, + RetainPtr stream); + static void ClearRegisteredFonts(); + + static bool AddFallbackFont(FontId font_id); + static void ClearFallbackFonts(); + static bool HasFallbackFonts(); + + static bool IsValidFont(FontId font_id); + static ByteString GetBaseFontName(FontId font_id); + static int GetStyleWeight(FontId font_id); + static bool IsStyleItalic(FontId font_id); + static bool SupportsUnicode(FontId font_id, uint32_t unicode); + static std::optional FindFallbackFont(uint32_t unicode, + int weight, + bool italic); + static std::unique_ptr CreateFont(FontId font_id); + + static void DestroyGlobals(); +}; + +#endif // CORE_FXGE_CFX_FONTREGISTRY_H_ diff --git a/fpdfsdk/BUILD.gn b/fpdfsdk/BUILD.gn index 6c8aa9f80a..b9be6f88d4 100644 --- a/fpdfsdk/BUILD.gn +++ b/fpdfsdk/BUILD.gn @@ -8,6 +8,8 @@ import("../testing/test.gni") source_set("fpdfsdk") { sources = [ "epdf_base_document.cpp", + # EmbedPDF: implements EPDFFont_* runtime font registration APIs. + "epdf_font.cpp", "epdf_layer.cpp", "epdf_outline.cpp", "epdf_page_content_helpers.cpp", diff --git a/fpdfsdk/epdf_font.cpp b/fpdfsdk/epdf_font.cpp new file mode 100644 index 0000000000..efe9c04db5 --- /dev/null +++ b/fpdfsdk/epdf_font.cpp @@ -0,0 +1,73 @@ +// Copyright 2026 The EmbedPDF Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "public/epdf_font.h" + +#include +#include + +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/span.h" +#include "core/fxge/cfx_fontregistry.h" +#include "fpdfsdk/cpdfsdk_customaccess.h" + +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV +EPDFFont_RegisterFont(FPDF_BYTESTRING family_name, + int weight, + int italic, + FPDF_FILEACCESS* file_access) { + if (!file_access) { + return CFX_FontRegistry::kInvalidFontId; + } + + ByteString font_family_name(family_name ? family_name : ""); + return CFX_FontRegistry::RegisterFont( + font_family_name, weight, italic, + pdfium::MakeRetain(file_access)); +} + +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV +EPDFFont_RegisterMemFont(FPDF_BYTESTRING family_name, + int weight, + int italic, + const void* data_buf, + int size) { + if (size < 0) { + return CFX_FontRegistry::kInvalidFontId; + } + return EPDFFont_RegisterMemFont64(family_name, weight, italic, data_buf, + static_cast(size)); +} + +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV +EPDFFont_RegisterMemFont64(FPDF_BYTESTRING family_name, + int weight, + int italic, + const void* data_buf, + size_t size) { + if (!data_buf || size == 0) { + return CFX_FontRegistry::kInvalidFontId; + } + + ByteString font_family_name(family_name ? family_name : ""); + // SAFETY: required from caller. + auto font_data = + UNSAFE_BUFFERS(pdfium::span(static_cast(data_buf), size)); + return CFX_FontRegistry::RegisterMemoryFont(font_family_name, weight, italic, + font_data); +} + +FPDF_EXPORT void FPDF_CALLCONV EPDFFont_ClearRegisteredFonts(void) { + CFX_FontRegistry::ClearRegisteredFonts(); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFont_AddFallbackFont(EPDF_FONT_ID font_id) { + return CFX_FontRegistry::AddFallbackFont(font_id); +} + +FPDF_EXPORT void FPDF_CALLCONV EPDFFont_ClearFallbackFonts(void) { + CFX_FontRegistry::ClearFallbackFonts(); +} diff --git a/fpdfsdk/fpdf_annot.cpp b/fpdfsdk/fpdf_annot.cpp index 8659c21a9e..10ee70472b 100644 --- a/fpdfsdk/fpdf_annot.cpp +++ b/fpdfsdk/fpdf_annot.cpp @@ -50,6 +50,7 @@ #include "core/fxcrt/ptr_util.h" #include "core/fxcrt/stl_util.h" #include "core/fxge/cfx_color.h" +#include "core/fxge/cfx_fontregistry.h" #include "fpdfsdk/cpdfsdk_formfillenvironment.h" #include "fpdfsdk/cpdfsdk_helpers.h" #include "fpdfsdk/cpdfsdk_interactiveform.h" @@ -3644,6 +3645,46 @@ EPDFAnnot_SetDefaultAppearance(FPDF_ANNOTATION annot, doc, annot_dict.Get(), internal_font, font_size, CFX_Color(R, G, B)); } +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetDefaultAppearanceRegisteredFont(FPDF_ANNOTATION annot, + EPDF_FONT_ID font_id, + float font_size, + unsigned int R, + unsigned int G, + unsigned int B) { + // EmbedPDF: annotation-specific bridge from public API to the registered font + // AP pipeline. Generic EPDFFont_* registration lives in epdf_font.cpp because + // the same registry is also used for page-rendering fallback. + 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) { + return false; + } + + if (!CFX_FontRegistry::IsValidFont(font_id) || font_size < 0 || R > 255 || + G > 255 || B > 255) { + return false; + } + + return CPDF_GenerateAP::UpdateDefaultAppearanceRegisteredFont( + doc, annot_dict.Get(), font_id, font_size, CFX_Color(R, G, B)); +} + FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetDefaultAppearance(FPDF_ANNOTATION annot, FPDF_STANDARD_FONT* font, diff --git a/fpdfsdk/fpdf_annot_embeddertest.cpp b/fpdfsdk/fpdf_annot_embeddertest.cpp index 47449aed4e..4245f224e2 100644 --- a/fpdfsdk/fpdf_annot_embeddertest.cpp +++ b/fpdfsdk/fpdf_annot_embeddertest.cpp @@ -9,20 +9,30 @@ #include #include #include +#include +#include #include #include #include #include "build/build_config.h" #include "constants/annotation_common.h" +#include "core/fpdfapi/font/cpdf_tounicodemap.h" #include "core/fpdfapi/page/cpdf_annotcontext.h" +#include "core/fpdfapi/page/cpdf_page.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_name.h" +#include "core/fpdfapi/parser/cpdf_number.h" #include "core/fpdfapi/parser/cpdf_read_only_graph_guard.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/fxcrt/compiler_specific.h" #include "core/fxcrt/containers/contains.h" #include "core/fxcrt/fx_memcpy_wrappers.h" +#include "core/fxcrt/fx_safe_types.h" #include "core/fxcrt/fx_system.h" #include "core/fxcrt/span.h" #include "core/fxge/cfx_defaultrenderdevice.h" @@ -31,6 +41,7 @@ #include "public/fpdf_attachment.h" #include "public/fpdf_edit.h" #include "public/fpdf_formfill.h" +#include "public/fpdf_save.h" #include "public/fpdf_text.h" #include "public/fpdfview.h" #include "testing/embedder_test.h" @@ -38,9 +49,12 @@ #include "testing/fx_string_testhelpers.h" #include "testing/gmock/include/gmock/gmock-matchers.h" #include "testing/gtest/include/gtest/gtest.h" +#include "testing/utils/file_util.h" #include "testing/utils/hash.h" +#include "testing/utils/path_service.h" using pdfium::kAnnotationStampWithApPng; +using testing::HasSubstr; namespace { @@ -64,6 +78,67 @@ std::wstring ExtractPageText(FPDF_PAGE page) { return GetPlatformWString(buffer.data()); } +std::vector LoadNotoSansSCFontData() { + std::string font_path = PathService::GetThirdPartyFilePath( + "NotoSansCJK/NotoSansSC-Regular.subset.otf"); + if (font_path.empty()) { + ADD_FAILURE() << "Failed to find NotoSansSC subset font"; + return {}; + } + return GetFileContents(font_path.c_str()); +} + +std::vector LoadRobotoFontData() { + std::string font_path = PathService::GetThirdPartyFilePath( + "harfbuzz-ng/src/perf/fonts/Roboto-Regular.ttf"); + if (font_path.empty()) { + ADD_FAILURE() << "Failed to find Roboto test font"; + return {}; + } + return GetFileContents(font_path.c_str()); +} + +std::vector LoadDroidSansFallbackFullFontData() { + std::string font_path = + PathService::GetTestFilePath("fonts/DroidSansFallbackFull.ttf"); + if (font_path.empty()) { + ADD_FAILURE() << "Failed to find DroidSansFallbackFull test font"; + return {}; + } + return GetFileContents(font_path.c_str()); +} + +EPDF_FONT_ID RegisterDroidSansFallbackFullFont() { + std::vector font_data = LoadDroidSansFallbackFullFontData(); + if (font_data.empty()) { + return 0; + } + + 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)) { + ADD_FAILURE() << "Failed to register DroidSansFallbackFull as fallback"; + return 0; + } + return font_id; +} + +std::wstring GetNormalAppearance(FPDF_ANNOTATION annot) { + unsigned long length_bytes = + FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_NORMAL, nullptr, 0); + if (length_bytes == 0) { + ADD_FAILURE() << "Missing normal appearance stream"; + return L""; + } + + std::vector buffer = GetFPDFWideStringBuffer(length_bytes); + EXPECT_EQ(length_bytes, + FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_NORMAL, + buffer.data(), length_bytes)); + return GetPlatformWString(buffer.data()); +} + struct RedactionReport { std::vector object_numbers; uint32_t written_count = 0; @@ -71,6 +146,243 @@ struct RedactionReport { uint32_t nm_utf8_bytes_used = 0; }; +class ScopedRegisteredFonts { + public: + ScopedRegisteredFonts() { EPDFFont_ClearRegisteredFonts(); } + ~ScopedRegisteredFonts() { EPDFFont_ClearRegisteredFonts(); } +}; + +class MemoryFontFileAccess final : public FPDF_FILEACCESS { + public: + explicit MemoryFontFileAccess(std::vector data) + : data_(std::move(data)) { + m_FileLen = static_cast(data_.size()); + m_GetBlock = &MemoryFontFileAccess::GetBlock; + m_Param = this; + } + + private: + static int GetBlock(void* param, + unsigned long pos, + unsigned char* buf, + unsigned long size) { + auto* file_access = static_cast(param); + if (!file_access || !buf || pos > file_access->data_.size() || + size > file_access->data_.size() - pos) { + return 0; + } + + std::copy_n(file_access->data_.data() + pos, size, buf); + return 1; + } + + std::vector data_; +}; + +ByteString RegisteredFontAlias(EPDF_FONT_ID font_id) { + return ByteString::Format("ERegF%u", font_id); +} + +ByteString GetDefaultAppearanceFontAlias(FPDF_ANNOTATION annot) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + if (!context) { + return ByteString(); + } + + const CPDF_Dictionary* annot_dict = context->GetAnnotDict(); + if (!annot_dict) { + return ByteString(); + } + + ByteString da = annot_dict->GetByteStringFor("DA"); + std::optional slash_pos = da.Find('/'); + if (!slash_pos.has_value()) { + return ByteString(); + } + + ByteStringView remainder = da.AsStringView().Substr(slash_pos.value() + 1); + std::optional end_pos = remainder.Find(' '); + if (!end_pos.has_value()) { + return ByteString(remainder); + } + return ByteString(remainder.First(end_pos.value())); +} + +ByteString GetNormalAppearanceStreamBytes(FPDF_ANNOTATION annot) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + if (!context) { + return ByteString(); + } + + const CPDF_Dictionary* annot_dict = context->GetAnnotDict(); + if (!annot_dict) { + return ByteString(); + } + + RetainPtr ap_dict = + annot_dict->GetDictFor(pdfium::annotation::kAP); + RetainPtr normal_stream = + ap_dict ? ap_dict->GetStreamFor("N") : nullptr; + if (!normal_stream) { + return ByteString(); + } + + RetainPtr stream_acc = + pdfium::MakeRetain(std::move(normal_stream)); + stream_acc->LoadAllDataFiltered(); + return ByteString(ByteStringView(stream_acc->GetSpan())); +} + +RetainPtr GetAppearanceFontDict( + FPDF_ANNOTATION annot, + const ByteString& font_alias) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + if (!context) { + return nullptr; + } + + const CPDF_Dictionary* annot_dict = context->GetAnnotDict(); + if (!annot_dict) { + return nullptr; + } + + RetainPtr ap_dict = + annot_dict->GetDictFor(pdfium::annotation::kAP); + RetainPtr stream_dict = + ap_dict ? ap_dict->GetDictFor("N") : nullptr; + RetainPtr resources_dict = + stream_dict ? stream_dict->GetDictFor("Resources") : nullptr; + RetainPtr font_dict = + resources_dict ? resources_dict->GetDictFor("Font") : nullptr; + return font_dict ? font_dict->GetDictFor(font_alias.AsStringView()) : nullptr; +} + +bool AppearanceFontHasEmbeddedSubset(const CPDF_Dictionary* font_dict) { + if (!font_dict || font_dict->GetNameFor("Subtype") != "Type0" || + !font_dict->GetStreamFor("ToUnicode")) { + return false; + } + + RetainPtr descendant_fonts = + font_dict->GetArrayFor("DescendantFonts"); + if (!descendant_fonts || descendant_fonts->size() == 0) { + return false; + } + + RetainPtr cid_font_dict = + descendant_fonts->GetDictAt(0); + RetainPtr font_descriptor = + cid_font_dict ? cid_font_dict->GetDictFor("FontDescriptor") : nullptr; + if (!font_descriptor || !font_descriptor->GetStreamFor("FontFile2")) { + return false; + } + + ByteString base_font = font_dict->GetNameFor("BaseFont"); + return base_font.GetLength() > 7 && base_font[6] == '+'; +} + +bool AppearanceFontMapsUnicode(const CPDF_Dictionary* font_dict, + wchar_t value) { + RetainPtr to_unicode = + font_dict ? font_dict->GetStreamFor("ToUnicode") : nullptr; + if (!to_unicode) { + return false; + } + + CPDF_ToUnicodeMap to_unicode_map(std::move(to_unicode)); + return to_unicode_map.ReverseLookup(value) != 0; +} + +void ExpectRegisteredAppearanceMapsUnicode( + FPDF_ANNOTATION annot, + EPDF_FONT_ID font_id, + std::initializer_list unicodes) { + RetainPtr font_dict = + GetAppearanceFontDict(annot, RegisteredFontAlias(font_id)); + ASSERT_TRUE(font_dict); + EXPECT_TRUE(AppearanceFontHasEmbeddedSubset(font_dict.Get())); + for (wchar_t unicode : unicodes) { + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), unicode)); + } +} + +std::string BitmapChecksum(FPDF_BITMAP bitmap) { + if (!bitmap) { + return std::string(); + } + + const int stride = FPDFBitmap_GetStride(bitmap); + const int height = FPDFBitmap_GetHeight(bitmap); + FX_SAFE_SIZE_T size = stride; + size *= height; + if (!size.IsValid()) { + return std::string(); + } + + return GenerateMD5Base16( + pdfium::span(static_cast(FPDFBitmap_GetBuffer(bitmap)), + size.ValueOrDie())); +} + +bool BitmapHasNonWhitePixels(FPDF_BITMAP bitmap) { + if (!bitmap) { + return false; + } + + const int stride = FPDFBitmap_GetStride(bitmap); + const int height = FPDFBitmap_GetHeight(bitmap); + FX_SAFE_SIZE_T size = stride; + size *= height; + if (!size.IsValid()) { + return false; + } + + pdfium::span bytes( + static_cast(FPDFBitmap_GetBuffer(bitmap)), + size.ValueOrDie()); + return std::ranges::any_of(bytes, + [](uint8_t value) { return value != 0xff; }); +} + +void AddBrokenTrueTypeCjkTextPageContent(FPDF_DOCUMENT doc, FPDF_PAGE page) { + CPDF_Document* cpdf_doc = CPDFDocumentFromFPDFDocument(doc); + CPDF_Page* cpdf_page = CPDFPageFromFPDFPage(page); + ASSERT_TRUE(cpdf_doc); + ASSERT_TRUE(cpdf_page); + + auto font_dict = cpdf_doc->NewIndirect(); + font_dict->SetNewFor("Type", "Font"); + font_dict->SetNewFor("Subtype", "TrueType"); + font_dict->SetNewFor("BaseFont", "Helvetica"); + + auto encoding_dict = pdfium::MakeRetain(); + encoding_dict->SetNewFor("Type", "Encoding"); + auto differences = pdfium::MakeRetain(); + differences->AppendNew(65); + differences->AppendNew("uni8FD9"); + encoding_dict->SetFor("Differences", std::move(differences)); + font_dict->SetFor("Encoding", std::move(encoding_dict)); + + RetainPtr page_dict = cpdf_page->GetMutableDict(); + RetainPtr resources = + page_dict->GetOrCreateDictFor("Resources"); + RetainPtr font_resources = + resources->GetOrCreateDictFor("Font"); + font_resources->SetNewFor("F1", cpdf_doc, + font_dict->GetObjNum()); + + const ByteString kContent = + "BT\n" + "/F1 72 Tf\n" + "40 100 Td\n" + "<41> Tj\n" + "ET\n"; + RetainPtr contents = + cpdf_doc->NewIndirect(kContent.unsigned_span()); + page_dict->SetNewFor("Contents", cpdf_doc, + contents->GetObjNum()); +} + RedactionReport ApplyRedactionWithReport(FPDF_PAGE page, FPDF_ANNOTATION annot) { std::array removed = {}; @@ -506,6 +818,528 @@ TEST_F(FPDFAnnotEmbedderTest, ExplicitGenerateAppearanceAllowed) { EXPECT_GT(doc->GetLastObjNum(), before); } +TEST_F(FPDFAnnotEmbedderTest, TextFieldGenerateAppearanceStreamIsStable) { + ASSERT_TRUE(OpenDocument("text_form.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + + static const FS_POINTF kTextFieldPoint = {120.0f, 120.0f}; + ScopedFPDFAnnotation annot(FPDFAnnot_GetFormFieldAtPoint( + form_handle(), page.get(), &kTextFieldPoint)); + ASSERT_TRUE(annot); + + ASSERT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot.get())); + ByteString appearance = GetNormalAppearanceStreamBytes(annot.get()); + ASSERT_FALSE(appearance.IsEmpty()); + EXPECT_EQ("68a94799890022965d780f65db1e7430", + GenerateMD5Base16(appearance.unsigned_span())); +} + +TEST_F(FPDFAnnotEmbedderTest, FreeTextAppearanceUsesRegisteredMemoryFont) { + ScopedRegisteredFonts scoped_fonts; + + std::vector font_data = LoadNotoSansSCFontData(); + ASSERT_FALSE(font_data.empty()); + + EPDF_FONT_ID font_id = + EPDFFont_RegisterMemFont64("NotoSansSC", /*weight=*/400, /*italic=*/0, + font_data.data(), font_data.size()); + ASSERT_NE(0u, font_id); + + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 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"这是第一句。"); + ASSERT_TRUE( + FPDFAnnot_SetStringValue(annot.get(), "Contents", contents.get())); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearanceRegisteredFont(annot.get(), font_id, + 18.0f, 0, 0, 0)); + + FPDF_STANDARD_FONT font = FPDF_FONT_COURIER; + float font_size = 0.0f; + unsigned int r = 0; + unsigned int g = 0; + unsigned int b = 0; + ASSERT_TRUE(EPDFAnnot_GetDefaultAppearance(annot.get(), &font, &font_size, &r, + &g, &b)); + EXPECT_EQ(FPDF_FONT_UNKNOWN, font); + EXPECT_FLOAT_EQ(18.0f, font_size); + + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + EXPECT_THAT(GetNormalAppearance(annot.get()), HasSubstr(L"/ERegF")); +} + +TEST_F(FPDFAnnotEmbedderTest, FreeTextAppearanceFallsBackToRegisteredFont) { + ScopedRegisteredFonts scoped_fonts; + + std::vector font_data = LoadNotoSansSCFontData(); + ASSERT_FALSE(font_data.empty()); + + EPDF_FONT_ID font_id = + EPDFFont_RegisterMemFont64("NotoSansSC", /*weight=*/400, /*italic=*/0, + font_data.data(), font_data.size()); + ASSERT_NE(0u, font_id); + ASSERT_TRUE(EPDFFont_AddFallbackFont(font_id)); + + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 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"Hello 这是"); + ASSERT_TRUE( + FPDFAnnot_SetStringValue(annot.get(), "Contents", contents.get())); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), FPDF_FONT_HELVETICA, + 18.0f, 0, 0, 0)); + + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + std::wstring appearance = GetNormalAppearance(annot.get()); + EXPECT_THAT(appearance, HasSubstr(L"/Helv")); + EXPECT_THAT(appearance, HasSubstr(L"/ERegF")); +} + +TEST_F(FPDFAnnotEmbedderTest, FreeTextKoreanUsesRegisteredDroidFallbackFont) { + ScopedRegisteredFonts scoped_fonts; + + EPDF_FONT_ID font_id = RegisterDroidSansFallbackFullFont(); + ASSERT_NE(0u, font_id); + + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 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"Hello \xD55C\xAE00"); + ASSERT_TRUE( + FPDFAnnot_SetStringValue(annot.get(), "Contents", contents.get())); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), FPDF_FONT_HELVETICA, + 18.0f, 0, 0, 0)); + + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + ExpectRegisteredAppearanceMapsUnicode(annot.get(), font_id, + {L'\xD55C', L'\xAE00'}); +} + +TEST_F(FPDFAnnotEmbedderTest, FreeTextRegisteredFontEmbedsSubsetInSavedPdf) { + ScopedRegisteredFonts scoped_fonts; + + std::vector font_data = LoadRobotoFontData(); + ASSERT_FALSE(font_data.empty()); + + EPDF_FONT_ID font_id = + EPDFFont_RegisterMemFont64("Roboto", /*weight=*/400, /*italic=*/0, + font_data.data(), font_data.size()); + ASSERT_NE(0u, font_id); + + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 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"ABC"); + ASSERT_TRUE( + FPDFAnnot_SetStringValue(annot.get(), "Contents", contents.get())); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearanceRegisteredFont(annot.get(), font_id, + 18.0f, 0, 0, 0)); + + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + RetainPtr font_dict = + GetAppearanceFontDict(annot.get(), RegisteredFontAlias(font_id)); + ASSERT_TRUE(font_dict); + EXPECT_TRUE(AppearanceFontHasEmbeddedSubset(font_dict.Get())); + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'A')); + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'B')); + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'C')); + + unsigned long saved_size = 0; + void* saved_buffer = + EPDF_SaveDocumentToOwnedBuffer(doc.get(), /*flags=*/0, &saved_size); + ASSERT_TRUE(saved_buffer); + std::string saved_pdf(static_cast(saved_buffer), saved_size); + EPDF_FreeBuffer(saved_buffer); + + EXPECT_LT(saved_pdf.size(), font_data.size() / 2); +} + +TEST_F(FPDFAnnotEmbedderTest, FreeTextRegistersFontFromFileAccess) { + ScopedRegisteredFonts scoped_fonts; + + std::vector font_data = LoadRobotoFontData(); + ASSERT_FALSE(font_data.empty()); + const size_t original_font_size = font_data.size(); + MemoryFontFileAccess font_access(std::move(font_data)); + + EPDF_FONT_ID font_id = EPDFFont_RegisterFont("Roboto", /*weight=*/400, + /*italic=*/0, &font_access); + ASSERT_NE(0u, font_id); + + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 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"ABC"); + ASSERT_TRUE( + FPDFAnnot_SetStringValue(annot.get(), "Contents", contents.get())); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearanceRegisteredFont(annot.get(), font_id, + 18.0f, 0, 0, 0)); + + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + RetainPtr font_dict = + GetAppearanceFontDict(annot.get(), RegisteredFontAlias(font_id)); + ASSERT_TRUE(font_dict); + EXPECT_TRUE(AppearanceFontHasEmbeddedSubset(font_dict.Get())); + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'A')); + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'B')); + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'C')); + + unsigned long saved_size = 0; + void* saved_buffer = + EPDF_SaveDocumentToOwnedBuffer(doc.get(), /*flags=*/0, &saved_size); + ASSERT_TRUE(saved_buffer); + std::string saved_pdf(static_cast(saved_buffer), saved_size); + EPDF_FreeBuffer(saved_buffer); + + EXPECT_LT(saved_pdf.size(), original_font_size / 2); +} + +TEST_F(FPDFAnnotEmbedderTest, FreeTextRegisteredFontMarkerSurvivesAliasSuffix) { + ScopedRegisteredFonts scoped_fonts; + + std::vector font_data = LoadRobotoFontData(); + ASSERT_FALSE(font_data.empty()); + + EPDF_FONT_ID font_id = + EPDFFont_RegisterMemFont64("Roboto", /*weight=*/400, /*italic=*/0, + font_data.data(), font_data.size()); + ASSERT_NE(0u, font_id); + + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); + ASSERT_TRUE(page); + + CPDF_Document* cpdf_doc = CPDFDocumentFromFPDFDocument(doc.get()); + ASSERT_TRUE(cpdf_doc); + RetainPtr root_dict = cpdf_doc->GetMutableRoot(); + ASSERT_TRUE(root_dict); + RetainPtr font_resources = + root_dict->GetOrCreateDictFor("AcroForm") + ->GetOrCreateDictFor("DR") + ->GetOrCreateDictFor("Font"); + + auto colliding_font_dict = cpdf_doc->NewIndirect(); + colliding_font_dict->SetNewFor("Type", "Font"); + colliding_font_dict->SetNewFor("Subtype", "Type1"); + colliding_font_dict->SetNewFor("BaseFont", "Helvetica"); + const ByteString base_alias = RegisteredFontAlias(font_id); + font_resources->SetNewFor(base_alias, cpdf_doc, + colliding_font_dict->GetObjNum()); + + 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"ABC"); + ASSERT_TRUE( + FPDFAnnot_SetStringValue(annot.get(), "Contents", contents.get())); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearanceRegisteredFont(annot.get(), font_id, + 18.0f, 0, 0, 0)); + + ByteString actual_alias = GetDefaultAppearanceFontAlias(annot.get()); + ASSERT_FALSE(actual_alias.IsEmpty()); + EXPECT_NE(base_alias, actual_alias); + EXPECT_EQ(base_alias, actual_alias.First(base_alias.GetLength())); + + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + RetainPtr font_dict = + GetAppearanceFontDict(annot.get(), actual_alias); + ASSERT_TRUE(font_dict); + EXPECT_TRUE(AppearanceFontHasEmbeddedSubset(font_dict.Get())); + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'A')); + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'B')); + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'C')); +} + +TEST_F(FPDFAnnotEmbedderTest, TextFieldKoreanUsesRegisteredDroidFallbackFont) { + ScopedRegisteredFonts scoped_fonts; + EPDF_FONT_ID font_id = RegisterDroidSansFallbackFullFont(); + ASSERT_NE(0u, font_id); + + CreateEmptyDocument(); + { + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ASSERT_TRUE(page); + + ScopedFPDFWideString field_name = GetFPDFWideString(L"korean_text"); + ScopedFPDFAnnotation annot(EPDFPage_CreateFormField( + page.get(), form_handle(), FPDF_FORMFIELD_TEXTFIELD, field_name.get())); + ASSERT_TRUE(annot); + + const FS_RECTF rect{50.0f, 250.0f, 350.0f, 320.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), FPDF_FONT_HELVETICA, + 18.0f, 0, 0, 0)); + ScopedFPDFWideString value = GetFPDFWideString(L"\xD55C\xAE00"); + ASSERT_TRUE( + EPDFAnnot_SetFormFieldValue(form_handle(), annot.get(), value.get())); + + ASSERT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot.get())); + ExpectRegisteredAppearanceMapsUnicode(annot.get(), font_id, + {L'\xD55C', L'\xAE00'}); + } + CloseDocument(); +} + +TEST_F(FPDFAnnotEmbedderTest, ComboBoxKoreanUsesRegisteredDroidFallbackFont) { + ScopedRegisteredFonts scoped_fonts; + EPDF_FONT_ID font_id = RegisterDroidSansFallbackFullFont(); + ASSERT_NE(0u, font_id); + + CreateEmptyDocument(); + { + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ASSERT_TRUE(page); + + ScopedFPDFWideString field_name = GetFPDFWideString(L"korean_combo"); + ScopedFPDFAnnotation annot(EPDFPage_CreateFormField( + page.get(), form_handle(), FPDF_FORMFIELD_COMBOBOX, field_name.get())); + ASSERT_TRUE(annot); + + const FS_RECTF rect{50.0f, 250.0f, 350.0f, 320.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), FPDF_FONT_HELVETICA, + 18.0f, 0, 0, 0)); + + ScopedFPDFWideString latin_option = GetFPDFWideString(L"Latin"); + ScopedFPDFWideString korean_option = GetFPDFWideString(L"\xD55C\xAE00"); + FPDF_WIDESTRING labels[] = {latin_option.get(), korean_option.get()}; + ASSERT_TRUE( + EPDFAnnot_SetFormFieldOptions(form_handle(), annot.get(), labels, 2)); + ASSERT_TRUE(EPDFAnnot_SetFormFieldValue(form_handle(), annot.get(), + korean_option.get())); + + ASSERT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot.get())); + ExpectRegisteredAppearanceMapsUnicode(annot.get(), font_id, + {L'\xD55C', L'\xAE00'}); + } + CloseDocument(); +} + +TEST_F(FPDFAnnotEmbedderTest, ListBoxKoreanUsesRegisteredDroidFallbackFont) { + ScopedRegisteredFonts scoped_fonts; + EPDF_FONT_ID font_id = RegisterDroidSansFallbackFullFont(); + ASSERT_NE(0u, font_id); + + CreateEmptyDocument(); + { + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ASSERT_TRUE(page); + + ScopedFPDFWideString field_name = GetFPDFWideString(L"korean_list"); + ScopedFPDFAnnotation annot(EPDFPage_CreateFormField( + page.get(), form_handle(), FPDF_FORMFIELD_LISTBOX, field_name.get())); + ASSERT_TRUE(annot); + + const FS_RECTF rect{50.0f, 220.0f, 350.0f, 330.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), FPDF_FONT_HELVETICA, + 18.0f, 0, 0, 0)); + + ScopedFPDFWideString latin_option = GetFPDFWideString(L"Latin"); + ScopedFPDFWideString korean_option = GetFPDFWideString(L"\xD55C\xAE00"); + FPDF_WIDESTRING labels[] = {latin_option.get(), korean_option.get()}; + ASSERT_TRUE( + EPDFAnnot_SetFormFieldOptions(form_handle(), annot.get(), labels, 2)); + ASSERT_TRUE(EPDFAnnot_SetFormFieldValue(form_handle(), annot.get(), + korean_option.get())); + + ASSERT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot.get())); + ExpectRegisteredAppearanceMapsUnicode(annot.get(), font_id, + {L'\xD55C', L'\xAE00'}); + } + CloseDocument(); +} + +TEST_F(FPDFAnnotEmbedderTest, FreeTextRegisteredFontSubsetsAreLayerLocal) { + ScopedRegisteredFonts scoped_fonts; + + std::vector font_data = LoadRobotoFontData(); + ASSERT_FALSE(font_data.empty()); + + EPDF_FONT_ID font_id = + EPDFFont_RegisterMemFont64("Roboto", /*weight=*/400, /*italic=*/0, + font_data.data(), font_data.size()); + ASSERT_NE(0u, font_id); + + FileAccessForTesting base_access("rectangles.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + struct LayerSubsetResult { + std::string delta; + ByteString subset_base_font; + }; + + auto save_layer_with_text = [&](const wchar_t* text, + const std::vector& expected_chars, + const std::vector& unexpected_chars) { + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + EXPECT_TRUE(layer); + + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), 0)); + EXPECT_TRUE(page); + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FREETEXT)); + EXPECT_TRUE(annot); + + const FS_RECTF rect{50.0f, 250.0f, 350.0f, 320.0f}; + EXPECT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ScopedFPDFWideString contents = GetFPDFWideString(text); + EXPECT_TRUE( + FPDFAnnot_SetStringValue(annot.get(), "Contents", contents.get())); + EXPECT_TRUE(EPDFAnnot_SetDefaultAppearanceRegisteredFont( + annot.get(), font_id, 18.0f, 0, 0, 0)); + EXPECT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + + RetainPtr font_dict = + GetAppearanceFontDict(annot.get(), RegisteredFontAlias(font_id)); + EXPECT_TRUE(font_dict); + EXPECT_TRUE(AppearanceFontHasEmbeddedSubset(font_dict.Get())); + for (char value : expected_chars) { + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), value)); + } + for (char value : unexpected_chars) { + EXPECT_FALSE(AppearanceFontMapsUnicode(font_dict.Get(), value)); + } + + ByteString subset_base_font = + font_dict ? font_dict->GetNameFor("BaseFont") : ByteString(); + + unsigned long delta_size = 0; + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + void* delta_buffer = EPDFLayer_SaveDeltaToOwnedBuffer( + layer.get(), &delta_size, &save_status); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + EXPECT_TRUE(delta_buffer); + std::string delta(static_cast(delta_buffer), delta_size); + EPDF_FreeBuffer(delta_buffer); + return LayerSubsetResult{std::move(delta), std::move(subset_base_font)}; + }; + + LayerSubsetResult layer_a = + save_layer_with_text(L"ABC", {'A', 'B', 'C'}, {'D', 'E', 'F'}); + LayerSubsetResult layer_b = + save_layer_with_text(L"DEF", {'D', 'E', 'F'}, {'A', 'B', 'C'}); + EPDF_ReleaseBaseDocument(base); + + EXPECT_FALSE(layer_a.delta.empty()); + EXPECT_FALSE(layer_b.delta.empty()); + EXPECT_LT(layer_a.delta.size(), font_data.size() / 2); + EXPECT_LT(layer_b.delta.size(), font_data.size() / 2); + EXPECT_NE(layer_a.subset_base_font, layer_b.subset_base_font); + EXPECT_NE(std::string::npos, + layer_a.delta.find(layer_a.subset_base_font.c_str())); + EXPECT_NE(std::string::npos, + layer_b.delta.find(layer_b.subset_base_font.c_str())); + EXPECT_EQ(std::string::npos, + layer_a.delta.find(layer_b.subset_base_font.c_str())); + EXPECT_EQ(std::string::npos, + layer_b.delta.find(layer_a.subset_base_font.c_str())); +} + +TEST_F(FPDFAnnotEmbedderTest, RegisteredFallbackFontRendersPageMissingGlyph) { + auto create_broken_pdf = []() { + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + EXPECT_TRUE(doc); + { + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 200, 200)); + EXPECT_TRUE(page); + AddBrokenTrueTypeCjkTextPageContent(doc.get(), page.get()); + } + + unsigned long saved_size = 0; + void* saved_buffer = + EPDF_SaveDocumentToOwnedBuffer(doc.get(), /*flags=*/0, &saved_size); + EXPECT_TRUE(saved_buffer); + if (!saved_buffer) { + return std::vector(); + } + std::vector pdf_bytes( + static_cast(saved_buffer), + static_cast(saved_buffer) + saved_size); + EPDF_FreeBuffer(saved_buffer); + return pdf_bytes; + }; + + auto render_broken_pdf = [](const std::vector& pdf_bytes) { + MemoryFontFileAccess file_access(pdf_bytes); + ScopedFPDFDocument doc(FPDF_LoadCustomDocument(&file_access, nullptr)); + EXPECT_TRUE(doc); + ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); + EXPECT_TRUE(page); + ScopedFPDFBitmap bitmap = EmbedderTest::RenderPage(page.get()); + EXPECT_TRUE(bitmap); + return bitmap; + }; + + ScopedRegisteredFonts scoped_fonts; + std::vector broken_pdf = create_broken_pdf(); + ASSERT_FALSE(broken_pdf.empty()); + ScopedFPDFBitmap bitmap_without_fallback = render_broken_pdf(broken_pdf); + ASSERT_TRUE(bitmap_without_fallback); + std::string without_registered_fallback = + BitmapChecksum(bitmap_without_fallback.get()); + ASSERT_FALSE(without_registered_fallback.empty()); + + std::vector font_data = LoadNotoSansSCFontData(); + ASSERT_FALSE(font_data.empty()); + + EPDF_FONT_ID font_id = + EPDFFont_RegisterMemFont64("NotoSansSC", /*weight=*/400, /*italic=*/0, + font_data.data(), font_data.size()); + ASSERT_NE(0u, font_id); + ASSERT_TRUE(EPDFFont_AddFallbackFont(font_id)); + + ScopedFPDFBitmap bitmap = render_broken_pdf(broken_pdf); + ASSERT_TRUE(bitmap); + + EXPECT_TRUE(BitmapHasNonWhitePixels(bitmap.get())); + EXPECT_NE(without_registered_fallback, BitmapChecksum(bitmap.get())); +} + TEST_F(FPDFAnnotEmbedderTest, ExtractHighlightLongContent) { // Open a file with one annotation and load its first page. ASSERT_TRUE(OpenDocument("annotation_highlight_long_content.pdf")); diff --git a/fpdfsdk/fpdf_view.cpp b/fpdfsdk/fpdf_view.cpp index cf7ade5998..1fbdf16242 100644 --- a/fpdfsdk/fpdf_view.cpp +++ b/fpdfsdk/fpdf_view.cpp @@ -56,6 +56,7 @@ #include "core/fxcrt/stl_util.h" #include "core/fxcrt/unowned_ptr.h" #include "core/fxge/cfx_defaultrenderdevice.h" +#include "core/fxge/cfx_fontregistry.h" #include "core/fxge/cfx_gemodule.h" #include "core/fxge/cfx_glyphcache.h" #include "core/fxge/cfx_renderdevice.h" @@ -280,6 +281,9 @@ FPDF_EXPORT void FPDF_CALLCONV FPDF_DestroyLibrary() { CFX_GlyphCache::DestroyGlobals(); #endif + // EmbedPDF: registered runtime fonts are global/TLS-backed PDFium state, so + // tear them down with the rest of the library singletons. + CFX_FontRegistry::DestroyGlobals(); pdfium::DestroyPageModule(); CFX_GEModule::Destroy(); CFX_Timer::DestroyGlobals(); diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c index cc3ae907c3..2b37d4e687 100644 --- a/fpdfsdk/fpdf_view_c_api_test.c +++ b/fpdfsdk/fpdf_view_c_api_test.c @@ -9,6 +9,7 @@ #include "fpdfsdk/fpdf_view_c_api_test.h" +#include "public/epdf_font.h" #include "public/fpdf_annot.h" #include "public/fpdf_attachment.h" #include "public/fpdf_catalog.h" @@ -106,6 +107,15 @@ int CheckPDFiumCApi() { CHK(FPDFPage_GetAnnotCount); CHK(FPDFPage_GetAnnotIndex); CHK(FPDFPage_RemoveAnnot); + CHK(EPDFAnnot_SetDefaultAppearanceRegisteredFont); + + // epdf_font.h + CHK(EPDFFont_AddFallbackFont); + CHK(EPDFFont_ClearFallbackFonts); + CHK(EPDFFont_ClearRegisteredFonts); + CHK(EPDFFont_RegisterFont); + CHK(EPDFFont_RegisterMemFont); + CHK(EPDFFont_RegisterMemFont64); // fpdf_attachment.h CHK(FPDFAttachment_GetFile); diff --git a/public/epdf_font.h b/public/epdf_font.h new file mode 100644 index 0000000000..9f2a5f4e70 --- /dev/null +++ b/public/epdf_font.h @@ -0,0 +1,96 @@ +// Copyright 2026 The EmbedPDF Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PUBLIC_EPDF_FONT_H_ +#define PUBLIC_EPDF_FONT_H_ + +#include +#include + +// NOLINTNEXTLINE(build/include) +#include "fpdfview.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Experimental EmbedPDF Extension API. +typedef uint32_t EPDF_FONT_ID; + +// Experimental EmbedPDF Extension API. +// Register a font for runtime fallback use and PDF authoring from file access. +// Font registration follows PDFium's normal handle/thread ownership model. In +// TLS builds, register and use fonts on the initialized worker thread that owns +// the document/page handles, and call EPDF_ShutdownThread() on that worker to +// release registered font state. In non-TLS builds, do not mutate the registry +// concurrently with rendering, saving, or editing. +// +// family_name - optional family/resource base name. Pass NULL or "" to +// infer from the font. +// weight - style weight for matching. Pass 0 to infer from the font. +// italic - style italic flag for matching. Pass -1 to infer from the +// font, 0 for non-italic, or 1 for italic. +// file_access - font bytes as FPDF_FILEACCESS. The underlying file resources +// must remain valid until EPDFFont_ClearRegisteredFonts() or +// PDFium shutdown. The FPDF_FILEACCESS struct itself may be +// stack-owned. +// +// Returns a non-zero font id on success, or 0 on failure. +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV +EPDFFont_RegisterFont(FPDF_BYTESTRING family_name, + int weight, + int italic, + FPDF_FILEACCESS* file_access); + +// Experimental EmbedPDF Extension API. +// Register an in-memory font for runtime fallback use and PDF authoring. +// +// family_name - optional family/resource base name. Pass NULL or "" to infer +// from the font. +// weight - style weight for matching. Pass 0 to infer from the font. +// italic - style italic flag for matching. Pass -1 to infer from the +// font, 0 for non-italic, or 1 for italic. +// data_buf - pointer to font bytes. +// size - size of |data_buf| in bytes. +// +// Returns a non-zero font id on success, or 0 on failure. +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV +EPDFFont_RegisterMemFont(FPDF_BYTESTRING family_name, + int weight, + int italic, + const void* data_buf, + int size); + +// Experimental EmbedPDF Extension API. +// Same as EPDFFont_RegisterMemFont(), but supports size_t byte counts. +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV +EPDFFont_RegisterMemFont64(FPDF_BYTESTRING family_name, + int weight, + int italic, + const void* data_buf, + size_t size); + +// Experimental EmbedPDF Extension API. +// Clear all registered fonts and the fallback font order. +// +// Existing documents may still contain registered-font DA marker resources +// after this call; those markers are invalid until their fonts are registered +// again. +FPDF_EXPORT void FPDF_CALLCONV EPDFFont_ClearRegisteredFonts(void); + +// 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. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFont_AddFallbackFont(EPDF_FONT_ID font_id); + +// Experimental EmbedPDF Extension API. +// Clear the ordered fallback font list without unregistering fonts. +FPDF_EXPORT void FPDF_CALLCONV EPDFFont_ClearFallbackFonts(void); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif // PUBLIC_EPDF_FONT_H_ diff --git a/public/fpdf_annot.h b/public/fpdf_annot.h index a9d7c84db4..43ab5fa281 100644 --- a/public/fpdf_annot.h +++ b/public/fpdf_annot.h @@ -6,13 +6,15 @@ #define PUBLIC_FPDF_ANNOT_H_ #include - // NOLINTNEXTLINE(build/include) #include "fpdfview.h" // NOLINTNEXTLINE(build/include) #include "fpdf_formfill.h" +// NOLINTNEXTLINE(build/include) +#include "epdf_font.h" + // NOLINTNEXTLINE(build/include) #include "epdf_redact.h" @@ -1611,6 +1613,24 @@ EPDFAnnot_SetDefaultAppearance(FPDF_ANNOTATION annot, unsigned int G, unsigned int B); +// Experimental EmbedPDF Extension API. +// Set the default appearance of a FreeText annotation using a registered font. +// +// annot - handle to an annotation. +// font_id - font id returned by EPDFFont_RegisterFont() or +// EPDFFont_RegisterMemFont64(). +// 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_SetDefaultAppearanceRegisteredFont(FPDF_ANNOTATION annot, + EPDF_FONT_ID font_id, + float font_size, + unsigned int R, + unsigned int G, + unsigned int B); + // Experimental EmbedPDF Extension API. // Get the default appearance of a FreeText annotation. // diff --git a/testing/resources/fonts/DroidSansFallbackFull.ttf b/testing/resources/fonts/DroidSansFallbackFull.ttf new file mode 100644 index 0000000000..c1f08d9dcd Binary files /dev/null and b/testing/resources/fonts/DroidSansFallbackFull.ttf differ