diff --git a/core/fpdfapi/parser/cpdf_document.h b/core/fpdfapi/parser/cpdf_document.h index 497737e5fc..bf15385ff1 100644 --- a/core/fpdfapi/parser/cpdf_document.h +++ b/core/fpdfapi/parser/cpdf_document.h @@ -139,6 +139,24 @@ class CPDF_Document : public Observable, epdf_attachment_ = std::move(attachment); } + // EmbedPDF(fonts): the document's font index (CPDF_DocumentFonts in + // core/fpdfdoc), made on first use: what each embedded font program is, + // and the session's font state. Destroyed before the parser. + class FontIndex { + public: + virtual ~FontIndex() = default; + // A layer's transaction ended, committed or aborted: |objnums| are every + // object number it wrote, created or hid, and the overlay epoch was + // |epoch_at_begin| when it began and is |epoch| now. + virtual void TransactionEnded(const std::set& objnums, + uint64_t epoch_at_begin, + uint64_t epoch) {} + }; + FontIndex* font_index() const { return font_index_.get(); } + void SetFontIndex(std::unique_ptr index) { + font_index_ = std::move(index); + } + // EmbedPDF(fonts): session-scoped provenance for a resource name this // document instance handed out for a registered runtime font (a /DA font // alias reserved before its /DR entry exists). Never written to the file; a @@ -438,6 +456,8 @@ class CPDF_Document : public Observable, // EmbedPDF(signatures): destroyed before everything declared above it (the // parser included), after the extension and the stock font clearer. std::unique_ptr epdf_attachment_; + // EmbedPDF(fonts): see font_index(); destroyed before the attachment. + std::unique_ptr font_index_; // Must be second to last. StockFontClearer stock_font_clearer_; diff --git a/core/fpdfapi/parser/cpdf_layer_document.cpp b/core/fpdfapi/parser/cpdf_layer_document.cpp index 255eaf8f6c..e9d4f5710c 100644 --- a/core/fpdfapi/parser/cpdf_layer_document.cpp +++ b/core/fpdfapi/parser/cpdf_layer_document.cpp @@ -504,6 +504,7 @@ bool CPDF_LayerDocument::BeginTransaction() { CPDF_WriteGeneration::SetCurrent(transaction_->generation); transaction_->root_before = GetCachedRootDict(); transaction_->info_before = GetCachedInfoDict(); + transaction_->epoch_at_begin = overlay_epoch_; return true; } @@ -541,6 +542,9 @@ bool CPDF_LayerDocument::CommitTransaction() { } last_transaction_stats_ = tx->stats; ++overlay_epoch_; + if (FontIndex* index = font_index()) { + index->TransactionEnded(tx->touched, tx->epoch_at_begin, overlay_epoch_); + } return true; } @@ -569,6 +573,9 @@ bool CPDF_LayerDocument::AbortTransaction() { ForgetDerivedDataOf(unreachable); last_transaction_stats_ = tx->stats; ++overlay_epoch_; + if (FontIndex* index = font_index()) { + index->TransactionEnded(tx->touched, tx->epoch_at_begin, overlay_epoch_); + } // tx->written and tx->hidden are dropped with tx. Nothing is undone. return true; } diff --git a/core/fpdfapi/parser/cpdf_layer_document.h b/core/fpdfapi/parser/cpdf_layer_document.h index d58e6a86ac..99a8056629 100644 --- a/core/fpdfapi/parser/cpdf_layer_document.h +++ b/core/fpdfapi/parser/cpdf_layer_document.h @@ -52,6 +52,11 @@ class CPDF_LayerDocument final : public CPDF_Document { bool CommitTransaction(); bool AbortTransaction(); bool InTransaction() const { return !!transaction_; } + // EmbedPDF(fonts): the open transaction, for a derived cache that reads + // mid-write (what it has touched so far); null outside one. + const CPDF_LayerTransaction* open_transaction() const { + return transaction_.get(); + } // What the last ended (or the open) transaction cost. const CPDF_LayerTransactionStats& GetTransactionStats() const { return transaction_ ? transaction_->stats : last_transaction_stats_; diff --git a/core/fpdfapi/parser/cpdf_layer_transaction.h b/core/fpdfapi/parser/cpdf_layer_transaction.h index f1b1500c16..04e61f32f5 100644 --- a/core/fpdfapi/parser/cpdf_layer_transaction.h +++ b/core/fpdfapi/parser/cpdf_layer_transaction.h @@ -75,6 +75,10 @@ struct CPDF_LayerTransaction { RetainPtr root_before; RetainPtr info_before; + // EmbedPDF(fonts): the overlay epoch at begin, for the font index to tell + // what changed outside any transaction. + uint64_t epoch_at_begin = 0; + CPDF_LayerTransactionStats stats; }; diff --git a/core/fpdfdoc/BUILD.gn b/core/fpdfdoc/BUILD.gn index 24fd87c902..4f4bd25da6 100644 --- a/core/fpdfdoc/BUILD.gn +++ b/core/fpdfdoc/BUILD.gn @@ -26,6 +26,15 @@ source_set("fpdfdoc") { # EmbedPDF: a font as a face (family, weight, italic), one reader for all. "cpdf_fontface.cpp", "cpdf_fontface.h", + # EmbedPDF: the fonts a document uses and what each program is, a + # document font as a browser can draw it, and the fonts a write would + # draw text with (a resolver read). + "cpdf_displayprogram.cpp", + "cpdf_displayprogram.h", + "cpdf_documentfonts.cpp", + "cpdf_documentfonts.h", + "cpdf_fontresolver.cpp", + "cpdf_fontresolver.h", "cpdf_richtext.h", "cpdf_richtextjson.cpp", "cpdf_richtextjson.h", diff --git a/core/fpdfdoc/cpdf_annotfontmap.cpp b/core/fpdfdoc/cpdf_annotfontmap.cpp index 5d754e61d3..653ff8dc6c 100644 --- a/core/fpdfdoc/cpdf_annotfontmap.cpp +++ b/core/fpdfdoc/cpdf_annotfontmap.cpp @@ -26,11 +26,11 @@ #include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fpdfapi/parser/cpdf_stream_acc.h" #include "core/fpdfdoc/cpdf_annotfontsubset.h" +#include "core/fpdfdoc/cpdf_documentfonts.h" #include "core/fpdfdoc/cpdf_fontface.h" #include "core/fpdfdoc/cpdf_interactiveform.h" #include "core/fxcrt/check.h" #include "core/fxcrt/code_point_view.h" -#include "core/fxcrt/fx_codepage.h" #include "core/fxcrt/fx_safe_types.h" #include "core/fxcrt/numerics/safe_conversions.h" #include "core/fxcrt/stl_util.h" @@ -76,27 +76,6 @@ RetainPtr GetOrCreateDrFontDict(CPDF_Document* doc) { // ---- Rich text helpers ------------------------------------------------------ -// Family names compare without case, spaces, hyphens and underscores: -// "Noto Sans" == "NotoSans" == "noto-sans". -ByteString FamilyKey(const WideString& family) { - ByteString key; - for (wchar_t ch : family) { - if (ch == L' ' || ch == L'-' || ch == L'_' || ch == L'\'' || ch == L'"') { - continue; - } - if (ch < 0x80) { - key += static_cast(std::tolower(static_cast(ch))); - } else { - key += WideString(ch).ToUTF8(); - } - } - return key; -} - -ByteString FamilyKey(const ByteString& family) { - return FamilyKey(WideString::FromUTF8(family.AsStringView())); -} - // The standard-14 family a rich text family stands for, if any. enum class StandardFamily { kNone, @@ -108,7 +87,7 @@ enum class StandardFamily { }; StandardFamily StandardFamilyFor(const WideString& family) { - const ByteString key = FamilyKey(family); + const ByteString key = FontFamilyKey(family); if (key == "helvetica" || key == "arial" || key == "arialmt" || key == "sansserif") { return StandardFamily::kHelvetica; @@ -223,29 +202,6 @@ void StandardMetrics(const ByteString& base_font_name, *descent = std::abs(desc) / 1000.0f; } -// hhea ascender/descender of an embedded program (Acrobat's choice for -// TrueType and CFF, plan §6). -void ProgramMetrics(const CFX_Font* font, float* ascent, float* descent) { - RetainPtr face = font ? font->GetFace() : nullptr; - const uint16_t upem = face ? face->GetUnitsPerEm() : 0; - if (!face || upem == 0) { - *ascent = 0.830f; - *descent = 0.170f; - return; - } - *ascent = static_cast(face->GetAscender()) / upem; - *descent = static_cast(-face->GetDescender()) / upem; - if (*ascent <= 0 || *descent < 0) { - *ascent = 0.830f; - *descent = 0.170f; - } -} - -// "ABCDEF+NotoSans": a producer's subset, cut for the text it was used for. -bool HasSubsetTag(const ByteString& base_font) { - return base_font.GetLength() > 7 && base_font[6] == '+'; -} - struct DocumentProgramCandidate { RetainPtr stream; ByteString base_font_name; @@ -336,6 +292,13 @@ std::vector> DocumentFontDicts( return dicts; } +// Joiners and variation selectors draw nothing of their own: a font covers a +// grapheme without them, as the layout skips them. +bool NeedsGlyph(char32_t cp) { + return cp != 0x200D && !(cp >= 0xFE00 && cp <= 0xFE0F) && + !(cp >= 0xE0100 && cp <= 0xE01EF); +} + bool PDFontSupportsUnicode(const RetainPtr& font, uint16_t word) { if (!font) { return false; @@ -398,13 +361,18 @@ CPDF_AnnotFontMap::CPDF_AnnotFontMap( font_id = CPDF_AnnotFontSubset::ResolveRegisteredFont( entry.font->GetFontDict().Get()); } + // A registered font not authorized for editing (preview-and-print) never + // lays out new text: the /DA font stays the dictionary /DR holds. + if (font_id.has_value() && !CFX_FontRegistry::IsEditingAuthorized(*font_id)) { + font_id.reset(); + } if (font_id.has_value()) { RetainPtr registered_font = CreateRegisteredLayoutFont(*font_id); if (registered_font) { entry.font = std::move(registered_font); entry.registered_font_id = *font_id; entry.source = Source::kRegistered; - ProgramMetrics(entry.font->GetFont(), &entry.ascent, &entry.descent); + ProgramLineMetrics(entry.font->GetFont(), &entry.ascent, &entry.descent); } } if (entry.source == Source::kDocumentFontDict && entry.font) { @@ -412,10 +380,24 @@ CPDF_AnnotFontMap::CPDF_AnnotFontMap( StandardMetrics(entry.font->GetBaseFontName(), entry.font.Get(), &entry.ascent, &entry.descent); } else { - ProgramMetrics(entry.font->GetFont(), &entry.ascent, &entry.descent); + ProgramLineMetrics(entry.font->GetFont(), &entry.ascent, &entry.descent); } } + // A /DA font over a program that can author text lays new text out with + // that program's own cmap, as Acrobat does: a composite font's ToUnicode + // only names the characters drawn so far (what the writer installs for a + // document program), and a simple font's encoding reaches 256 codes at + // most (no Hebrew in a Mac Roman font whose program has it). + std::optional candidate; + if (entry.source == Source::kDocumentFontDict && entry.font) { + candidate = CandidateFromFontDict(entry.font->GetFontDict().Get()); + } fonts_.push_back(std::move(entry)); + if (candidate.has_value()) { + UseDocumentProgramForDefault(std::move(candidate->stream), + candidate->base_font_name, + candidate->identity); + } } CPDF_AnnotFontMap::~CPDF_AnnotFontMap() { @@ -751,89 +733,6 @@ ByteString CPDF_AnnotFontMap::GetPDFFontAlias(int32_t font_index) { return fonts_[font_index].alias; } -int32_t CPDF_AnnotFontMap::GetWordFontIndex(uint16_t word, - FX_Charset charset, - int32_t font_index) { - if (SupportsWord(font_index, word)) { - return font_index; - } - if (SupportsWord(0, word)) { - return 0; - } - - for (size_t i = 1; i < fonts_.size(); ++i) { - if (SupportsWord(pdfium::checked_cast(i), word)) { - return pdfium::checked_cast(i); - } - } - - 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, - /*for_authoring=*/true); - 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::ReleaseLayoutFonts() { // Rendering caches fonts in the document's page data. Release those cache // references before the scratch streams that their dictionaries reference. @@ -879,7 +778,7 @@ int32_t CPDF_AnnotFontMap::AddRegisteredFallbackFont( entry.alias = AllocateAppearanceAlias(ResourceKeyForRegisteredFont(font_id)); entry.registered_font_id = font_id; entry.source = Source::kRegistered; - ProgramMetrics(entry.font->GetFont(), &entry.ascent, &entry.descent); + ProgramLineMetrics(entry.font->GetFont(), &entry.ascent, &entry.descent); fonts_.push_back(std::move(entry)); return pdfium::checked_cast(fonts_.size() - 1); } @@ -901,9 +800,11 @@ void CPDF_AnnotFontMap::PinRichFace(const WideString& family, // static ByteString CPDF_AnnotFontMap::FaceRequestKey(const WideString& family, int weight, - bool italic) { - return ByteString::Format("%s|%d|%d", FamilyKey(family).c_str(), - std::clamp(weight, 100, 900), italic ? 1 : 0); + bool italic, + uint32_t program) { + return ByteString::Format("%s|%d|%d|%u", FontFamilyKey(family).c_str(), + std::clamp(weight, 100, 900), italic ? 1 : 0, + program); } bool CPDF_AnnotFontMap::CoversText(int entry, WideStringView text) { @@ -913,9 +814,8 @@ bool CPDF_AnnotFontMap::CoversText(int entry, WideStringView text) { // Scalars, not code units: on a 16-bit wchar_t platform a supplementary // character is a surrogate pair, and neither half has a glyph anywhere. for (char32_t cp : pdfium::CodePointView(text)) { - if (cp == 0x200D || (cp >= 0xFE00 && cp <= 0xFE0F) || - (cp >= 0xE0100 && cp <= 0xE01EF)) { - continue; // joiners and variation selectors, as the layout skips them + if (!NeedsGlyph(cp)) { + continue; } if (RichGlyphFor(entry, static_cast(cp)) == 0) { return false; @@ -927,13 +827,15 @@ bool CPDF_AnnotFontMap::CoversText(int entry, WideStringView text) { int CPDF_AnnotFontMap::ResolveRichFace(const WideString& family, int weight, bool italic, - WideStringView text) { - const ByteString request = FaceRequestKey(family, weight, italic); + WideStringView text, + uint32_t program) { + const ByteString request = FaceRequestKey(family, weight, italic, program); auto cached = resolved_faces_.find(request); if (cached != resolved_faces_.end()) { return cached->second; } - const int entry = ResolveRichFaceUncached(family, weight, italic, text); + const int entry = + ResolveRichFaceUncached(family, weight, italic, text, program); if (entry >= 0) { resolved_faces_[request] = entry; } @@ -943,17 +845,26 @@ int CPDF_AnnotFontMap::ResolveRichFace(const WideString& family, int CPDF_AnnotFontMap::ResolveRichFaceUncached(const WideString& family, int weight, bool italic, - WideStringView text) { + WideStringView text, + uint32_t program) { weight = std::clamp(weight, 100, 900); bool degraded = false; + // A program the writer named is the face, whatever the family resolves to. + if (program != 0) { + const int entry = EntryForDocumentProgram(program); + if (entry >= 0) { + return entry; + } + } + // 0. A face the caller pinned to an entry (the /DA font of a regenerated // plain box), if it can draw the text; a box's own font that lacks a // glyph the edit needs is not the edit's face. - const ByteString wanted_key = FamilyKey(family); + const ByteString wanted_key = FontFamilyKey(family); for (const PinnedFace& pinned : pinned_faces_) { if (pinned.weight == weight && pinned.italic == italic && - FamilyKey(pinned.family) == wanted_key) { + FontFamilyKey(pinned.family) == wanted_key) { if (CoversText(pinned.entry, text)) { return pinned.entry; } @@ -1007,23 +918,41 @@ int CPDF_AnnotFontMap::ResolveRichFaceUncached(const WideString& family, return entry; } -int CPDF_AnnotFontMap::FindRichFallback(uint32_t unicode, +int CPDF_AnnotFontMap::FindRichFallback(WideStringView grapheme, int weight, bool italic) { if (!allow_registered_fallbacks_) { return -1; } - std::optional font_id = - CFX_FontRegistry::FindFallbackFont(unicode, weight, italic, - /*for_authoring=*/true); - if (!font_id.has_value()) { - return -1; + for (CFX_FontRegistry::FontId font_id : CFX_FontRegistry::GetFallbackOrder( + weight, italic, /*for_authoring=*/true)) { + const bool covers = std::ranges::all_of( + pdfium::CodePointView(grapheme), [font_id](char32_t cp) { + return !NeedsGlyph(cp) || CFX_FontRegistry::SupportsUnicode( + font_id, static_cast(cp)); + }); + if (covers) { + return EntryForRegisteredFont(font_id); + } } - int32_t existing = FindExistingRegisteredFont(*font_id); - if (existing >= 0) { - return existing; + return -1; +} + +int CPDF_AnnotFontMap::FindLastResort() { + const CFX_FontRegistry::FontId font_id = + CFX_FontRegistry::GetLastResortFont(); + if (!allow_registered_fallbacks_ || + font_id == CFX_FontRegistry::kInvalidFontId || + !CFX_FontRegistry::IsEditingAuthorized(font_id)) { + return -1; } - return AddRegisteredFallbackFont(*font_id); + return EntryForRegisteredFont(font_id); +} + +int CPDF_AnnotFontMap::EntryForRegisteredFont( + CFX_FontRegistry::FontId font_id) { + const int32_t existing = FindExistingRegisteredFont(font_id); + return existing >= 0 ? existing : AddRegisteredFallbackFont(font_id); } CPDF_AnnotFontMap::RichFace CPDF_AnnotFontMap::GetRichFace(int entry) const { @@ -1193,7 +1122,7 @@ int CPDF_AnnotFontMap::FindDocumentProgram(const WideString& family, if (!doc_) { return -1; } - const ByteString wanted = FamilyKey(family); + const ByteString wanted = FontFamilyKey(family); if (wanted.IsEmpty()) { return -1; } @@ -1203,7 +1132,7 @@ int CPDF_AnnotFontMap::FindDocumentProgram(const WideString& family, std::optional candidate = CandidateFromFontDict(dict.Get()); if (!candidate.has_value() || - FamilyKey(candidate->identity.family) != wanted || + FontFamilyKey(candidate->identity.family) != wanted || !seen.insert(candidate->stream.Get()).second) { continue; } @@ -1219,8 +1148,8 @@ int CPDF_AnnotFontMap::FindDocumentProgram(const WideString& family, if (a.score != b.score) { return a.score < b.score; } - return !HasSubsetTag(a.base_font_name) && - HasSubsetTag(b.base_font_name); + return !HasFontSubsetTag(a.base_font_name) && + HasFontSubsetTag(b.base_font_name); }); for (DocumentProgramCandidate& candidate : candidates) { int entry = -1; @@ -1245,21 +1174,63 @@ int CPDF_AnnotFontMap::FindDocumentProgram(const WideString& family, return -1; } +int CPDF_AnnotFontMap::EntryForDocumentProgram(uint32_t objnum) { + for (size_t i = 0; i < fonts_.size(); ++i) { + if (fonts_[i].source == Source::kDocumentProgram && + fonts_[i].program_stream && + fonts_[i].program_stream->GetObjNum() == objnum) { + return static_cast(i); + } + } + if (!doc_) { + return -1; + } + CPDF_DocumentFonts* document_fonts = CPDF_DocumentFonts::For(doc_); + std::optional described = + document_fonts->DescribeProgram(objnum); + RetainPtr stream = document_fonts->ProgramStream(objnum); + if (!described.has_value() || !stream) { + return -1; + } + return AddDocumentProgram( + std::move(stream), described->base_font_name, + {described->family.ToUTF8(), described->weight, described->italic}); +} + int CPDF_AnnotFontMap::AddDocumentProgram( RetainPtr stream, const ByteString& base_font_name, const CPDF_AnnotFontSubset::FaceIdentity& identity) { + std::optional entry = + MakeDocumentProgramEntry(std::move(stream), base_font_name, identity); + if (!entry.has_value()) { + return -1; + } + entry->alias = AllocateAppearanceAlias( + ByteString::Format("EDocF%u", entry->program_stream->GetObjNum())); + fonts_.push_back(std::move(*entry)); + return static_cast(fonts_.size() - 1); +} + +std::optional +CPDF_AnnotFontMap::MakeDocumentProgramEntry( + RetainPtr stream, + const ByteString& base_font_name, + const CPDF_AnnotFontSubset::FaceIdentity& identity) { + if (!doc_ || !stream || stream->GetObjNum() == 0) { + return std::nullopt; + } auto* page_data = CPDF_DocPageData::FromDocument(doc_); RetainPtr data = page_data->GetFontFileStreamAcc(stream); if (!data || data->GetSpan().empty()) { - return -1; + return std::nullopt; } // Eligibility on the bytes (C note §1.3): an sfnt we can emit, whose // licence allows authoring. if (CFX_FontRegistry::DetectProgramFormat(data->GetSpan()) == CFX_FontRegistry::ProgramFormat::kUnsupported) { page_data->MaybePurgeFontFileStreamAcc(std::move(data)); - return -1; + return std::nullopt; } auto program = std::make_unique(); if (!program->LoadFaceZeroFromSpan(data->GetSpan(), /*force_vertical=*/false, @@ -1272,27 +1243,120 @@ int CPDF_AnnotFontMap::AddDocumentProgram( // asked for explicitly, a Mac Roman-only cmap is refused too. !program->GetFace()->SelectCharMap(fxge::FontEncoding::kUnicode)) { page_data->MaybePurgeFontFileStreamAcc(std::move(data)); - return -1; + return std::nullopt; } const CFX_FontRegistry::EmbeddingPermission permission = CFX_FontRegistry::ClassifyFsType(program->GetFace()->GetFsTypeFlags()); if (permission != CFX_FontRegistry::EmbeddingPermission::kInstallable && permission != CFX_FontRegistry::EmbeddingPermission::kEditable) { page_data->MaybePurgeFontFileStreamAcc(std::move(data)); - return -1; // preview-and-print: renders, never authors (no override) + return std::nullopt; // preview-and-print: renders, never authors } FontEntry entry; entry.source = Source::kDocumentProgram; - entry.alias = AllocateAppearanceAlias( - ByteString::Format("EDocF%u", stream->GetObjNum())); + CPDF_AnnotFontSubset::LayoutFont layout_font = + CPDF_AnnotFontSubset::CreateLayoutFontForProgram( + doc_, program.get(), base_font_name, identity); + if (layout_font.scratch) { + layout_scratch_.push_back(std::move(layout_font.scratch)); + } + entry.font = std::move(layout_font.font); entry.program = std::move(program); entry.program_data = std::move(data); entry.program_stream = std::move(stream); entry.base_font_name = base_font_name; entry.identity = identity; - ProgramMetrics(entry.program.get(), &entry.ascent, &entry.descent); - fonts_.push_back(std::move(entry)); - return static_cast(fonts_.size() - 1); + ProgramLineMetrics(entry.program.get(), &entry.ascent, &entry.descent); + return entry; +} + +bool CPDF_AnnotFontMap::UseDocumentProgramForDefault( + RetainPtr program, + const ByteString& base_font_name, + const CPDF_AnnotFontSubset::FaceIdentity& identity) { + if (fonts_.empty()) { + return false; + } + std::optional entry = + MakeDocumentProgramEntry(std::move(program), base_font_name, identity); + if (!entry.has_value()) { + return false; + } + entry->alias = fonts_.front().alias; + fonts_.front() = std::move(*entry); + return true; +} + +CPDF_AnnotFontMap::EntryFacts CPDF_AnnotFontMap::DescribeEntry( + int entry) const { + EntryFacts facts; + if (!fxcrt::IndexInBounds(fonts_, entry)) { + return facts; + } + const FontEntry& font_entry = fonts_[entry]; + facts.source = font_entry.source; + facts.degraded = font_entry.degraded; + switch (font_entry.source) { + case Source::kRegistered: + facts.registered_font_id = font_entry.registered_font_id; + facts.family = WideString::FromUTF8( + CFX_FontRegistry::GetFamilyName(font_entry.registered_font_id) + .AsStringView()); + facts.weight = + CFX_FontRegistry::GetStyleWeight(font_entry.registered_font_id); + facts.italic = + CFX_FontRegistry::IsStyleItalic(font_entry.registered_font_id); + break; + case Source::kDocumentProgram: + facts.program_objnum = font_entry.program_stream + ? font_entry.program_stream->GetObjNum() + : 0; + facts.base_font_name = font_entry.base_font_name; + facts.family = + WideString::FromUTF8(font_entry.identity.family.AsStringView()); + facts.weight = font_entry.identity.weight; + facts.italic = font_entry.identity.italic; + break; + case Source::kStandard14: { + facts.base_font_name = font_entry.base_font_name; + const CPDF_FontFace face = FaceOfBaseFontName(font_entry.base_font_name); + facts.family = face.family; + facts.weight = face.weight; + facts.italic = face.italic; + break; + } + case Source::kDocumentFontDict: { + RetainPtr font_dict = + font_entry.font ? font_entry.font->GetFontDict() : nullptr; + if (!font_dict) { + break; + } + const CPDF_FontFace face = FaceOfFontDict(font_dict.Get()); + facts.family = face.family; + facts.weight = face.weight; + facts.italic = face.italic; + facts.base_font_name = font_dict->GetNameFor("BaseFont"); + facts.font_dict = font_dict; + RetainPtr descriptor = + FontDescriptorOf(font_dict.Get()); + for (const char* key : {"FontFile2", "FontFile3", "FontFile"}) { + RetainPtr stream = + descriptor ? descriptor->GetStreamFor(key) : nullptr; + if (stream) { + facts.program_objnum = stream->GetObjNum(); + break; + } + } + break; + } + } + return facts; +} + +void CPDF_AnnotFontMap::NoteMissingCharacter(const WideString& grapheme) { + if (doc_) { + CPDF_DocumentFonts::For(doc_)->NoteMissingCharacter(grapheme); + } } int CPDF_AnnotFontMap::AddStandardFont(const ByteString& base_font_name) { diff --git a/core/fpdfdoc/cpdf_annotfontmap.h b/core/fpdfdoc/cpdf_annotfontmap.h index fb6a023df7..7384cc48d6 100644 --- a/core/fpdfdoc/cpdf_annotfontmap.h +++ b/core/fpdfdoc/cpdf_annotfontmap.h @@ -17,7 +17,6 @@ #include "core/fxcrt/widestring.h" #include "core/fpdfdoc/cpdf_annotfontsubset.h" -#include "core/fpdfdoc/ipvt_fontmap.h" #include "core/fxcrt/bytestring.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/unowned_ptr.h" @@ -31,7 +30,7 @@ class CPDF_IndirectObjectHolder; class CPDF_Stream; class CPDF_StreamAcc; -class CPDF_AnnotFontMap final : public IPVT_FontMap { +class CPDF_AnnotFontMap final { public: // Whose text the appearance is, for the document's embedding policy (§2 of // the Phase C note): under the DEFAULT policy annotation text (FreeText, @@ -52,7 +51,7 @@ class CPDF_AnnotFontMap final : public IPVT_FontMap { CFX_FontRegistry::kInvalidFontId, bool install_dr_entry = false, Owner owner = Owner::kAnnotation); - ~CPDF_AnnotFontMap() override; + ~CPDF_AnnotFontMap(); // Pick the /DR resource name a DA string uses for |font_id|: "ERegF", // suffixed "_N" when a foreign entry already owns that key. Choose reads @@ -158,14 +157,22 @@ class CPDF_AnnotFontMap final : public IPVT_FontMap { // next rung takes over for the whole request, so a subset never draws // glyph 0 for the characters it lacks. Registered and standard faces keep // the layout's per-glyph fallback (rung 4) for their own gaps. + // + // A |program| (a document program's object number) is the face exactly, + // before every rung: the writer chose it by the name the font list gives + // it, which the family can't tell from another program that describes + // itself alike. Like a registered face it keeps the per-glyph fallback for + // its gaps. A program that can't author text leaves the family to decide. int ResolveRichFace(const WideString& family, int weight, bool italic, - WideStringView text = WideStringView()); + WideStringView text = WideStringView(), + uint32_t program = 0); // The key ResolveRichFace caches by: what the layout groups text with. static ByteString FaceRequestKey(const WideString& family, int weight, - bool italic); + bool italic, + uint32_t program = 0); // Does |entry| map every base character of |text|? Joiners and variation // selectors are exempt, as in the layout. Empty text is covered. bool CoversText(int entry, WideStringView text); @@ -175,9 +182,14 @@ class CPDF_AnnotFontMap final : public IPVT_FontMap { // subset name), so the rich layout keeps naming the /DA alias, exactly as // CPVT always did. A pinned face wins over every resolution step. void PinRichFace(const WideString& family, int weight, bool italic, int entry); - // A registered fallback font, authorised for authoring, covering - // |unicode|; -1 when there is none or fallbacks are not allowed. - int FindRichFallback(uint32_t unicode, int weight, bool italic); + // The first registered fallback font, in the order for the style + // (CFX_FontRegistry::GetFallbackOrder()), that has every character of + // |grapheme|; -1 when none does or fallbacks are not allowed. + int FindRichFallback(WideStringView grapheme, int weight, bool italic); + // The registered last-resort font (CFX_FontRegistry::GetLastResortFont()), + // which draws a grapheme no font has as its box; -1 when there is none or + // fallbacks are not allowed. + int FindLastResort(); RichFace GetRichFace(int entry) const; // The glyph of |entry| for a scalar; 0 when the face lacks it. For a // standard-14 face this is the charcode (one glyph per character). @@ -200,14 +212,47 @@ class CPDF_AnnotFontMap final : public IPVT_FontMap { void SetDefaultAppearanceEntry(int entry, const ByteString& alias); int GetDefaultAppearanceEntry() const { return da_entry_; } - // 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; + // Makes the /DA entry a program already in the document, under the alias + // the /DA string names: what a /DA font a session reserved for a document + // program draws with before its /DR entry exists. False, and the entry + // stays as it was, when the program can't author text (see + // FindDocumentProgram()). + bool UseDocumentProgramForDefault( + RetainPtr program, + const ByteString& base_font_name, + const CPDF_AnnotFontSubset::FaceIdentity& identity); + + // What an entry is, for a resolver read: where its program comes from and + // which one it is. + struct EntryFacts { + Source source = Source::kStandard14; + CFX_FontRegistry::FontId registered_font_id = + CFX_FontRegistry::kInvalidFontId; + // kDocumentProgram's stream, and kDocumentFontDict's when it embeds one. + uint32_t program_objnum = 0; + // kDocumentFontDict's dictionary, which says how its program is read. + RetainPtr font_dict; + // kStandard14: the standard name; else the font's /BaseFont. + ByteString base_font_name; + // The face it draws: its family, weight and italic. + WideString family; + int weight = 400; + bool italic = false; + bool degraded = false; + }; + EntryFacts DescribeEntry(int entry) const; + + // A grapheme drawn with no font that has it, for the write's report + // (CPDF_DocumentFonts::NoteMissingCharacter()). + void NoteMissingCharacter(const WideString& grapheme); + + // The font |font_index| lays out with (for a registered font or a + // document program, a composite font over its program); null out of + // range. + RetainPtr GetPDFFont(int32_t font_index); + // The resource name the appearance draws |font_index| with. Naming it + // makes its resource part of the appearance. + ByteString GetPDFFontAlias(int32_t font_index); private: struct FontEntry { @@ -236,7 +281,6 @@ class CPDF_AnnotFontMap final : public IPVT_FontMap { std::unique_ptr glyph_identity; }; - bool SupportsWord(int32_t font_index, uint16_t word) const; ByteString AllocateAppearanceAlias(const ByteString& preferred) const; CPDF_AnnotFontSubset::Embedding EmbeddingForRegisteredFonts() const; void InstallDrEntry(const ByteString& alias, @@ -247,10 +291,16 @@ class CPDF_AnnotFontMap final : public IPVT_FontMap { CFX_FontRegistry::FontId font_id); int32_t FindExistingRegisteredFont(CFX_FontRegistry::FontId font_id) const; int32_t AddRegisteredFallbackFont(CFX_FontRegistry::FontId font_id); + // The entry drawing with a registered font, added when it is the first. + int EntryForRegisteredFont(CFX_FontRegistry::FontId font_id); int ResolveRichFaceUncached(const WideString& family, int weight, bool italic, - WideStringView text); + WideStringView text, + uint32_t program); + // The entry drawing with the document program |objnum|, added when it is + // the first; -1 when the walk didn't find it or it can't author text. + int EntryForDocumentProgram(uint32_t objnum); // The best-styled document program under |family| that is eligible and // covers |text|; -1 when none is. int FindDocumentProgram(const WideString& family, @@ -260,6 +310,13 @@ class CPDF_AnnotFontMap final : public IPVT_FontMap { int AddDocumentProgram(RetainPtr stream, const ByteString& base_font_name, const CPDF_AnnotFontSubset::FaceIdentity& identity); + // The entry for a document program, when it can author text: an sfnt that + // loads, has glyphs and a Unicode cmap, and whose licence allows editing. + // Its layout font is what CPVT reads widths and charcodes from. + std::optional MakeDocumentProgramEntry( + RetainPtr stream, + const ByteString& base_font_name, + const CPDF_AnnotFontSubset::FaceIdentity& identity); int AddStandardFont(const ByteString& base_font_name); const CPDF_AnnotFontSubset::GlyphIdentity* GlyphIdentityOf(FontEntry* entry); diff --git a/core/fpdfdoc/cpdf_annotfontsubset.cpp b/core/fpdfdoc/cpdf_annotfontsubset.cpp index 8c64116969..4b4f730bfa 100644 --- a/core/fpdfdoc/cpdf_annotfontsubset.cpp +++ b/core/fpdfdoc/cpdf_annotfontsubset.cpp @@ -618,48 +618,53 @@ CPDF_AnnotFontSubset::BuildEphemeralFontResource(StagedFontResource staged) { CPDF_AnnotFontSubset::LayoutFont CPDF_AnnotFontSubset::CreateLayoutFont( CPDF_Document* doc, CFX_FontRegistry::FontId font_id) { - LayoutFont result; if (!doc || !CFX_FontRegistry::IsValidFont(font_id)) { - return result; + return LayoutFont(); } - std::unique_ptr font = CFX_FontRegistry::CreateFont(font_id); - if (!font || !font->HasAnyGlyphs()) { - return result; + if (!font) { + return LayoutFont(); } + return CreateLayoutFontForProgram( + doc, font.get(), BaseFontNameForRegisteredFont(font_id, font.get()), + IdentityForRegisteredFont(font_id)); +} - auto char_codes_and_indices = - font->GetCharCodesAndIndices(pdfium::kMaximumSupplementaryCodePoint); - if (char_codes_and_indices.empty()) { +// static +CPDF_AnnotFontSubset::LayoutFont +CPDF_AnnotFontSubset::CreateLayoutFontForProgram( + CPDF_Document* doc, + CFX_Font* program, + const ByteString& base_font_name, + const FaceIdentity& identity) { + LayoutFont result; + if (!doc || !program || !program->HasAnyGlyphs()) { return result; } // The layout font has the same shape as the resource that will be written, // so the charcodes it hands out (CIDs under Identity-H) are the ones the // appearance bytes carry. For a CID-keyed CFF that is the charset's CID. - const GlyphIdentity identity(font.get()); - std::multimap to_unicode; + // Every glyph has its width, shaping can draw glyphs the cmap doesn't + // reach (ligatures, alternates); there is no ToUnicode, the layout maps + // characters through the program itself. + const GlyphIdentity glyphs(program); std::map widths; - for (const auto& item : char_codes_and_indices) { - std::optional cid = identity.CidOf(item.glyph_index); - if (!cid.has_value()) { - continue; - } - if (!pdfium::Contains(widths, *cid)) { - widths[*cid] = font->GetGlyphWidth(item.glyph_index); + RetainPtr face = program->GetFace(); + const int glyph_count = face ? face->GetGlyphCount() : 0; + for (int gid = 0; gid < glyph_count; ++gid) { + std::optional cid = glyphs.CidOf(gid); + if (cid.has_value() && !pdfium::Contains(widths, *cid)) { + widths[*cid] = program->GetGlyphWidth(gid); } - to_unicode.emplace(*cid, item.char_code); } - if (widths.empty() || to_unicode.empty()) { + if (widths.empty()) { return result; } - const ByteString base_font_name = - BaseFontNameForRegisteredFont(font_id, font.get()); StagedFontResource parts; - if (!BuildCompositeFontParts(font.get(), base_font_name, font->GetFontSpan(), - IdentityForRegisteredFont(font_id), widths, - to_unicode, &parts)) { + if (!BuildCompositeFontParts(program, base_font_name, program->GetFontSpan(), + identity, widths, /*to_unicode=*/{}, &parts)) { return result; } // The streams go into a scratch holder of their own: CPDF_Font resolves diff --git a/core/fpdfdoc/cpdf_annotfontsubset.h b/core/fpdfdoc/cpdf_annotfontsubset.h index 7ebb5a2c9e..021e5a6edb 100644 --- a/core/fpdfdoc/cpdf_annotfontsubset.h +++ b/core/fpdfdoc/cpdf_annotfontsubset.h @@ -68,11 +68,11 @@ class CPDF_AnnotFontSubset final { std::map cid_to_gid_; }; - // A registered font as CPVT lays text out with it. Its program and - // ToUnicode streams live in |scratch|, a holder of its own, never in the - // document: laying out allocates no document object number and leaves - // nothing behind. |scratch| must outlive |font| (it is declared first, so - // it is destroyed last). + // A registered font as the layout measures it: a composite font over its + // program. The program stream lives in |scratch|, a holder of its own, + // never in the document: laying out allocates no document object number + // and leaves nothing behind. |scratch| must outlive |font| (it is declared + // first, so it is destroyed last). struct LayoutFont { LayoutFont(); LayoutFont(LayoutFont&& that) noexcept; @@ -100,6 +100,14 @@ class CPDF_AnnotFontSubset final { }; static FaceIdentity IdentityForRegisteredFont(CFX_FontRegistry::FontId id); + // A layout font over any program the writer can emit (a document's own, + // when a widget's /DA font is one): the same shape CreateLayoutFont() + // builds for a registered font, named and identified as given. + static LayoutFont CreateLayoutFontForProgram(CPDF_Document* doc, + CFX_Font* program, + const ByteString& base_font_name, + const FaceIdentity& identity); + // How much of the program a resource carries (§2 of the Phase C note). // The caller resolves the document's policy and the resource's owner into // one of these; fsType "no subsetting" forces kFull regardless. diff --git a/core/fpdfdoc/cpdf_appearancegenerator.h b/core/fpdfdoc/cpdf_appearancegenerator.h index 8c4003a6ff..147832e7eb 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator.h +++ b/core/fpdfdoc/cpdf_appearancegenerator.h @@ -129,6 +129,17 @@ class CPDF_AppearanceGenerator { float font_size, const CFX_Color& color); + // EmbedPDF: set /DA to a program already in the document, by its stream's + // object number (CPDF_DocumentFonts). Like a registered font, /DA names a + // session alias; the appearance installs the /DR entry. False for a + // program the layout can't write new text with: not an sfnt, no Unicode + // cmap, or a licence that doesn't allow editing. + static bool UpdateDefaultAppearanceDocumentFont(CPDF_Document* doc, + CPDF_Dictionary* annot_dict, + uint32_t program_objnum, + float font_size, + const CFX_Color& color); + // EmbedPDF (Phase D): a FreeText appearance laid out from a rich text // document, in the two halves of the Phase C note §6. Prepare lays out // and builds the stream and its font resources off to the side: no diff --git a/core/fpdfdoc/cpdf_appearancegenerator_forms.cpp b/core/fpdfdoc/cpdf_appearancegenerator_forms.cpp index f728dbe543..f7e051cbd0 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_forms.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_forms.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -20,7 +21,6 @@ #include "constants/appearance.h" #include "constants/form_fields.h" #include "constants/form_flags.h" -#include "core/fxcrt/stream_write_utils.h" #include "core/fpdfapi/font/cpdf_font.h" #include "core/fpdfapi/page/cpdf_docpagedata.h" #include "core/fpdfapi/parser/cpdf_array.h" @@ -37,10 +37,10 @@ #include "core/fpdfdoc/cpdf_defaultappearance.h" #include "core/fpdfdoc/cpdf_formfield.h" #include "core/fpdfdoc/cpdf_interactiveform.h" -#include "core/fpdfdoc/cpvt_fontmap.h" -#include "core/fpdfdoc/cpvt_variabletext.h" +#include "core/fpdfdoc/cpdf_richtextlayout.h" #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/fx_system.h" +#include "core/fxcrt/stream_write_utils.h" #include "core/fxge/cfx_fontregistry.h" #include "core/fxge/cfx_renderdevice.h" @@ -48,17 +48,15 @@ using appearance_generator::AnnotationDimensionsAndColor; using appearance_generator::APGenerationTarget; using appearance_generator::AppearanceCharacteristics; using appearance_generator::BorderStyleInfo; -using appearance_generator::CloneResourcesDictIfMissingFromStream; using appearance_generator::DaFontResolution; using appearance_generator::DefaultAppearanceInfo; +using appearance_generator::EmitFieldTextBody; using appearance_generator::GenerateAPDict; using appearance_generator::GenerateBorderAP; using appearance_generator::GenerateColorAP; using appearance_generator::GenerateDirectFallbackFontDict; -using appearance_generator::GenerateEditAP; using appearance_generator::GenerateEphemeralDefaultAcroFormDict; using appearance_generator::GenerateExtGStateDict; -using appearance_generator::GenerateResourceFontDict; using appearance_generator::GenerateResourcesDict; using appearance_generator::GetAnnotationDimensionsAndColor; using appearance_generator::GetAppearanceCharacteristics; @@ -67,77 +65,81 @@ using appearance_generator::GetDefaultAppearanceInfo; using appearance_generator::PaintOperation; using appearance_generator::ResolveDaFontForEphemeralTarget; using appearance_generator::ResolveDaFontForPersistentTarget; -using appearance_generator::SetVtFontSize; -using appearance_generator::ValidateOrCreateFontResources; namespace { +// A field's text laid out in |plate| (CPDF_RichTextLayout::ArrangeField()) +// and written in the /DA font, which is |map|'s first entry: Td, Tf and a +// string per run. |content_height| receives the height the lines take. +ByteString FieldText(CPDF_AnnotFontMap& map, + const CPDF_RichTextLayout::Options& options, + CPDF_RichTextLayout::Field field, + const CFX_FloatRect& plate, + float* content_height = nullptr) { + const CPDF_AnnotFontMap::EntryFacts face = map.DescribeEntry(0); + field.face_entry = 0; + field.weight = face.weight; + field.italic = face.italic; + CPDF_RichTextLayout layout(&map, options); + const CPDF_RichTextLayout::Result laid = layout.ArrangeField(field, plate); + if (content_height) { + *content_height = laid.content_height; + } + fxcrt::ostringstream body; + EmitFieldTextBody(body, laid, map, plate); + return ByteString(body); +} + +// /Q as an alignment: 0 left, 1 centre, 2 right; anything else is left. +int FieldAlignment(int32_t q) { + return q == 1 || q == 2 ? q : 0; +} + ByteString GenerateTextFieldAP(const CPDF_Dictionary* annot_dict, const CFX_FloatRect& body_rect, float font_size, - CPVT_VariableText& vt, + CPDF_AnnotFontMap& map, + const CPDF_RichTextLayout::Options& options, const WideString* value_override) { RetainPtr v_field = CPDF_FormField::GetFieldAttrForDict(annot_dict, pdfium::form_fields::kV); - WideString value = value_override - ? *value_override - : (v_field ? v_field->GetUnicodeText() : WideString()); RetainPtr q_field = CPDF_FormField::GetFieldAttrForDict(annot_dict, "Q"); - const int32_t align = q_field ? q_field->GetInteger() : 0; RetainPtr ff_field = CPDF_FormField::GetFieldAttrForDict(annot_dict, pdfium::form_fields::kFf); const uint32_t flags = ff_field ? ff_field->GetInteger() : 0; RetainPtr max_len_field = CPDF_FormField::GetFieldAttrForDict(annot_dict, "MaxLen"); const uint32_t max_len = max_len_field ? max_len_field->GetInteger() : 0; - vt.SetPlateRect(body_rect); - vt.SetAlignment(CPVT_VariableText::ToAlignment(align)); - SetVtFontSize(font_size, vt); - - bool is_multi_line = (flags >> 12) & 1; - if (is_multi_line) { - vt.SetMultiLine(true); - vt.SetAutoReturn(true); - } - uint16_t sub_word = 0; - if ((flags >> 13) & 1) { - sub_word = '*'; - vt.SetPasswordChar(sub_word); - } - bool is_char_array = (flags >> 24) & 1; - if (is_char_array) { - vt.SetCharArray(max_len); - } else { - vt.SetLimitChar(max_len); - } - vt.Initialize(); - vt.SetText(value); - vt.RearrangeAll(); - CFX_PointF offset; - if (!is_multi_line) { - offset = CFX_PointF( - 0.0f, (vt.GetContentRect().Height() - body_rect.Height()) / 2.0f); + CPDF_RichTextLayout::Field field; + field.text = value_override + ? *value_override + : (v_field ? v_field->GetUnicodeText() : WideString()); + field.size = font_size; + field.alignment = FieldAlignment(q_field ? q_field->GetInteger() : 0); + field.multiline = (flags >> 12) & 1; + field.password = (flags >> 13) & 1; + if ((flags >> 24) & 1) { + field.cells = max_len; + } else { + field.limit = max_len; } - return GenerateEditAP(vt.GetProvider()->GetFontMap(), vt.GetIterator(), - offset, !is_char_array, sub_word); + field.centered = !field.multiline; + return FieldText(map, options, std::move(field), body_rect); } ByteString GenerateComboBoxAP(const CPDF_Dictionary* annot_dict, const CFX_FloatRect& body_rect, const CFX_Color& text_color, float font_size, - CPVT_VariableText::Provider& provider, + CPDF_AnnotFontMap& map, + const CPDF_RichTextLayout::Options& options, const WideString* value_override) { fxcrt::ostringstream body_stream; RetainPtr v_field = CPDF_FormField::GetFieldAttrForDict(annot_dict, pdfium::form_fields::kV); - WideString value = value_override - ? *value_override - : (v_field ? v_field->GetUnicodeText() : WideString()); - CPVT_VariableText vt(&provider); CFX_FloatRect button_rect = body_rect; button_rect.left = button_rect.right - 13; button_rect.Normalize(); @@ -145,17 +147,14 @@ ByteString GenerateComboBoxAP(const CPDF_Dictionary* annot_dict, edit_rect.right = button_rect.left; edit_rect.Normalize(); edit_rect.Deflate(4.0f, 0); - vt.SetPlateRect(edit_rect); - SetVtFontSize(font_size, vt); - - vt.Initialize(); - vt.SetText(value); - vt.RearrangeAll(); - CFX_FloatRect content_rect = vt.GetContentRect(); - CFX_PointF offset = - CFX_PointF(0.0f, (content_rect.Height() - edit_rect.Height()) / 2.0f); - ByteString edit = - GenerateEditAP(provider.GetFontMap(), vt.GetIterator(), offset, true, 0); + + CPDF_RichTextLayout::Field field; + field.text = value_override + ? *value_override + : (v_field ? v_field->GetUnicodeText() : WideString()); + field.size = font_size; + field.centered = true; + const ByteString edit = FieldText(map, options, std::move(field), edit_rect); if (edit.GetLength() > 0) { body_stream << "/Tx BMC\nq\n"; WriteRect(body_stream, edit_rect) << " re\nW\nn\n"; @@ -184,7 +183,8 @@ ByteString GenerateListBoxAP(const CPDF_Dictionary* annot_dict, const CFX_FloatRect& body_rect, const CFX_Color& text_color, float font_size, - CPVT_VariableText::Provider& provider) { + CPDF_AnnotFontMap& map, + const CPDF_RichTextLayout::Options& options) { RetainPtr opts = ToArray(CPDF_FormField::GetFieldAttrForDict(annot_dict, "Opt")); if (!opts) { @@ -205,13 +205,13 @@ ByteString GenerateListBoxAP(const CPDF_Dictionary* annot_dict, } if (RetainPtr opt = opts->GetDirectObjectAt(i)) { - WideString item; + CPDF_RichTextLayout::Field field; if (opt->IsString()) { - item = opt->GetUnicodeText(); + field.text = opt->GetUnicodeText(); } else if (const CPDF_Array* opt_array = opt->AsArray()) { RetainPtr opt_item = opt_array->GetDirectObjectAt(1); if (opt_item) { - item = opt_item->GetUnicodeText(); + field.text = opt_item->GetUnicodeText(); } } bool is_selected = false; @@ -224,15 +224,12 @@ ByteString GenerateListBoxAP(const CPDF_Dictionary* annot_dict, } } } - CPVT_VariableText vt(&provider); - vt.SetPlateRect( - CFX_FloatRect(body_rect.left, 0.0f, body_rect.right, 0.0f)); - vt.SetFontSize(FXSYS_IsFloatZero(font_size) ? 12.0f : font_size); - vt.Initialize(); - vt.SetText(item); - vt.RearrangeAll(); - - const float item_height = vt.GetContentRect().Height(); + // A row is one line, from the top of what is left. + field.size = FXSYS_IsFloatZero(font_size) ? 12.0f : font_size; + float item_height = 0; + const ByteString row = FieldText( + map, options, std::move(field), + CFX_FloatRect(body_rect.left, fy, body_rect.right, fy), &item_height); if (is_selected) { CFX_FloatRect item_rect = CFX_FloatRect( body_rect.left, fy - item_height, body_rect.right, fy); @@ -244,14 +241,10 @@ ByteString GenerateListBoxAP(const CPDF_Dictionary* annot_dict, body_stream << "BT\n" << GenerateColorAP(CFX_Color::MakeGray(1), PaintOperation::kFill) - << GenerateEditAP(provider.GetFontMap(), vt.GetIterator(), - CFX_PointF(0.0f, fy), true, 0) - << "ET\n"; + << row << "ET\n"; } else { body_stream << "BT\n" - << GenerateColorAP(text_color, PaintOperation::kFill) - << GenerateEditAP(provider.GetFontMap(), vt.GetIterator(), - CFX_PointF(0.0f, fy), true, 0) + << GenerateColorAP(text_color, PaintOperation::kFill) << row << "ET\n"; } fy -= item_height; @@ -264,7 +257,8 @@ void GenerateTextFieldFormAP(fxcrt::ostringstream& app_stream, const CPDF_Dictionary* annot_dict, const CFX_FloatRect& bbox, const DefaultAppearanceInfo& da_info, - CPVT_VariableText::Provider& provider, + CPDF_AnnotFontMap& map, + const CPDF_RichTextLayout::Options& options, const WideString* value_override) { const AppearanceCharacteristics mk = GetAppearanceCharacteristics(annot_dict->GetDictFor("MK")); @@ -311,9 +305,8 @@ void GenerateTextFieldFormAP(fxcrt::ostringstream& app_stream, clip_rect.Deflate(bw, bw); WriteRect(app_stream, clip_rect) << " re W n\n"; - CPVT_VariableText vt(&provider); - ByteString body = GenerateTextFieldAP(annot_dict, body_rect, - da_info.font_size, vt, value_override); + ByteString body = GenerateTextFieldAP( + annot_dict, body_rect, da_info.font_size, map, options, value_override); app_stream << "BT\n"; app_stream << GenerateColorAP(da_info.text_color, PaintOperation::kStroke); @@ -332,7 +325,8 @@ void GenerateComboBoxFormAP(fxcrt::ostringstream& app_stream, const CPDF_Dictionary* annot_dict, const CFX_FloatRect& bbox, const DefaultAppearanceInfo& da_info, - CPVT_VariableText::Provider& provider, + CPDF_AnnotFontMap& map, + const CPDF_RichTextLayout::Options& options, const WideString* value_override) { const AnnotationDimensionsAndColor dims = GetAnnotationDimensionsAndColor(annot_dict); @@ -356,14 +350,16 @@ void GenerateComboBoxFormAP(fxcrt::ostringstream& app_stream, body_rect.Deflate(border_info.width, border_info.width); app_stream << GenerateComboBoxAP(annot_dict, body_rect, da_info.text_color, - da_info.font_size, provider, value_override); + da_info.font_size, map, options, + value_override); } void GenerateListBoxFormAP(fxcrt::ostringstream& app_stream, const CPDF_Dictionary* annot_dict, const CFX_FloatRect& bbox, const DefaultAppearanceInfo& da_info, - CPVT_VariableText::Provider& provider) { + CPDF_AnnotFontMap& map, + const CPDF_RichTextLayout::Options& options) { const AnnotationDimensionsAndColor dims = GetAnnotationDimensionsAndColor(annot_dict); const BorderStyleInfo border_info = @@ -385,8 +381,9 @@ void GenerateListBoxFormAP(fxcrt::ostringstream& app_stream, CFX_FloatRect body_rect = bbox; body_rect.Deflate(border_info.width, border_info.width); - const ByteString body = GenerateListBoxAP( - annot_dict, body_rect, da_info.text_color, da_info.font_size, provider); + const ByteString body = + GenerateListBoxAP(annot_dict, body_rect, da_info.text_color, + da_info.font_size, map, options); if (body.GetLength() > 0) { app_stream << "/Tx BMC\nq\n"; WriteRect(app_stream, body_rect) << " re\nW\nn\n" << body << "Q\nEMC\n"; @@ -479,7 +476,8 @@ void GeneratePushButtonFormAP(fxcrt::ostringstream& app_stream, const CPDF_Dictionary* annot_dict, const CFX_FloatRect& bbox, const DefaultAppearanceInfo& da_info, - CPVT_VariableText::Provider& provider) { + CPDF_AnnotFontMap& map, + const CPDF_RichTextLayout::Options& options) { const AppearanceCharacteristics mk = GetAppearanceCharacteristics(annot_dict->GetDictFor("MK")); const BorderStyleInfo bs = GetBorderStyleInfo(annot_dict->GetDictFor("BS")); @@ -494,17 +492,13 @@ void GeneratePushButtonFormAP(fxcrt::ostringstream& app_stream, CFX_FloatRect body_rect = bbox; body_rect.Deflate(2.0f * bw, bw); - CPVT_VariableText vt(&provider); - vt.SetPlateRect(body_rect); - vt.SetAlignment(CPVT_VariableText::Alignment::kCenter); - SetVtFontSize(da_info.font_size, vt); - vt.Initialize(); - vt.SetText(mk.caption); - vt.RearrangeAll(); - const CFX_PointF offset( - 0.0f, (vt.GetContentRect().Height() - body_rect.Height()) / 2.0f); + CPDF_RichTextLayout::Field field; + field.text = mk.caption; + field.size = da_info.font_size; + field.alignment = 1; + field.centered = true; const ByteString caption = - GenerateEditAP(provider.GetFontMap(), vt.GetIterator(), offset, true, 0); + FieldText(map, options, std::move(field), body_rect); if (caption.IsEmpty()) { return; } @@ -726,7 +720,6 @@ bool GenerateFormAPToTarget(APGenerationTarget* target, } const ByteString& font_name = default_appearance_info.value().font_name; - RetainPtr font_dict; DaFontResolution da_font; if (target->IsPersistent()) { RetainPtr mutable_dr_font_dict; @@ -743,7 +736,6 @@ bool GenerateFormAPToTarget(APGenerationTarget* target, mutable_dr_font_dict = mutable_form_dict->GetOrCreateDictFor("DR")->GetOrCreateDictFor( "Font"); - dr_dict = mutable_form_dict->GetDictFor("DR"); } da_font = ResolveDaFontForPersistentTarget(doc, mutable_dr_font_dict.Get(), font_name); @@ -751,138 +743,93 @@ bool GenerateFormAPToTarget(APGenerationTarget* target, da_font = ResolveDaFontForEphemeralTarget(doc, dr_font_dict.Get(), font_name); } - font_dict = da_font.font_dict; - const bool use_registered_font_map = - target->IsPersistent() && - (CFX_FontRegistry::HasFallbackFonts() || - da_font.registered_font_id != CFX_FontRegistry::kInvalidFontId); - if (!font_dict && !use_registered_font_map) { - // Ephemeral render of a widget whose /DA names a registered font that has - // no /DR entry yet: draw with the fallback rather than nothing. + RetainPtr font_dict = da_font.font_dict; + if (!font_dict && + da_font.registered_font_id == CFX_FontRegistry::kInvalidFontId && + !da_font.document_program) { + // A /DA font the form doesn't have: draw with the fallback rather than + // nothing. font_dict = GenerateDirectFallbackFontDict(); } - auto* doc_page_data = CPDF_DocPageData::FromDocument(doc); RetainPtr default_font = - font_dict ? doc_page_data->GetFont(font_dict) : nullptr; - if (!default_font && !use_registered_font_map) { + font_dict ? CPDF_DocPageData::FromDocument(doc)->GetFont(font_dict) + : nullptr; + + // The text lays out in the shared engine, through the annotation font + // map: the /DA font first, then the fallbacks and the last resort, each + // embedded as the field's appearance uses it. + auto owned_map = std::make_unique( + doc, std::move(default_font), font_name, + /*allow_registered_fallbacks=*/true, da_font.registered_font_id, + /*install_dr_entry=*/target->IsPersistent(), + CPDF_AnnotFontMap::Owner::kWidget); + CPDF_AnnotFontMap& map = *owned_map; + da_font.ApplyTo(&map); + if (!map.HasDefaultFont()) { return false; } + CPDF_RichTextLayout::Options options; + options.typographic_features = doc->GetTypographicFeaturesEnabled(); const AnnotationDimensionsAndColor dims = GetAnnotationDimensionsAndColor(annot_dict); - - RetainPtr resources_dict; - RetainPtr normal_stream; - if (target->IsPersistent()) { - RetainPtr ap_dict = - annot_dict->GetOrCreateDictFor(pdfium::annotation::kAP); - normal_stream = ap_dict->GetMutableStreamFor("N"); - if (normal_stream) { - RetainPtr stream_dict = normal_stream->GetMutableDict(); - const bool cloned = - CloneResourcesDictIfMissingFromStream(stream_dict, dr_dict.Get()); - if (!cloned) { - if (!ValidateOrCreateFontResources(doc, stream_dict, font_dict, - font_name)) { - return false; - } - } - resources_dict = stream_dict->GetMutableDictFor("Resources"); - } else { - normal_stream = - doc->NewIndirect(pdfium::MakeRetain()); - ap_dict->SetNewFor("N", doc, normal_stream->GetObjNum()); - } - } else { - auto gs_dict = GenerateExtGStateDict(*annot_dict, "Normal"); - auto resource_font_dict = - GenerateResourceFontDict(doc, font_name, font_dict.Get()); - resources_dict = GenerateResourcesDict(doc, std::move(gs_dict), - std::move(resource_font_dict)); + fxcrt::ostringstream app_stream; + switch (type) { + case CPDF_AppearanceGenerator::kTextField: + GenerateTextFieldFormAP(app_stream, annot_dict, dims.bbox, + default_appearance_info.value(), map, options, + value_override); + break; + case CPDF_AppearanceGenerator::kComboBox: + GenerateComboBoxFormAP(app_stream, annot_dict, dims.bbox, + default_appearance_info.value(), map, options, + value_override); + break; + case CPDF_AppearanceGenerator::kListBox: + GenerateListBoxFormAP(app_stream, annot_dict, dims.bbox, + default_appearance_info.value(), map, options); + break; + case CPDF_AppearanceGenerator::kPushButton: + GeneratePushButtonFormAP(app_stream, annot_dict, dims.bbox, + default_appearance_info.value(), map, options); + break; } - auto generate_form_stream = [&](CPVT_VariableText::Provider& provider, - fxcrt::ostringstream& app_stream) { - switch (type) { - case CPDF_AppearanceGenerator::kTextField: - GenerateTextFieldFormAP(app_stream, annot_dict, dims.bbox, - default_appearance_info.value(), provider, - value_override); - break; - case CPDF_AppearanceGenerator::kComboBox: - GenerateComboBoxFormAP(app_stream, annot_dict, dims.bbox, - default_appearance_info.value(), provider, - value_override); - break; - case CPDF_AppearanceGenerator::kListBox: - GenerateListBoxFormAP(app_stream, annot_dict, dims.bbox, - default_appearance_info.value(), provider); - break; - case CPDF_AppearanceGenerator::kPushButton: - GeneratePushButtonFormAP(app_stream, annot_dict, dims.bbox, - default_appearance_info.value(), provider); - break; - } - }; - - if (use_registered_font_map) { - // EmbedPDF: form widgets need the same registered fallback/subset path as - // FreeText when their value/options contain glyphs outside the DA font. - // Keep the old CPVT_FontMap path unless a registered font is actually - // involved so existing form AP output remains stable by default. - CPDF_AnnotFontMap map( - doc, std::move(default_font), font_name, - /*allow_registered_fallbacks=*/true, da_font.registered_font_id, - /*install_dr_entry=*/true, CPDF_AnnotFontMap::Owner::kWidget); - if (!map.HasDefaultFont()) { + if (!target->IsPersistent()) { + RetainPtr fonts = map.CreateEphemeralFontResourceDict(); + if (!fonts) { return false; } - CPVT_VariableText::Provider provider(&map); - - fxcrt::ostringstream app_stream; - generate_form_stream(provider, app_stream); - - normal_stream->SetDataFromStringstreamAndRemoveFilter(&app_stream); - RetainPtr stream_dict = normal_stream->GetMutableDict(); - stream_dict->SetMatrixFor("Matrix", dims.matrix); - stream_dict->SetRectFor("BBox", dims.bbox); - RetainPtr stream_resources = - stream_dict->GetOrCreateDictFor("Resources"); - RetainPtr resource_font_dict = - map.CreateFontResourceDict(); - if (!resource_font_dict) { - return false; // registered /DA font without a resource: no appearance - } - stream_resources->SetFor("Font", std::move(resource_font_dict)); - return true; - } - - RetainPtr ephemeral_resources_dict = resources_dict; - CPVT_FontMap map(doc, std::move(resources_dict), std::move(default_font), - font_name); - CPVT_VariableText::Provider provider(&map); - - fxcrt::ostringstream app_stream; - generate_form_stream(provider, app_stream); - - if (!target->IsPersistent()) { + auto resources_dict = GenerateResourcesDict( + doc, GenerateExtGStateDict(*annot_dict, "Normal"), std::move(fonts)); + target->ephemeral_fonts = std::move(owned_map); return GenerateAPDict( - target, annot_dict, &app_stream, std::move(ephemeral_resources_dict), + target, annot_dict, &app_stream, std::move(resources_dict), /*is_text_markup_annotation=*/false, dims.matrix, dims.bbox); } + RetainPtr resource_font_dict = map.CreateFontResourceDict(); + if (!resource_font_dict) { + return false; // registered /DA font without a resource: no appearance + } + RetainPtr ap_dict = + annot_dict->GetOrCreateDictFor(pdfium::annotation::kAP); + RetainPtr normal_stream = ap_dict->GetMutableStreamFor("N"); + if (!normal_stream) { + normal_stream = + doc->NewIndirect(pdfium::MakeRetain()); + ap_dict->SetNewFor("N", doc, normal_stream->GetObjNum()); + } normal_stream->SetDataFromStringstreamAndRemoveFilter(&app_stream); RetainPtr stream_dict = normal_stream->GetMutableDict(); + // A form XObject, said so: a stream made here has no type yet, and + // readers look for /Subtype /Form. + stream_dict->SetNewFor("Type", "XObject"); + stream_dict->SetNewFor("Subtype", "Form"); stream_dict->SetMatrixFor("Matrix", dims.matrix); stream_dict->SetRectFor("BBox", dims.bbox); - - const bool cloned = - CloneResourcesDictIfMissingFromStream(stream_dict, dr_dict); - if (cloned) { - return true; - } - - ValidateOrCreateFontResources(doc, stream_dict, font_dict, font_name); + stream_dict->GetOrCreateDictFor("Resources") + ->SetFor("Font", std::move(resource_font_dict)); return true; } diff --git a/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp b/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp index 544d8d7781..3c65f94bf3 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp @@ -18,7 +18,6 @@ #include #include "constants/annotation_common.h" -#include "core/fxcrt/stream_write_utils.h" #include "core/fpdfapi/font/cpdf_font.h" #include "core/fpdfapi/page/cpdf_docpagedata.h" #include "core/fpdfapi/parser/cpdf_array.h" @@ -32,12 +31,14 @@ #include "core/fpdfdoc/cpdf_appearancegenerator_internal.h" #include "core/fpdfdoc/cpdf_color_utils.h" #include "core/fpdfdoc/cpdf_defaultappearance.h" +#include "core/fpdfdoc/cpdf_documentfonts.h" #include "core/fpdfdoc/cpdf_interactiveform.h" #include "core/fpdfdoc/cpdf_richtext.h" #include "core/fpdfdoc/cpdf_richtextlayout.h" #include "core/fpdfdoc/cpdf_richtextparser.h" #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/fx_system.h" +#include "core/fxcrt/stream_write_utils.h" #include "core/fxcrt/utf16.h" #include "core/fxge/cfx_fontregistry.h" @@ -353,6 +354,8 @@ struct RichFreeTextInputs { ByteString font_name; // its alias CFX_FontRegistry::FontId registered_font_id = CFX_FontRegistry::kInvalidFontId; + // The /DA alias resolved to a reserved document program, if it did. + DaFontResolution da_font; bool persistent = true; bool author_default_appearance = false; // writer: body face -> /DA }; @@ -373,6 +376,7 @@ PrepareRichFreeTextAPInternal(CPDF_Document* doc, /*install_dr_entry=*/in.persistent, CPDF_AnnotFontMap::Owner::kAnnotation); CPDF_AnnotFontMap& map = *prepared->fonts; + in.da_font.ApplyTo(&map); if (!in.author_default_appearance && in.document->source == CPDF_RichTextDocument::Source::kContents && map.HasDefaultFont() && !map.DefaultFontIsStandard()) { @@ -458,8 +462,8 @@ PrepareRichFreeTextAPInternal(CPDF_Document* doc, // request resolves here without text. Aliases are settled before the // content names them. const CPDF_RichTextStyle& body = in.document->body; - const int body_entry = - map.ResolveRichFace(body.family, body.weight, body.italic); + const int body_entry = map.ResolveRichFace( + body.family, body.weight, body.italic, WideStringView(), body.program); if (body_entry < 0) { return nullptr; } @@ -531,6 +535,7 @@ bool GenerateRichFreeTextAP(APGenerationTarget* target, in.default_font = std::move(default_font); in.font_name = font_name; in.registered_font_id = da_font.registered_font_id; + in.da_font = da_font; in.persistent = target->IsPersistent(); std::unique_ptr prepared = PrepareRichFreeTextAPInternal(doc, annot_dict, blend_name, std::move(in)); @@ -540,21 +545,20 @@ bool GenerateRichFreeTextAP(APGenerationTarget* target, return PublishRichFreeTextAPToTarget(target, annot_dict, std::move(prepared)); } -} // namespace - -namespace appearance_generator { - -// The text body of a rich FreeText appearance (C note §5): one Tj per -// word, relative Td per word, state operators only when they change, -// ActualText around runs whose glyphs do not spell their text. -void EmitRichTextBody(fxcrt::ostringstream& s, - const CPDF_RichTextLayout::Result& layout, - CPDF_AnnotFontMap& map, - const CFX_FloatRect& area) { +// The runs of |layout|, inside a text object: relative Td per string, state +// operators only when they change, ActualText around runs whose glyphs do +// not spell their text. A FreeText writes a string per word in its runs' +// colours, as Acrobat does, and collects its decorations; a form field +// writes a string per run in the colour set before it, as form fillers do. +void EmitTextRuns(fxcrt::ostringstream& s, + const CPDF_RichTextLayout::Result& layout, + CPDF_AnnotFontMap& map, + const CFX_FloatRect& area, + bool field, + std::vector* decorations) { using GlyphRun = CPDF_RichTextLayout::GlyphRun; using ShapedGlyph = CPDF_RichTextLayout::ShapedGlyph; - s << "BT\n"; int cur_entry = -1; float cur_size = -1; std::optional cur_color; @@ -562,7 +566,6 @@ void EmitRichTextBody(fxcrt::ostringstream& s, float cur_ts = 0; float cur_tz = 100; CFX_PointF origin(0, 0); // the last Td, in the form's space - std::vector decorations; for (const CPDF_RichTextLayout::Line& line : layout.lines) { const float baseline = area.top - line.baseline_y; @@ -581,7 +584,7 @@ void EmitRichTextBody(fxcrt::ostringstream& s, cur_entry = run.font_entry; cur_size = size; } - if (!cur_color.has_value() || *cur_color != run.style.color) { + if (!field && (!cur_color.has_value() || *cur_color != run.style.color)) { s << RichColorOperator(run.style.color, /*fill=*/true); cur_color = run.style.color; } @@ -627,14 +630,15 @@ void EmitRichTextBody(fxcrt::ostringstream& s, return (width * size / 1000.0f + run.style.letter_spacing) * scale; }; - // Words: a space ends the word it follows, as in Acrobat's streams. - // Run positions are relative to the text area's left edge. + // Words: a space ends the word it follows, as in Acrobat's streams; + // a field's run is one string. Run positions are relative to the text + // area's left edge. const float run_left = area.left + run.x; float x = run_left; size_t i = 0; const size_t count = run.glyphs.size(); while (i < count) { - size_t j = i; + size_t j = field ? count : i; while (j < count) { const bool space = is_space(run.glyphs[j]); ++j; @@ -736,14 +740,14 @@ void EmitRichTextBody(fxcrt::ostringstream& s, // baseline, 0.16 · descent thick; line-through 0.39 · ascent above, // 0.04 · ascent thick; `word` skips the spaces. const uint8_t decoration = run.style.decoration; - if (decoration) { + if (decoration && decorations) { const float run_baseline = baseline + run.rise; const float descent = face.descent * size; const float ascent = face.ascent * size; if (decoration & CPDF_RichTextStyle::kUnderline) { - decorations.push_back({run_left, run_left + run.width, - run_baseline - 0.44f * descent, - 0.16f * descent, run.style.color}); + decorations->push_back({run_left, run_left + run.width, + run_baseline - 0.44f * descent, + 0.16f * descent, run.style.color}); } if (decoration & CPDF_RichTextStyle::kWordUnderline) { float wx = run_left; @@ -752,9 +756,9 @@ void EmitRichTextBody(fxcrt::ostringstream& s, for (const ShapedGlyph& glyph : run.glyphs) { if (is_space(glyph)) { if (in_word) { - decorations.push_back({word_start, wx, - run_baseline - 0.44f * descent, - 0.16f * descent, run.style.color}); + decorations->push_back({word_start, wx, + run_baseline - 0.44f * descent, + 0.16f * descent, run.style.color}); in_word = false; } } else if (!in_word) { @@ -764,19 +768,32 @@ void EmitRichTextBody(fxcrt::ostringstream& s, wx += glyph.x_advance; } if (in_word) { - decorations.push_back({word_start, wx, - run_baseline - 0.44f * descent, - 0.16f * descent, run.style.color}); + decorations->push_back({word_start, wx, + run_baseline - 0.44f * descent, + 0.16f * descent, run.style.color}); } } if (decoration & CPDF_RichTextStyle::kLineThrough) { - decorations.push_back({run_left, run_left + run.width, - run_baseline + 0.39f * ascent, 0.04f * ascent, - run.style.color}); + decorations->push_back({run_left, run_left + run.width, + run_baseline + 0.39f * ascent, 0.04f * ascent, + run.style.color}); } } } } +} + +} // namespace + +namespace appearance_generator { + +void EmitRichTextBody(fxcrt::ostringstream& s, + const CPDF_RichTextLayout::Result& layout, + CPDF_AnnotFontMap& map, + const CFX_FloatRect& area) { + std::vector decorations; + s << "BT\n"; + EmitTextRuns(s, layout, map, area, /*field=*/false, &decorations); s << "ET\n"; std::optional stroke_color; for (const DecorationSegment& segment : decorations) { @@ -795,6 +812,13 @@ void EmitRichTextBody(fxcrt::ostringstream& s, } } +void EmitFieldTextBody(fxcrt::ostringstream& s, + const CPDF_RichTextLayout::Result& layout, + CPDF_AnnotFontMap& map, + const CFX_FloatRect& plate) { + EmitTextRuns(s, layout, map, plate, /*field=*/true, /*decorations=*/nullptr); +} + bool GenerateFreeTextAP(APGenerationTarget* target, CPDF_Dictionary* annot_dict, const ByteString& blend_name) { @@ -844,7 +868,8 @@ bool GenerateFreeTextAP(APGenerationTarget* target, RetainPtr default_font = da_font.font_dict ? doc_page_data->GetFont(da_font.font_dict) : nullptr; if (!default_font && - da_font.registered_font_id == CFX_FontRegistry::kInvalidFontId) { + da_font.registered_font_id == CFX_FontRegistry::kInvalidFontId && + !da_font.document_program) { return false; } @@ -1014,6 +1039,47 @@ bool CPDF_AppearanceGenerator::UpdateDefaultAppearanceRegisteredFont( return true; } +bool CPDF_AppearanceGenerator::UpdateDefaultAppearanceDocumentFont( + CPDF_Document* doc, + CPDF_Dictionary* annot_dict, + uint32_t program_objnum, + float font_size, + const CFX_Color& color) { + if (!doc || !annot_dict || program_objnum == 0) { + return false; + } + CPDF_DocumentFonts* fonts = CPDF_DocumentFonts::For(doc); + // The program must be one the walk found, and one the layout can author + // with (the same test the font map applies before it writes with one). + bool can_author = false; + for (const CPDF_DocumentFonts::Face& face : fonts->CollectFaces()) { + for (const CPDF_DocumentFonts::Program& program : face.programs) { + if (program.objnum != program_objnum) { + continue; + } + can_author = program.maps_unicode && program.permission.has_value() && + (*program.permission == + CFX_FontRegistry::EmbeddingPermission::kInstallable || + *program.permission == + CFX_FontRegistry::EmbeddingPermission::kEditable); + } + } + ByteString resource_key; + if (!can_author || + !fonts->ReserveProgramAlias(program_objnum, &resource_key)) { + return false; + } + ByteString da_font_part = DefaultAppearanceFontPart(resource_key, font_size); + if (da_font_part.IsEmpty()) { + return false; + } + ByteString da_color_part = GenerateColorAP(color, PaintOperation::kFill); + da_color_part.TrimBack('\n'); + + annot_dict->SetNewFor("DA", da_color_part + " " + da_font_part); + return true; +} + CPDF_AppearanceGenerator::PreparedRichFreeTextAP::PreparedRichFreeTextAP() = default; diff --git a/core/fpdfdoc/cpdf_appearancegenerator_internal.h b/core/fpdfdoc/cpdf_appearancegenerator_internal.h index 35ad59fa2d..70f4e0cbf3 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_internal.h +++ b/core/fpdfdoc/cpdf_appearancegenerator_internal.h @@ -19,7 +19,7 @@ #include #include -#include "core/fxcrt/stream_write_utils.h" +#include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fpdfdoc/cpdf_annot.h" #include "core/fpdfdoc/cpdf_annotfontmap.h" #include "core/fpdfdoc/cpdf_appearancegenerator.h" @@ -30,6 +30,7 @@ #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/span.h" +#include "core/fxcrt/stream_write_utils.h" #include "core/fxcrt/widestring.h" #include "core/fxge/cfx_color.h" #include "core/fxge/cfx_fontregistry.h" @@ -116,6 +117,15 @@ struct DaFontResolution { RetainPtr font_dict; CFX_FontRegistry::FontId registered_font_id = CFX_FontRegistry::kInvalidFontId; + // An alias this session reserved for a program already in the document + // (CPDF_DocumentFonts): the program the /DA entry draws with until the + // appearance installs its /DR entry. + RetainPtr document_program; + ByteString document_program_base_font; + CPDF_AnnotFontSubset::FaceIdentity document_program_identity; + + // Makes |map|'s /DA entry the reserved document program, if there is one. + void ApplyTo(CPDF_AnnotFontMap* map) const; }; struct APGenerationTarget { @@ -191,7 +201,6 @@ ByteString StringFromFontNameAndSize(const ByteString& font_name, float font_size); ByteString DefaultAppearanceFontPart(const ByteString& font_name, float font_size); -void SetVtFontSize(float font_size, CPVT_VariableText& vt); std::optional GetDefaultAppearanceInfo( const CPDF_Dictionary* annot_dict, const CPDF_Dictionary* acroform_dict); @@ -298,10 +307,18 @@ bool GenerateCaretAP(APGenerationTarget* target, const ByteString& blend_name); // cpdf_appearancegenerator_freetext.cpp +// A FreeText's text, or a caption's, in its own text object: a string per +// word, in its runs' colours, with their decorations. void EmitRichTextBody(fxcrt::ostringstream& s, const CPDF_RichTextLayout::Result& layout, CPDF_AnnotFontMap& map, const CFX_FloatRect& area); +// A form field's text (CPDF_RichTextLayout::ArrangeField()), inside the +// caller's text object, in the colour it set: Td, Tf and a string per run. +void EmitFieldTextBody(fxcrt::ostringstream& s, + const CPDF_RichTextLayout::Result& layout, + CPDF_AnnotFontMap& map, + const CFX_FloatRect& plate); bool GenerateFreeTextAP(APGenerationTarget* target, CPDF_Dictionary* annot_dict, const ByteString& blend_name); diff --git a/core/fpdfdoc/cpdf_appearancegenerator_redaction.cpp b/core/fpdfdoc/cpdf_appearancegenerator_redaction.cpp index 69510cc564..5ee9b64f6f 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_redaction.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_redaction.cpp @@ -17,7 +17,6 @@ #include #include "constants/annotation_common.h" -#include "core/fxcrt/stream_write_utils.h" #include "core/fpdfapi/font/cpdf_font.h" #include "core/fpdfapi/page/cpdf_docpagedata.h" #include "core/fpdfapi/parser/cpdf_array.h" @@ -32,16 +31,17 @@ #include "core/fpdfdoc/cpdf_appearancegenerator_internal.h" #include "core/fpdfdoc/cpdf_color_utils.h" #include "core/fpdfdoc/cpdf_interactiveform.h" -#include "core/fpdfdoc/cpvt_variabletext.h" +#include "core/fpdfdoc/cpdf_richtextlayout.h" #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/fx_system.h" +#include "core/fxcrt/stream_write_utils.h" #include "core/fxge/cfx_fontregistry.h" using appearance_generator::APGenerationTarget; using appearance_generator::DaFontResolution; using appearance_generator::DefaultAppearanceInfo; +using appearance_generator::EmitFieldTextBody; using appearance_generator::GenerateColorAP; -using appearance_generator::GenerateEditAP; using appearance_generator::GenerateExtGStateDict; using appearance_generator::GenerateFallbackFontDict; using appearance_generator::GenerateResourcesDict; @@ -55,7 +55,6 @@ using appearance_generator::MarkupQuad; using appearance_generator::PaintOperation; using appearance_generator::ReadMarkupQuad; using appearance_generator::ResolveDaFontForPersistentTarget; -using appearance_generator::SetVtFontSize; namespace { @@ -169,73 +168,71 @@ constexpr float kRedactRepeatFallbackFontSize = 12.0f; constexpr int kRedactMaxRepeatDoublings = 10; // 2^10 = 1024 label instances -// EmbedPDF: lay out the /OverlayText label for one redacted region with the -// same CPVT + annotation-font-map stack as FreeText appearances, so /DA fonts -// (standard, DR-resolved, or registered runtime fonts) shape and subset -// identically. Top-aligned in reading order; ISO 32000-2 prescribes neither -// the vertical placement nor the /Repeat tiling, so /Repeat is expressed as -// "repeat the label, space-joined, wrapped to the region, clipped". +// EmbedPDF: lay out the /OverlayText label for one redacted region as a +// form field's text, in the shared text engine with the annotation font map, +// so /DA fonts (standard, DR-resolved, or registered runtime fonts) shape and +// subset as every appearance's do. Top-aligned in reading order; ISO 32000-2 +// prescribes neither the vertical placement nor the /Repeat tiling, so +// /Repeat is expressed as "repeat the label, space-joined, wrapped to the +// region, clipped". void AppendRedactLabelForRegion(CPDF_AnnotFontMap& map, + const CPDF_RichTextLayout::Options& options, const CPDF_Dictionary* annot_dict, const WideString& overlay_text, float da_font_size, const CFX_Color& label_color, const CFX_FloatRect& region, fxcrt::ostringstream& stream) { - CPVT_VariableText::Provider provider(&map); - CPVT_VariableText vt(&provider); - vt.SetPlateRect(region); - vt.SetAlignment( - CPVT_VariableText::ToAlignment(annot_dict->GetIntegerFor("Q"))); + const CPDF_AnnotFontMap::EntryFacts face = map.DescribeEntry(0); + CPDF_RichTextLayout::Field field; + field.text = overlay_text; + field.face_entry = 0; + field.weight = face.weight; + field.italic = face.italic; + const int q = annot_dict->GetIntegerFor("Q"); + field.alignment = q == 1 || q == 2 ? q : 0; const bool repeat = annot_dict->GetBooleanFor("Repeat", false); - // A label shown once at auto-size (0) fits the region on one line: CPVT - // auto-sizes multi-line text only up to 12pt (the rule for multi-line form + // A label shown once at auto-size (0) fits the region on one line: a + // fitted size of several lines stops at 12pt (the rule for multi-line form // fields), which would leave a large region mostly empty. A label at a set // size, or a repeated one, wraps to the region. const bool fit_one_line = !repeat && FXSYS_IsFloatZero(da_font_size); - vt.SetMultiLine(!fit_one_line); - vt.SetAutoReturn(!fit_one_line); - // CPVT auto-sizing fits ALL text into the plate, so it cannot combine with - // /Repeat (more repetitions would only shrink the font); pin a concrete - // size for the repeat case when /DA asks for auto (size 0). - float font_size = da_font_size; - if (repeat && FXSYS_IsFloatZero(font_size)) { - font_size = kRedactRepeatFallbackFontSize; + field.multiline = !fit_one_line; + // A fitted size fits ALL the text into the region, so it cannot combine + // with /Repeat (more repetitions would only shrink the font); pin a + // concrete size for the repeat case when /DA asks for auto (size 0). + field.size = da_font_size; + if (repeat && FXSYS_IsFloatZero(field.size)) { + field.size = kRedactRepeatFallbackFontSize; } - SetVtFontSize(font_size, vt); - vt.Initialize(); - vt.SetText(overlay_text); - vt.RearrangeAll(); + CPDF_RichTextLayout layout(&map, options); + CPDF_RichTextLayout::Result laid = layout.ArrangeField(field, region); if (repeat) { // Double the space-joined text until the wrapped layout covers the region // vertically; the last row's surplus is removed by the region clip below. // Bounded to keep tiny-font/huge-region combinations sane. - WideString tiled = overlay_text; - for (int i = 0; i < kRedactMaxRepeatDoublings && - vt.GetContentRect().Height() < region.Height(); + for (int i = 0; + i < kRedactMaxRepeatDoublings && laid.content_height < region.Height(); ++i) { - WideString doubled = tiled; + WideString doubled = field.text; doubled += L' '; - doubled += tiled; - tiled = std::move(doubled); - vt.SetText(tiled); - vt.RearrangeAll(); + doubled += field.text; + field.text = std::move(doubled); + laid = layout.ArrangeField(field, region); } } - const ByteString body = - GenerateEditAP(vt.GetProvider()->GetFontMap(), vt.GetIterator(), - CFX_PointF(0.0f, 0.0f), /*continuous=*/true, - /*sub_word=*/0); - if (body.IsEmpty()) { + fxcrt::ostringstream body; + EmitFieldTextBody(body, laid, map, region); + if (body.str().empty()) { return; } stream << "q\n"; WriteRect(stream, region) << " re W n\n"; stream << "BT\n" - << GenerateColorAP(label_color, PaintOperation::kFill) << body + << GenerateColorAP(label_color, PaintOperation::kFill) << body.str() << "ET\nQ\n"; } @@ -333,7 +330,8 @@ bool AppendRedactOverlayOps(CPDF_Document* doc, ? CPDF_DocPageData::FromDocument(doc)->GetFont(da_font.font_dict) : nullptr; if (!default_font && - da_font.registered_font_id == CFX_FontRegistry::kInvalidFontId) { + da_font.registered_font_id == CFX_FontRegistry::kInvalidFontId && + !da_font.document_program) { return has_fill; } @@ -341,12 +339,15 @@ bool AppendRedactOverlayOps(CPDF_Document* doc, /*allow_registered_fallbacks=*/true, da_font.registered_font_id, /*install_dr_entry=*/true); + da_font.ApplyTo(&map); if (!map.HasDefaultFont()) { return has_fill; } + CPDF_RichTextLayout::Options options; + options.typographic_features = doc->GetTypographicFeaturesEnabled(); for (const RedactOverlayRegion& region : regions) { - AppendRedactLabelForRegion(map, annot_dict, overlay_text, da_font_size, - label_color, region.bbox, stream); + AppendRedactLabelForRegion(map, options, annot_dict, overlay_text, + da_font_size, label_color, region.bbox, stream); } *out_font_resources = map.CreateFontResourceDict(); return !!*out_font_resources; diff --git a/core/fpdfdoc/cpdf_appearancegenerator_text.cpp b/core/fpdfdoc/cpdf_appearancegenerator_text.cpp index 4e49dc7bd0..7a27ff908f 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_text.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_text.cpp @@ -14,7 +14,6 @@ #include #include "constants/font_encodings.h" -#include "core/fxcrt/stream_write_utils.h" #include "core/fpdfapi/font/cpdf_font.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_document.h" @@ -28,11 +27,14 @@ #include "core/fpdfdoc/cpdf_annotfontsubset.h" #include "core/fpdfdoc/cpdf_appearancegenerator_internal.h" #include "core/fpdfdoc/cpdf_defaultappearance.h" +#include "core/fpdfdoc/cpdf_documentfonts.h" #include "core/fpdfdoc/cpdf_embed_metadata.h" #include "core/fpdfdoc/cpvt_variabletext.h" #include "core/fpdfdoc/cpvt_word.h" +#include "core/fpdfdoc/ipvt_fontmap.h" #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/fx_system.h" +#include "core/fxcrt/stream_write_utils.h" #include "core/fxge/cfx_fontregistry.h" using appearance_generator::DaFontResolution; @@ -132,14 +134,6 @@ ByteString DefaultAppearanceFontPart(const ByteString& font_name, return ByteString(font_stream); } -void SetVtFontSize(float font_size, CPVT_VariableText& vt) { - if (FXSYS_IsFloatZero(font_size)) { - vt.SetAutoFontSize(true); - } else { - vt.SetFontSize(font_size); - } -} - std::optional GetDefaultAppearanceInfo( const CPDF_Dictionary* annot_dict, const CPDF_Dictionary* acroform_dict) { @@ -348,6 +342,27 @@ RetainPtr GetFontFromDrFontDictOrGenerateFallback( return new_font_dict; } +// A /DA alias this session reserved for a document program. +bool ResolveReservedDocumentProgram(const CPDF_Document* doc, + const ByteString& font_name, + DaFontResolution* result) { + CPDF_DocumentFonts* fonts = CPDF_DocumentFonts::Find(doc); + std::optional reserved = + fonts ? fonts->ProgramForAlias(font_name) : std::nullopt; + if (!reserved.has_value()) { + return false; + } + result->document_program = fonts->ProgramStream(reserved->objnum); + if (!result->document_program) { + return false; + } + result->document_program_base_font = reserved->base_font_name; + result->document_program_identity.family = reserved->family.ToUTF8(); + result->document_program_identity.weight = reserved->weight; + result->document_program_identity.italic = reserved->italic; + return true; +} + DaFontResolution ResolveDaFontForPersistentTarget(CPDF_Document* doc, CPDF_Dictionary* dr_font_dict, const ByteString& font_name) { @@ -364,6 +379,9 @@ DaFontResolution ResolveDaFontForPersistentTarget(CPDF_Document* doc, result.registered_font_id = *font_id; return result; } + if (ResolveReservedDocumentProgram(doc, font_name, &result)) { + return result; + } // EmbedPDF: the fallback is a write, so it goes into /DR/Font as the // document opens it for writing - a layer transaction copies the catalog's // chain up first. |dr_font_dict| may be a read-path view of a committed @@ -405,10 +423,21 @@ DaFontResolution ResolveDaFontForEphemeralTarget( result.registered_font_id = *font_id; return result; } + if (ResolveReservedDocumentProgram(doc, font_name, &result)) { + return result; + } result.font_dict = GenerateDirectFallbackFontDict(); return result; } +void DaFontResolution::ApplyTo(CPDF_AnnotFontMap* map) const { + if (document_program) { + map->UseDocumentProgramForDefault(document_program, + document_program_base_font, + document_program_identity); + } +} + RetainPtr GenerateResourceFontDict( CPDF_Document* doc, const ByteString& font_name, diff --git a/core/fpdfdoc/cpdf_displayprogram.cpp b/core/fpdfdoc/cpdf_displayprogram.cpp new file mode 100644 index 0000000000..548dd5adcd --- /dev/null +++ b/core/fpdfdoc/cpdf_displayprogram.cpp @@ -0,0 +1,187 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fpdfdoc/cpdf_displayprogram.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "core/fpdfapi/font/cpdf_font.h" +#include "core/fpdfapi/page/cpdf_docpagedata.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfdoc/cpdf_fontface.h" +#include "core/fxcrt/fx_string.h" +#include "core/fxge/cfx_face.h" +#include "core/fxge/cfx_font.h" +#include "core/fxge/cfx_glyphoutline.h" +#include "core/fxge/cfx_opentypewriter.h" +#include "core/fxge/fx_font.h" + +namespace { + +// What the engine writes for a character: the glyph it draws and the advance +// it lays it out with, in thousandths of the font size. +struct Written { + uint32_t glyph; + int width; +}; + +bool operator<(const Written& left, const Written& right) { + return std::tie(left.glyph, left.width) < std::tie(right.glyph, right.width); +} + +// Every character the engine can write with |font|: the layout asks the +// font's encoding for each one's code (CharCodeFromUnicode) and draws that +// code's glyph, so a character is written when that code exists and has a +// glyph. The layout writes Basic Multilingual Plane characters only. The +// candidates are what the font's codes read as. +std::map WrittenCharacters(CPDF_Font* font) { + std::map written; + const uint32_t last_code = font->IsCIDFont() ? 0xffff : 0xff; + for (uint32_t code = 0; code <= last_code; ++code) { + const WideString text = font->UnicodeFromCharCode(code); + if (text.GetLength() != 1) { + continue; + } + const uint32_t scalar = static_cast(text[0]); + if (scalar == 0 || scalar > 0xffff || + (scalar >= 0xd800 && scalar <= 0xdfff) || written.contains(scalar)) { + continue; + } + const uint32_t write_code = + font->CharCodeFromUnicode(static_cast(scalar)); + if (write_code == CPDF_Font::kInvalidCharCode || write_code == 0) { + continue; + } + bool vertical = false; + const int glyph = font->GlyphFromCharCode(write_code, &vertical); + if (glyph <= 0) { + continue; + } + written[scalar] = {static_cast(glyph), + font->GetCharWidth(write_code)}; + } + return written; +} + +// |text| with only what a name table and a PostScript name may hold. +ByteString AsciiName(const WideString& text, bool postscript) { + ByteString name; + for (wchar_t c : text) { + const bool letter_or_digit = (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9'); + if (letter_or_digit || c == '-' || (!postscript && c == ' ')) { + name += static_cast(c); + } + if (name.GetLength() >= 63) { + break; + } + } + return name; +} + +} // namespace + +CPDF_DisplayProgram::CPDF_DisplayProgram() = default; +CPDF_DisplayProgram::CPDF_DisplayProgram(CPDF_DisplayProgram&&) noexcept = + default; +CPDF_DisplayProgram& CPDF_DisplayProgram::operator=( + CPDF_DisplayProgram&&) noexcept = default; +CPDF_DisplayProgram::~CPDF_DisplayProgram() = default; + +std::optional BuildDisplayProgram( + CPDF_Document* doc, + const CPDF_Dictionary* font_dict, + const ByteString& postscript_name, + const CPDF_FontFace& face) { + RetainPtr font = CPDF_DocPageData::FromDocument(doc)->GetFont( + pdfium::WrapRetain(const_cast(font_dict))); + if (!font || font->IsType3Font() || !font->IsEmbedded()) { + return std::nullopt; + } + CFX_Font* program = font->GetFont(); + RetainPtr program_face = program->GetFace(); + // A program that didn't load is drawn with a substitute: nothing of its own. + if (!program_face || program->GetSubstFont()) { + return std::nullopt; + } + const std::map written = WrittenCharacters(font.Get()); + if (written.empty()) { + return std::nullopt; + } + + CFX_OpenTypeFont display; + display.units_per_em = + program_face->GetUnitsPerEm() ? program_face->GetUnitsPerEm() : 1000; + const float em = display.units_per_em / 1000.0f; + auto advance = [&](int thousandths) { + return static_cast( + std::clamp(std::lround(thousandths * em), 0, 0xffff)); + }; + auto glyph_of = [&](uint32_t source) { + CFX_OpenTypeFont::Glyph glyph; + std::optional outline = + program_face->LoadUnscaledGlyphOutline(source); + if (outline.has_value()) { + glyph.outline = std::move(*outline); + } + return glyph; + }; + + // .notdef first; a browser never draws it, the cmap only maps written + // characters. + display.glyphs.push_back(glyph_of(0)); + display.glyphs.back().advance = advance(500); + // One glyph per source glyph and advance: two codes may reach one glyph + // with different widths. + std::map glyph_index; + for (const auto& [scalar, character] : written) { + auto [it, added] = glyph_index.emplace( + character, static_cast(display.glyphs.size())); + if (added) { + if (display.glyphs.size() >= 0xffff) { + return std::nullopt; + } + display.glyphs.push_back(glyph_of(character.glyph)); + display.glyphs.back().advance = advance(character.width); + } + display.cmap.emplace_back(scalar, it->second); + } + + float ascent = 0; + float descent = 0; + ProgramLineMetrics(program, &ascent, &descent); + display.ascent = static_cast(std::lround(ascent * display.units_per_em)); + display.descent = + -static_cast(std::lround(descent * display.units_per_em)); + display.postscript_name = + AsciiName(WideString::FromLatin1(postscript_name.AsStringView()), + /*postscript=*/true); + if (display.postscript_name.IsEmpty()) { + display.postscript_name = "EmbedPDFDisplay"; + } + display.family = AsciiName(face.family, /*postscript=*/false); + if (display.family.IsEmpty()) { + display.family = display.postscript_name; + } + display.weight = face.weight; + display.italic = face.italic; + display.italic_angle = static_cast(font->GetItalicAngle()); + display.fixed_pitch = + (font->GetFontFlags() & pdfium::kFontStyleFixedPitch) != 0; + CPDF_DisplayProgram result; + result.bytes = WriteOpenTypeCFF(display); + if (result.bytes.empty()) { + return std::nullopt; + } + for (const auto& [scalar, glyph] : display.cmap) { + result.characters.push_back(scalar); + } + return result; +} diff --git a/core/fpdfdoc/cpdf_displayprogram.h b/core/fpdfdoc/cpdf_displayprogram.h new file mode 100644 index 0000000000..fa06df5789 --- /dev/null +++ b/core/fpdfdoc/cpdf_displayprogram.h @@ -0,0 +1,50 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 +// +// EmbedPDF(fonts): a document font as a browser can draw it. A browser loads +// TrueType and OpenType, not Type 1 or bare CFF, and it reaches glyphs through +// a font's own cmap and spaces them by its own metrics, where the engine goes +// through the PDF's encoding and its /Widths. So the engine's view of the font +// is written as an OpenType font of its own: the same glyphs, reached by the +// same characters, at the same advances. + +#ifndef CORE_FPDFDOC_CPDF_DISPLAYPROGRAM_H_ +#define CORE_FPDFDOC_CPDF_DISPLAYPROGRAM_H_ + +#include + +#include +#include + +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/data_vector.h" + +class CPDF_Dictionary; +class CPDF_Document; +struct CPDF_FontFace; + +struct CPDF_DisplayProgram { + CPDF_DisplayProgram(); + CPDF_DisplayProgram(CPDF_DisplayProgram&&) noexcept; + CPDF_DisplayProgram& operator=(CPDF_DisplayProgram&&) noexcept; + ~CPDF_DisplayProgram(); + + // An OpenType font with CFF outlines. + DataVector bytes; + // The characters its cmap maps, in order. + std::vector characters; +}; + +// The display program of |font_dict|: the glyphs its embedded program draws; +// a Unicode cmap holding exactly the characters the engine writes with the +// font, each mapped through the PDF's encoding to the glyph it draws; and the +// advances the engine lays them out with. |postscript_name| and |face| name +// it. nullopt when the font has no outlines of its own (Type 3, nothing +// embedded, a program that doesn't load) or writes no character. +std::optional BuildDisplayProgram( + CPDF_Document* doc, + const CPDF_Dictionary* font_dict, + const ByteString& postscript_name, + const CPDF_FontFace& face); + +#endif // CORE_FPDFDOC_CPDF_DISPLAYPROGRAM_H_ diff --git a/core/fpdfdoc/cpdf_documentfonts.cpp b/core/fpdfdoc/cpdf_documentfonts.cpp new file mode 100644 index 0000000000..10fd372d68 --- /dev/null +++ b/core/fpdfdoc/cpdf_documentfonts.cpp @@ -0,0 +1,834 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fpdfdoc/cpdf_documentfonts.h" + +#include +#include +#include +#include +#include + +#include "core/fdrm/fx_crypt_sha.h" +#include "core/fpdfapi/page/cpdf_docpagedata.h" +#include "core/fpdfapi/parser/cpdf_array.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_layer_document.h" +#include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fpdfapi/parser/cpdf_stream_acc.h" +#include "core/fpdfdoc/cpdf_displayprogram.h" +#include "core/fpdfdoc/cpdf_fontface.h" +#include "core/fxcrt/utf16.h" +#include "core/fxge/cfx_face.h" +#include "core/fxge/cfx_font.h" +#include "core/fxge/cfx_sfnt.h" +#include "core/fxge/fx_fontencoding.h" + +namespace { + +// Deep enough for any real nesting of pages, forms and patterns; a cycle the +// walked set misses (a direct object repeated) stops here. +constexpr int kMaxDepth = 64; + +// The font dictionaries the drawings of one place can use, each once: the +// AcroForm's, a page's, or the whole document's. Notes the object number of +// every indirect object it reads, so a change to any of them can be told. +class FontWalk { + public: + explicit FontWalk(CPDF_Document* doc) : doc_(doc) {} + + void Form() { + RetainPtr root = + Noted(pdfium::WrapRetain(doc_->GetRoot())); + RetainPtr acroform = + root ? Noted(root->GetDictFor("AcroForm")) : nullptr; + if (acroform) { + Resources(acroform->GetDictFor("DR"), 0); + Fields(acroform->GetArrayFor("Fields"), 0); + } + } + + void Page(RetainPtr page) { + if (!Noted(page)) { + return; + } + Resources(InheritedResources(page), 0); + RetainPtr annots = Noted(page->GetArrayFor("Annots")); + for (size_t j = 0; annots && j < annots->size(); ++j) { + Appearance(annots->GetDictAt(j), 0); + } + } + + void Document() { + Form(); + const int page_count = doc_->GetPageCount(); + for (int i = 0; i < page_count; ++i) { + Page(doc_->GetPageDictionary(i)); + } + } + + std::vector> TakeFonts() { + return std::move(fonts_); + } + // Sorted. + std::vector TakeReads() { + return std::vector(reads_.begin(), reads_.end()); + } + + private: + template + RetainPtr Noted(RetainPtr object) { + if (object && object->GetObjNum() != 0) { + reads_.insert(object->GetObjNum()); + } + return object; + } + + // A page's /Resources, or the nearest one up its /Parent chain. + RetainPtr InheritedResources( + RetainPtr page) { + std::set seen; + RetainPtr node = std::move(page); + for (int i = 0; node && i < kMaxDepth && seen.insert(node.Get()).second; + ++i) { + if (RetainPtr resources = + node->GetDictFor("Resources")) { + return resources; + } + node = Noted(node->GetDictFor("Parent")); + } + return nullptr; + } + + void Resources(RetainPtr resources, int depth) { + if (!Noted(resources) || depth > kMaxDepth || + !walked_.insert(resources.Get()).second) { + return; + } + if (RetainPtr fonts = + Noted(resources->GetDictFor("Font"))) { + CPDF_DictionaryLocker locker(fonts); + for (const auto& [key, value] : locker) { + Font(value ? ToDictionary(value->GetDirect()) : nullptr, depth); + } + } + if (RetainPtr xobjects = + Noted(resources->GetDictFor("XObject"))) { + CPDF_DictionaryLocker locker(xobjects); + for (const auto& [key, value] : locker) { + RetainPtr stream = + value ? ToStream(value->GetDirect()) : nullptr; + if (stream && stream->GetDict()->GetNameFor("Subtype") == "Form") { + FormStream(std::move(stream), depth); + } + } + } + // Tiling patterns are streams with their own resources; shading + // patterns are dictionaries and draw no text. + if (RetainPtr patterns = + Noted(resources->GetDictFor("Pattern"))) { + CPDF_DictionaryLocker locker(patterns); + for (const auto& [key, value] : locker) { + FormStream(value ? ToStream(value->GetDirect()) : nullptr, depth); + } + } + if (RetainPtr states = + Noted(resources->GetDictFor("ExtGState"))) { + CPDF_DictionaryLocker locker(states); + for (const auto& [key, value] : locker) { + RetainPtr state = + Noted(value ? ToDictionary(value->GetDirect()) : nullptr); + RetainPtr mask = + state ? Noted(state->GetDictFor("SMask")) : nullptr; + FormStream(mask ? mask->GetStreamFor("G") : nullptr, depth); + } + } + } + + void FormStream(RetainPtr stream, int depth) { + if (!Noted(stream) || depth > kMaxDepth || + !walked_.insert(stream.Get()).second) { + return; + } + Resources(stream->GetDict()->GetDictFor("Resources"), depth + 1); + } + + void Font(RetainPtr font, int depth) { + if (!Noted(font) || !font_set_.insert(font.Get()).second) { + return; + } + if (font->GetNameFor("Subtype") == "Type3") { + // A Type 3 glyph is a drawing, and may draw text in other fonts. + Resources(font->GetDictFor("Resources"), depth + 1); + } + fonts_.push_back(std::move(font)); + } + + // Every state of the normal, rollover and down appearances. + void Appearance(RetainPtr annot, int depth) { + RetainPtr ap = + Noted(annot) ? Noted(annot->GetDictFor("AP")) : nullptr; + if (!ap) { + return; + } + for (const char* key : {"N", "R", "D"}) { + RetainPtr entry = Noted(ap->GetDirectObjectFor(key)); + if (const CPDF_Stream* stream = entry ? entry->AsStream() : nullptr) { + FormStream(pdfium::WrapRetain(stream), depth); + continue; + } + const CPDF_Dictionary* states = entry ? entry->AsDictionary() : nullptr; + if (!states) { + continue; + } + CPDF_DictionaryLocker locker(states); + for (const auto& [state, value] : locker) { + FormStream(value ? ToStream(value->GetDirect()) : nullptr, depth); + } + } + } + + // Fields' own /DR, down their /Kids. + void Fields(RetainPtr fields, int depth) { + if (!Noted(fields)) { + return; + } + for (size_t i = 0; depth <= kMaxDepth && i < fields->size(); ++i) { + RetainPtr field = Noted(fields->GetDictAt(i)); + if (!field || !walked_.insert(field.Get()).second) { + continue; + } + Resources(field->GetDictFor("DR"), depth + 1); + Fields(field->GetArrayFor("Kids"), depth + 1); + } + } + + UnownedPtr const doc_; + std::set walked_; + std::set font_set_; + std::vector> fonts_; + std::set reads_; +}; + +// The program stream a font dictionary carries, and what kind it says it is. +// An /OpenType /FontFile3 says nothing about its outlines: its bytes do. +std::pair, CPDF_DocumentFonts::Format> ProgramOf( + const CPDF_Dictionary* font) { + using Format = CPDF_DocumentFonts::Format; + if (font->GetNameFor("Subtype") == "Type3") { + return {nullptr, Format::kType3}; + } + RetainPtr descriptor = FontDescriptorOf(font); + if (!descriptor) { + return {nullptr, Format::kNone}; + } + if (RetainPtr stream = + descriptor->GetStreamFor("FontFile2")) { + return {std::move(stream), Format::kTrueType}; + } + if (RetainPtr stream = + descriptor->GetStreamFor("FontFile")) { + return {std::move(stream), Format::kType1}; + } + if (RetainPtr stream = + descriptor->GetStreamFor("FontFile3")) { + const bool sfnt = stream->GetDict()->GetNameFor("Subtype") == "OpenType"; + return {std::move(stream), sfnt ? Format::kTrueType : Format::kCFF}; + } + return {nullptr, Format::kNone}; +} + +// The /BaseFont that names the program: a Type0 font's descendant's. +ByteString ProgramBaseFont(const CPDF_Dictionary* font) { + if (font->GetNameFor("Subtype") == "Type0") { + RetainPtr descendants = + font->GetArrayFor("DescendantFonts"); + RetainPtr descendant = + descendants ? descendants->GetDictAt(0) : nullptr; + if (descendant && !descendant->GetNameFor("BaseFont").IsEmpty()) { + return descendant->GetNameFor("BaseFont"); + } + } + return font->GetNameFor("BaseFont"); +} + +bool IsSfnt(CPDF_DocumentFonts::Format format) { + return format == CPDF_DocumentFonts::Format::kTrueType || + format == CPDF_DocumentFonts::Format::kOpenTypeCFF; +} + +// Whether a browser loads |program| as it is: an sfnt that maps characters +// through a Unicode cmap, its tables where the font format puts them. +bool LoadsAsIs(const CPDF_DocumentFonts::Program& program) { + return IsSfnt(program.format) && program.maps_unicode && + program.tables_aligned; +} + +// |scalars| as ranges, in order. +std::vector RangesOf(std::vector scalars) { + std::ranges::sort(scalars); + scalars.erase(std::unique(scalars.begin(), scalars.end()), scalars.end()); + std::vector ranges; + for (uint32_t scalar : scalars) { + CFX_AppendCodePoint(&ranges, scalar); + } + return ranges; +} + +} // namespace + +CPDF_DocumentFonts::CPDF_DocumentFonts(CPDF_Document* doc) : doc_(doc) {} + +CPDF_DocumentFonts::~CPDF_DocumentFonts() = default; + +// static +CPDF_DocumentFonts* CPDF_DocumentFonts::For(CPDF_Document* doc) { + if (!doc->font_index()) { + doc->SetFontIndex(std::make_unique(doc)); + } + return static_cast(doc->font_index()); +} + +// static +CPDF_DocumentFonts* CPDF_DocumentFonts::Find(const CPDF_Document* doc) { + return doc ? static_cast(doc->font_index()) : nullptr; +} + +std::vector CPDF_DocumentFonts::CollectFaces() { + std::vector faces; + std::map face_index; // family key|weight|italic + for (const RetainPtr& font : FontDicts()) { + const CPDF_FontFace face = FaceOfFontDict(font.Get()); + const ByteString key = + ByteString::Format("%s|%d|%d", FontFamilyKey(face.family).c_str(), + face.weight, face.italic ? 1 : 0); + auto [found, added] = face_index.emplace(key, faces.size()); + if (added) { + faces.push_back({face.family, face.weight, face.italic, {}}); + } + Face& entry = faces[found->second]; + + Program program = ReadProgram(font.Get(), face); + const bool known = + std::ranges::any_of(entry.programs, [&](const Program& other) { + return program.objnum != 0 + ? other.objnum == program.objnum && + other.display_sha256 == program.display_sha256 + : other.objnum == 0 && other.format == program.format && + other.sha256 == program.sha256; + }); + if (!known) { + entry.programs.push_back(std::move(program)); + } + } + return faces; +} + +DataVector CPDF_DocumentFonts::FacesFingerprint() { + // Every field fixed-width or length-prefixed: two different lists never + // serialize alike. + DataVector bytes; + auto put = [&](uint32_t value) { + for (int shift = 24; shift >= 0; shift -= 8) { + bytes.push_back(static_cast(value >> shift)); + } + }; + auto put_bytes = [&](pdfium::span data) { + put(static_cast(data.size())); + bytes.insert(bytes.end(), data.begin(), data.end()); + }; + const std::vector faces = CollectFaces(); + put(static_cast(faces.size())); + for (const Face& face : faces) { + put_bytes(face.family.ToUTF8().unsigned_span()); + put(static_cast(face.weight)); + put(face.italic); + put(static_cast(face.programs.size())); + for (const Program& program : face.programs) { + put(program.objnum); + put(static_cast(program.format)); + put(static_cast(program.embedded)); + put_bytes(program.postscript_name.unsigned_span()); + put(program.permission.has_value() + ? 1 + static_cast(*program.permission) + : 0); + put(program.maps_unicode); + put(program.tables_aligned); + put_bytes(program.sha256); + put_bytes(program.display_sha256); + put(std::bit_cast(program.ascent)); + put(std::bit_cast(program.descent)); + } + } + return CryptSha256Generate(bytes); +} + +std::vector> CPDF_DocumentFonts::FontDicts() { + CPDF_LayerDocument* layer = CPDF_LayerDocument::FromDocument(doc_.get()); + if (!layer) { + FontWalk walk(doc_); + walk.Document(); + ++places_walked_; + return walk.TakeFonts(); + } + if (const CPDF_LayerTransaction* write = layer->open_transaction()) { + // Mid-write: what the write touched so far is read as it is now. The + // places walked here are forgotten again when the write ends. + if (write->epoch_at_begin != known_epoch_) { + ForgetPlaces(); + known_epoch_ = write->epoch_at_begin; + } + Forget(write->touched); + } else if (layer->GetOverlayEpoch() != known_epoch_) { + // Something changed outside any transaction this index was told of. + ForgetPlaces(); + known_epoch_ = layer->GetOverlayEpoch(); + } + auto walk_place = [&](auto&& walk_it) { + FontWalk walk(doc_); + walk_it(walk); + ++places_walked_; + return Place{walk.TakeFonts(), walk.TakeReads()}; + }; + std::vector> fonts; + std::set seen; + auto take = [&](const Place& place) { + for (const RetainPtr& font : place.fonts) { + if (seen.insert(font.Get()).second) { + fonts.push_back(font); + } + } + }; + if (!form_place_.has_value()) { + form_place_ = walk_place([](FontWalk& walk) { walk.Form(); }); + } + take(*form_place_); + // Pages a write removed drop out; pages it added are walked. + std::map pages; + const int page_count = doc_->GetPageCount(); + for (int i = 0; i < page_count; ++i) { + RetainPtr page = doc_->GetPageDictionary(i); + if (!page) { + continue; + } + const uint32_t objnum = page->GetObjNum(); + auto kept = page_places_.find(objnum); + if (objnum != 0 && kept != page_places_.end()) { + take(kept->second); + pages.emplace(objnum, std::move(kept->second)); + page_places_.erase(kept); + continue; + } + if (objnum != 0 && pages.contains(objnum)) { + take(pages[objnum]); + continue; + } + Place place = walk_place([&](FontWalk& walk) { walk.Page(page); }); + take(place); + if (objnum != 0) { + pages.emplace(objnum, std::move(place)); + } + } + page_places_ = std::move(pages); + return fonts; +} + +void CPDF_DocumentFonts::TransactionEnded(const std::set& objnums, + uint64_t epoch_at_begin, + uint64_t epoch) { + if (epoch_at_begin != known_epoch_) { + // Something changed before the transaction began that this index wasn't + // told of. + ForgetPlaces(); + } + Forget(objnums); + known_epoch_ = epoch; +} + +void CPDF_DocumentFonts::Forget(const std::set& objnums) { + if (objnums.empty()) { + return; + } + auto reads_any = [&](const Place& place) { + return std::ranges::any_of(objnums, [&](uint32_t objnum) { + return std::ranges::binary_search(place.reads, objnum); + }); + }; + if (form_place_.has_value() && reads_any(*form_place_)) { + form_place_.reset(); + } + std::erase_if(page_places_, + [&](const auto& entry) { return reads_any(entry.second); }); + for (uint32_t objnum : objnums) { + inspected_.erase(objnum); + } + for (auto& [objnum, inspected] : inspected_) { + std::erase_if(inspected.display_of_dict, [&](const auto& entry) { + return objnums.contains(entry.first); + }); + } +} + +void CPDF_DocumentFonts::ForgetPlaces() { + form_place_.reset(); + page_places_.clear(); +} + +CPDF_DocumentFonts::Program CPDF_DocumentFonts::Inspect( + RetainPtr stream, + Format format) { + const uint32_t objnum = stream->GetObjNum(); + if (objnum != 0) { + auto it = inspected_.find(objnum); + if (it != inspected_.end() && it->second.stream == stream && + it->second.raw_size == stream->GetRawSize()) { + return it->second.facts; + } + } + Program facts; + facts.objnum = objnum; + facts.format = format; + CPDF_DocPageData* page_data = CPDF_DocPageData::FromDocument(doc_); + RetainPtr data = page_data->GetFontFileStreamAcc(stream); + if (data && !data->GetSpan().empty()) { + facts.sha256 = CryptSha256Generate(data->GetSpan()); + if (IsSfnt(format) && + CFX_FontRegistry::DetectProgramFormat(data->GetSpan()) == + CFX_FontRegistry::ProgramFormat::kOpenTypeCFF) { + facts.format = Format::kOpenTypeCFF; + } + auto program = std::make_unique(); + if (program->LoadFaceZeroFromSpan(data->GetSpan(), + /*force_vertical=*/false, + /*object_tag=*/0) && + program->HasAnyGlyphs()) { + ProgramLineMetrics(program.get(), &facts.ascent, &facts.descent); + if (IsSfnt(facts.format)) { + RetainPtr face = program->GetFace(); + facts.permission = + CFX_FontRegistry::ClassifyFsType(face->GetFsTypeFlags()); + facts.maps_unicode = face->SelectCharMap(fxge::FontEncoding::kUnicode); + facts.tables_aligned = SfntTablesAligned(data->GetSpan()); + } + } + } + if (data) { + page_data->MaybePurgeFontFileStreamAcc(std::move(data)); + } + // An inline program is inspected again each time: nothing addresses it. + if (objnum != 0) { + inspected_[objnum] = {stream, stream->GetRawSize(), facts, std::nullopt}; + } + return facts; +} + +CPDF_DocumentFonts::Program CPDF_DocumentFonts::ReadProgram( + const CPDF_Dictionary* font, + const CPDF_FontFace& face) { + auto [stream, format] = ProgramOf(font); + const ByteString base_font = ProgramBaseFont(font); + Program program; + if (stream) { + program = Inspect(stream, format); + program.embedded = + HasFontSubsetTag(base_font) ? Embedded::kSubset : Embedded::kWhole; + } else { + program.format = format; + } + program.postscript_name = BaseFontNameWithoutSubsetTag(base_font); + if (stream) { + program.display_sha256 = DisplayHashOf(program, font, face); + } + return program; +} + +CPDF_DocumentFonts::Inspected* CPDF_DocumentFonts::FindInspected( + uint32_t objnum) { + if (objnum == 0) { + return nullptr; + } + auto found = [&]() -> Inspected* { + auto it = inspected_.find(objnum); + if (it == inspected_.end()) { + return nullptr; + } + // The version inspected must still be the document's. + RetainPtr current = doc_->GetIndirectObject(objnum); + if (current != it->second.stream || + it->second.stream->GetRawSize() != it->second.raw_size) { + return nullptr; + } + return &it->second; + }; + if (Inspected* inspected = found()) { + return inspected; + } + // Not inspected yet, or changed since: walk again, which inspects what + // the document uses now. + CollectFaces(); + return found(); +} + +DataVector CPDF_DocumentFonts::ProgramData(uint32_t objnum) { + Inspected* inspected = FindInspected(objnum); + if (!inspected || inspected->facts.sha256.empty()) { + return {}; + } + return BytesOf(*inspected); +} + +DataVector CPDF_DocumentFonts::BytesOf(const Inspected& inspected) { + CPDF_DocPageData* page_data = CPDF_DocPageData::FromDocument(doc_); + RetainPtr data = + page_data->GetFontFileStreamAcc(inspected.stream); + if (!data) { + return {}; + } + DataVector bytes(data->GetSpan().begin(), data->GetSpan().end()); + page_data->MaybePurgeFontFileStreamAcc(std::move(data)); + return bytes; +} + +DataVector CPDF_DocumentFonts::DisplayHashOf( + const Program& program, + const CPDF_Dictionary* font_dict, + const CPDF_FontFace& face) { + if (program.sha256.empty()) { + return {}; + } + if (LoadsAsIs(program)) { + return program.sha256; + } + // An inline program can't be addressed, so nothing could fetch it. + if (program.objnum == 0) { + return {}; + } + auto it = inspected_.find(program.objnum); + if (it == inspected_.end()) { + return {}; + } + Inspected& inspected = it->second; + if (IsSfnt(program.format) && program.maps_unicode) { + // Only its tables are out of place: the same program, realigned, whatever + // reads it. Its coverage is its own cmap's. + if (!inspected.aligned_built) { + inspected.aligned_built = true; + DataVector aligned = AlignSfntTables(BytesOf(inspected)); + if (!aligned.empty()) { + Display display; + display.sha256 = CryptSha256Generate(aligned); + display.bytes = std::move(aligned); + inspected.aligned = std::move(display); + } + } + return inspected.aligned.has_value() ? inspected.aligned->sha256 + : DataVector(); + } + // Any other program is drawn as a dictionary reads it: its encoding, + // widths and flags go into the display program, so two dictionaries that + // share a stream can need two. + if (!font_dict) { + return {}; + } + const uint32_t dict_objnum = font_dict->GetObjNum(); + if (dict_objnum != 0) { + auto known = inspected.display_of_dict.find(dict_objnum); + if (known != inspected.display_of_dict.end()) { + return known->second.value_or(DataVector()); + } + } + std::optional> sha256; + std::optional built = + BuildDisplayProgram(doc_, font_dict, program.postscript_name, face); + if (built.has_value()) { + Display display; + display.sha256 = CryptSha256Generate(built->bytes); + display.coverage = RangesOf(std::move(built->characters)); + display.bytes = std::move(built->bytes); + sha256 = display.sha256; + inspected.displays.try_emplace(display.sha256, std::move(display)); + } + if (dict_objnum != 0) { + inspected.display_of_dict[dict_objnum] = sha256; + } + return sha256.value_or(DataVector()); +} + +CPDF_DocumentFonts::Inspected* CPDF_DocumentFonts::FindDisplay( + pdfium::span sha256, + const Display** display) { + const DataVector wanted(sha256.begin(), sha256.end()); + auto found = [&]() -> Inspected* { + for (auto& [objnum, inspected] : inspected_) { + // Only a version the document still has. + if (doc_->GetIndirectObject(objnum) != inspected.stream || + inspected.stream->GetRawSize() != inspected.raw_size) { + continue; + } + if (LoadsAsIs(inspected.facts) && inspected.facts.sha256 == wanted) { + *display = nullptr; + return &inspected; + } + if (inspected.aligned.has_value() && + inspected.aligned->sha256 == wanted) { + *display = &*inspected.aligned; + return &inspected; + } + auto built = inspected.displays.find(wanted); + if (built != inspected.displays.end()) { + *display = &built->second; + return &inspected; + } + } + return nullptr; + }; + if (Inspected* inspected = found()) { + return inspected; + } + // Not found by a walk yet, or changed since: walk again. + CollectFaces(); + return found(); +} + +DataVector CPDF_DocumentFonts::DisplayProgramData( + pdfium::span sha256) { + const Display* display = nullptr; + Inspected* inspected = FindDisplay(sha256, &display); + if (!inspected) { + return {}; + } + return display ? display->bytes : BytesOf(*inspected); +} + +std::vector CPDF_DocumentFonts::DisplayProgramCoverage( + pdfium::span sha256) { + const Display* display = nullptr; + Inspected* inspected = FindDisplay(sha256, &display); + if (!inspected) { + return {}; + } + // A program that maps Unicode is drawn through its own cmap, its tables + // realigned or not; any other through its display program's. + if (inspected->facts.maps_unicode) { + return CmapCoverage(*inspected); + } + return display ? display->coverage : std::vector(); +} + +const std::vector& CPDF_DocumentFonts::CmapCoverage( + Inspected& inspected) { + if (inspected.coverage.has_value()) { + return *inspected.coverage; + } + std::vector ranges; + { + DataVector bytes = BytesOf(inspected); + auto program = std::make_unique(); + if (!bytes.empty() && + program->LoadFaceZeroFromSpan(bytes, /*force_vertical=*/false, + /*object_tag=*/0) && + program->GetFace()->SelectCharMap(fxge::FontEncoding::kUnicode)) { + ranges = program->GetFace()->GetMappedCodePointRanges( + pdfium::kMaximumSupplementaryCodePoint); + } + } + inspected.coverage = std::move(ranges); + return *inspected.coverage; +} + +RetainPtr CPDF_DocumentFonts::ProgramStream(uint32_t objnum) { + Inspected* inspected = FindInspected(objnum); + if (!inspected) { + return nullptr; + } + return pdfium::WrapRetain(const_cast(inspected->stream.Get())); +} + +DataVector CPDF_DocumentFonts::DisplayHashOfFont( + const CPDF_Dictionary* font_dict) { + if (!font_dict) { + return {}; + } + return ReadProgram(font_dict, FaceOfFontDict(font_dict)).display_sha256; +} + +DataVector CPDF_DocumentFonts::DisplayHashOfProgram(uint32_t objnum) { + Inspected* inspected = FindInspected(objnum); + if (!inspected) { + return {}; + } + // No dictionary to read it through: only a program every dictionary reads + // alike has one. + return DisplayHashOf(inspected->facts, /*font_dict=*/nullptr, + CPDF_FontFace()); +} + +bool CPDF_DocumentFonts::ReserveProgramAlias(uint32_t objnum, + ByteString* alias) { + if (objnum == 0) { + return false; + } + for (const auto& [reserved, program] : program_aliases_) { + if (program.objnum == objnum) { + *alias = reserved; + return true; + } + } + std::optional found = DescribeProgram(objnum); + if (!found.has_value()) { + return false; + } + const CPDF_Dictionary* root = doc_->GetRoot(); + RetainPtr acroform = + root ? root->GetDictFor("AcroForm") : nullptr; + RetainPtr dr = + acroform ? acroform->GetDictFor("DR") : nullptr; + RetainPtr dr_fonts = + dr ? dr->GetDictFor("Font") : nullptr; + const ByteString base = ByteString::Format("EDocF%u", objnum); + ByteString candidate = base; + for (int suffix = 1; + (dr_fonts && dr_fonts->KeyExist(candidate.AsStringView())) || + program_aliases_.contains(candidate); + ++suffix) { + candidate = ByteString::Format("%s_%d", base.c_str(), suffix); + } + program_aliases_[candidate] = std::move(*found); + *alias = candidate; + return true; +} + +std::optional +CPDF_DocumentFonts::DescribeProgram(uint32_t objnum) { + if (objnum == 0) { + return std::nullopt; + } + for (const Face& face : CollectFaces()) { + for (const Program& program : face.programs) { + if (program.objnum == objnum) { + return ReservedProgram{objnum, program.postscript_name, face.family, + face.weight, face.italic}; + } + } + } + return std::nullopt; +} + +std::optional +CPDF_DocumentFonts::ProgramForAlias(const ByteString& alias) const { + auto it = program_aliases_.find(alias); + if (it == program_aliases_.end()) { + return std::nullopt; + } + return it->second; +} + +void CPDF_DocumentFonts::NoteMissingCharacter(const WideString& grapheme) { + if (grapheme.IsEmpty() || + missing_characters_.size() >= kMaxMissingCharacters || + std::ranges::find(missing_characters_, grapheme) != + missing_characters_.end()) { + return; + } + missing_characters_.push_back(grapheme); +} diff --git a/core/fpdfdoc/cpdf_documentfonts.h b/core/fpdfdoc/cpdf_documentfonts.h new file mode 100644 index 0000000000..5c31dee4f5 --- /dev/null +++ b/core/fpdfdoc/cpdf_documentfonts.h @@ -0,0 +1,255 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 +// +// EmbedPDF: the fonts a document uses, found in its dictionaries without +// loading a page, and the facts about each embedded program. Facts only: what +// may be written with a font, or sent where, is the engine's decision. + +#ifndef CORE_FPDFDOC_CPDF_DOCUMENTFONTS_H_ +#define CORE_FPDFDOC_CPDF_DOCUMENTFONTS_H_ + +#include +#include + +#include +#include +#include +#include +#include + +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/data_vector.h" +#include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/span.h" +#include "core/fxcrt/unowned_ptr.h" +#include "core/fxcrt/widestring.h" +#include "core/fxge/cfx_codepointrange.h" +#include "core/fxge/cfx_fontregistry.h" + +class CPDF_Dictionary; +class CPDF_Stream; +struct CPDF_FontFace; + +class CPDF_DocumentFonts final : public CPDF_Document::FontIndex { + public: + // What a font program is, from the font dictionary that holds it. + enum class Format : uint8_t { + kNone, // nothing embedded: the PDF only names the font + kTrueType, // /FontFile2, or /FontFile3 /OpenType with glyf outlines + kOpenTypeCFF, // /FontFile3 /OpenType with CFF outlines + kCFF, // /FontFile3 /Type1C or /CIDFontType0C: bare CFF + kType1, // /FontFile + kType3, // glyphs drawn by content streams: no program + }; + enum class Embedded : uint8_t { kNone, kWhole, kSubset }; + + struct Program { + // The program stream's object number; 0 when there is no program, or + // when the stream is inline (it can't be addressed). + uint32_t objnum = 0; + Format format = Format::kNone; + Embedded embedded = Embedded::kNone; + // /BaseFont without its subset tag (a Type0 font's descendant's). + ByteString postscript_name; + // What the OS/2 fsType grants; nullopt when the program has no OS/2 + // table to say (Type 1, bare CFF) or there is no program. + std::optional permission; + // The program loads, has glyphs and maps characters through a Unicode + // cmap: the layout can write any character it maps. + bool maps_unicode = false; + // An sfnt whose tables each start on a four-byte boundary, as the font + // format puts them. FreeType reads one whose tables don't; a browser + // refuses it. + bool tables_aligned = false; + // SHA-256 of the decoded program; empty without one. + DataVector sha256; + // SHA-256 of what a browser loads for it: the program itself when it is + // an sfnt that maps characters through a Unicode cmap, with its tables + // realigned when they aren't (cfx_sfnt.h); else its display program + // (cpdf_displayprogram.h). Empty when a browser can't draw it. + DataVector display_sha256; + // Line metrics as the layout reads them (em, descent positive); 0 when + // the program doesn't load. + float ascent = 0; + float descent = 0; + }; + + struct Face { + WideString family; + int weight = 400; + bool italic = false; + // One per program stream and display program, in the order the walk + // found them (a stream two dictionaries read differently is two); + // fonts without a program (not embedded, Type 3) add one entry per + // format. + std::vector programs; + }; + + explicit CPDF_DocumentFonts(CPDF_Document* doc); + ~CPDF_DocumentFonts() override; + + // The document's index, made on first use. + static CPDF_DocumentFonts* For(CPDF_Document* doc); + // The document's index if it has one yet; null otherwise. + static CPDF_DocumentFonts* Find(const CPDF_Document* doc); + + // Every face the document's fonts are, grouped by family, weight and + // italic. Found in /AcroForm /DR, fields' own /DR, every page's resources + // (inherited ones too), form XObjects, tiling patterns, soft masks, Type 3 + // glyph resources and every state of every annotation appearance. Each + // dictionary is walked once; cycles stop. Programs are inspected once per + // stream version. + // + // On a layer, what each place (the AcroForm, each page) reaches is kept + // and walked again only after a transaction touches something it read + // (TransactionEnded), so a list after an edit walks the pages the edit + // changed. A plain document is walked whole every time: nothing says what + // changed. + std::vector CollectFaces(); + + // SHA-256 of the faces CollectFaces() returns, every fact of each in + // order: equal fingerprints, the same faces. Read before and after a write, + // it says whether the write changed the document's fonts. + DataVector FacesFingerprint(); + + // How many places (the AcroForm, a page, or the whole of a plain + // document) have been walked: what an edit costs, for tests. + size_t places_walked() const { return places_walked_; } + + // CPDF_Document::FontIndex: + void TransactionEnded(const std::set& objnums, + uint64_t epoch_at_begin, + uint64_t epoch) override; + + // A program found by the walk, by its stream's object number: its decoded + // bytes, the program as the layout reads it. Empty for any other number. + DataVector ProgramData(uint32_t objnum); + // What a browser loads, by its SHA-256 (a Program::display_sha256 the walk + // found); empty for any other hash. + DataVector DisplayProgramData(pdfium::span sha256); + // The scalars a browser draws with it, which are the ones the engine writes + // with it, as ranges in order; empty when it maps none or |sha256| isn't + // one the walk found. + std::vector DisplayProgramCoverage( + pdfium::span sha256); + // The program stream itself, for the writer; null when |objnum| isn't one. + RetainPtr ProgramStream(uint32_t objnum); + // What a browser loads for the program |font_dict| embeds, as that + // dictionary reads it: the display hash CollectFaces() lists for it. Empty + // when it embeds none or a browser can't draw it. + DataVector DisplayHashOfFont(const CPDF_Dictionary* font_dict); + // What a browser loads for program stream |objnum| when every dictionary + // reads it alike (an sfnt with a Unicode cmap, as every document program + // the layout writes with is); empty for any other program. + DataVector DisplayHashOfProgram(uint32_t objnum); + + // A /DA font naming a document program: like a registered font's, the + // alias is this session's until an appearance installs the /DR entry. + // Reserve picks "EDocF", suffixed when /DR holds the key already; + // false when |objnum| isn't a program the walk found. + struct ReservedProgram { + uint32_t objnum = 0; + ByteString base_font_name; // without its subset tag + WideString family; + int weight = 400; + bool italic = false; + }; + bool ReserveProgramAlias(uint32_t objnum, ByteString* alias); + std::optional ProgramForAlias(const ByteString& alias) const; + // A program the walk found, as its font dictionary describes it (the first + // that names it); nullopt for any other number. + std::optional DescribeProgram(uint32_t objnum); + + // Characters an appearance drew with no font that has them (each a + // grapheme, drawn as its face's missing glyph), first seen first, at most + // kMaxMissingCharacters. The engine clears them before a write and reads + // them after it. + static constexpr size_t kMaxMissingCharacters = 256; + void NoteMissingCharacter(const WideString& grapheme); + const std::vector& missing_characters() const { + return missing_characters_; + } + void ClearMissingCharacters() { missing_characters_.clear(); } + + private: + // What the walk of one place found. + struct Place { + std::vector> fonts; + // Every indirect object the walk read, sorted: a transaction that + // touches one of them makes the place's walk stale. + std::vector reads; + }; + + // What a browser loads in place of a program it can't load as it is. + struct Display { + DataVector bytes; + DataVector sha256; + std::vector coverage; + }; + + struct Inspected { + RetainPtr stream; // the version inspected + size_t raw_size = 0; + Program facts; + // The scalars its own cmap maps. + std::optional> coverage; + // A Unicode sfnt a browser would refuse, its tables realigned: the same + // whichever dictionary reads it. + bool aligned_built = false; + std::optional aligned; + // Display programs by their SHA-256. Any other program is read through + // each font dictionary's encoding, widths and flags, so one stream can + // be drawn as more than one. + std::map, Display> displays; + // The display each font dictionary (by object number) reads the stream + // as; nullopt when it builds none. Forgotten when the dictionary + // changes; one that isn't an indirect object builds its display again + // each time. + std::map>> display_of_dict; + }; + + // Every font dictionary the document's drawings can use, each once, in + // the order the walk meets them. + std::vector> FontDicts(); + // Forgets the places that read any of |objnums|, and the inspections of + // programs among them. + void Forget(const std::set& objnums); + void ForgetPlaces(); + + // The facts about |stream|, inspected once per version. + Program Inspect(RetainPtr stream, Format format); + // The program |font| embeds as |font| (whose face is |face|) reads it, its + // display hash too; only its format when it embeds none. + Program ReadProgram(const CPDF_Dictionary* font, const CPDF_FontFace& face); + Inspected* FindInspected(uint32_t objnum); + // The decoded bytes of the stream |inspected| is. + DataVector BytesOf(const Inspected& inspected); + // The scalars |inspected|'s own cmap maps, read once. + const std::vector& CmapCoverage(Inspected& inspected); + // The program a browser loads by |sha256|: its inspection, with |display| + // the display program it is, or null when it is the program as it is. + // Walks again when no current inspection has it. + Inspected* FindDisplay(pdfium::span sha256, + const Display** display); + // |program|'s display hash, its display program built through + // |font_dict| (whose face is |face|) once per stream version when it needs + // one; empty for such a program without |font_dict|. + DataVector DisplayHashOf(const Program& program, + const CPDF_Dictionary* font_dict, + const CPDF_FontFace& face); + + UnownedPtr const doc_; + // A layer's places, kept between lists: the AcroForm's, and each page's by + // its dictionary's object number. + std::optional form_place_; + std::map page_places_; + // The layer's overlay epoch the places are known to be current at. + uint64_t known_epoch_ = 0; + size_t places_walked_ = 0; + std::map inspected_; // by program stream objnum + std::map program_aliases_; + std::vector missing_characters_; +}; + +#endif // CORE_FPDFDOC_CPDF_DOCUMENTFONTS_H_ diff --git a/core/fpdfdoc/cpdf_fontface.cpp b/core/fpdfdoc/cpdf_fontface.cpp index 4e312fca3a..6b9a3f3943 100644 --- a/core/fpdfdoc/cpdf_fontface.cpp +++ b/core/fpdfdoc/cpdf_fontface.cpp @@ -4,11 +4,15 @@ #include "core/fpdfdoc/cpdf_fontface.h" #include +#include #include #include "core/fpdfapi/parser/cpdf_array.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfdoc/cpdf_annotfontmap.h" +#include "core/fpdfdoc/cpdf_documentfonts.h" +#include "core/fxge/cfx_face.h" +#include "core/fxge/cfx_font.h" #include "core/fxge/cfx_fontregistry.h" #include "core/fxge/fx_font.h" @@ -66,6 +70,59 @@ std::optional StandardFontFace(const ByteString& name) { return std::nullopt; } +ByteString FontFamilyKey(const WideString& family) { + ByteString key; + for (wchar_t ch : family) { + if (ch == L' ' || ch == L'-' || ch == L'_' || ch == L'\'' || ch == L'"') { + continue; + } + if (ch < 0x80) { + key += static_cast(std::tolower(static_cast(ch))); + } else { + key += WideString(ch).ToUTF8(); + } + } + return key; +} + +ByteString FontFamilyKey(const ByteString& family_utf8) { + return FontFamilyKey(WideString::FromUTF8(family_utf8.AsStringView())); +} + +void ProgramLineMetrics(const CFX_Font* program, + float* ascent, + float* descent) { + RetainPtr face = program ? program->GetFace() : nullptr; + const uint16_t upem = face ? face->GetUnitsPerEm() : 0; + if (!face || upem == 0) { + *ascent = 0.830f; + *descent = 0.170f; + return; + } + *ascent = static_cast(face->GetAscender()) / upem; + *descent = static_cast(-face->GetDescender()) / upem; + if (*ascent <= 0 || *descent < 0) { + *ascent = 0.830f; + *descent = 0.170f; + } +} + +bool HasFontSubsetTag(const ByteString& base_font) { + if (base_font.GetLength() <= 7 || base_font[6] != '+') { + return false; + } + for (size_t i = 0; i < 6; ++i) { + if (base_font[i] < 'A' || base_font[i] > 'Z') { + return false; + } + } + return true; +} + +ByteString BaseFontNameWithoutSubsetTag(const ByteString& base_font) { + return HasFontSubsetTag(base_font) ? base_font.Substr(7) : base_font; +} + CPDF_FontFace FaceOfBaseFontName(ByteString base_font) { if (base_font.GetLength() > 7 && base_font[6] == '+') { base_font = base_font.Substr(7); @@ -164,6 +221,14 @@ CPDF_FontFace FaceOfDefaultAppearanceFont( CFX_FontRegistry::GetStyleWeight(*registered), CFX_FontRegistry::IsStyleItalic(*registered)}; } + // The same for a document program's alias (CPDF_DocumentFonts). + CPDF_DocumentFonts* document_fonts = CPDF_DocumentFonts::Find(doc); + std::optional program = + document_fonts ? document_fonts->ProgramForAlias(font_name) + : std::nullopt; + if (program.has_value()) { + return CPDF_FontFace{program->family, program->weight, program->italic}; + } } if (std::optional standard = StandardFontFace(font_name)) { return *standard; diff --git a/core/fpdfdoc/cpdf_fontface.h b/core/fpdfdoc/cpdf_fontface.h index 7734a209b8..5a93a78346 100644 --- a/core/fpdfdoc/cpdf_fontface.h +++ b/core/fpdfdoc/cpdf_fontface.h @@ -11,6 +11,7 @@ #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/widestring.h" +class CFX_Font; class CPDF_Dictionary; class CPDF_Document; @@ -22,6 +23,22 @@ struct CPDF_FontFace { bool italic = false; }; +// The key two family names compare by: case, spaces, hyphens, underscores +// and quotes don't count, so "Noto Sans" == "NotoSans" == "noto-sans". +ByteString FontFamilyKey(const WideString& family); +ByteString FontFamilyKey(const ByteString& family_utf8); + +// A program's line metrics as the layout reads them: its hhea ascender and +// descender over its units per em (Acrobat's choice for TrueType and CFF), +// descent positive. 0.83 and 0.17 when the program has none. +void ProgramLineMetrics(const CFX_Font* program, float* ascent, float* descent); + +// A /BaseFont name without its subset tag: "ABCDEF+MinionPro-Bold" is +// "MinionPro-Bold". +ByteString BaseFontNameWithoutSubsetTag(const ByteString& base_font); +// "ABCDEF+NotoSans": six capital letters and a plus, a producer's subset. +bool HasFontSubsetTag(const ByteString& base_font); + // The face a standard 14 name or one of Acrobat's aliases (Helv, TiBo, ...) // stands for: its standard family ("Arial" is Helvetica). std::optional StandardFontFace(const ByteString& name); diff --git a/core/fpdfdoc/cpdf_fontresolver.cpp b/core/fpdfdoc/cpdf_fontresolver.cpp new file mode 100644 index 0000000000..139798297a --- /dev/null +++ b/core/fpdfdoc/cpdf_fontresolver.cpp @@ -0,0 +1,139 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fpdfdoc/cpdf_fontresolver.h" + +#include +#include +#include +#include + +#include "core/fpdfapi/font/cpdf_font.h" +#include "core/fpdfapi/page/cpdf_docpagedata.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfdoc/cpdf_appearancegenerator_internal.h" +#include "core/fpdfdoc/cpdf_documentfonts.h" +#include "core/fpdfdoc/cpdf_fontface.h" +#include "core/fpdfdoc/cpdf_richtextlayout.h" + +namespace { + +// What a browser loads for the program |face| draws with: a document font's +// program as its dictionary reads it, a document program as it is. +DataVector DisplayHashOf(CPDF_Document* doc, + const CPDF_AnnotFontMap::EntryFacts& face) { + if (face.source == CPDF_AnnotFontMap::Source::kDocumentFontDict) { + return CPDF_DocumentFonts::For(doc)->DisplayHashOfFont( + face.font_dict.Get()); + } + if (face.source == CPDF_AnnotFontMap::Source::kDocumentProgram) { + return CPDF_DocumentFonts::For(doc)->DisplayHashOfProgram( + face.program_objnum); + } + return {}; +} + +} // namespace + +// static +CPDF_FontResolver::Result CPDF_FontResolver::Resolve( + CPDF_Document* doc, + const CPDF_Dictionary* annot_dict, + const std::vector& runs) { + Result result; + if (!doc) { + return result; + } + const CPDF_Dictionary* root = doc->GetRoot(); + RetainPtr acroform = + root ? root->GetDictFor("AcroForm") : nullptr; + RetainPtr dr = + acroform ? acroform->GetDictFor("DR") : nullptr; + RetainPtr dr_fonts = + dr ? dr->GetDictFor("Font") : nullptr; + + // The box's /DA font, as its appearance would find it (read only). + RetainPtr default_font; + ByteString font_name; + appearance_generator::DaFontResolution da_font; + if (annot_dict) { + std::optional info = + appearance_generator::GetDefaultAppearanceInfo(annot_dict, + acroform.Get()); + if (info.has_value()) { + font_name = info->font_name; + da_font = appearance_generator::ResolveDaFontForEphemeralTarget( + doc, dr_fonts.Get(), font_name); + if (da_font.font_dict) { + default_font = + CPDF_DocPageData::FromDocument(doc)->GetFont(da_font.font_dict); + } + } + } + const bool widget = + annot_dict && annot_dict->GetNameFor("Subtype") == "Widget"; + CPDF_AnnotFontMap map(doc, std::move(default_font), font_name, + /*allow_registered_fallbacks=*/true, + da_font.registered_font_id, + /*install_dr_entry=*/false, + widget ? CPDF_AnnotFontMap::Owner::kWidget + : CPDF_AnnotFontMap::Owner::kAnnotation); + da_font.ApplyTo(&map); + // The box keeps its own font while that font covers the text: requests + // for its face answer with it first. + if (map.HasDefaultFont() && !map.DefaultFontIsStandard()) { + const CPDF_FontFace face = + FaceOfDefaultAppearanceFont(doc, font_name, {dr.Get()}); + map.PinRichFace(face.family, face.weight, face.italic, 0); + } + + // Each face resolves once over every character asked of it, as the + // layout primes it, so two runs of one face get one program. + auto key_of = [](const Run& run) { + return CPDF_AnnotFontMap::FaceRequestKey(run.family, run.weight, run.italic, + run.program); + }; + std::map text_of; + for (const Run& run : runs) { + text_of[key_of(run)] += run.text; + } + for (const Run& run : runs) { + map.ResolveRichFace(run.family, run.weight, run.italic, + text_of[key_of(run)].AsStringView(), run.program); + } + + for (const Run& run : runs) { + const int entry = map.ResolveRichFace(run.family, run.weight, run.italic, + WideStringView(), run.program); + ResolvedRun resolved; + resolved.face = map.DescribeEntry(entry); + resolved.display_sha256 = DisplayHashOf(doc, resolved.face); + resolved.fallbacks = CFX_FontRegistry::GetFallbackOrder( + run.weight, run.italic, /*for_authoring=*/true); + const std::vector starts = + CPDF_RichTextLayout::GraphemeStarts(run.text); + const size_t length = run.text.GetLength(); + for (size_t begin = 0; begin < length;) { + size_t end = begin + 1; + while (end < length && !starts[end]) { + ++end; + } + const WideStringView grapheme = + run.text.AsStringView().Substr(begin, end - begin); + const CPDF_RichTextLayout::GraphemeMatch match = + CPDF_RichTextLayout::MatchGrapheme(&map, entry, grapheme, run.weight, + run.italic); + if (!match.covered && + !CPDF_RichTextLayout::IsInvisibleGrapheme(grapheme) && + std::ranges::none_of( + result.missing_characters, + [&](const WideString& missing) { return missing == grapheme; })) { + result.missing_characters.emplace_back(grapheme); + } + begin = end; + } + result.runs.push_back(std::move(resolved)); + } + return result; +} diff --git a/core/fpdfdoc/cpdf_fontresolver.h b/core/fpdfdoc/cpdf_fontresolver.h new file mode 100644 index 0000000000..c264d21fcf --- /dev/null +++ b/core/fpdfdoc/cpdf_fontresolver.h @@ -0,0 +1,61 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 +// +// EmbedPDF: the fonts a write would draw text with, found the way the write +// finds them, without writing: the same /DA resolution, the same font map, +// each face resolved once over all of its text, and the same per-grapheme +// fallback. What an editor shows while someone types. + +#ifndef CORE_FPDFDOC_CPDF_FONTRESOLVER_H_ +#define CORE_FPDFDOC_CPDF_FONTRESOLVER_H_ + +#include + +#include "core/fpdfdoc/cpdf_annotfontmap.h" +#include "core/fxcrt/data_vector.h" +#include "core/fxcrt/widestring.h" +#include "core/fxge/cfx_fontregistry.h" + +class CPDF_Dictionary; +class CPDF_Document; + +class CPDF_FontResolver { + public: + // A run of text and the face it asks for. + struct Run { + WideString text; + WideString family; // a family name, or a registered font's family + int weight = 400; + bool italic = false; + // The document program the write would name (its stream's object + // number), the face exactly; 0 when the family decides. + uint32_t program = 0; + }; + + struct ResolvedRun { + // The entry the run's face resolves to: where its program comes from, + // which one, the face it draws, and whether it substitutes the request. + CPDF_AnnotFontMap::EntryFacts face; + // SHA-256 of what a browser loads for a document program or document + // font's program, as the dictionary the run draws with reads it (what + // CPDF_DocumentFonts::CollectFaces() lists for it); empty otherwise. + DataVector display_sha256; + // The registered fallback fonts in the order the layout tries them for + // the run's style (authorized for editing only). + std::vector fallbacks; + }; + + struct Result { + std::vector runs; + // Graphemes no font covers, first seen first: drawn as missing glyphs. + std::vector missing_characters; + }; + + // |annot_dict| is the box being edited (its /DA font is the box's own, + // kept while it covers the text), or null for a new box. + static Result Resolve(CPDF_Document* doc, + const CPDF_Dictionary* annot_dict, + const std::vector& runs); +}; + +#endif // CORE_FPDFDOC_CPDF_FONTRESOLVER_H_ diff --git a/core/fpdfdoc/cpdf_richtext.h b/core/fpdfdoc/cpdf_richtext.h index 2dc082126f..a209f45f40 100644 --- a/core/fpdfdoc/cpdf_richtext.h +++ b/core/fpdfdoc/cpdf_richtext.h @@ -36,6 +36,12 @@ struct CPDF_RichTextStyle { Script script = Script::kNormal; float letter_spacing = 0.0f; // pt float horz_scale = 1.0f; // xfa-font-horizontal-scale / 100 + // The document program (its stream's object number) the writer chose for + // this face, which the family alone can't tell from another program that + // describes itself the same way; 0 when the family decides. Never written + // to /RC or /DS, which name families only: the body's reaches the file as + // the /DA font. + uint32_t program = 0; }; // What a run overrides. Unknown declarations are kept verbatim so a later @@ -50,11 +56,15 @@ struct CPDF_RichTextStyleDelta { std::optional script; std::optional letter_spacing; std::optional horz_scale; + std::optional program; WideString unknown_declarations; // "kerning-mode:pair;..." as written bool IsEmpty() const; }; +// |base| with |delta| over it. A program names one face: a delta that asks +// for another family, weight or italic without naming a program of its own +// leaves the base's. CPDF_RichTextStyle ApplyRichTextStyleDelta( const CPDF_RichTextStyle& base, const CPDF_RichTextStyleDelta& delta); diff --git a/core/fpdfdoc/cpdf_richtextjson.cpp b/core/fpdfdoc/cpdf_richtextjson.cpp index 9312964292..3fceb7a5c7 100644 --- a/core/fpdfdoc/cpdf_richtextjson.cpp +++ b/core/fpdfdoc/cpdf_richtextjson.cpp @@ -463,6 +463,14 @@ bool ReadStyleDelta(const JsonValue* object, CPDF_RichTextStyleDelta* delta) { if (!delta->horz_scale || *delta->horz_scale <= 0) { return false; } + } else if (key == "program") { + // A document program's object number: the exact font for the face. + if (value.type != JsonValue::Type::kNumber || value.number < 1 || + value.number > 0x7FFFFFFF || + value.number != static_cast(value.number)) { + return false; + } + delta->program = static_cast(value.number); } else if (key == "unknown") { std::optional unknown = String(&value); if (!unknown) { diff --git a/core/fpdfdoc/cpdf_richtextlayout.cpp b/core/fpdfdoc/cpdf_richtextlayout.cpp index d739e6bc82..8aed12e47b 100644 --- a/core/fpdfdoc/cpdf_richtextlayout.cpp +++ b/core/fpdfdoc/cpdf_richtextlayout.cpp @@ -13,10 +13,14 @@ #include "core/fpdfapi/font/cpdf_font.h" #include "core/fpdfdoc/cpdf_annotfontmap.h" +#include "core/fpdfdoc/cpvt_section.h" +#include "core/fxcrt/cfx_bidiparagraph.h" #include "core/fxcrt/code_point_view.h" #include "core/fxcrt/fx_linebreak.h" +#include "core/fxcrt/fx_system.h" #include "core/fxcrt/fx_unicode.h" #include "core/fxcrt/numerics/safe_conversions.h" +#include "core/fxcrt/utf16.h" #include "core/fxge/cfx_face.h" #include "core/fxge/cfx_font.h" #include "hb-ot.h" // nogncheck @@ -38,7 +42,8 @@ constexpr float kSubRise = -0.15f; constexpr float kFallbackSize = 12.0f; constexpr float kEpsilon = 0.01f; -// CPVT's auto font size ladder, kept for plain FreeText with a DA size of 0. +// CPVT's auto font size ladder: plain FreeText and form fields with a DA +// size of 0. A field of several lines tries its first quarter. constexpr std::array kAutoSizeSteps = { 4, 6, 8, 9, 10, 12, 14, 18, 20, 25, 30, 35, 40, 45, 50, 55, 60, 70, 80, 90, 100, 110, 120, 130, 144}; @@ -109,10 +114,6 @@ bool IsCommonScript(hb_script_t script) { script == HB_SCRIPT_UNKNOWN; } -bool IsRtlScript(hb_script_t script) { - return hb_script_get_horizontal_direction(script) == HB_DIRECTION_RTL; -} - // Scripts whose shaping is a matter of default features only: the Latin // features Acrobat leaves off are turned off for them (C note §4.2). bool IsSimpleScript(hb_script_t script) { @@ -176,16 +177,18 @@ struct ResolvedStyle { float drawn_size = 0; // after sub/superscript scaling float rise = 0; // pt bool has_size_override = false; + int face_entry = -1; // the face, when it isn't resolved from the style }; -// One shaping unit: one font entry, one style, one script and direction, -// covering [begin, end) of the segment's text. +// One shaping unit: one font entry, one style, one script and one bidi +// level, covering [begin, end) of the segment's text. struct Item { int entry = -1; int style_index = 0; size_t begin = 0; size_t end = 0; hb_script_t script = HB_SCRIPT_COMMON; + uint8_t level = 0; // odd: shaped right to left bool rtl = false; std::vector glyphs; // clusters are absolute segment indices float ascent = 0; // pt @@ -198,6 +201,7 @@ struct Segment { WideString text; // without the hard break std::vector style_of; // per character std::vector source_pos; // per character: index in the paragraph + std::optional bidi; std::vector items; int break_style = 0; // the style at the hard break, for an empty line bool ends_with_hard_break = false; @@ -227,6 +231,49 @@ std::vector CPDF_RichTextLayout::GraphemeStarts(const WideString& text) { return starts; } +// static +CPDF_RichTextLayout::GraphemeMatch CPDF_RichTextLayout::MatchGrapheme( + CPDF_AnnotFontMap* fonts, + int face_entry, + WideStringView grapheme, + int weight, + bool italic) { + if (face_entry < 0) { + return {}; + } + if (fonts->CoversText(face_entry, grapheme)) { + return {face_entry, true}; + } + const int fallback = fonts->FindRichFallback(grapheme, weight, italic); + if (fallback >= 0) { + return {fallback, true}; + } + // No font has it: the last resort draws it as its box. A grapheme that + // draws nothing stays with the face, which hides it. + const int last_resort = + IsInvisibleGrapheme(grapheme) ? -1 : fonts->FindLastResort(); + return {last_resort >= 0 ? last_resort : face_entry, false}; +} + +// static +bool CPDF_RichTextLayout::IsInvisibleGrapheme(WideStringView grapheme) { + for (char32_t cp : pdfium::CodePointView(grapheme)) { + const bool invisible = + cp < 0x20 || (cp >= 0x7F && cp <= 0x9F) || cp == 0x00AD || + cp == 0x034F || cp == 0x061C || (cp >= 0x115F && cp <= 0x1160) || + (cp >= 0x17B4 && cp <= 0x17B5) || (cp >= 0x180B && cp <= 0x180F) || + (cp >= 0x200B && cp <= 0x200F) || (cp >= 0x202A && cp <= 0x202E) || + (cp >= 0x2060 && cp <= 0x206F) || cp == 0x3164 || + (cp >= 0xFE00 && cp <= 0xFE0F) || cp == 0xFEFF || cp == 0xFFA0 || + (cp >= 0xFFF0 && cp <= 0xFFF8) || (cp >= 0x1BCA0 && cp <= 0x1BCA3) || + (cp >= 0x1D173 && cp <= 0x1D17A) || (cp >= 0xE0000 && cp <= 0xE0FFF); + if (!invisible) { + return false; + } + } + return true; +} + // static std::vector CPDF_RichTextLayout::BreakOpportunities( const WideString& text) { @@ -297,6 +344,14 @@ struct CPDF_RichTextLayout::Impl { std::map> hb_fonts; std::map faces; bool degraded = false; + // A form field: widths in CPVT's arithmetic, so its lines break where + // CPVT's did. + bool field = false; + + // |width| thousandths of an em at |size| pt. + float Scaled(int width, float size) const { + return field ? width * size * 0.001f : width * size / 1000.0f; + } const CPDF_AnnotFontMap::RichFace& FaceOf(int entry) { auto it = faces.find(entry); @@ -318,23 +373,6 @@ struct CPDF_RichTextLayout::Impl { return fonts->RichGlyphFor(entry, static_cast(ch)); } - // Does |entry| have a glyph for every character of the grapheme at - // [begin, end)? Joiners and variation selectors may be missing. - bool Covers(int entry, const WideString& text, size_t begin, size_t end) { - // Scalars, not code units: a supplementary character is one grapheme - // of two units on a 16-bit wchar_t platform. - for (char32_t cp : pdfium::CodePointView( - text.AsStringView().Substr(begin, end - begin))) { - if (cp == 0x200D || IsVariationSelector(cp)) { - continue; - } - if (fonts->RichGlyphFor(entry, static_cast(cp)) == 0) { - return false; - } - } - return true; - } - // Shapes one item into glyphs with absolute clusters. void Shape(Item* item, const WideString& text, @@ -347,23 +385,31 @@ struct CPDF_RichTextLayout::Impl { item->ascent = face.ascent * size; item->descent = face.descent * size; item->degraded = face.degraded; - item->rtl = IsRtlScript(item->script); + item->rtl = (item->level & 1) != 0; HbFont* hb = face.program ? HbFontOf(item->entry) : nullptr; if (!hb) { // Standard 14 (or a program HarfBuzz refused): one glyph per - // character through the font's encoding. + // character through the font's encoding; right to left, in reverse + // and mirrored, as HarfBuzz would. for (size_t i = item->begin; i < item->end; ++i) { - const uint32_t code = GlyphFor(item->entry, text[i]); + uint32_t ch = static_cast(text[i]); + if (item->rtl) { + ch = hb_unicode_mirroring(hb_unicode_funcs_get_default(), ch); + } + const uint32_t code = GlyphFor(item->entry, static_cast(ch)); ShapedGlyph glyph; glyph.gid = code; glyph.cluster = pdfium::checked_cast(i); const int width = face.pdf_font && code ? face.pdf_font->GetCharWidth(code) : 0; - glyph.x_advance = (width * size / 1000.0f + letter_spacing) * scale_x; + glyph.x_advance = (Scaled(width, size) + letter_spacing) * scale_x; item->glyphs.push_back(glyph); } - item->needs_actual_text = false; + if (item->rtl) { + std::reverse(item->glyphs.begin(), item->glyphs.end()); + } + item->needs_actual_text = item->rtl; return; } @@ -424,7 +470,7 @@ struct CPDF_RichTextLayout::Impl { // it exactly so the appearance needs no TJ adjustment. float advance = positions[i].x_advance * unit; const float pdf_width = - face.program->GetGlyphWidth(glyph.gid) * size / 1000.0f; + Scaled(face.program->GetGlyphWidth(glyph.gid), size); if (std::fabs(advance - pdf_width) * 1000.0f / size < 0.75f) { advance = pdf_width; } @@ -467,12 +513,8 @@ struct CPDF_RichTextLayout::Impl { return width; } - // Ink width of the segment's text in [begin, end): trailing spaces do - // not count (Acrobat centres and wraps without them). - static float InkWidth(const Segment& segment, size_t begin, size_t end) { - while (end > begin && IsSpace(segment.text[end - 1])) { - --end; - } + // Width of the segment's text in [begin, end). + static float RangeWidth(const Segment& segment, size_t begin, size_t end) { float width = 0; for (const Item& item : segment.items) { if (item.end <= begin || item.begin >= end) { @@ -484,6 +526,19 @@ struct CPDF_RichTextLayout::Impl { return width; } + // Where the ink of [begin, end) ends: trailing spaces do not count + // (Acrobat centres and wraps without them). + static size_t InkEnd(const Segment& segment, size_t begin, size_t end) { + while (end > begin && IsSpace(segment.text[end - 1])) { + --end; + } + return end; + } + + static float InkWidth(const Segment& segment, size_t begin, size_t end) { + return RangeWidth(segment, begin, InkEnd(segment, begin, end)); + } + static bool IsClusterBoundary(const Segment& segment, size_t pos) { for (const Item& item : segment.items) { if (pos == item.begin || pos == item.end) { @@ -522,22 +577,28 @@ struct CPDF_RichTextLayout::Impl { } } - // Builds the items of a segment: runs of one style, split by script and - // by the face that covers each grapheme, then shaped. - void Itemize(Segment* segment, const std::vector& styles) { + // Builds the items of a segment of a paragraph that reads right to left + // when |rtl|: runs of one style, split by script, by bidi level and by the + // face that covers each grapheme, then shaped. + void Itemize(Segment* segment, + bool rtl, + const std::vector& styles) { const WideString& text = segment->text; const size_t length = text.GetLength(); segment->items.clear(); + segment->bidi.emplace(text, rtl); if (length == 0) { return; } const std::vector grapheme_starts = GraphemeStarts(text); + const std::vector& levels = segment->bidi->levels(); - // Per grapheme: style, script, entry. + // Per grapheme: style, script, level, entry. struct Piece { size_t begin, end; int style; hb_script_t script; + uint8_t level; int entry; }; std::vector pieces; @@ -555,27 +616,20 @@ struct CPDF_RichTextLayout::Impl { } const int style = segment->style_of[begin]; const CPDF_RichTextStyle& resolved = styles[style].style; - int entry = fonts->ResolveRichFace(resolved.family, resolved.weight, - resolved.italic); - if (entry >= 0 && !Covers(entry, text, begin, end)) { - // The grapheme's base scalar picks the fallback font. - uint32_t base = 0; - for (char32_t cp : pdfium::CodePointView( - text.AsStringView().Substr(begin, end - begin))) { - if (base == 0 || !IsMark(static_cast(cp))) { - base = static_cast(cp); - if (!IsMark(base)) { - break; - } - } - } - const int fallback = - fonts->FindRichFallback(base, resolved.weight, resolved.italic); - if (fallback >= 0 && Covers(fallback, text, begin, end)) { - entry = fallback; - } + const WideStringView grapheme = + text.AsStringView().Substr(begin, end - begin); + const int face = + styles[style].face_entry >= 0 + ? styles[style].face_entry + : fonts->ResolveRichFace(resolved.family, resolved.weight, + resolved.italic, WideStringView(), + resolved.program); + const GraphemeMatch match = MatchGrapheme( + fonts, face, grapheme, resolved.weight, resolved.italic); + if (!match.covered && !IsInvisibleGrapheme(grapheme)) { + fonts->NoteMissingCharacter(WideString(grapheme)); } - pieces.push_back({begin, end, style, script, entry}); + pieces.push_back({begin, end, style, script, levels[begin], match.entry}); begin = end; } // A leading common-script piece takes the script that follows it. @@ -588,7 +642,7 @@ struct CPDF_RichTextLayout::Impl { for (const Piece& piece : pieces) { Item* last = segment->items.empty() ? nullptr : &segment->items.back(); if (last && last->style_index == piece.style && - last->entry == piece.entry && + last->entry == piece.entry && last->level == piece.level && (last->script == piece.script || IsCommonScript(piece.script))) { last->end = piece.end; continue; @@ -599,6 +653,7 @@ struct CPDF_RichTextLayout::Impl { item.begin = piece.begin; item.end = piece.end; item.script = piece.script; + item.level = piece.level; segment->items.push_back(std::move(item)); } for (Item& item : segment->items) { @@ -609,46 +664,73 @@ struct CPDF_RichTextLayout::Impl { } } - // The runs of a line covering [begin, end) of the segment. - std::vector RunsFor(const Segment& segment, - size_t begin, - size_t end, - const std::vector& styles) { + // The run drawing [begin, end) of |item| at bidi level |level|. + static GlyphRun RunOf(const Segment& segment, + const Item& item, + size_t begin, + size_t end, + uint8_t level, + const ResolvedStyle& resolved) { + GlyphRun run; + run.font_entry = item.entry; + run.style = resolved.style; + run.style.size = resolved.drawn_size; + run.rise = resolved.rise; + run.text = segment.text.Substr(begin, end - begin); + run.text_begin = segment.source_pos[begin]; + run.text_end = end < segment.source_pos.size() + ? segment.source_pos[end] + : segment.source_pos[end - 1] + 1; + run.rtl = item.rtl; + run.bidi_level = level; + run.ascent = item.ascent; + run.descent = item.descent; + run.needs_actual_text = item.needs_actual_text; + run.degraded = item.degraded; + for (const ShapedGlyph& glyph : item.glyphs) { + if (glyph.cluster >= begin && glyph.cluster < end) { + ShapedGlyph copy = glyph; + copy.cluster -= pdfium::checked_cast(begin); + run.glyphs.push_back(copy); + run.width += glyph.x_advance; + } + } + return run; + } + + // The runs of a line covering [begin, end) of the segment, in the order + // they are drawn, left to right. + static std::vector RunsFor( + const Segment& segment, + size_t begin, + size_t end, + const std::vector& styles) { + // Whitespace ending the line takes the paragraph's level, so an item's + // last characters may run at another level than the rest of it. + const std::vector levels = segment.bidi->LineLevels(begin, end); std::vector runs; + std::vector run_levels; for (const Item& item : segment.items) { - if (item.end <= begin || item.begin >= end) { - continue; - } - const size_t run_begin = std::max(begin, item.begin); - const size_t run_end = std::min(end, item.end); - const ResolvedStyle& resolved = styles[item.style_index]; - GlyphRun run; - run.font_entry = item.entry; - run.style = resolved.style; - run.style.size = resolved.drawn_size; - run.rise = resolved.rise; - run.text = segment.text.Substr(run_begin, run_end - run_begin); - run.text_begin = segment.source_pos[run_begin]; - run.text_end = run_end < segment.source_pos.size() - ? segment.source_pos[run_end] - : segment.source_pos[run_end - 1] + 1; - run.rtl = item.rtl; - run.bidi_level = item.rtl ? 1 : 0; - run.ascent = item.ascent; - run.descent = item.descent; - run.needs_actual_text = item.needs_actual_text; - run.degraded = item.degraded; - for (const ShapedGlyph& glyph : item.glyphs) { - if (glyph.cluster >= run_begin && glyph.cluster < run_end) { - ShapedGlyph copy = glyph; - copy.cluster -= pdfium::checked_cast(run_begin); - run.glyphs.push_back(copy); - run.width += glyph.x_advance; + const size_t item_end = std::min(end, item.end); + for (size_t run_begin = std::max(begin, item.begin); + run_begin < item_end;) { + const uint8_t level = levels[run_begin - begin]; + size_t run_end = run_begin + 1; + while (run_end < item_end && levels[run_end - begin] == level) { + ++run_end; } + runs.push_back(RunOf(segment, item, run_begin, run_end, level, + styles[item.style_index])); + run_levels.push_back(level); + run_begin = run_end; } - runs.push_back(std::move(run)); } - return runs; + std::vector visual; + visual.reserve(runs.size()); + for (size_t index : CFX_BidiParagraph::VisualOrder(run_levels)) { + visual.push_back(std::move(runs[index])); + } + return visual; } }; @@ -720,7 +802,7 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( std::vector requests; auto note = [&](const CPDF_RichTextStyle& style, const WideString& text) { const ByteString key = CPDF_AnnotFontMap::FaceRequestKey( - style.family, style.weight, style.italic); + style.family, style.weight, style.italic, style.program); auto it = std::ranges::find_if( requests, [&](const RequestText& r) { return r.key == key; }); if (it == requests.end()) { @@ -741,8 +823,8 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( } for (const RequestText& request : requests) { fonts_->ResolveRichFace(request.style.family, request.style.weight, - request.style.italic, - request.text.AsStringView()); + request.style.italic, request.text.AsStringView(), + request.style.program); } } @@ -820,12 +902,23 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( } segments.back().break_style = last_style; + // The direction the paragraph reads in: its dir, or its text's. + bool rtl = props.rtl; + if (options_.direction_from_text) { + WideString text; + for (const Segment& segment : segments) { + text += segment.text; + } + rtl = CFX_BidiParagraph::FirstStrongIsRtl(text.AsStringView()) + .value_or(false); + } + // 3. Each segment: itemise, shape, break into lines. const float margin_left = std::max(0.0f, props.margin_left); const float margin_right = std::max(0.0f, props.margin_right); bool first_line_of_paragraph = true; for (Segment& segment : segments) { - impl.Itemize(&segment, styles); + impl.Itemize(&segment, rtl, styles); const size_t length = segment.text.GetLength(); const std::vector opportunities = BreakOpportunities(segment.text); @@ -890,9 +983,10 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( // 4. The line box (C note §4.4). if (line.runs.empty()) { const ResolvedStyle& at_break = styles[segment.break_style]; - int entry = fonts_->ResolveRichFace(at_break.style.family, - at_break.style.weight, - at_break.style.italic); + int entry = fonts_->ResolveRichFace( + at_break.style.family, at_break.style.weight, + at_break.style.italic, WideStringView(), + at_break.style.program); const CPDF_AnnotFontMap::RichFace face = impl.FaceOf(entry); const float size = at_break.style.size; // parent size line.ascent = face.ascent * size; @@ -920,11 +1014,13 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( line.leading = kLeadingFactor * max_size; } // 5. Horizontal placement. - line.width = Impl::InkWidth(segment, pos, best); + const size_t ink_end = Impl::InkEnd(segment, pos, best); + line.width = Impl::RangeWidth(segment, pos, ink_end); const float slack = std::max(0.0f, avail - line.width); + // A paragraph whose dir is rtl names its sides from where it + // starts: left (the default) is its right side. Align align = props.align; if (props.rtl) { - std::reverse(line.runs.begin(), line.runs.end()); if (align == Align::kLeft) { align = Align::kRight; } else if (align == Align::kRight) { @@ -934,7 +1030,8 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( const bool justify_this_line = align == Align::kJustify && best < length && !line.ends_with_hard_break; - float start = margin_left + indent; + // The indent is on the side the paragraph starts from. + float start = margin_left + (rtl ? 0 : indent); switch (align) { case Align::kLeft: case Align::kJustify: @@ -947,11 +1044,20 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( break; } if (justify_this_line) { + // The spaces between words stretch; the line's trailing ones + // hang. + const size_t ink_source_end = + segment.source_pos[pos] + (ink_end - pos); + auto stretches = [&](const GlyphRun& run, + const ShapedGlyph& glyph) { + return glyph.cluster < run.text.GetLength() && + IsSpace(run.text[glyph.cluster]) && + run.text_begin + glyph.cluster < ink_source_end; + }; int spaces = 0; for (const GlyphRun& run : line.runs) { for (const ShapedGlyph& glyph : run.glyphs) { - if (glyph.cluster < run.text.GetLength() && - IsSpace(run.text[glyph.cluster])) { + if (stretches(run, glyph)) { ++spaces; } } @@ -960,8 +1066,7 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( const float extra = slack / spaces; for (GlyphRun& run : line.runs) { for (ShapedGlyph& glyph : run.glyphs) { - if (glyph.cluster < run.text.GetLength() && - IsSpace(run.text[glyph.cluster])) { + if (stretches(run, glyph)) { glyph.x_advance += extra; run.width += extra; } @@ -969,8 +1074,13 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( } } } + // The line's trailing spaces hang past its end: past the left + // edge when the paragraph reads right to left. line.start_x = start; float x = start; + if (rtl) { + x -= Impl::RangeWidth(segment, ink_end, best); + } for (GlyphRun& run : line.runs) { run.x = x; x += run.width; @@ -1044,3 +1154,474 @@ CPDF_RichTextLayout::Result CPDF_RichTextLayout::Arrange( } return result; } + +// ---- Form fields +// -------------------------------------------------------------- + +namespace { + +// A cluster of a field's text: what CPVT called a word. +struct FieldUnit { + size_t begin = 0; // in its segment's text + size_t end = 0; + float width = 0; + uint16_t code = 0; // its first character, as CPVT's word breaking sees it + float ascent = 0; // its font's at the size, as CPVT measures it + float descent = 0; // negative +}; + +struct FieldLine { + int begin = -1; // units; -1 for an empty section's one line + int end = -1; + float width = 0; + float ascent = 0; + float descent = 0; // negative +}; + +// What a field's text breaks into at line breaks: CPVT's sections. +struct FieldSection { + Segment segment; + std::vector units; + std::vector lines; + std::vector cell_x; // a comb's: where each unit starts + float left = 0; // the box its lines take, y down from the text's top + float top = 0; + float right = 0; + float bottom = 0; +}; + +struct FieldExtent { + float width = 0; // the widest line + float height = 0; // the lines' heights, summed +}; + +// CPVT's character for its word breaking: a code unit, so a supplementary +// character is its high surrogate. +uint16_t BreakCodeOf(wchar_t ch) { + const uint32_t code_point = static_cast(ch); + if (pdfium::IsSupplementary(code_point)) { + return pdfium::SurrogatePair(static_cast(code_point)).high(); + } + return static_cast(code_point); +} + +size_t Utf16LengthOf(WideStringView text) { + size_t length = 0; + for (char32_t code_point : pdfium::CodePointView(text)) { + length += pdfium::IsSupplementary(code_point) ? 2 : 1; + } + return length; +} + +// The text's sections and characters as CPVT's SetText() takes them: line +// breaks (CR, LF or both) start a section in a multi-line field and are +// dropped in any other, a tab is a space, and the limit or the comb's cells +// stop it. Characters (graphemes) are kept whole; a password's are '*'. +std::vector FieldSectionsOf( + const CPDF_RichTextLayout::Field& field) { + const WideString& text = field.text; + const std::vector starts = CPDF_RichTextLayout::GraphemeStarts(text); + std::vector sections(1); + // What CPVT counts: the text taken, line breaks included, and what it + // kept, a break between sections included; the limit in code units, the + // cells in characters. + size_t taken_units = 0; + size_t taken_characters = 0; + size_t kept_units = 0; + size_t kept_characters = 0; + for (size_t begin = 0; begin < text.GetLength();) { + size_t end = begin + 1; + while (end < text.GetLength() && !starts[end]) { + ++end; + } + const WideStringView grapheme = + text.AsStringView().Substr(begin, end - begin); + const size_t units = Utf16LengthOf(grapheme); + if (field.limit > 0 && taken_units + units > field.limit) { + break; + } + if (field.cells > 0 && taken_characters >= field.cells) { + break; + } + const wchar_t first = text[begin]; + if (first == L'\r' || first == L'\n') { + if (field.multiline) { + // CR LF is one break. + const wchar_t pair = first == L'\r' ? L'\n' : L'\r'; + if (end < text.GetLength() && text[end] == pair) { + ++end; + } + sections.emplace_back(); + ++kept_units; + ++kept_characters; + } + } else { + const bool full = (field.limit > 0 && kept_units + units > field.limit) || + (field.cells > 0 && kept_characters >= field.cells); + if (!full) { + if (field.password) { + sections.back() += L'*'; + } else if (first == L'\t') { + sections.back() += L' '; + } else { + sections.back() += WideString(grapheme); + } + kept_units += units; + ++kept_characters; + } + } + taken_units += units; + ++taken_characters; + begin = end; + } + return sections; +} + +// CPVT's SplitLines() over a section's units: the lines they break into +// when |wrap| (before the word that would pass |typeset_width|, or before +// the unit when one word is wider), and the box they take. +FieldExtent SplitFieldLines(const std::vector& units, + bool wrap, + float typeset_width, + float empty_ascent, + float empty_descent, + std::vector* lines) { + if (units.empty()) { + if (lines) { + lines->push_back({-1, -1, 0, empty_ascent, empty_descent}); + } + return {0, empty_ascent - empty_descent}; + } + const int total = pdfium::checked_cast(units.size()); + int head = 0; + float max_x = 0; + float max_y = 0; + float line_width = 0; + float backup_width = 0; + float line_ascent = 0; + float backup_ascent = 0; + float line_descent = 0; + float backup_descent = 0; + int word_start = 0; + bool full_word = false; + int full_words = 0; + int char_index = 0; + bool opened = false; + auto add_line = [&](int tail) { + if (lines) { + lines->push_back({head, tail, line_width, line_ascent, line_descent}); + } + max_y += line_ascent; + max_y -= line_descent; + max_x = std::max(line_width, max_x); + }; + int i = 0; + while (i < total) { + const FieldUnit& unit = units[i]; + const FieldUnit& previous = units[i > 0 ? i - 1 : i]; + line_ascent = std::max(line_ascent, unit.ascent); + line_descent = std::min(line_descent, unit.descent); + if (!opened) { + if (CPVT_IsOpenStylePunctuation(unit.code)) { + opened = true; + full_word = true; + } else if (CPVT_NeedDivision(previous.code, unit.code)) { + full_word = true; + } + } else if (!CPVT_IsSpace(unit.code) && + !CPVT_IsOpenStylePunctuation(unit.code)) { + opened = false; + } + if (full_word) { + full_word = false; + if (char_index > 0) { + ++full_words; + } + word_start = i; + backup_width = line_width; + backup_ascent = line_ascent; + backup_descent = line_descent; + } + ++char_index; + if (wrap && typeset_width > 0 && line_width + unit.width > typeset_width) { + if (full_words > 0) { + i = word_start; + line_width = backup_width; + line_ascent = backup_ascent; + line_descent = backup_descent; + } + if (char_index == 1) { + line_width = unit.width; + ++i; + } + add_line(i - 1); + head = i; + line_width = 0; + line_ascent = 0; + line_descent = 0; + char_index = 0; + full_words = 0; + full_word = false; + } else { + line_width += unit.width; + ++i; + } + } + if (head <= total - 1) { + add_line(total - 1); + } + return {max_x, max_y}; +} + +// Where a line |width| wide starts in |typeset_width|, by /Q. +float FieldAlignmentOffset(int alignment, float typeset_width, float width) { + switch (alignment) { + case 1: + return (typeset_width - width) * 0.5f; + case 2: + return typeset_width - width; + default: + return 0.0f; + } +} + +} // namespace + +CPDF_RichTextLayout::Result CPDF_RichTextLayout::ArrangeField( + const Field& field, + const CFX_FloatRect& plate) { + Result result; + const float plate_width = plate.right - plate.left; + const float plate_height = plate.top - plate.bottom; + const std::vector texts = FieldSectionsOf(field); + // A field reads in the direction of its first strong character. + WideString all_text; + for (const WideString& text : texts) { + all_text += text; + } + const bool rtl = CFX_BidiParagraph::FirstStrongIsRtl(all_text.AsStringView()) + .value_or(false); + const bool comb = field.cells > 0; + + // The fonts' ascent and descent, as CPVT measures a line with them. + std::map> metrics; + auto metrics_of = [&](int entry) { + auto it = metrics.find(entry); + if (it == metrics.end()) { + RetainPtr font = fonts_->GetPDFFont(entry); + it = metrics + .emplace(entry, font ? std::make_pair(font->GetTypeAscent(), + font->GetTypeDescent()) + : std::make_pair(0, 0)) + .first; + } + return it->second; + }; + + // The sections, itemised and shaped at |size|, cut into units. + auto sections_at = [&](Impl* impl, float size) { + ResolvedStyle style; + style.style.size = size; + style.style.weight = field.weight; + style.style.italic = field.italic; + style.drawn_size = size; + style.face_entry = field.face_entry; + const std::vector styles = {style}; + std::vector sections(texts.size()); + size_t source_pos = 0; + for (size_t s = 0; s < texts.size(); ++s) { + Segment& segment = sections[s].segment; + segment.text = texts[s]; + segment.style_of.assign(segment.text.GetLength(), 0); + for (size_t i = 0; i < segment.text.GetLength(); ++i) { + segment.source_pos.push_back(source_pos++); + } + ++source_pos; // the line break + impl->Itemize(&segment, rtl, styles); + const std::vector starts = GraphemeStarts(segment.text); + const size_t length = segment.text.GetLength(); + for (size_t begin = 0; begin < length;) { + size_t end = begin + 1; + while (end < length && + !(starts[end] && Impl::IsClusterBoundary(segment, end))) { + ++end; + } + FieldUnit unit; + unit.begin = begin; + unit.end = end; + unit.width = Impl::RangeWidth(segment, begin, end); + unit.code = BreakCodeOf(segment.text[begin]); + for (const Item& item : segment.items) { + if (begin >= item.begin && begin < item.end) { + const auto [ascent, descent] = metrics_of(item.entry); + unit.ascent = ascent * size * 0.001f; + unit.descent = descent * size * 0.001f; + break; + } + } + sections[s].units.push_back(unit); + begin = end; + } + } + return sections; + }; + auto face_ascent = [&](float size) { + return metrics_of(field.face_entry).first * size * 0.001f; + }; + auto face_descent = [&](float size) { + return metrics_of(field.face_entry).second * size * 0.001f; + }; + const float typeset_width = std::max(plate_width, 0.0f); + + // The size: the biggest step of the ladder the text fits in, searched as + // CPVT's std::lower_bound searches it. + float size = field.size; + if (FXSYS_IsFloatZero(size)) { + size = 0; + if (plate_width > 0) { + const size_t steps = + field.multiline ? kAutoSizeSteps.size() / 4 : kAutoSizeSteps.size(); + auto too_big = [&](float candidate) { + Impl impl(fonts_, options_); + impl.field = true; + float width = 0; + float height = 0; + for (const FieldSection& section : sections_at(&impl, candidate)) { + const FieldExtent extent = SplitFieldLines( + section.units, field.multiline, typeset_width, + face_ascent(candidate), face_descent(candidate), nullptr); + width = std::max(extent.width, width); + height += extent.height; + if (FXSYS_IsFloatBigger(width, plate_width) || + FXSYS_IsFloatBigger(height, plate_height)) { + return true; + } + } + return false; + }; + size_t first = 0; + size_t count = steps; + while (count > 0) { + const size_t step = count / 2; + if (!too_big(kAutoSizeSteps[first + step])) { + first += step + 1; + count -= step + 1; + } else { + count = step; + } + } + size = first == steps ? kAutoSizeSteps[steps - 1] + : kAutoSizeSteps[first == 0 ? 0 : first - 1]; + } + } + + Impl impl(fonts_, options_); + impl.field = true; + std::vector sections = sections_at(&impl, size); + const std::vector styles = [&] { + ResolvedStyle style; + style.style.size = size; + style.drawn_size = size; + style.face_entry = field.face_entry; + return std::vector{style}; + }(); + + // Each section's lines, placed as CPVT's RearrangeTypeset() and + // RearrangeCharArray() place them, the sections one under the other. + float position_y = 0; + float content_height = 0; + for (FieldSection& section : sections) { + if (comb) { + // Each unit in the middle of its cell, the cells filled from the + // left, the middle or the right; every baseline at the face's ascent. + const float node_width = plate_width / static_cast(field.cells); + const int units = pdfium::checked_cast(section.units.size()); + const int cells = pdfium::checked_cast(field.cells); + int start = 0; + if (field.alignment == 1) { + start = (cells - units) / 2; + } else if (field.alignment == 2) { + start = cells - units; + } + const float ascent = face_ascent(size); + float line_descent = face_descent(size); + float x = 0; + for (int i = 0; i < units && i < cells; ++i) { + const FieldUnit& unit = section.units[i]; + // In double, as CPVT computes it. + x = static_cast(node_width * (i + start + 0.5) - + unit.width * 0.5f); + section.cell_x.push_back(x); + x += unit.width; + line_descent = std::min(line_descent, unit.descent); + } + section.lines = { + {units > 0 ? 0 : -1, units - 1, 0, ascent, line_descent}}; + section.top = position_y; + section.right = x; + section.bottom = section.top + (ascent - line_descent); + } else { + const FieldExtent extent = SplitFieldLines( + section.units, field.multiline, typeset_width, face_ascent(size), + face_descent(size), §ion.lines); + section.left = + FieldAlignmentOffset(field.alignment, typeset_width, extent.width); + section.right = section.left + extent.width; + section.top = position_y; + section.bottom = position_y + extent.height; + } + position_y += section.bottom - section.top; + content_height += section.bottom - section.top; + } + + // A one-line field's text has its middle at the plate's middle. + const float offset_y = + field.centered ? (content_height - plate_height) / 2.0f : 0.0f; + + for (FieldSection& section : sections) { + const Segment& segment = section.segment; + float line_top = 0; // the baseline steps down from the section's top + for (const FieldLine& field_line : section.lines) { + Line line; + line.ascent = field_line.ascent; + line.descent = -field_line.descent; + line.width = field_line.width; + line_top += field_line.ascent; + line.baseline_y = (line_top + section.top) - offset_y; + line_top -= field_line.descent; + if (field_line.begin >= 0) { + if (comb) { + // A run per cell. + for (size_t i = 0; i < section.cell_x.size(); ++i) { + const FieldUnit& unit = section.units[i]; + for (GlyphRun& run : + Impl::RunsFor(segment, unit.begin, unit.end, styles)) { + run.x = section.cell_x[i]; + line.runs.push_back(std::move(run)); + } + } + line.start_x = line.runs.empty() ? 0 : line.runs.front().x; + } else { + const size_t begin = section.units[field_line.begin].begin; + const size_t end = section.units[field_line.end].end; + float x = FieldAlignmentOffset(field.alignment, typeset_width, + field_line.width); + line.start_x = (x - section.left) + section.left; + for (GlyphRun& run : Impl::RunsFor(segment, begin, end, styles)) { + run.x = (x - section.left) + section.left; + x += run.width; + line.runs.push_back(std::move(run)); + } + } + for (const GlyphRun& run : line.runs) { + result.degraded = result.degraded || run.degraded; + } + } + result.lines.push_back(std::move(line)); + } + } + result.content_height = content_height; + result.body_size = size; + result.degraded = result.degraded || impl.degraded; + return result; +} diff --git a/core/fpdfdoc/cpdf_richtextlayout.h b/core/fpdfdoc/cpdf_richtextlayout.h index e37f04514c..adf45f94eb 100644 --- a/core/fpdfdoc/cpdf_richtextlayout.h +++ b/core/fpdfdoc/cpdf_richtextlayout.h @@ -4,8 +4,9 @@ // EmbedPDF: lays a CPDF_RichTextDocument out into lines of shaped glyph // runs, the way Acrobat does it (plan §4.4, Phase C note §4): HarfBuzz // shaping per run, UAX #14 break opportunities snapped to grapheme -// boundaries, and the measured line model (ascent = max, descent = the last -// run's, leading = 0.2 em of the largest size). Fonts come from a +// boundaries, the Unicode bidi algorithm (UAX #9) ordering each line, and the +// measured line model (ascent = max, descent = the last run's, leading = +// 0.2 em of the largest size). Fonts come from a // CPDF_AnnotFontMap, which the layout extends with fallbacks as it goes. // Nothing here touches a document. @@ -46,8 +47,8 @@ class CPDF_RichTextLayout { WideString text; // the logical text this run draws size_t text_begin = 0; // range within the paragraph's logical text size_t text_end = 0; - uint8_t bidi_level = 0; // D: 0 or 1 from the run's script - bool rtl = false; // shaped right-to-left + uint8_t bidi_level = 0; // resolved embedding level; odd = right to left + bool rtl = false; // shaped right to left std::vector glyphs; // visual order for the run's direction float x = 0; // start, from the text area's left edge, pt float width = 0; // sum of advances @@ -73,6 +74,9 @@ class CPDF_RichTextLayout { bool typographic_features = false; // Acrobat's line descent is the last run's; false = max over runs. bool parity_descent = true; + // A paragraph reads in the direction of its first strong character (left + // to right when it has none), as a form field does, instead of its dir. + bool direction_from_text = false; }; struct Result { @@ -92,6 +96,37 @@ class CPDF_RichTextLayout { const CFX_FloatRect& area, CPDF_Annot::VerticalAlignment vertical_alignment); + // A form field's text, or a redaction label, laid out with the geometry + // PDFium's form filler gives it (CPVT): words break where CPVT breaks them, + // a line is as high as its fonts' ascent and descent with no leading and + // is aligned on its own, a size of 0 is the biggest of CPVT's sizes the + // text fits in (12 pt at most for several lines), and a comb puts one + // character in the middle of each cell. Fonts, shaping and order are this + // layout's: each cluster in the first font that has it, then the last + // resort; the Unicode bidi algorithm with the direction of the first strong + // character. + struct Field { + WideString text; + int face_entry = -1; // the /DA font + int weight = 400; // its style, for the fallbacks + bool italic = false; + float size = 0; // pt; 0 fits the text + int alignment = 0; // /Q: 0 left, 1 centre, 2 right + bool multiline = false; // line breaks start lines; long lines wrap + bool password = false; // each character drawn as '*' + // At most this many UTF-16 code units, line breaks counted (/MaxLen); + // 0 for no limit. A character is kept whole or not at all. + size_t limit = 0; + // A comb of this many cells, one character in each; 0 for none. + size_t cells = 0; + // The text's middle at the plate's middle (a one-line field), else its + // first line at the plate's top. + bool centered = false; + }; + // Line positions come back relative to |plate|'s top-left corner, as + // Arrange()'s do; |Result::body_size| is the size laid out with. + Result ArrangeField(const Field& field, const CFX_FloatRect& plate); + // Pure pieces, exposed for tests. // True at |i| when a line may start with character |i| (never at 0). static std::vector BreakOpportunities(const WideString& text); @@ -99,6 +134,27 @@ class CPDF_RichTextLayout { // selectors, ZWJ sequences and low surrogates extend the previous one). static std::vector GraphemeStarts(const WideString& text); + // The font a grapheme draws with when its run's face is |face_entry|: the + // face, when it maps every character of the grapheme; else the first + // fallback font, in the order for the style, that maps every character. + // |covered| is false when none does: the grapheme draws as the last-resort + // font's box, or as the face's missing glyph when there is no last resort + // or the grapheme draws nothing. Joiners and variation selectors don't + // need a glyph. + struct GraphemeMatch { + int entry = -1; + bool covered = false; + }; + static GraphemeMatch MatchGrapheme(CPDF_AnnotFontMap* fonts, + int face_entry, + WideStringView grapheme, + int weight, + bool italic); + // Whether a grapheme draws nothing a font must provide: controls and + // default-ignorable characters (joiners, direction marks, variation + // selectors, the zero width space, the soft hyphen, ...). + static bool IsInvisibleGrapheme(WideStringView grapheme); + private: struct Impl; UnownedPtr const fonts_; diff --git a/core/fpdfdoc/cpdf_richtextparser.cpp b/core/fpdfdoc/cpdf_richtextparser.cpp index eeaccabd9e..bca8d00b81 100644 --- a/core/fpdfdoc/cpdf_richtextparser.cpp +++ b/core/fpdfdoc/cpdf_richtextparser.cpp @@ -1099,7 +1099,7 @@ ByteString JsonParagraphProps(const CPDF_RichTextParagraphProps& props, bool CPDF_RichTextStyleDelta::IsEmpty() const { return !family && !weight && !italic && !size && !color && !decoration && - !script && !letter_spacing && !horz_scale && + !script && !letter_spacing && !horz_scale && !program && unknown_declarations.IsEmpty(); } @@ -1134,6 +1134,14 @@ CPDF_RichTextStyle ApplyRichTextStyleDelta( if (delta.horz_scale) { out.horz_scale = *delta.horz_scale; } + // A program draws the base's face only: a run that restates that face + // keeps it, one in another face finds that face by its family. + if (delta.program) { + out.program = *delta.program; + } else if (out.weight != base.weight || out.italic != base.italic || + FontFamilyKey(out.family) != FontFamilyKey(base.family)) { + out.program = 0; + } return out; } diff --git a/core/fpdfdoc/cpdf_richtextparser_unittest.cpp b/core/fpdfdoc/cpdf_richtextparser_unittest.cpp index 8b1ed422cc..99137c8f98 100644 --- a/core/fpdfdoc/cpdf_richtextparser_unittest.cpp +++ b/core/fpdfdoc/cpdf_richtextparser_unittest.cpp @@ -318,3 +318,31 @@ TEST(CPDF_RichTextParserTest, JsonShape) { EXPECT_TRUE(CPDF_RichTextParser::ToJSON(plain).Contains( "{\"text\":\"a \\\"quoted\\\" \\\\ line\\ttab\"}")); } + +// A program draws the base's face only: a run keeps it while it restates that +// face (the family as it compares), and finds another face by its family. +TEST(CPDF_RichTextParserTest, AProgramFollowsItsFaceOnly) { + CPDF_RichTextStyle base = Defaults(); + base.family = L"Mono"; + base.program = 8; + + CPDF_RichTextStyleDelta same; + same.family = L"mono"; + same.size = 14.0f; + EXPECT_EQ(8u, ApplyRichTextStyleDelta(base, same).program); + EXPECT_EQ(8u, + ApplyRichTextStyleDelta(base, CPDF_RichTextStyleDelta()).program); + + CPDF_RichTextStyleDelta bold; + bold.weight = 700; + EXPECT_EQ(0u, ApplyRichTextStyleDelta(base, bold).program); + CPDF_RichTextStyleDelta other; + other.family = L"Helvetica"; + EXPECT_EQ(0u, ApplyRichTextStyleDelta(base, other).program); + + CPDF_RichTextStyleDelta named; + named.weight = 700; + named.program = 5; + EXPECT_EQ(5u, ApplyRichTextStyleDelta(base, named).program); + EXPECT_FALSE(named.IsEmpty()); +} diff --git a/core/fpdfdoc/cpvt_section.cpp b/core/fpdfdoc/cpvt_section.cpp index 95378e5fe3..fe9551e315 100644 --- a/core/fpdfdoc/cpvt_section.cpp +++ b/core/fpdfdoc/cpvt_section.cpp @@ -175,6 +175,19 @@ bool NeedDivision(uint16_t prevWord, uint16_t curWord) { } // namespace +// EmbedPDF(fonts): CPVT's word breaking for the shared layout. +bool CPVT_NeedDivision(uint16_t previous, uint16_t current) { + return NeedDivision(previous, current); +} + +bool CPVT_IsOpenStylePunctuation(uint16_t word) { + return IsOpenStylePunctuation(word); +} + +bool CPVT_IsSpace(uint16_t word) { + return IsSpace(word); +} + CPVT_Section::Line::Line(const CPVT_LineInfo& lineinfo) : line_info_(lineinfo) {} diff --git a/core/fpdfdoc/cpvt_section.h b/core/fpdfdoc/cpvt_section.h index f35f8b76e9..28ad812bfd 100644 --- a/core/fpdfdoc/cpvt_section.h +++ b/core/fpdfdoc/cpvt_section.h @@ -116,4 +116,12 @@ class CPVT_Section final { UnownedPtr const vt_; }; +// EmbedPDF(fonts): CPVT's word breaking, which the shared layout breaks a +// form field's lines with (CPDF_RichTextLayout::ArrangeField()): whether a +// line may break between two characters, the punctuation that opens a word, +// and the spaces that don't close one. +bool CPVT_NeedDivision(uint16_t previous, uint16_t current); +bool CPVT_IsOpenStylePunctuation(uint16_t word); +bool CPVT_IsSpace(uint16_t word); + #endif // CORE_FPDFDOC_CPVT_SECTION_H_ diff --git a/core/fpdftext/cpdf_textpage.cpp b/core/fpdftext/cpdf_textpage.cpp index 2059d4e07c..97dd8a251c 100644 --- a/core/fpdftext/cpdf_textpage.cpp +++ b/core/fpdftext/cpdf_textpage.cpp @@ -449,6 +449,78 @@ bool FollowsLiteralRightToLeftText( return false; } +// EmbedPDF(bug-fix): the text a line's bidi segments are cut from. An +// /ActualText span is logical text, so inside one, a mark takes the direction +// of the character before it, and characters with no strong direction between +// two with the same one take theirs (UAX #9 W1 and N1); the span's leading and +// trailing ones keep their own. Cut at its marks and inner spaces, a span came +// out as its segments in visual order, each kept forward: a pointed Hebrew +// word read back scrambled, and a right-to-left span of two words with the +// words swapped. +WideString BidiTextOf(const WideString& str, + pdfium::span chars) { + enum class Strong { kNone, kLeft, kRight }; + auto strong_of = [](wchar_t c) { + switch (pdfium::unicode::GetBidiClass(c)) { + case BidiClass::kL: + return Strong::kLeft; + case BidiClass::kR: + case BidiClass::kAL: + return Strong::kRight; + default: + return Strong::kNone; + } + }; + + WideString text = str; + const size_t count = std::min(str.GetLength(), chars.size()); + size_t begin = 0; + while (begin < count) { + const CPDF_TextPage::CharInfo& first = chars[begin]; + size_t end = begin + 1; + if (first.char_type() != CPDF_TextPage::CharType::kActualText) { + begin = end; + continue; + } + while (end < count && + chars[end].char_type() == CPDF_TextPage::CharType::kActualText && + chars[end].text_object() == first.text_object()) { + ++end; + } + + std::vector resolved; + for (size_t i = begin; i < end; ++i) { + Strong direction = strong_of(str[i]); + if (direction == Strong::kNone && i > begin && + pdfium::unicode::GetBidiClass(str[i]) == BidiClass::kNSM) { + direction = resolved.back(); + } + resolved.push_back(direction); + } + std::optional last_strong; + for (size_t i = 0; i < resolved.size(); ++i) { + if (resolved[i] == Strong::kNone) { + continue; + } + if (last_strong && resolved[*last_strong] == resolved[i]) { + std::fill(resolved.begin() + *last_strong + 1, resolved.begin() + i, + resolved[i]); + } + last_strong = i; + } + + // Stand-ins of the resolved direction: Hebrew alef, Latin a. + for (size_t i = begin; i < end; ++i) { + const Strong direction = resolved[i - begin]; + if (direction != Strong::kNone && strong_of(str[i]) == Strong::kNone) { + text.SetAt(i, direction == Strong::kRight ? 0x05D0 : L'a'); + } + } + begin = end; + } + return text; +} + } // namespace CPDF_TextPage::TransformedTextObject::TransformedTextObject() = default; @@ -1017,7 +1089,10 @@ void CPDF_TextPage::CloseTempLine() { } const WideString str = temp_text_buf_.MakeString(); - CFX_BidiString bidi(str, /*auto_order=*/true); + // EmbedPDF(bug-fix): /ActualText spans aren't cut at their marks and inner + // spaces (BidiTextOf); the characters are still those of `str`. + const WideString bidi_text = BidiTextOf(str, temp_char_list_); + CFX_BidiString bidi(bidi_text, /*auto_order=*/true); if (rtl_) { bidi.SetOverallDirectionRight(); } @@ -1204,7 +1279,13 @@ bool CPDF_TextPage::ProcessMarkedContent(const TransformedTextObject& obj) { (prev_text_obj_ && IsRightToLeft(*prev_text_obj_) && CFX_BidiString(actual_text, /*auto_order=*/true).OverallDirection() == CFX_BidiChar::Direction::kRight); - CFX_Matrix matrix = text_obj->GetTextMatrix() * obj.form_matrix_; + // EmbedPDF(bug-fix): the characters are boxed and placed in page space, as + // every other character is. The text object's rect and position are in the + // space of the form XObject that draws it; upstream used them as they are, + // so /ActualText in a form drawn anywhere but the page's origin, such as a + // flattened field, was boxed in the wrong place. + const CFX_Matrix& form_matrix = obj.form_matrix_; + CFX_Matrix matrix = text_obj->GetTextMatrix() * form_matrix; CFX_FloatRect rect = text_obj->GetRect(); float step = 0; @@ -1229,10 +1310,12 @@ bool CPDF_TextPage::ProcessMarkedContent(const TransformedTextObject& obj) { CFX_FloatRect char_box(rect); char_box.Translate(k * step, 0); temp_text_buf_.AppendChar(wc); - // EmbedPDF(text-geometry): not a real glyph, so no local origin. - temp_char_list_.push_back( - CharInfo::Synthetic(CharType::kActualText, wc, text_obj->GetPos(), - char_box, matrix, text_obj)); + // EmbedPDF(text-geometry, bug-fix): not a real glyph, so no local origin. + // The position and box are taken through the form's matrix into page + // space (see `form_matrix` above). + temp_char_list_.push_back(CharInfo::Synthetic( + CharType::kActualText, wc, form_matrix.Transform(text_obj->GetPos()), + form_matrix.TransformRect(char_box), matrix, text_obj)); } return is_rtl; } diff --git a/core/fxcrt/BUILD.gn b/core/fxcrt/BUILD.gn index 37d4c807c5..6b47d26965 100644 --- a/core/fxcrt/BUILD.gn +++ b/core/fxcrt/BUILD.gn @@ -33,6 +33,10 @@ source_set("fxcrt") { "bytestring_pool.h", "cfx_bidi_resolver.cpp", "cfx_bidi_resolver.h", + # EmbedPDF(fonts): the Unicode bidirectional algorithm over one paragraph, + # through ICU's ubidi; the rich text layout orders a line's runs with it. + "cfx_bidiparagraph.cpp", + "cfx_bidiparagraph.h", "cfx_bitstream.cpp", "cfx_bitstream.h", "cfx_datetime.cpp", @@ -249,6 +253,7 @@ pdfium_unittest_source_set("unittests") { "bytestring_pool_unittest.cpp", "bytestring_unittest.cpp", "cfx_bidi_resolver_unittest.cpp", + "cfx_bidiparagraph_unittest.cpp", "cfx_bitstream_unittest.cpp", "cfx_datetime_unittest.cpp", "cfx_read_only_container_stream_unittest.cpp", diff --git a/core/fxcrt/cfx_bidiparagraph.cpp b/core/fxcrt/cfx_bidiparagraph.cpp new file mode 100644 index 0000000000..18f53d6e39 --- /dev/null +++ b/core/fxcrt/cfx_bidiparagraph.cpp @@ -0,0 +1,170 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fxcrt/cfx_bidiparagraph.h" + +#include +#include +#include +#include + +#include "core/fxcrt/check_op.h" +#include "core/fxcrt/code_point_view.h" +#include "core/fxcrt/numerics/safe_conversions.h" +#include "core/fxcrt/utf16.h" + +#if defined(USE_SYSTEM_ICUUC) +#include +#include +#else +#include "third_party/icu/source/common/unicode/ubidi.h" +#include "third_party/icu/source/common/unicode/uchar.h" +#endif + +namespace { + +struct UBiDiDeleter { + void operator()(UBiDi* bidi) const { ubidi_close(bidi); } +}; + +// What rule L1 resets at the end of a line: whitespace, separators, and the +// formatting characters the algorithm otherwise passes over. +bool TrailsAtParagraphLevel(uint32_t code_point) { + switch (u_charDirection(static_cast(code_point))) { + case U_WHITE_SPACE_NEUTRAL: + case U_SEGMENT_SEPARATOR: + case U_BLOCK_SEPARATOR: + case U_BOUNDARY_NEUTRAL: + case U_LEFT_TO_RIGHT_EMBEDDING: + case U_LEFT_TO_RIGHT_OVERRIDE: + case U_RIGHT_TO_LEFT_EMBEDDING: + case U_RIGHT_TO_LEFT_OVERRIDE: + case U_POP_DIRECTIONAL_FORMAT: + case U_LEFT_TO_RIGHT_ISOLATE: + case U_RIGHT_TO_LEFT_ISOLATE: + case U_FIRST_STRONG_ISOLATE: + case U_POP_DIRECTIONAL_ISOLATE: + return true; + default: + return false; + } +} + +} // namespace + +CFX_BidiParagraph::CFX_BidiParagraph(const WideString& text, bool rtl) + : text_(text), + base_level_(rtl ? 1 : 0), + levels_(text.GetLength(), base_level_) { + // ICU counts UTF-16 code units; each character's level is its first + // unit's. + std::u16string units; + std::vector first_unit; + first_unit.reserve(text.GetLength()); + for (wchar_t ch : text) { + first_unit.push_back(units.size()); + const uint32_t code_point = static_cast(ch); + if (pdfium::IsSupplementary(code_point)) { + const pdfium::SurrogatePair pair(static_cast(code_point)); + units += pair.high(); + units += pair.low(); + } else if (code_point > 0xFFFF) { + units += u'�'; + } else { + units += static_cast(code_point); + } + } + // ICU's run bookkeeping overflows past this length (crbug.com/504629701). + constexpr size_t kMaxUnits = + std::numeric_limits::max() / (3 * sizeof(int32_t)); + if (units.empty() || units.size() > kMaxUnits) { + return; + } + const int32_t length = pdfium::checked_cast(units.size()); + UErrorCode status = U_ZERO_ERROR; + std::unique_ptr bidi( + ubidi_openSized(length, /*maxRunCount=*/0, &status)); + if (U_FAILURE(status)) { + return; + } + ubidi_setPara(bidi.get(), units.data(), length, base_level_, + /*embeddingLevels=*/nullptr, &status); + if (U_FAILURE(status)) { + return; + } + const UBiDiLevel* levels = ubidi_getLevels(bidi.get(), &status); + if (U_FAILURE(status) || !levels) { + return; + } + for (size_t i = 0; i < first_unit.size(); ++i) { + levels_[i] = levels[first_unit[i]]; + } +} + +CFX_BidiParagraph::~CFX_BidiParagraph() = default; + +std::vector CFX_BidiParagraph::LineLevels(size_t begin, + size_t end) const { + CHECK_LE(begin, end); + CHECK_LE(end, levels_.size()); + std::vector line(levels_.begin() + begin, levels_.begin() + end); + for (size_t i = line.size(); + i > 0 && + TrailsAtParagraphLevel(static_cast(text_[begin + i - 1])); + --i) { + line[i - 1] = base_level_; + } + return line; +} + +// static +std::optional CFX_BidiParagraph::FirstStrongIsRtl(WideStringView text) { + int open_isolates = 0; + for (char32_t code_point : pdfium::CodePointView(text)) { + switch (u_charDirection(static_cast(code_point))) { + case U_LEFT_TO_RIGHT: + if (open_isolates == 0) { + return false; + } + break; + case U_RIGHT_TO_LEFT: + case U_RIGHT_TO_LEFT_ARABIC: + if (open_isolates == 0) { + return true; + } + break; + case U_LEFT_TO_RIGHT_ISOLATE: + case U_RIGHT_TO_LEFT_ISOLATE: + case U_FIRST_STRONG_ISOLATE: + ++open_isolates; + break; + case U_POP_DIRECTIONAL_ISOLATE: + if (open_isolates > 0) { + --open_isolates; + } + break; + default: + break; + } + } + return std::nullopt; +} + +// static +std::vector CFX_BidiParagraph::VisualOrder( + pdfium::span levels) { + std::vector order(levels.size()); + std::iota(order.begin(), order.end(), 0); + if (levels.empty()) { + return order; + } + std::vector logical_index(levels.size()); + std::iota(logical_index.begin(), logical_index.end(), 0); + ubidi_reorderVisual(levels.data(), + pdfium::checked_cast(levels.size()), + logical_index.data()); + for (size_t i = 0; i < order.size(); ++i) { + order[i] = pdfium::checked_cast(logical_index[i]); + } + return order; +} diff --git a/core/fxcrt/cfx_bidiparagraph.h b/core/fxcrt/cfx_bidiparagraph.h new file mode 100644 index 0000000000..f12bc3199f --- /dev/null +++ b/core/fxcrt/cfx_bidiparagraph.h @@ -0,0 +1,50 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 +// +// EmbedPDF(fonts): the Unicode bidirectional algorithm (UAX #9) over one +// paragraph, through ICU's ubidi: the level each character resolves to, the +// levels of one line of it, and the order a line's runs are drawn in. + +#ifndef CORE_FXCRT_CFX_BIDIPARAGRAPH_H_ +#define CORE_FXCRT_CFX_BIDIPARAGRAPH_H_ + +#include +#include + +#include +#include + +#include "core/fxcrt/span.h" +#include "core/fxcrt/widestring.h" + +class CFX_BidiParagraph { + public: + // Resolves |text| as one paragraph whose level is 1 when |rtl|, else 0. + CFX_BidiParagraph(const WideString& text, bool rtl); + ~CFX_BidiParagraph(); + + uint8_t base_level() const { return base_level_; } + + // One level per character of the text; an odd level runs right to left. + const std::vector& levels() const { return levels_; } + + // The levels of [begin, end) laid out as one line: whitespace at the end of + // the line takes the paragraph's level (rule L1), so it trails the line in + // the paragraph's direction. + std::vector LineLevels(size_t begin, size_t end) const; + + // Whether |text| reads right to left by its first strong character outside + // isolates (rules P2 and P3). nullopt when it has no strong character. + static std::optional FirstStrongIsRtl(WideStringView text); + + // The order to draw runs with these levels in, left to right, as their + // logical indices (rule L2). + static std::vector VisualOrder(pdfium::span levels); + + private: + const WideString text_; + const uint8_t base_level_; + std::vector levels_; +}; + +#endif // CORE_FXCRT_CFX_BIDIPARAGRAPH_H_ diff --git a/core/fxcrt/cfx_bidiparagraph_unittest.cpp b/core/fxcrt/cfx_bidiparagraph_unittest.cpp new file mode 100644 index 0000000000..73e356f9c9 --- /dev/null +++ b/core/fxcrt/cfx_bidiparagraph_unittest.cpp @@ -0,0 +1,67 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fxcrt/cfx_bidiparagraph.h" + +#include + +#include "testing/gtest/include/gtest/gtest.h" + +using Levels = std::vector; + +TEST(CFXBidiParagraphTest, HebrewAndDigitsInALeftToRightParagraph) { + // "Hello שלום 123": the space before the digits sits between two + // right-to-left neighbours (digits count as such, rule N1), and digits + // take the next even level (rule I1). + CFX_BidiParagraph bidi(L"Hello \x05E9\x05DC\x05D5\x05DD 123", false); + EXPECT_EQ(0, bidi.base_level()); + EXPECT_EQ((Levels{0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2}), bidi.levels()); +} + +TEST(CFXBidiParagraphTest, DigitsBeforeHebrewInARightToLeftParagraph) { + // "123 שלום" at level 1: digits at an odd level go up one (rule I2). + CFX_BidiParagraph bidi(L"123 \x05E9\x05DC\x05D5\x05DD", true); + EXPECT_EQ(1, bidi.base_level()); + EXPECT_EQ((Levels{2, 2, 2, 1, 1, 1, 1, 1}), bidi.levels()); +} + +TEST(CFXBidiParagraphTest, OneLevelPerCharacter) { + // A supplementary character is one character, whatever wchar_t holds. + const WideString text(L"a\U0001D400\x05E9"); + CFX_BidiParagraph bidi(text, false); + ASSERT_EQ(text.GetLength(), bidi.levels().size()); + EXPECT_EQ(0, bidi.levels().front()); + EXPECT_EQ(1, bidi.levels().back()); +} + +TEST(CFXBidiParagraphTest, LineLevelsResetTrailingWhitespace) { + // Latin in a right-to-left paragraph is at level 2, its inner space too; + // ending a line, the space goes to the paragraph's level (rule L1). + CFX_BidiParagraph bidi(L"abc def", true); + EXPECT_EQ((Levels{2, 2, 2, 2, 2, 2, 2}), bidi.levels()); + EXPECT_EQ((Levels{2, 2, 2, 1}), bidi.LineLevels(0, 4)); + EXPECT_EQ((Levels{2, 2, 2}), bidi.LineLevels(4, 7)); + EXPECT_TRUE(bidi.LineLevels(3, 3).empty()); +} + +TEST(CFXBidiParagraphTest, FirstStrongIsRtl) { + EXPECT_EQ(true, CFX_BidiParagraph::FirstStrongIsRtl(L"123 \x05E9\x05DC")); + EXPECT_EQ(false, CFX_BidiParagraph::FirstStrongIsRtl(L"abc \x05E9\x05DC")); + EXPECT_EQ(true, CFX_BidiParagraph::FirstStrongIsRtl(L"\x0634\x0643")); + EXPECT_EQ(std::nullopt, CFX_BidiParagraph::FirstStrongIsRtl(L"123 !")); + EXPECT_EQ(std::nullopt, CFX_BidiParagraph::FirstStrongIsRtl(L"")); + // An isolate's content doesn't count (rule P2). + EXPECT_EQ(true, CFX_BidiParagraph::FirstStrongIsRtl(L"\x2066" + L"abc\x2069 \x05E9")); +} + +TEST(CFXBidiParagraphTest, VisualOrder) { + EXPECT_EQ((std::vector{0, 2, 1, 3}), + CFX_BidiParagraph::VisualOrder(Levels{0, 1, 1, 0})); + // Higher levels reverse first, then the whole odd stretch. + EXPECT_EQ((std::vector{3, 1, 2, 0}), + CFX_BidiParagraph::VisualOrder(Levels{1, 2, 2, 1})); + EXPECT_EQ((std::vector{0, 1, 2}), + CFX_BidiParagraph::VisualOrder(Levels{0, 0, 0})); + EXPECT_TRUE(CFX_BidiParagraph::VisualOrder(Levels{}).empty()); +} diff --git a/core/fxge/BUILD.gn b/core/fxge/BUILD.gn index d0b9e2029f..4e139163dd 100644 --- a/core/fxge/BUILD.gn +++ b/core/fxge/BUILD.gn @@ -52,14 +52,28 @@ source_set("fxge") { "cfx_glyphbitmap.h", "cfx_glyphcache.cpp", "cfx_glyphcache.h", + # EmbedPDF(fonts): the code points a font maps, as ranges. + "cfx_codepointrange.cpp", + "cfx_codepointrange.h", + # EmbedPDF(fonts): a glyph's outline in font units, unscaled and unhinted. + "cfx_glyphoutline.cpp", + "cfx_glyphoutline.h", "cfx_graphstate.cpp", "cfx_graphstate.h", "cfx_graphstatedata.cpp", "cfx_graphstatedata.h", + # EmbedPDF(fonts): an OpenType font written from glyph outlines, the + # display program of a document font a browser can't load as it is. + "cfx_opentypewriter.cpp", + "cfx_opentypewriter.h", "cfx_path.cpp", "cfx_path.h", "cfx_renderdevice.cpp", "cfx_renderdevice.h", + # EmbedPDF(fonts): the sfnt container: writing one, and its tables + # realigned for a browser. + "cfx_sfnt.cpp", + "cfx_sfnt.h", "cfx_standardfont.cpp", "cfx_standardfont.h", "cfx_substfont.cpp", @@ -295,6 +309,7 @@ source_set("test_support") { pdfium_unittest_source_set("unittests") { sources = [ + "cfx_codepointrange_unittest.cpp", "cfx_color_unittest.cpp", "cfx_face_unittest.cpp", "cfx_folderfontinfo_unittest.cpp", diff --git a/core/fxge/cfx_codepointrange.cpp b/core/fxge/cfx_codepointrange.cpp new file mode 100644 index 0000000000..e0d1ea9546 --- /dev/null +++ b/core/fxge/cfx_codepointrange.cpp @@ -0,0 +1,64 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fxge/cfx_codepointrange.h" + +#include + +#include "core/fxge/cfx_face.h" + +#if defined(PDF_ENABLE_FREETYPE) +#include "core/fxge/freetype/fx_freetype.h" +#endif + +#if defined(PDF_ENABLE_FONTATIONS) +#include "core/fxge/cfx_gemodule.h" +#endif + +bool CFX_CodePointRangesContain(pdfium::span ranges, + uint32_t code_point) { + // The last range starting at or below |code_point|. + auto after = std::ranges::upper_bound(ranges, code_point, {}, + &CFX_CodePointRange::first); + if (after == ranges.begin()) { + return false; + } + const CFX_CodePointRange& range = *(after - 1); + return code_point - range.first < range.count; +} + +void CFX_AppendCodePoint(std::vector* ranges, + uint32_t code_point) { + if (!ranges->empty() && + ranges->back().first + ranges->back().count == code_point) { + ++ranges->back().count; + return; + } + ranges->push_back({code_point, 1}); +} + +std::vector CFX_Face::GetMappedCodePointRanges( + char32_t max_char) { + std::vector ranges; +#if defined(PDF_ENABLE_FONTATIONS) + if (CFX_GEModule::IsFontations()) { + for (const CharCodeAndIndex& item : GetCharCodesAndIndices(max_char)) { + if (item.glyph_index != 0) { + CFX_AppendCodePoint(&ranges, item.char_code); + } + } + return ranges; + } +#endif // defined(PDF_ENABLE_FONTATIONS) +#if defined(PDF_ENABLE_FREETYPE) + // Walked one code point at a time, never collected: a font that maps all + // of Unicode is a million of them. + FT_UInt glyph_index = 0; + FT_ULong code_point = FT_Get_First_Char(GetRec(), &glyph_index); + while (glyph_index != 0 && code_point <= max_char) { + CFX_AppendCodePoint(&ranges, static_cast(code_point)); + code_point = FT_Get_Next_Char(GetRec(), code_point, &glyph_index); + } +#endif // defined(PDF_ENABLE_FREETYPE) + return ranges; +} diff --git a/core/fxge/cfx_codepointrange.h b/core/fxge/cfx_codepointrange.h new file mode 100644 index 0000000000..d5cfdcf8da --- /dev/null +++ b/core/fxge/cfx_codepointrange.h @@ -0,0 +1,29 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 +// +// EmbedPDF(fonts): the characters a font maps, as runs of consecutive code +// points. A font that maps every code point is a handful of runs. + +#ifndef CORE_FXGE_CFX_CODEPOINTRANGE_H_ +#define CORE_FXGE_CFX_CODEPOINTRANGE_H_ + +#include + +#include + +#include "core/fxcrt/span.h" + +struct CFX_CodePointRange { + uint32_t first = 0; + uint32_t count = 0; +}; + +// Whether |ranges|, in order and apart, hold |code_point|. +bool CFX_CodePointRangesContain(pdfium::span ranges, + uint32_t code_point); + +// Adds |code_point|, which is above every code point |ranges| holds. +void CFX_AppendCodePoint(std::vector* ranges, + uint32_t code_point); + +#endif // CORE_FXGE_CFX_CODEPOINTRANGE_H_ diff --git a/core/fxge/cfx_codepointrange_unittest.cpp b/core/fxge/cfx_codepointrange_unittest.cpp new file mode 100644 index 0000000000..f9d4024db0 --- /dev/null +++ b/core/fxge/cfx_codepointrange_unittest.cpp @@ -0,0 +1,34 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fxge/cfx_codepointrange.h" + +#include + +#include "testing/gtest/include/gtest/gtest.h" + +TEST(CFXCodePointRangeTest, AppendJoinsNeighbours) { + std::vector ranges; + for (uint32_t code_point : {0x20u, 0x21u, 0x22u, 0x30u, 0x10FFFFu}) { + CFX_AppendCodePoint(&ranges, code_point); + } + ASSERT_EQ(3u, ranges.size()); + EXPECT_EQ(0x20u, ranges[0].first); + EXPECT_EQ(3u, ranges[0].count); + EXPECT_EQ(0x30u, ranges[1].first); + EXPECT_EQ(1u, ranges[1].count); + EXPECT_EQ(0x10FFFFu, ranges[2].first); + EXPECT_EQ(1u, ranges[2].count); +} + +TEST(CFXCodePointRangeTest, ContainEdges) { + const std::vector ranges = {{0x20, 3}, {0x30, 1}}; + EXPECT_FALSE(CFX_CodePointRangesContain(ranges, 0x1F)); + EXPECT_TRUE(CFX_CodePointRangesContain(ranges, 0x20)); + EXPECT_TRUE(CFX_CodePointRangesContain(ranges, 0x22)); + EXPECT_FALSE(CFX_CodePointRangesContain(ranges, 0x23)); + EXPECT_FALSE(CFX_CodePointRangesContain(ranges, 0x2F)); + EXPECT_TRUE(CFX_CodePointRangesContain(ranges, 0x30)); + EXPECT_FALSE(CFX_CodePointRangesContain(ranges, 0x31)); + EXPECT_FALSE(CFX_CodePointRangesContain({}, 0x20)); +} diff --git a/core/fxge/cfx_face.h b/core/fxge/cfx_face.h index 223b2cc480..d82bc50d95 100644 --- a/core/fxge/cfx_face.h +++ b/core/fxge/cfx_face.h @@ -19,6 +19,8 @@ #include "core/fxcrt/observed_ptr.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/span.h" +#include "core/fxge/cfx_codepointrange.h" // EmbedPDF(fonts) +#include "core/fxge/cfx_glyphoutline.h" // EmbedPDF(fonts) #include "core/fxge/fx_font.h" #include "core/fxge/fx_fontencoding.h" @@ -125,6 +127,11 @@ class CFX_Face final : public Retainable, public Observable { int dest_width, bool is_vertical, const CFX_SubstFont* subst_font); + // EmbedPDF(fonts): |glyph_index| in font units, unscaled and unhinted, so + // another program can carry it exactly (cfx_glyphoutline.cpp). nullopt + // when the glyph doesn't load or isn't an outline. + std::optional LoadUnscaledGlyphOutline( + uint32_t glyph_index); int GetGlyphTTWidth(uint32_t glyph_index) const; int GetGlyphWidth(uint32_t glyph_index, int dest_width, @@ -146,6 +153,9 @@ class CFX_Face final : public Retainable, public Observable { FX_RECT GetCharBBox(uint32_t code, int glyph_index); std::vector GetCharCodesAndIndices(char32_t max_char); + // EmbedPDF(fonts): the code points up to |max_char| the current charmap + // maps to a glyph, as ranges (cfx_codepointrange.cpp). + std::vector GetMappedCodePointRanges(char32_t max_char); CharMap GetCurrentCharMap() const; std::optional GetCurrentCharMapEncoding() const; diff --git a/core/fxge/cfx_fontregistry.cpp b/core/fxge/cfx_fontregistry.cpp index 8d2cde7773..820364df1b 100644 --- a/core/fxge/cfx_fontregistry.cpp +++ b/core/fxge/cfx_fontregistry.cpp @@ -13,7 +13,6 @@ #include #include "core/fdrm/fx_crypt_sha.h" -#include "core/fxcrt/containers/contains.h" #include "core/fxcrt/data_vector.h" #include "core/fxcrt/epdf_tls.h" #include "core/fxcrt/fx_codepage.h" @@ -35,7 +34,7 @@ struct RegisteredFont { ByteString family_name; // as registered, spaces kept int weight = pdfium::kFontWeightNormal; bool italic = false; - std::vector supported_unicodes; + std::vector coverage; DataVector memory_data; RetainPtr stream; CFX_FontRegistry::EmbeddingPermission permission = @@ -131,10 +130,16 @@ Instancing InstanceVariableFont(pdfium::span data, return outcome; } +struct Fallback { + CFX_FontRegistry::FontId id; + int tier; +}; + struct RegistryState { CFX_FontRegistry::FontId next_font_id = 1; std::vector> fonts; - std::vector fallback_order; + std::vector fallbacks; // in the order they were added + CFX_FontRegistry::FontId last_resort = CFX_FontRegistry::kInvalidFontId; }; EPDF_TLS RegistryState* g_registry = nullptr; @@ -213,26 +218,12 @@ std::unique_ptr LoadFont(pdfium::span data) { return font; } -std::vector CollectSupportedUnicodes(CFX_Font* font) { - if (!font) { +std::vector CollectCoverage(CFX_Font* font) { + RetainPtr face = font ? font->GetFace() : nullptr; + if (!face) { return {}; } - - auto char_codes_and_indices = - font->GetCharCodesAndIndices(pdfium::kMaximumSupplementaryCodePoint); - std::vector supported_unicodes; - supported_unicodes.reserve(char_codes_and_indices.size()); - for (const auto& item : char_codes_and_indices) { - if (item.glyph_index != 0) { - supported_unicodes.push_back(item.char_code); - } - } - - std::ranges::sort(supported_unicodes); - supported_unicodes.erase( - std::unique(supported_unicodes.begin(), supported_unicodes.end()), - supported_unicodes.end()); - return supported_unicodes; + return face->GetMappedCodePointRanges(pdfium::kMaximumSupplementaryCodePoint); } CFX_FontRegistry::FontId RegisterLoadedFontSource( @@ -297,9 +288,8 @@ CFX_FontRegistry::FontId RegisterLoadedFontSource( } } - std::vector supported_unicodes = - CollectSupportedUnicodes(font.get()); - if (supported_unicodes.empty()) { + std::vector coverage = CollectCoverage(font.get()); + if (coverage.empty()) { return CFX_FontRegistry::kInvalidFontId; } @@ -327,7 +317,7 @@ CFX_FontRegistry::FontId RegisterLoadedFontSource( registered_font->family_name.Trim(' '); registered_font->weight = NormalizeWeight(weight, *font); registered_font->italic = NormalizeItalic(italic, *font); - registered_font->supported_unicodes = std::move(supported_unicodes); + registered_font->coverage = std::move(coverage); registered_font->memory_data = std::move(memory_data); registered_font->stream = std::move(stream); @@ -402,7 +392,8 @@ void CFX_FontRegistry::ClearRegisteredFonts() { if (!g_registry) { return; } - g_registry->fallback_order.clear(); + g_registry->fallbacks.clear(); + g_registry->last_resort = kInvalidFontId; g_registry->fonts.clear(); // EmbedPDF: do not reset next_font_id. Documents can keep registered-font // marker resources after ClearRegisteredFonts(); reusing ids could make an @@ -411,16 +402,19 @@ void CFX_FontRegistry::ClearRegisteredFonts() { } // static -bool CFX_FontRegistry::AddFallbackFont(FontId font_id) { +bool CFX_FontRegistry::AddFallbackFont(FontId font_id, int tier) { if (!IsValidFont(font_id)) { return false; } RegistryState* registry = GetRegistry(); - if (pdfium::Contains(registry->fallback_order, font_id)) { + auto existing = + std::ranges::find(registry->fallbacks, font_id, &Fallback::id); + if (existing != registry->fallbacks.end()) { + existing->tier = tier; return true; } - registry->fallback_order.push_back(font_id); + registry->fallbacks.push_back({font_id, tier}); return true; } @@ -429,12 +423,26 @@ void CFX_FontRegistry::ClearFallbackFonts() { if (!g_registry) { return; } - g_registry->fallback_order.clear(); + g_registry->fallbacks.clear(); } // static bool CFX_FontRegistry::HasFallbackFonts() { - return g_registry && !g_registry->fallback_order.empty(); + return g_registry && !g_registry->fallbacks.empty(); +} + +// static +bool CFX_FontRegistry::SetLastResortFont(FontId font_id) { + if (font_id != kInvalidFontId && !IsValidFont(font_id)) { + return false; + } + GetRegistry()->last_resort = font_id; + return true; +} + +// static +CFX_FontRegistry::FontId CFX_FontRegistry::GetLastResortFont() { + return g_registry ? g_registry->last_resort : kInvalidFontId; } // static @@ -481,6 +489,44 @@ pdfium::span CFX_FontRegistry::GetSourceHash(FontId font_id) { : pdfium::span(); } +// static +DataVector CFX_FontRegistry::GetProgramData(FontId font_id) { + RegisteredFont* font = GetRegisteredFont(font_id); + if (!font) { + return {}; + } + if (!font->memory_data.empty()) { + return font->memory_data; + } + return ReadStreamToData(font->stream.Get()); +} + +// static +CFX_FontRegistry::ProgramFormat CFX_FontRegistry::GetProgramFormat( + FontId font_id) { + RegisteredFont* font = GetRegisteredFont(font_id); + if (!font) { + return ProgramFormat::kUnsupported; + } + if (!font->memory_data.empty()) { + return DetectProgramFormat(font->memory_data); + } + uint8_t head[4] = {}; + if (!font->stream || + !font->stream->ReadBlockAtOffset(pdfium::span(head), /*offset=*/0)) { + return ProgramFormat::kUnsupported; + } + return DetectProgramFormat(head); +} + +// static +pdfium::span CFX_FontRegistry::GetCoverage( + FontId font_id) { + RegisteredFont* font = GetRegisteredFont(font_id); + return font ? pdfium::span(font->coverage) + : pdfium::span(); +} + // static bool CFX_FontRegistry::IsInstanced(FontId font_id) { RegisteredFont* font = GetRegisteredFont(font_id); @@ -561,7 +607,7 @@ bool CFX_FontRegistry::SupportsUnicode(FontId font_id, uint32_t unicode) { if (!font) { return false; } - return std::ranges::binary_search(font->supported_unicodes, unicode); + return CFX_CodePointRangesContain(font->coverage, unicode); } // static @@ -570,28 +616,45 @@ std::optional CFX_FontRegistry::FindFallbackFont( int weight, bool italic, bool for_authoring) { - if (!g_registry) { - return std::nullopt; - } - - std::optional best_font_id; - int best_score = std::numeric_limits::max(); - for (FontId font_id : g_registry->fallback_order) { - RegisteredFont* font = GetRegisteredFont(font_id); - if (!font || !SupportsUnicode(font_id, unicode)) { - continue; - } - if (for_authoring && !font->editing_authorized) { - continue; // preview-and-print: may render, may not write new text + for (FontId font_id : GetFallbackOrder(weight, italic, for_authoring)) { + if (SupportsUnicode(font_id, unicode)) { + return font_id; } + } + return std::nullopt; +} - const int score = StyleScore(*font, weight, italic); - if (!best_font_id.has_value() || score < best_score) { - best_font_id = font_id; - best_score = score; +// static +std::vector CFX_FontRegistry::GetFallbackOrder( + int weight, + bool italic, + bool for_authoring) { + struct Ranked { + FontId id; + int tier; + int score; + }; + std::vector ranked; + if (g_registry) { + for (const Fallback& fallback : g_registry->fallbacks) { + RegisteredFont* font = GetRegisteredFont(fallback.id); + if (!font || (for_authoring && !font->editing_authorized)) { + continue; // preview-and-print: may render, may not write new text + } + ranked.push_back( + {fallback.id, fallback.tier, StyleScore(*font, weight, italic)}); } } - return best_font_id; + // Stable: among equal tiers and scores the font added first stays first. + std::ranges::stable_sort(ranked, [](const Ranked& a, const Ranked& b) { + return a.tier != b.tier ? a.tier < b.tier : a.score < b.score; + }); + std::vector order; + order.reserve(ranked.size()); + for (const Ranked& entry : ranked) { + order.push_back(entry.id); + } + return order; } // static diff --git a/core/fxge/cfx_fontregistry.h b/core/fxge/cfx_fontregistry.h index 0a6d1a3ed5..31e437b2f2 100644 --- a/core/fxge/cfx_fontregistry.h +++ b/core/fxge/cfx_fontregistry.h @@ -11,10 +11,13 @@ #include #include +#include #include "core/fxcrt/bytestring.h" +#include "core/fxcrt/data_vector.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/span.h" +#include "core/fxge/cfx_codepointrange.h" class CFX_Font; class IFX_SeekableReadStream; @@ -55,10 +58,19 @@ class CFX_FontRegistry { RetainPtr stream); static void ClearRegisteredFonts(); - static bool AddFallbackFont(FontId font_id); + // Adds a registered font to the fallback fonts, in |tier|: lower tiers are + // tried first. Adding a font that is already one changes its tier only. + static bool AddFallbackFont(FontId font_id, int tier); static void ClearFallbackFonts(); static bool HasFallbackFonts(); + // The font new text draws a character with when neither its font nor any + // fallback has it: one box for every code point, so the character shows as + // missing and the text keeps it. kInvalidFontId clears it; false for an + // unknown id. Unlike the fallbacks, it never draws a page's own text. + static bool SetLastResortFont(FontId font_id); + static FontId GetLastResortFont(); + // EmbedPDF: permissions (§3.4 of the rich text plan). static std::optional GetEmbeddingPermission( FontId font_id); @@ -72,6 +84,16 @@ class CFX_FontRegistry { // SHA-256 of the registered (instanced) bytes: the source identity a font // pool shares programs by. Empty for an unknown id. static pdfium::span GetSourceHash(FontId font_id); + // The registered program's bytes: exactly what the writer embeds and + // GetSourceHash() names (the static instance for a variable font). Empty + // for an unknown id. + static DataVector GetProgramData(FontId font_id); + // What the registered program is (registration accepts kTrueType and + // kOpenTypeCFF only); kUnsupported for an unknown id. + static ProgramFormat GetProgramFormat(FontId font_id); + // The code points the program maps to a glyph, as ranges in order. Empty + // for an unknown id. + static pdfium::span GetCoverage(FontId font_id); // True when the registered bytes are a static instance made from a // variable font at registration. A variable font that cannot be instanced // is refused, so every registered program is static. @@ -100,6 +122,13 @@ class CFX_FontRegistry { int weight, bool italic, bool for_authoring = false); + // The fallback fonts in the order FindFallbackFont() tries them for a + // style: tier by tier, lowest first; within a tier the closest weight and + // italic first, then the order they were added in. The first of them that + // maps a scalar is the one it returns. + static std::vector GetFallbackOrder(int weight, + bool italic, + bool for_authoring); static std::unique_ptr CreateFont(FontId font_id); static void DestroyGlobals(); diff --git a/core/fxge/cfx_glyphoutline.cpp b/core/fxge/cfx_glyphoutline.cpp new file mode 100644 index 0000000000..7b3b3697ad --- /dev/null +++ b/core/fxge/cfx_glyphoutline.cpp @@ -0,0 +1,106 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fxge/cfx_glyphoutline.h" + +#include + +#include "core/fxge/cfx_face.h" + +#if defined(PDF_ENABLE_FREETYPE) +#include "core/fxge/freetype/fx_freetype.h" +#endif + +#if defined(PDF_ENABLE_FREETYPE) +namespace { + +struct Decomposition { + CFX_GlyphOutline* outline; + CFX_PointF current; +}; + +CFX_PointF PointOf(const FT_Vector* vector) { + return CFX_PointF(static_cast(vector->x), + static_cast(vector->y)); +} + +int MoveTo(const FT_Vector* to, void* user) { + auto* decomposition = static_cast(user); + decomposition->current = PointOf(to); + decomposition->outline->segments.push_back( + {CFX_GlyphOutline::Op::kMove, {decomposition->current, {}, {}}}); + return 0; +} + +int LineTo(const FT_Vector* to, void* user) { + auto* decomposition = static_cast(user); + decomposition->current = PointOf(to); + decomposition->outline->segments.push_back( + {CFX_GlyphOutline::Op::kLine, {decomposition->current, {}, {}}}); + return 0; +} + +// A quadratic Bézier is the cubic whose control points are two thirds of the +// way from each end to its one control point. +int ConicTo(const FT_Vector* control, const FT_Vector* to, void* user) { + auto* decomposition = static_cast(user); + const CFX_PointF from = decomposition->current; + const CFX_PointF via = PointOf(control); + const CFX_PointF end = PointOf(to); + const CFX_PointF first(from.x + (via.x - from.x) * 2 / 3, + from.y + (via.y - from.y) * 2 / 3); + const CFX_PointF second(end.x + (via.x - end.x) * 2 / 3, + end.y + (via.y - end.y) * 2 / 3); + decomposition->current = end; + decomposition->outline->segments.push_back( + {CFX_GlyphOutline::Op::kCubic, {first, second, end}}); + return 0; +} + +int CubicTo(const FT_Vector* first, + const FT_Vector* second, + const FT_Vector* to, + void* user) { + auto* decomposition = static_cast(user); + decomposition->current = PointOf(to); + decomposition->outline->segments.push_back( + {CFX_GlyphOutline::Op::kCubic, + {PointOf(first), PointOf(second), decomposition->current}}); + return 0; +} + +} // namespace +#endif // defined(PDF_ENABLE_FREETYPE) + +std::optional CFX_Face::LoadUnscaledGlyphOutline( + uint32_t glyph_index) { +#if defined(PDF_ENABLE_FREETYPE) + FT_FaceRec* rec = GetRec(); + if (!rec) { + return std::nullopt; + } + if (FT_Load_Glyph( + rec, glyph_index, + FT_LOAD_NO_SCALE | FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP)) { + return std::nullopt; + } + if (rec->glyph->format != FT_GLYPH_FORMAT_OUTLINE) { + return std::nullopt; + } + CFX_GlyphOutline outline; + Decomposition decomposition{&outline, CFX_PointF()}; + FT_Outline_Funcs funcs; + funcs.move_to = MoveTo; + funcs.line_to = LineTo; + funcs.conic_to = ConicTo; + funcs.cubic_to = CubicTo; + funcs.shift = 0; + funcs.delta = 0; + if (FT_Outline_Decompose(&rec->glyph->outline, &funcs, &decomposition)) { + return std::nullopt; + } + return outline; +#else + return std::nullopt; +#endif // defined(PDF_ENABLE_FREETYPE) +} diff --git a/core/fxge/cfx_glyphoutline.h b/core/fxge/cfx_glyphoutline.h new file mode 100644 index 0000000000..9738dfb020 --- /dev/null +++ b/core/fxge/cfx_glyphoutline.h @@ -0,0 +1,33 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 +// +// EmbedPDF(fonts): a glyph's outline in font units, as the font program +// draws it: unscaled and unhinted, so another program can carry it exactly. + +#ifndef CORE_FXGE_CFX_GLYPHOUTLINE_H_ +#define CORE_FXGE_CFX_GLYPHOUTLINE_H_ + +#include + +#include +#include + +#include "core/fxcrt/fx_coordinates.h" + +struct CFX_GlyphOutline { + enum class Op : uint8_t { + kMove, // starts a contour at points[0] + kLine, // to points[0] + kCubic, // through points[0] and points[1] to points[2] + }; + struct Segment { + Op op; + std::array points; + }; + + // Contours close where they started. Empty for a glyph that draws nothing + // (a space). A quadratic curve is the cubic it is. + std::vector segments; +}; + +#endif // CORE_FXGE_CFX_GLYPHOUTLINE_H_ diff --git a/core/fxge/cfx_opentypewriter.cpp b/core/fxge/cfx_opentypewriter.cpp new file mode 100644 index 0000000000..c4ebf36eb3 --- /dev/null +++ b/core/fxge/cfx_opentypewriter.cpp @@ -0,0 +1,612 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fxge/cfx_opentypewriter.h" + +#include + +#include +#include +#include +#include + +#include "core/fxcrt/numerics/safe_conversions.h" +#include "core/fxge/cfx_sfnt.h" + +CFX_OpenTypeFont::CFX_OpenTypeFont() = default; +CFX_OpenTypeFont::CFX_OpenTypeFont(CFX_OpenTypeFont&&) noexcept = default; +CFX_OpenTypeFont& CFX_OpenTypeFont::operator=(CFX_OpenTypeFont&&) noexcept = + default; +CFX_OpenTypeFont::~CFX_OpenTypeFont() = default; + +namespace { + +// Big-endian bytes, as every OpenType table and CFF structure is. +class Writer { + public: + // Each writes the low bytes of |value|: the caller has checked it fits. + Writer& U8(uint64_t value) { + bytes_.push_back(static_cast(value)); + return *this; + } + Writer& U16(uint64_t value) { return U8(value >> 8).U8(value); } + Writer& I16(int value) { return U16(static_cast(value)); } + Writer& U32(uint64_t value) { return U16(value >> 16).U16(value); } + // A 16.16 fixed-point number. + Writer& Fixed(double value) { + return U32(static_cast( + static_cast(std::lround(value * 65536.0)))); + } + Writer& Tag(const char* tag) { + for (int i = 0; i < 4; ++i) { + U8(static_cast(tag[i])); + } + return *this; + } + Writer& Zeros(size_t count) { + bytes_.insert(bytes_.end(), count, 0); + return *this; + } + Writer& Bytes(const DataVector& bytes) { + bytes_.insert(bytes_.end(), bytes.begin(), bytes.end()); + return *this; + } + size_t size() const { return bytes_.size(); } + DataVector Take() { return std::move(bytes_); } + + private: + DataVector bytes_; +}; + +// ---- Bounds ---------------------------------------------------------------- + +struct Bounds { + float x_min = std::numeric_limits::max(); + float y_min = std::numeric_limits::max(); + float x_max = std::numeric_limits::lowest(); + float y_max = std::numeric_limits::lowest(); + + bool empty() const { return x_min > x_max; } + void Add(const CFX_PointF& point) { + x_min = std::min(x_min, point.x); + y_min = std::min(y_min, point.y); + x_max = std::max(x_max, point.x); + y_max = std::max(y_max, point.y); + } + void Add(const Bounds& other) { + if (!other.empty()) { + Add(CFX_PointF(other.x_min, other.y_min)); + Add(CFX_PointF(other.x_max, other.y_max)); + } + } +}; + +// The bounds of every point the outline names, control points included: the +// glyph lies inside them. +Bounds BoundsOf(const CFX_GlyphOutline& outline) { + Bounds bounds; + for (const CFX_GlyphOutline::Segment& segment : outline.segments) { + const size_t points = segment.op == CFX_GlyphOutline::Op::kCubic ? 3 : 1; + for (size_t i = 0; i < points; ++i) { + bounds.Add(segment.points[i]); + } + } + return bounds; +} + +int Floor(float value) { + return pdfium::saturated_cast(std::floor(value)); +} + +int Ceil(float value) { + return pdfium::saturated_cast(std::ceil(value)); +} + +// ---- CFF ------------------------------------------------------------------- + +// A Type 2 charstring operand: an integer in its shortest form, anything +// else as 16.16 fixed point. +void CharstringNumber(DataVector* out, float value) { + const float rounded = std::round(value); + if (std::fabs(value - rounded) < 1.0f / 65536 && std::fabs(rounded) < 32768) { + const int number = static_cast(rounded); + if (number >= -107 && number <= 107) { + out->push_back(static_cast(number + 139)); + } else if (number >= 108 && number <= 1131) { + const int offset = number - 108; + out->push_back(static_cast((offset >> 8) + 247)); + out->push_back(static_cast(offset & 0xff)); + } else if (number >= -1131 && number <= -108) { + const int offset = -number - 108; + out->push_back(static_cast((offset >> 8) + 251)); + out->push_back(static_cast(offset & 0xff)); + } else { + out->push_back(28); + out->push_back(static_cast((number >> 8) & 0xff)); + out->push_back(static_cast(number & 0xff)); + } + return; + } + const uint32_t fixed = static_cast( + static_cast(std::lround(static_cast(value) * 65536.0))); + out->push_back(255); + out->push_back(static_cast(fixed >> 24)); + out->push_back(static_cast(fixed >> 16)); + out->push_back(static_cast(fixed >> 8)); + out->push_back(static_cast(fixed)); +} + +// What a charstring operand encodes as: the value a reader gets back. +float CharstringValue(float value) { + const float rounded = std::round(value); + if (std::fabs(value - rounded) < 1.0f / 65536 && std::fabs(rounded) < 32768) { + return rounded; + } + return static_cast(std::lround(static_cast(value) * 65536.0) / + 65536.0); +} + +// The glyph as Type 2 operators: rmoveto, rlineto and rrcurveto from the +// pen's position, then endchar. Each contour closes at the next rmoveto or +// at endchar. No width: the advance is hmtx's. +DataVector Charstring(const CFX_GlyphOutline& outline) { + constexpr uint8_t kRLineTo = 5; + constexpr uint8_t kRRCurveTo = 8; + constexpr uint8_t kEndChar = 14; + constexpr uint8_t kRMoveTo = 21; + DataVector out; + // The pen as a reader will have it, so rounding never accumulates. + CFX_PointF pen; + auto relative = [&](const CFX_PointF& point) { + const float dx = CharstringValue(point.x - pen.x); + const float dy = CharstringValue(point.y - pen.y); + CharstringNumber(&out, dx); + CharstringNumber(&out, dy); + pen = CFX_PointF(pen.x + dx, pen.y + dy); + }; + for (const CFX_GlyphOutline::Segment& segment : outline.segments) { + switch (segment.op) { + case CFX_GlyphOutline::Op::kMove: + relative(segment.points[0]); + out.push_back(kRMoveTo); + break; + case CFX_GlyphOutline::Op::kLine: + relative(segment.points[0]); + out.push_back(kRLineTo); + break; + case CFX_GlyphOutline::Op::kCubic: + relative(segment.points[0]); + relative(segment.points[1]); + relative(segment.points[2]); + out.push_back(kRRCurveTo); + break; + } + } + out.push_back(kEndChar); + return out; +} + +// A CFF DICT integer operand in its shortest form. +void DictInteger(Writer* out, int number) { + if (number >= -107 && number <= 107) { + out->U8(number + 139); + } else if (number >= 108 && number <= 1131) { + out->U8(((number - 108) >> 8) + 247).U8((number - 108) & 0xff); + } else if (number >= -1131 && number <= -108) { + out->U8(((-number - 108) >> 8) + 251).U8((-number - 108) & 0xff); + } else if (number >= -32768 && number <= 32767) { + out->U8(28).I16(number); + } else { + out->U8(29).U32(static_cast(number)); + } +} + +// An offset operand, always five bytes, so a DICT's size doesn't depend on +// the offsets it holds. +void DictOffset(Writer* out, uint32_t offset) { + out->U8(29).U32(offset); +} + +// A CFF DICT real operand: its decimal digits as nibbles. +void DictReal(Writer* out, double value) { + char text[32]; + snprintf(text, sizeof(text), "%.10g", value); + std::vector nibbles; + for (const char* c = text; *c; ++c) { + if (*c >= '0' && *c <= '9') { + nibbles.push_back(static_cast(*c - '0')); + } else if (*c == '.') { + nibbles.push_back(0xa); + } else if (*c == '-') { + nibbles.push_back(0xe); + } else if (*c == 'e' || *c == 'E') { + if (c[1] == '-') { + nibbles.push_back(0xc); + ++c; + } else { + nibbles.push_back(0xb); + if (c[1] == '+') { + ++c; + } + } + } + } + nibbles.push_back(0xf); + if (nibbles.size() % 2) { + nibbles.push_back(0xf); + } + out->U8(30); + for (size_t i = 0; i < nibbles.size(); i += 2) { + out->U8((nibbles[i] << 4) | nibbles[i + 1]); + } +} + +// A CFF INDEX of |items|. +void Index(Writer* out, const std::vector>& items) { + out->U16(pdfium::checked_cast(items.size())); + if (items.empty()) { + return; + } + size_t total = 1; + for (const DataVector& item : items) { + total += item.size(); + } + const uint8_t offset_size = total <= 0xff ? 1 + : total <= 0xffff ? 2 + : total <= 0xffffff ? 3 + : 4; + out->U8(offset_size); + auto offset = [&](size_t value) { + for (int shift = (offset_size - 1) * 8; shift >= 0; shift -= 8) { + out->U8(static_cast(value >> shift)); + } + }; + size_t at = 1; + offset(at); + for (const DataVector& item : items) { + at += item.size(); + offset(at); + } + for (const DataVector& item : items) { + out->Bytes(item); + } +} + +DataVector BytesOf(const ByteString& text) { + return DataVector(text.unsigned_span().begin(), + text.unsigned_span().end()); +} + +// The first string ID after CFF's standard strings. +constexpr int kFirstCustomString = 391; + +// A name-keyed CFF font: glyph names "g1", "g2", … (the cmap names the +// characters), no hints, no subroutines. +DataVector CffTable(const CFX_OpenTypeFont& font, + const Bounds& font_bounds) { + const size_t glyph_count = font.glyphs.size(); + std::vector> names = {BytesOf(font.postscript_name)}; + std::vector> strings; + for (size_t glyph = 1; glyph < glyph_count; ++glyph) { + strings.push_back(BytesOf(ByteString::Format("g%zu", glyph))); + } + std::vector> charstrings; + for (const CFX_OpenTypeFont::Glyph& glyph : font.glyphs) { + charstrings.push_back(Charstring(glyph.outline)); + } + + Writer charset; + charset.U8(0); + for (size_t glyph = 1; glyph < glyph_count; ++glyph) { + charset.U16(pdfium::checked_cast(kFirstCustomString + glyph - 1)); + } + Writer private_dict; + DictInteger(&private_dict, 0); + private_dict.U8(20); // defaultWidthX + DictInteger(&private_dict, 0); + private_dict.U8(21); // nominalWidthX + + // The top DICT, with the offsets of what follows it. + auto top_dict = [&](uint32_t charset_offset, uint32_t charstrings_offset, + uint32_t private_offset) { + Writer dict; + DictInteger(&dict, font_bounds.empty() ? 0 : Floor(font_bounds.x_min)); + DictInteger(&dict, font_bounds.empty() ? 0 : Floor(font_bounds.y_min)); + DictInteger(&dict, font_bounds.empty() ? 0 : Ceil(font_bounds.x_max)); + DictInteger(&dict, font_bounds.empty() ? 0 : Ceil(font_bounds.y_max)); + dict.U8(5); // FontBBox + if (font.units_per_em != 1000) { + const double scale = 1.0 / font.units_per_em; + for (double value : {scale, 0.0, 0.0, scale, 0.0, 0.0}) { + DictReal(&dict, value); + } + dict.U8(12).U8(7); // FontMatrix + } + DictOffset(&dict, charset_offset); + dict.U8(15); // charset + DictOffset(&dict, charstrings_offset); + dict.U8(17); // CharStrings + DictInteger(&dict, pdfium::checked_cast(private_dict.size())); + DictOffset(&dict, private_offset); + dict.U8(18); // Private + return dict.Take(); + }; + + // Lay it out once to learn where each part starts, then write it. + auto layout = [&](uint32_t charset_offset, uint32_t charstrings_offset, + uint32_t private_offset, Writer* out) { + out->U8(1).U8(0).U8(4).U8(4); // header: version 1.0, offsets of 4 bytes + Index(out, names); + Index(out, {top_dict(charset_offset, charstrings_offset, private_offset)}); + Index(out, strings); + Index(out, {}); // global subroutines + }; + Writer probe; + layout(0, 0, 0, &probe); + const uint32_t charset_offset = pdfium::checked_cast(probe.size()); + const uint32_t charstrings_offset = + charset_offset + pdfium::checked_cast(charset.size()); + Writer charstrings_index; + Index(&charstrings_index, charstrings); + const uint32_t private_offset = + charstrings_offset + + pdfium::checked_cast(charstrings_index.size()); + + Writer cff; + layout(charset_offset, charstrings_offset, private_offset, &cff); + cff.Bytes(charset.Take()); + cff.Bytes(charstrings_index.Take()); + cff.Bytes(private_dict.Take()); + return cff.Take(); +} + +// ---- The other tables +// -------------------------------------------------------- + +// cmap: Windows Unicode BMP (format 4), runs of consecutive characters on +// consecutive glyphs as one segment; when that doesn't fit its 16-bit +// lengths, Windows Unicode full (format 12) instead. +DataVector CmapTable( + const std::vector>& cmap) { + struct Run { + uint32_t start; + uint32_t end; + uint16_t glyph; + }; + std::vector runs; + for (const auto& [scalar, glyph] : cmap) { + if (!runs.empty() && runs.back().end + 1 == scalar && + runs.back().glyph + (scalar - runs.back().start) == glyph) { + runs.back().end = scalar; + continue; + } + runs.push_back({scalar, scalar, glyph}); + } + const bool bmp = cmap.empty() || cmap.back().first <= 0xfffe; + const size_t seg_count = runs.size() + 1; + const size_t format4_length = 16 + seg_count * 8; + Writer table; + table.U16(0).U16(1); + if (bmp && format4_length <= 0xffff) { + table.U16(3).U16(1).U32(12); + size_t entry_selector = 0; + while ((size_t{2} << entry_selector) <= seg_count) { + ++entry_selector; + } + const size_t search_range = size_t{2} << entry_selector; + table.U16(4).U16(format4_length).U16(0); + table.U16(seg_count * 2) + .U16(search_range) + .U16(entry_selector) + .U16(seg_count * 2 - search_range); + for (const Run& run : runs) { + table.U16(run.end); + } + table.U16(0xffff).U16(0); + for (const Run& run : runs) { + table.U16(run.start); + } + table.U16(0xffff); + for (const Run& run : runs) { + table.U16((run.glyph - run.start) & 0xffff); + } + table.U16(1); + table.Zeros(seg_count * 2); // idRangeOffset + return table.Take(); + } + table.U16(3).U16(10).U32(12); + table.U16(12).U16(0).U32(16 + runs.size() * 12).U32(0).U32(runs.size()); + for (const Run& run : runs) { + table.U32(run.start).U32(run.end).U32(run.glyph); + } + return table.Take(); +} + +DataVector NameTable( + const std::vector>& records) { + Writer table; + table.U16(0).U16(records.size()).U16(6 + records.size() * 12); + size_t offset = 0; + for (const auto& [name_id, text] : records) { + table.U16(3) + .U16(1) + .U16(0x409) + .U16(name_id) + .U16(text.GetLength() * 2) + .U16(offset); + offset += text.GetLength() * 2; + } + for (const auto& [name_id, text] : records) { + for (char c : text) { + table.U16(static_cast(c)); + } + } + return table.Take(); +} + +ByteString StyleName(const CFX_OpenTypeFont& font) { + const bool bold = font.weight >= 600; + if (bold && font.italic) { + return "Bold Italic"; + } + return bold ? "Bold" : font.italic ? "Italic" : "Regular"; +} + +} // namespace + +DataVector WriteOpenTypeCFF(const CFX_OpenTypeFont& font) { + const size_t glyph_count = font.glyphs.size(); + if (glyph_count == 0 || glyph_count > 0xffff || font.units_per_em < 16) { + return {}; + } + const float em = font.units_per_em / 1000.0f; + auto units = [&](float thousandths) { + return static_cast(std::lround(thousandths * em)); + }; + + Bounds font_bounds; + std::vector glyph_bounds; + int advance_max = 0; + int min_lsb = std::numeric_limits::max(); + int min_rsb = std::numeric_limits::max(); + int x_max_extent = 0; + int advance_sum = 0; + int advance_count = 0; + for (const CFX_OpenTypeFont::Glyph& glyph : font.glyphs) { + glyph_bounds.push_back(BoundsOf(glyph.outline)); + const Bounds& bounds = glyph_bounds.back(); + font_bounds.Add(bounds); + advance_max = std::max(advance_max, glyph.advance); + if (glyph.advance > 0) { + advance_sum += glyph.advance; + ++advance_count; + } + if (!bounds.empty()) { + min_lsb = std::min(min_lsb, Floor(bounds.x_min)); + min_rsb = std::min(min_rsb, glyph.advance - Ceil(bounds.x_max)); + x_max_extent = std::max(x_max_extent, Ceil(bounds.x_max)); + } + } + if (min_lsb == std::numeric_limits::max()) { + min_lsb = 0; + min_rsb = 0; + } + const bool bold = font.weight >= 600; + + std::map> tables; + tables["CFF "] = CffTable(font, font_bounds); + + Writer head; + head.Fixed(1) + .Fixed(1) + .U32(0) // checkSumAdjustment, set once the font is assembled + .U32(0x5f0f3cf5) + .U16(0x0003) + .U16(font.units_per_em) + .Zeros(16) // created and modified: none, so the bytes never change + .I16(font_bounds.empty() ? 0 : Floor(font_bounds.x_min)) + .I16(font_bounds.empty() ? 0 : Floor(font_bounds.y_min)) + .I16(font_bounds.empty() ? 0 : Ceil(font_bounds.x_max)) + .I16(font_bounds.empty() ? 0 : Ceil(font_bounds.y_max)) + .U16((bold ? 1 : 0) | (font.italic ? 2 : 0)) + .U16(8) + .I16(2) + .I16(0) + .I16(0); + tables["head"] = head.Take(); + + Writer hhea; + hhea.Fixed(1) + .I16(font.ascent) + .I16(font.descent) + .I16(0) + .U16(advance_max) + .I16(min_lsb) + .I16(min_rsb) + .I16(x_max_extent) + .I16(1) + .I16(0) + .I16(0) + .Zeros(8) + .I16(0) + .U16(glyph_count); + tables["hhea"] = hhea.Take(); + + Writer hmtx; + for (size_t i = 0; i < glyph_count; ++i) { + hmtx.U16(font.glyphs[i].advance) + .I16(glyph_bounds[i].empty() ? 0 : Floor(glyph_bounds[i].x_min)); + } + tables["hmtx"] = hmtx.Take(); + + Writer maxp; + maxp.U32(0x00005000).U16(glyph_count); + tables["maxp"] = maxp.Take(); + + uint32_t first_char = 0xffff; + uint32_t last_char = 0; + for (const auto& [scalar, glyph] : font.cmap) { + first_char = std::min(first_char, scalar); + last_char = std::max(last_char, scalar); + } + if (font.cmap.empty()) { + first_char = 0; + } + Writer os2; + os2.U16(4) + .I16(advance_count ? advance_sum / advance_count : 0) + .U16(std::clamp(font.weight, 100, 900)) + .U16(5) + .U16(0) // fsType: no restriction; the engine decides who receives it + .I16(units(650)) + .I16(units(600)) + .I16(0) + .I16(units(75)) + .I16(units(650)) + .I16(units(600)) + .I16(0) + .I16(units(350)) + .I16(units(50)) + .I16(units(250)) + .I16(0) + .Zeros(10) // panose + .Zeros(16) // ulUnicodeRange + .Tag("EPDF") + .U16((font.italic ? 1 : 0) | (bold ? 0x20 : 0) | + (!bold && !font.italic ? 0x40 : 0) | 0x80) + .U16(std::min(first_char, 0xffff)) + .U16(std::min(last_char, 0xffff)) + .I16(font.ascent) + .I16(font.descent) + .I16(0) + .U16(std::max(font.ascent, 0)) + .U16(std::max(-font.descent, 0)) + .U32(1) // ulCodePageRange1: Latin 1 + .U32(0) + .I16(0) + .I16(0) + .U16(0) + .U16(32) + .U16(1); + tables["OS/2"] = os2.Take(); + + Writer post; + post.Fixed(3) + .Fixed(font.italic_angle) + .I16(units(-100)) + .I16(units(50)) + .U32(font.fixed_pitch ? 1 : 0) + .Zeros(16); + tables["post"] = post.Take(); + + const ByteString style = StyleName(font); + const ByteString full_name = + style == "Regular" ? font.family : font.family + " " + style; + tables["name"] = NameTable({{1, font.family}, + {2, style}, + {3, font.postscript_name + ";EmbedPDF"}, + {4, full_name}, + {6, font.postscript_name}}); + tables["cmap"] = CmapTable(font.cmap); + return WriteSfnt(0x4f54544f, tables); // 'OTTO' +} diff --git a/core/fxge/cfx_opentypewriter.h b/core/fxge/cfx_opentypewriter.h new file mode 100644 index 0000000000..b0c686f9b4 --- /dev/null +++ b/core/fxge/cfx_opentypewriter.h @@ -0,0 +1,54 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 +// +// EmbedPDF(fonts): an OpenType font with CFF outlines, written from glyph +// outlines, a Unicode cmap and metrics: a font a browser loads. + +#ifndef CORE_FXGE_CFX_OPENTYPEWRITER_H_ +#define CORE_FXGE_CFX_OPENTYPEWRITER_H_ + +#include + +#include +#include + +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/data_vector.h" +#include "core/fxge/cfx_glyphoutline.h" + +struct CFX_OpenTypeFont { + CFX_OpenTypeFont(); + CFX_OpenTypeFont(CFX_OpenTypeFont&&) noexcept; + CFX_OpenTypeFont& operator=(CFX_OpenTypeFont&&) noexcept; + ~CFX_OpenTypeFont(); + + struct Glyph { + CFX_GlyphOutline outline; + // In font units. + uint16_t advance = 0; + }; + + // The names its 'name' table carries; ASCII. + ByteString postscript_name; + ByteString family; + int weight = 400; + bool italic = false; + bool fixed_pitch = false; + // Degrees, counter-clockwise from vertical. + float italic_angle = 0; + uint16_t units_per_em = 1000; + // Line metrics in font units; descent is negative. + int ascent = 0; + int descent = 0; + // Glyph 0 is .notdef. + std::vector glyphs; + // Unicode scalars and the glyph each maps to, in scalar order, each scalar + // once. + std::vector> cmap; +}; + +// The font's bytes: the same bytes for the same font. Empty when it has no +// glyphs or more than an OpenType font can hold. +DataVector WriteOpenTypeCFF(const CFX_OpenTypeFont& font); + +#endif // CORE_FXGE_CFX_OPENTYPEWRITER_H_ diff --git a/core/fxge/cfx_sfnt.cpp b/core/fxge/cfx_sfnt.cpp new file mode 100644 index 0000000000..77ab8fa9f1 --- /dev/null +++ b/core/fxge/cfx_sfnt.cpp @@ -0,0 +1,185 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fxge/cfx_sfnt.h" + +#include +#include + +#include "core/fxcrt/byteorder.h" + +namespace { + +// The versions a TrueType or an OpenType font starts with: 1.0, Apple's +// 'true', and 'OTTO' for CFF outlines. +constexpr uint32_t kTrueTypeVersion = 0x00010000; +constexpr uint32_t kAppleTrueTypeVersion = 0x74727565; // 'true' +constexpr uint32_t kOpenTypeCffVersion = 0x4f54544f; // 'OTTO' + +// What head's checkSumAdjustment makes the whole font's checksum. +constexpr uint32_t kFontChecksum = 0xb1b0afba; +// Where head holds checkSumAdjustment. +constexpr size_t kChecksumAdjustmentOffset = 8; + +constexpr size_t kHeaderSize = 12; +constexpr size_t kTableRecordSize = 16; + +struct TableRecord { + ByteString tag; + size_t offset = 0; + size_t length = 0; +}; + +struct Directory { + uint32_t version = 0; + std::vector tables; +}; + +uint32_t U32At(pdfium::span bytes, size_t offset) { + return fxcrt::GetUInt32MSBFirst(bytes.subspan(offset).first<4u>()); +} + +// |bytes|' table directory; nullopt when it isn't an sfnt or names a table +// that doesn't lie inside it. +std::optional ReadDirectory(pdfium::span bytes) { + if (bytes.size() < kHeaderSize) { + return std::nullopt; + } + Directory directory; + directory.version = U32At(bytes, 0); + if (directory.version != kTrueTypeVersion && + directory.version != kAppleTrueTypeVersion && + directory.version != kOpenTypeCffVersion) { + return std::nullopt; + } + const size_t count = + fxcrt::GetUInt16MSBFirst(bytes.subspan<4u>().first<2u>()); + if (count == 0 || bytes.size() < kHeaderSize + count * kTableRecordSize) { + return std::nullopt; + } + for (size_t i = 0; i < count; ++i) { + const size_t record = kHeaderSize + i * kTableRecordSize; + TableRecord table; + table.tag = ByteString(ByteStringView(bytes.subspan(record, 4u))); + table.offset = U32At(bytes, record + 8); + table.length = U32At(bytes, record + 12); + if (table.offset > bytes.size() || + table.length > bytes.size() - table.offset) { + return std::nullopt; + } + directory.tables.push_back(std::move(table)); + } + return directory; +} + +// Each writes the low bytes of |value|, big-endian: the caller has checked it +// fits. +void PutU16(DataVector* out, uint64_t value) { + out->push_back(static_cast(value >> 8)); + out->push_back(static_cast(value)); +} + +void PutU32(DataVector* out, uint64_t value) { + PutU16(out, value >> 16); + PutU16(out, value); +} + +uint32_t Checksum(pdfium::span bytes) { + uint32_t sum = 0; + for (size_t i = 0; i < bytes.size(); i += 4) { + uint32_t word = 0; + for (size_t j = 0; j < 4; ++j) { + word = (word << 8) | (i + j < bytes.size() ? bytes[i + j] : 0); + } + sum += word; + } + return sum; +} + +} // namespace + +DataVector WriteSfnt( + uint32_t version, + const std::map>& tables) { + const size_t count = tables.size(); + size_t entry_selector = 0; + while ((size_t{2} << entry_selector) <= count) { + ++entry_selector; + } + const size_t search_range = (size_t{1} << entry_selector) * 16; + DataVector font; + PutU32(&font, version); + PutU16(&font, count); + PutU16(&font, search_range); + PutU16(&font, entry_selector); + PutU16(&font, count * 16 - search_range); + + // head's checksum, and the font's, are taken with checkSumAdjustment 0. + std::map> written = tables; + auto head = written.find("head"); + const bool adjusts = head != written.end() && + head->second.size() >= kChecksumAdjustmentOffset + 4; + if (adjusts) { + fxcrt::PutUInt32MSBFirst(0, pdfium::span(head->second) + .subspan(kChecksumAdjustmentOffset) + .first<4u>()); + } + size_t offset = kHeaderSize + count * kTableRecordSize; + size_t head_offset = 0; + for (const auto& [tag, table] : written) { + if (tag == "head") { + head_offset = offset; + } + for (char c : tag) { + font.push_back(static_cast(c)); + } + PutU32(&font, Checksum(table)); + PutU32(&font, offset); + PutU32(&font, table.size()); + offset += (table.size() + 3) / 4 * 4; + } + for (const auto& [tag, table] : written) { + font.insert(font.end(), table.begin(), table.end()); + font.insert(font.end(), (4 - table.size() % 4) % 4, 0); + } + if (adjusts) { + fxcrt::PutUInt32MSBFirst( + kFontChecksum - Checksum(font), + pdfium::span(font) + .subspan(head_offset + kChecksumAdjustmentOffset) + .first<4u>()); + } + return font; +} + +bool SfntTablesAligned(pdfium::span bytes) { + std::optional directory = ReadDirectory(bytes); + if (!directory.has_value()) { + return false; + } + for (const TableRecord& table : directory->tables) { + if (table.offset % 4 != 0) { + return false; + } + } + return true; +} + +DataVector AlignSfntTables(pdfium::span bytes) { + std::optional directory = ReadDirectory(bytes); + if (!directory.has_value()) { + return {}; + } + std::map> tables; + for (const TableRecord& table : directory->tables) { + pdfium::span data = + bytes.subspan(table.offset, table.length); + // A tag named twice: which table is meant can't be told. + if (!tables + .emplace(table.tag, DataVector(data.begin(), data.end())) + .second) { + return {}; + } + } + return WriteSfnt(directory->version, tables); +} diff --git a/core/fxge/cfx_sfnt.h b/core/fxge/cfx_sfnt.h new file mode 100644 index 0000000000..cec1559e4b --- /dev/null +++ b/core/fxge/cfx_sfnt.h @@ -0,0 +1,36 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 +// +// EmbedPDF(fonts): the container TrueType and OpenType fonts share, an sfnt: +// a table directory, then the tables it names. The font format starts each +// table on a four-byte boundary. FreeType reads a font whose tables don't; a +// browser's font sanitizer refuses it whole. + +#ifndef CORE_FXGE_CFX_SFNT_H_ +#define CORE_FXGE_CFX_SFNT_H_ + +#include + +#include + +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/data_vector.h" +#include "core/fxcrt/span.h" + +// An sfnt of |tables|, by tag: the table directory in tag order with each +// table's checksum, each table padded to four bytes, and head's +// checkSumAdjustment set for the whole font (whatever head held there). +DataVector WriteSfnt( + uint32_t version, + const std::map>& tables); + +// Whether |bytes| is an sfnt whose tables each start on a four-byte +// boundary; false for anything else. +bool SfntTablesAligned(pdfium::span bytes); + +// |bytes|' tables written again (WriteSfnt): the same tables, each on a +// four-byte boundary. Empty when |bytes| isn't an sfnt whose tables lie +// inside it. +DataVector AlignSfntTables(pdfium::span bytes); + +#endif // CORE_FXGE_CFX_SFNT_H_ diff --git a/docs/embedpdf/fork-ledger.md b/docs/embedpdf/fork-ledger.md index c26688d705..e89de2b696 100644 --- a/docs/embedpdf/fork-ledger.md +++ b/docs/embedpdf/fork-ledger.md @@ -407,13 +407,24 @@ the same in every engine, WebAssembly (which has no system fonts) included. **Where.** `cfx_fontregistry` (ours), `cpdf_font*`, `cfx_fontmapper.cpp`. Upstream's embedder tests expect standard fonts from the test fonts, so the embedder test environment turns on `CFX_FontMapper::SetStandardFontsFromSystemForTesting()`; our tests of the rule turn it off. +A document's fonts, their display programs and the resolver are ours (`cpdf_documentfonts`, +`cpdf_displayprogram`, `cpdf_fontresolver`, `cfx_sfnt`, `cfx_opentypewriter`, `cfx_glyphoutline`, +`cfx_codepointrange`, `cfx_bidiparagraph`). They reach into upstream files at three places: +`CPDF_Document::FontIndex`, the document's hold on its font index; `CPVT_NeedDivision`, +`CPVT_IsOpenStylePunctuation` and `CPVT_IsSpace` in `cpvt_section`, CPVT's own word breaking, +which the rich text layout breaks a field's lines with; and `CFX_Face::LoadUnscaledGlyphOutline` +and `GetMappedCodePointRanges`, defined in our `cfx_glyphoutline.cpp` and `cfx_codepointrange.cpp`. **Guarded by.** The font embedder tests; `cfx_fontmapper_unittest.cpp` and -`EPDFViewEmbedderTest.StandardFontsDrawWithBuiltInFaces` for standard fonts. +`EPDFViewEmbedderTest.StandardFontsDrawWithBuiltInFaces` for standard fonts; +`epdf_font_embeddertest.cpp` and `epdf_richtextlayout_embeddertest.cpp` (`FieldMatchesCpvt` for +the line breaks) for a document's fonts and the layout. **When upstream touches this.** Font loading and mapping. A new upstream test that expects the test fonts for a standard font passes as it is; one of ours that expects the built-in faces turns the switch off. Our font code calls FreeType directly (`CFX_Face::GetFsTypeFlags()`, CID-keyed CFF, the registry), so our builds keep `pdf_enable_freetype` on; a Fontations-only build would need -Skrifa versions of them. +Skrifa versions of them. When upstream changes CPVT's word breaking, the layout follows on purpose: +a field breaks its lines where upstream's would. Keep `font_index_` declared after +`epdf_attachment_` in `CPDF_Document`, so the index goes before the attachment and the parser. ### measurement **Rule.** Measurement dictionaries and dimension appearances. diff --git a/fpdfsdk/BUILD.gn b/fpdfsdk/BUILD.gn index e9a433962d..1ede4810cd 100644 --- a/fpdfsdk/BUILD.gn +++ b/fpdfsdk/BUILD.gn @@ -236,6 +236,11 @@ pdfium_embeddertest_source_set("embeddertests") { "epdf_annot_embeddertest.cpp", # EmbedPDF: covers attachments (public/epdf_attachment.h). "epdf_attachment_embeddertest.cpp", + # EmbedPDF: covers a document's fonts, the resolver read and the + # registry's program reads (public/epdf_font.h). + "epdf_font_embeddertest.cpp", + # EmbedPDF: covers the rich text layout with real fonts. + "epdf_richtextlayout_embeddertest.cpp", # EmbedPDF: covers flattening and annotation export (public/epdf_flatten.h). "epdf_flatten_embeddertest.cpp", # EmbedPDF: covers base documents and layers (public/epdf_layer.h). diff --git a/fpdfsdk/epdf_annot.cpp b/fpdfsdk/epdf_annot.cpp index 25be04b584..5bf2591fc0 100644 --- a/fpdfsdk/epdf_annot.cpp +++ b/fpdfsdk/epdf_annot.cpp @@ -23,7 +23,6 @@ #include "constants/appearance.h" #include "constants/form_fields.h" #include "core/fdrm/fx_crypt.h" -#include "core/fxcrt/stream_write_utils.h" #include "core/fpdfapi/edit/cpdf_stringarchivestream.h" #include "core/fpdfapi/page/cpdf_annotcontext.h" #include "core/fpdfapi/page/cpdf_form.h" @@ -46,6 +45,7 @@ #include "core/fpdfdoc/cpdf_annot_rotation.h" #include "core/fpdfdoc/cpdf_appearancegenerator.h" #include "core/fpdfdoc/cpdf_color_utils.h" +#include "core/fpdfdoc/cpdf_documentfonts.h" #include "core/fpdfdoc/cpdf_embed_metadata.h" #include "core/fpdfdoc/cpdf_fontface.h" #include "core/fpdfdoc/cpdf_formfield.h" @@ -56,6 +56,7 @@ #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/numerics/safe_conversions.h" #include "core/fxcrt/stl_util.h" +#include "core/fxcrt/stream_write_utils.h" #include "core/fxge/cfx_color.h" #include "core/fxge/cfx_fontregistry.h" #include "fpdfsdk/cpdfsdk_helpers.h" @@ -2285,6 +2286,113 @@ EPDFAnnot_GetDefaultAppearanceFontFace(FPDF_ANNOTATION annot, face.family, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(family, buflen))); } +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAnnot_GetDefaultAppearanceFontPostScriptName(FPDF_ANNOTATION annot, + char* buffer, + unsigned long buflen) { + ScopedFPDFAnnotationView annot_view(annot); + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + if (!context) { + return 0; + } + FPDF_ANNOTATION_SUBTYPE subtype = FPDFAnnot_GetSubtype(annot); + if (subtype != FPDF_ANNOT_FREETEXT && subtype != FPDF_ANNOT_WIDGET && + subtype != FPDF_ANNOT_REDACT) { + return 0; + } + CPDF_Document* doc = context->GetPage()->GetDocument(); + if (!doc) { + return 0; + } + CPDF_DocumentViewScope document_view(doc); + const CPDF_Dictionary* annot_dict = context->GetAnnotDict(); + if (!annot_dict) { + return 0; + } + const CPDF_Dictionary* root_dict = doc->GetRoot(); + RetainPtr acroform_dict = + root_dict ? root_dict->GetDictFor("AcroForm") : nullptr; + CPDF_DefaultAppearance da(annot_dict, acroform_dict.Get()); + std::optional font_info = + da.GetFont(); + if (!font_info.has_value() || font_info.value().name.IsEmpty()) { + return 0; + } + const ByteString& font_name = font_info.value().name; + ByteString base_font; + // The same /DR lookup as EPDFAnnot_GetDefaultAppearanceFontFace(). + RetainPtr inherited_dr = + ToDictionary(CPDF_FormField::GetFieldAttrForDict(annot_dict, "DR")); + RetainPtr acroform_dr = + acroform_dict ? acroform_dict->GetDictFor("DR") : nullptr; + for (const CPDF_Dictionary* dr : {inherited_dr.Get(), acroform_dr.Get()}) { + RetainPtr fonts = + dr ? dr->GetDictFor("Font") : nullptr; + RetainPtr font_dict = + fonts ? fonts->GetDictFor(font_name.AsStringView()) : nullptr; + if (!font_dict) { + continue; + } + base_font = font_dict->GetNameFor("BaseFont"); + if (font_dict->GetNameFor("Subtype") == "Type0") { + RetainPtr descendants = + font_dict->GetArrayFor("DescendantFonts"); + RetainPtr descendant = + descendants ? descendants->GetDictAt(0) : nullptr; + if (descendant && !descendant->GetNameFor("BaseFont").IsEmpty()) { + base_font = descendant->GetNameFor("BaseFont"); + } + } + break; + } + if (base_font.IsEmpty()) { + CPDF_DocumentFonts* fonts = CPDF_DocumentFonts::Find(doc); + std::optional program = + fonts ? fonts->ProgramForAlias(font_name) : std::nullopt; + if (program.has_value()) { + base_font = program->base_font_name; + } + } + base_font = BaseFontNameWithoutSubsetTag(base_font); + if (base_font.IsEmpty()) { + return 0; + } + // SAFETY: required from caller. + return NulTerminateMaybeCopyAndReturnLength( + base_font, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetDefaultAppearanceDocumentFont(FPDF_ANNOTATION annot, + unsigned long program_objnum, + float font_size, + unsigned int R, + unsigned int G, + unsigned int B) { + ScopedFPDFAnnotationView annot_view(annot); + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + if (!context) { + return false; + } + RetainPtr annot_dict = context->GetMutableAnnotDict(); + if (!annot_dict) { + return false; + } + FPDF_ANNOTATION_SUBTYPE subtype = FPDFAnnot_GetSubtype(annot); + if (subtype != FPDF_ANNOT_FREETEXT && subtype != FPDF_ANNOT_WIDGET && + subtype != FPDF_ANNOT_REDACT) { + return false; + } + CPDF_Document* doc = context->GetPage()->GetDocument(); + if (!doc || program_objnum == 0 || font_size < 0 || R > 255 || G > 255 || + B > 255) { + return false; + } + return CPDF_AppearanceGenerator::UpdateDefaultAppearanceDocumentFont( + doc, annot_dict.Get(), static_cast(program_objnum), font_size, + CFX_Color::MakeRGBBytes(R, G, B)); +} + FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetTextAlignment(FPDF_ANNOTATION annot, FPDF_TEXT_ALIGNMENT alignment) { diff --git a/fpdfsdk/epdf_annot_embeddertest.cpp b/fpdfsdk/epdf_annot_embeddertest.cpp index 97d998b185..271d915e43 100644 --- a/fpdfsdk/epdf_annot_embeddertest.cpp +++ b/fpdfsdk/epdf_annot_embeddertest.cpp @@ -50,6 +50,7 @@ #include "core/fxcrt/fx_safe_types.h" #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/span.h" +#include "core/fxge/cfx_face.h" #include "core/fxge/cfx_font.h" #include "core/fxge/cfx_fontregistry.h" #include "fpdfsdk/cpdfsdk_helpers.h" @@ -180,7 +181,7 @@ EPDF_FONT_ID RegisterDroidSansFallbackFullFont() { EPDF_FONT_ID font_id = EPDFFont_RegisterMemFont64( "DroidSansFallbackFull", /*weight=*/400, /*italic=*/0, font_data.data(), font_data.size()); - if (font_id == 0 || !EPDFFont_AddFallbackFont(font_id)) { + if (font_id == 0 || !EPDFFont_AddFallbackFont(font_id, /*tier=*/0)) { ADD_FAILURE() << "Failed to register DroidSansFallbackFull as fallback"; return 0; } @@ -1353,7 +1354,7 @@ TEST_F(EPDFAnnotEmbedderTest, FreeTextAppearanceFallsBackToRegisteredFont) { EPDFFont_RegisterMemFont64("NotoSansSC", /*weight=*/400, /*italic=*/0, font_data.data(), font_data.size()); ASSERT_NE(0u, font_id); - ASSERT_TRUE(EPDFFont_AddFallbackFont(font_id)); + ASSERT_TRUE(EPDFFont_AddFallbackFont(font_id, /*tier=*/0)); ScopedFPDFDocument doc(FPDF_CreateNewDocument()); ASSERT_TRUE(doc); @@ -2411,7 +2412,7 @@ TEST_F(EPDFAnnotEmbedderTest, "DroidSansFallbackFull", /*weight=*/400, /*italic=*/0, droid_preview.data(), droid_preview.size()); ASSERT_NE(0u, droid_id); - ASSERT_TRUE(EPDFFont_AddFallbackFont(droid_id)); + ASSERT_TRUE(EPDFFont_AddFallbackFont(droid_id, /*tier=*/0)); ScopedFPDFDocument doc(FPDF_CreateNewDocument()); ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); @@ -2969,7 +2970,7 @@ TEST_F(EPDFAnnotEmbedderTest, "NotoSansSC", /*weight=*/400, /*italic=*/0, noto.data(), noto.size()); ASSERT_NE(0u, roboto_id); ASSERT_NE(0u, noto_id); - ASSERT_TRUE(EPDFFont_AddFallbackFont(noto_id)); + ASSERT_TRUE(EPDFFont_AddFallbackFont(noto_id, /*tier=*/0)); ScopedFPDFDocument doc(FPDF_CreateNewDocument()); ASSERT_TRUE(doc); @@ -3040,9 +3041,11 @@ TEST_F(EPDFAnnotEmbedderTest, RegisteredLayoutFontAllocatesNoDocumentObject) { CPDF_AnnotFontSubset::CreateLayoutFont(cpdf_doc, roboto_id); ASSERT_TRUE(layout.font); ASSERT_TRUE(layout.scratch); - EXPECT_NE(0u, layout.font->CharCodeFromUnicode('A')); - EXPECT_GT(layout.font->GetCharWidth(layout.font->CharCodeFromUnicode('A')), - 0); + // It measures glyphs, found through the program's own cmap: a + // TrueType program's glyph is its CID. + const int glyph = layout.font->GetFont()->GetFace()->GetCharIndex('A'); + ASSERT_GT(glyph, 0); + EXPECT_GT(layout.font->GetCharWidth(static_cast(glyph)), 0); EXPECT_EQ(0u, layout.font->GetFontDict()->GetObjNum()); } EXPECT_EQ(last_obj_before, cpdf_doc->GetLastObjNum()); @@ -4279,6 +4282,112 @@ TEST_F(EPDFAnnotEmbedderTest, RichTextRtlRunCarriesActualTextAndExtracts) { << "extracted: " << codepoints; } +// A character no font has is written with the last-resort font, subset into +// the appearance like any fallback, so every viewer shows the same box. Two +// such characters share its one glyph, so the run carries its text as +// ActualText and extracts as typed; the write reports both. +TEST_F(EPDFAnnotEmbedderTest, RichTextLastResortDrawsWhatNoFontHas) { + ScopedRegisteredFonts scoped_fonts; + std::vector roboto = LoadRobotoFontData(); + std::vector box = GetFileContents( + PathService::GetTestFilePath("fonts/last_resort.ttf").c_str()); + ASSERT_FALSE(roboto.empty()); + ASSERT_FALSE(box.empty()); + ASSERT_NE(0u, EPDFFont_RegisterMemFont64("Roboto", /*weight=*/400, + /*italic=*/0, roboto.data(), + roboto.size())); + const EPDF_FONT_ID box_id = EPDFFont_RegisterMemFont64( + "Last Resort", /*weight=*/400, /*italic=*/0, box.data(), box.size()); + ASSERT_NE(0u, box_id); + ASSERT_TRUE(EPDFFont_SetLastResortFont(box_id)); + + std::string saved; + { + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FREETEXT)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 320.0f, 350.0f, 250.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetRichTextJSON( + annot.get(), + "{\"body\":{\"family\":\"Roboto\",\"size\":18}," + "\"paragraphs\":[{\"runs\":[{\"text\":\"A\\u4E2D\\u4E2EB\"}]}]}")); + EXPECT_EQ(2, EPDFDoc_CountMissingCharacters(doc.get())); + RetainPtr font = + GetAppearanceFontDict(annot.get(), RegisteredFontAlias(box_id)); + ASSERT_TRUE(font); + RetainPtr descriptor = + GetType0FontDescriptor(font.Get()); + ASSERT_TRUE(descriptor); + RetainPtr program = + descriptor->GetStreamFor("FontFile2"); + ASSERT_TRUE(program); + // The box costs what the font does, not a cmap entry per code point. + EXPECT_EQ(box.size(), program->GetRawSize()); + EXPECT_THAT(GetNormalAppearance(annot.get()), + HasSubstr(L"/Span <>> BDC")); + + ASSERT_TRUE(FPDFPage_Flatten(page.get(), FLAT_NORMALDISPLAY)); + unsigned long saved_size = 0; + void* saved_buffer = + EPDF_SaveDocumentToOwnedBuffer(doc.get(), /*flags=*/0, &saved_size); + ASSERT_TRUE(saved_buffer); + saved.assign(static_cast(saved_buffer), saved_size); + EPDF_FreeBuffer(saved_buffer); + } + ScopedFPDFDocument doc(FPDF_LoadMemDocument( + saved.data(), static_cast(saved.size()), nullptr)); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); + ASSERT_TRUE(page); + EXPECT_NE(std::wstring::npos, ExtractPageText(page.get()) + .find(L"A\x4E2D\x4E2E" + L"B")); +} + +// A last resort whose licence allows preview and print only writes no new +// text until the app authorizes it, like any other font. +TEST_F(EPDFAnnotEmbedderTest, RichTextPreviewAndPrintLastResortWaits) { + ScopedRegisteredFonts scoped_fonts; + std::vector roboto = LoadRobotoFontData(); + // Preview and print, and still never subset. + std::vector box = WithFsType( + GetFileContents( + PathService::GetTestFilePath("fonts/last_resort.ttf").c_str()), + 0x0104); + ASSERT_FALSE(roboto.empty()); + ASSERT_FALSE(box.empty()); + ASSERT_NE(0u, EPDFFont_RegisterMemFont64("Roboto", /*weight=*/400, + /*italic=*/0, roboto.data(), + roboto.size())); + const EPDF_FONT_ID box_id = EPDFFont_RegisterMemFont64( + "Last Resort", /*weight=*/400, /*italic=*/0, box.data(), box.size()); + ASSERT_NE(0u, box_id); + ASSERT_TRUE(EPDFFont_SetLastResortFont(box_id)); + + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); + const char kJson[] = + "{\"body\":{\"family\":\"Roboto\",\"size\":18}," + "\"paragraphs\":[{\"runs\":[{\"text\":\"A\\u4E2DB\"}]}]}"; + for (const bool authorized : {false, true}) { + SCOPED_TRACE(authorized); + if (authorized) { + ASSERT_TRUE(EPDFFont_AuthorizeEditing(box_id)); + } + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FREETEXT)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 320.0f, 350.0f, 250.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetRichTextJSON(annot.get(), kJson)); + EXPECT_EQ(authorized, !!GetAppearanceFontDict(annot.get(), + RegisteredFontAlias(box_id))); + } +} + // Rung 2 of plan §3.3 (C note §1.3): a family that is no longer registered // but whose program is already in the document resolves to that program, // through a new Type0 dictionary that references the existing stream. @@ -5073,7 +5182,7 @@ TEST_F(EPDFAnnotEmbedderTest, RegisteredFallbackFontRendersPageMissingGlyph) { EPDFFont_RegisterMemFont64("NotoSansSC", /*weight=*/400, /*italic=*/0, font_data.data(), font_data.size()); ASSERT_NE(0u, font_id); - ASSERT_TRUE(EPDFFont_AddFallbackFont(font_id)); + ASSERT_TRUE(EPDFFont_AddFallbackFont(font_id, /*tier=*/0)); ScopedFPDFBitmap bitmap = render_broken_pdf(broken_pdf); ASSERT_TRUE(bitmap); diff --git a/fpdfsdk/epdf_c_api_test.c b/fpdfsdk/epdf_c_api_test.c index 249c8d0185..3eb6633079 100644 --- a/fpdfsdk/epdf_c_api_test.c +++ b/fpdfsdk/epdf_c_api_test.c @@ -112,6 +112,7 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFAnnot_GetColor); CHK(EPDFAnnot_GetDefaultAppearance); CHK(EPDFAnnot_GetDefaultAppearanceFontFace); + CHK(EPDFAnnot_GetDefaultAppearanceFontPostScriptName); CHK(EPDFAnnot_GetDrawingRect); CHK(EPDFAnnot_GetEmbedMetadataBoolean); CHK(EPDFAnnot_GetEmbedMetadataJSON); @@ -154,6 +155,7 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFAnnot_SetCalloutLine); CHK(EPDFAnnot_SetColor); CHK(EPDFAnnot_SetDefaultAppearance); + CHK(EPDFAnnot_SetDefaultAppearanceDocumentFont); CHK(EPDFAnnot_SetDefaultAppearanceRegisteredFont); CHK(EPDFAnnot_SetEmbedMetadataBoolean); CHK(EPDFAnnot_SetEmbedMetadataJSON); @@ -280,14 +282,59 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFPage_FlattenAnnotations); // epdf_font.h + CHK(EPDFDoc_ClearMissingCharacters); + CHK(EPDFDoc_CountMissingCharacters); + CHK(EPDFDoc_GetFontDisplayProgramCoverage); + CHK(EPDFDoc_GetFontDisplayProgramData); CHK(EPDFDoc_GetFontEmbeddingPolicy); + CHK(EPDFDoc_GetFontProgramData); + CHK(EPDFDoc_GetFontsFingerprint); + CHK(EPDFDoc_GetMissingCharacter); + CHK(EPDFDoc_ResolveFonts); CHK(EPDFDoc_SetFontEmbeddingPolicy); + CHK(EPDFFontFaces_Close); + CHK(EPDFFontFaces_Count); + CHK(EPDFFontFaces_CountPrograms); + CHK(EPDFFontFaces_GetFamily); + CHK(EPDFFontFaces_GetProgramDisplayHash); + CHK(EPDFFontFaces_GetProgramEmbedded); + CHK(EPDFFontFaces_GetProgramEmbeddingPermission); + CHK(EPDFFontFaces_GetProgramFormat); + CHK(EPDFFontFaces_GetProgramHash); + CHK(EPDFFontFaces_GetProgramMetrics); + CHK(EPDFFontFaces_GetProgramObjNum); + CHK(EPDFFontFaces_GetProgramPostScriptName); + CHK(EPDFFontFaces_GetWeight); + CHK(EPDFFontFaces_IsItalic); + CHK(EPDFFontFaces_Load); + CHK(EPDFFontFaces_ProgramMapsUnicode); + CHK(EPDFFontResolution_Close); + CHK(EPDFFontResolution_CountMissingCharacters); + CHK(EPDFFontResolution_GetBaseFontName); + CHK(EPDFFontResolution_GetFallbacks); + CHK(EPDFFontResolution_GetFamily); + CHK(EPDFFontResolution_GetMissingCharacter); + CHK(EPDFFontResolution_GetProgramDisplayHash); + CHK(EPDFFontResolution_GetProgramObjNum); + CHK(EPDFFontResolution_GetRegisteredFont); + CHK(EPDFFontResolution_GetSource); + CHK(EPDFFontResolution_GetWeight); + CHK(EPDFFontResolution_IsDegraded); + CHK(EPDFFontResolution_IsItalic); CHK(EPDFFont_AddFallbackFont); CHK(EPDFFont_AuthorizeEditing); CHK(EPDFFont_ClearFallbackFonts); CHK(EPDFFont_ClearRegisteredFonts); + CHK(EPDFFont_GetCoverage); CHK(EPDFFont_GetEmbeddingPermission); + CHK(EPDFFont_GetFallbackOrder); CHK(EPDFFont_GetFamilyName); + CHK(EPDFFont_GetLastResortFont); + CHK(EPDFFont_GetMetrics); + CHK(EPDFFont_GetPostScriptName); + CHK(EPDFFont_GetProgramData); + CHK(EPDFFont_GetProgramFormat); + CHK(EPDFFont_GetProgramHash); CHK(EPDFFont_GetWeight); CHK(EPDFFont_IsEditingAuthorized); CHK(EPDFFont_IsInstanced); @@ -295,6 +342,7 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFFont_RegisterFont); CHK(EPDFFont_RegisterMemFont); CHK(EPDFFont_RegisterMemFont64); + CHK(EPDFFont_SetLastResortFont); // epdf_form.h CHK(EPDFForm_AttachWidget); diff --git a/fpdfsdk/epdf_edit_test_util.cpp b/fpdfsdk/epdf_edit_test_util.cpp index 4f0edf7590..610e1fc539 100644 --- a/fpdfsdk/epdf_edit_test_util.cpp +++ b/fpdfsdk/epdf_edit_test_util.cpp @@ -103,8 +103,12 @@ std::string MakePdf(const std::vector& objects) { } std::string Stream(const std::string& data) { - return "<< /Length " + std::to_string(data.size()) + " >>\nstream\n" + data + - "\nendstream"; + return Stream("", data); +} + +std::string Stream(const std::string& entries, const std::string& data) { + return "<< " + entries + (entries.empty() ? "" : " ") + "/Length " + + std::to_string(data.size()) + " >>\nstream\n" + data + "\nendstream"; } std::map ObjectGraph(CPDF_Document* doc) { diff --git a/fpdfsdk/epdf_edit_test_util.h b/fpdfsdk/epdf_edit_test_util.h index 2c5178e402..75ef0b1d72 100644 --- a/fpdfsdk/epdf_edit_test_util.h +++ b/fpdfsdk/epdf_edit_test_util.h @@ -45,6 +45,10 @@ std::string MakePdf(const std::vector& objects); // A stream object body holding `data`. std::string Stream(const std::string& data); +// A stream object body holding `data`, with `entries` in its dictionary +// before /Length. +std::string Stream(const std::string& entries, const std::string& data); + // Every indirect object, by number, from 1 to the last, as text: streams with // their raw bytes, so two graphs compare by value. std::map ObjectGraph(CPDF_Document* doc); diff --git a/fpdfsdk/epdf_font.cpp b/fpdfsdk/epdf_font.cpp index 8ee3de5514..f24533575f 100644 --- a/fpdfsdk/epdf_font.cpp +++ b/fpdfsdk/epdf_font.cpp @@ -6,13 +6,27 @@ #include #include +#include +#include #include +#include +#include +#include "core/fpdfapi/page/cpdf_annotcontext.h" #include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfdoc/cpdf_annotfontmap.h" +#include "core/fpdfdoc/cpdf_documentfonts.h" +#include "core/fpdfdoc/cpdf_fontface.h" +#include "core/fpdfdoc/cpdf_fontresolver.h" #include "core/fxcrt/bytestring.h" #include "core/fxcrt/compiler_specific.h" +#include "core/fxcrt/data_vector.h" +#include "core/fxcrt/numerics/safe_conversions.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/span.h" +#include "core/fxcrt/span_util.h" +#include "core/fxcrt/stl_util.h" +#include "core/fxge/cfx_font.h" #include "core/fxge/cfx_fontregistry.h" #include "fpdfsdk/cpdfsdk_customaccess.h" #include "fpdfsdk/cpdfsdk_helpers.h" @@ -166,10 +180,639 @@ EPDFDoc_GetFontEmbeddingPolicy(FPDF_DOCUMENT document) { } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFFont_AddFallbackFont(EPDF_FONT_ID font_id) { - return CFX_FontRegistry::AddFallbackFont(font_id); +EPDFFont_AddFallbackFont(EPDF_FONT_ID font_id, int tier) { + return CFX_FontRegistry::AddFallbackFont(font_id, tier); } FPDF_EXPORT void FPDF_CALLCONV EPDFFont_ClearFallbackFonts(void) { CFX_FontRegistry::ClearFallbackFonts(); } + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFont_SetLastResortFont(EPDF_FONT_ID font_id) { + return CFX_FontRegistry::SetLastResortFont(font_id); +} + +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV EPDFFont_GetLastResortFont(void) { + return CFX_FontRegistry::GetLastResortFont(); +} + +// ---- Registered fonts: programs, coverage, fallback order ------------------- + +namespace { + +// Copies |bytes| into the caller's buffer when it is big enough; returns how +// many bytes there are either way. +unsigned long CopyBytesAndReturnLength(pdfium::span bytes, + void* buffer, + unsigned long buflen) { + // SAFETY: required from caller. + pdfium::span destination = + UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen)); + fxcrt::try_spancpy(pdfium::as_writable_bytes(destination), bytes); + return pdfium::checked_cast(bytes.size()); +} + +// Writes (first, count) pairs into the caller's array, up to |capacity| +// pairs; returns how many ranges there are either way. +unsigned long CopyRangesAndReturnCount( + pdfium::span ranges, + uint32_t* out, + unsigned long capacity) { + if (out) { + const size_t count = std::min(ranges.size(), capacity); + // SAFETY: required from caller: |out| holds 2 * |capacity| values. + pdfium::span destination = + UNSAFE_BUFFERS(pdfium::span(out, count * 2)); + for (size_t i = 0; i < count; ++i) { + destination[2 * i] = ranges[i].first; + destination[2 * i + 1] = ranges[i].count; + } + } + return pdfium::checked_cast(ranges.size()); +} + +unsigned long CopyFontIdsAndReturnCount( + const std::vector& ids, + EPDF_FONT_ID* out, + unsigned long capacity) { + if (out) { + const size_t count = std::min(ids.size(), capacity); + // SAFETY: required from caller. + pdfium::span destination = + UNSAFE_BUFFERS(pdfium::span(out, count)); + for (size_t i = 0; i < count; ++i) { + destination[i] = ids[i]; + } + } + return pdfium::checked_cast(ids.size()); +} + +bool CopySha256(pdfium::span hash, unsigned char* out) { + if (!out || hash.size() != 32) { + return false; + } + // SAFETY: required from caller: |out| holds 32 bytes. + fxcrt::spancpy(UNSAFE_BUFFERS(pdfium::span(out, 32u)), hash); + return true; +} + +} // namespace + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFont_GetFallbackOrder(int weight, + FPDF_BOOL italic, + EPDF_FONT_ID* ids, + unsigned long capacity) { + return CopyFontIdsAndReturnCount( + CFX_FontRegistry::GetFallbackOrder(weight, !!italic, + /*for_authoring=*/true), + ids, capacity); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFont_GetProgramData(EPDF_FONT_ID font_id, + void* buffer, + unsigned long buflen) { + if (!CFX_FontRegistry::IsValidFont(font_id)) { + return 0; + } + const DataVector data = CFX_FontRegistry::GetProgramData(font_id); + return CopyBytesAndReturnLength(data, buffer, buflen); +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFFont_GetProgramFormat(EPDF_FONT_ID font_id) { + switch (CFX_FontRegistry::GetProgramFormat(font_id)) { + case CFX_FontRegistry::ProgramFormat::kTrueType: + return EPDF_FONT_FORMAT_TRUETYPE; + case CFX_FontRegistry::ProgramFormat::kOpenTypeCFF: + return EPDF_FONT_FORMAT_OPENTYPE_CFF; + case CFX_FontRegistry::ProgramFormat::kUnsupported: + return -1; + } +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFont_GetProgramHash(EPDF_FONT_ID font_id, unsigned char* sha256) { + return CopySha256(CFX_FontRegistry::GetSourceHash(font_id), sha256); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFont_GetCoverage(EPDF_FONT_ID font_id, + uint32_t* ranges, + unsigned long range_capacity) { + return CopyRangesAndReturnCount(CFX_FontRegistry::GetCoverage(font_id), + ranges, range_capacity); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFFont_GetMetrics(EPDF_FONT_ID font_id, + float* ascent, + float* descent) { + if (!ascent || !descent) { + return false; + } + std::unique_ptr program = CFX_FontRegistry::CreateFont(font_id); + if (!program) { + return false; + } + ProgramLineMetrics(program.get(), ascent, descent); + return true; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFont_GetPostScriptName(EPDF_FONT_ID font_id, + char* buffer, + unsigned long buflen) { + if (!CFX_FontRegistry::IsValidFont(font_id)) { + return 0; + } + // SAFETY: required from caller. + return NulTerminateMaybeCopyAndReturnLength( + BaseFontNameWithoutSubsetTag(CFX_FontRegistry::GetBaseFontName(font_id)), + UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +// ---- A document's fonts +// ------------------------------------------------------ + +struct epdf_font_faces_t__ { + std::vector faces; +}; + +namespace { + +const CPDF_DocumentFonts::Face* FaceAt(EPDF_FONT_FACES faces, int face) { + if (!faces || !fxcrt::IndexInBounds(faces->faces, face)) { + return nullptr; + } + return &faces->faces[face]; +} + +const CPDF_DocumentFonts::Program* ProgramAt(EPDF_FONT_FACES faces, + int face, + int program) { + const CPDF_DocumentFonts::Face* entry = FaceAt(faces, face); + if (!entry || !fxcrt::IndexInBounds(entry->programs, program)) { + return nullptr; + } + return &entry->programs[program]; +} + +int FormatCode(CPDF_DocumentFonts::Format format) { + switch (format) { + case CPDF_DocumentFonts::Format::kNone: + return EPDF_FONT_FORMAT_NONE; + case CPDF_DocumentFonts::Format::kTrueType: + return EPDF_FONT_FORMAT_TRUETYPE; + case CPDF_DocumentFonts::Format::kOpenTypeCFF: + return EPDF_FONT_FORMAT_OPENTYPE_CFF; + case CPDF_DocumentFonts::Format::kCFF: + return EPDF_FONT_FORMAT_CFF; + case CPDF_DocumentFonts::Format::kType1: + return EPDF_FONT_FORMAT_TYPE1; + case CPDF_DocumentFonts::Format::kType3: + return EPDF_FONT_FORMAT_TYPE3; + } +} + +int EmbeddedCode(CPDF_DocumentFonts::Embedded embedded) { + switch (embedded) { + case CPDF_DocumentFonts::Embedded::kNone: + return EPDF_FONT_EMBEDDED_NONE; + case CPDF_DocumentFonts::Embedded::kWhole: + return EPDF_FONT_EMBEDDED_WHOLE; + case CPDF_DocumentFonts::Embedded::kSubset: + return EPDF_FONT_EMBEDDED_SUBSET; + } +} + +} // namespace + +FPDF_EXPORT EPDF_FONT_FACES FPDF_CALLCONV +EPDFFontFaces_Load(FPDF_DOCUMENT document) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + if (!doc) { + return nullptr; + } + auto faces = std::make_unique(); + faces->faces = CPDF_DocumentFonts::For(doc)->CollectFaces(); + return faces.release(); +} + +FPDF_EXPORT void FPDF_CALLCONV EPDFFontFaces_Close(EPDF_FONT_FACES faces) { + delete faces; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_GetFontsFingerprint(FPDF_DOCUMENT document, unsigned char* sha256) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + if (!doc) { + return false; + } + return CopySha256(CPDF_DocumentFonts::For(doc)->FacesFingerprint(), sha256); +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFFontFaces_Count(EPDF_FONT_FACES faces) { + return faces ? fxcrt::CollectionSize(faces->faces) : -1; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontFaces_GetFamily(EPDF_FONT_FACES faces, + int face, + FPDF_WCHAR* buffer, + unsigned long buflen) { + const CPDF_DocumentFonts::Face* entry = FaceAt(faces, face); + if (!entry) { + return 0; + } + // SAFETY: required from caller. + return Utf16EncodeMaybeCopyAndReturnLength( + entry->family, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFFontFaces_GetWeight(EPDF_FONT_FACES faces, + int face) { + const CPDF_DocumentFonts::Face* entry = FaceAt(faces, face); + return entry ? entry->weight : 0; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_IsItalic(EPDF_FONT_FACES faces, int face) { + const CPDF_DocumentFonts::Face* entry = FaceAt(faces, face); + return entry && entry->italic; +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFFontFaces_CountPrograms(EPDF_FONT_FACES faces, + int face) { + const CPDF_DocumentFonts::Face* entry = FaceAt(faces, face); + return entry ? fxcrt::CollectionSize(entry->programs) : -1; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontFaces_GetProgramObjNum(EPDF_FONT_FACES faces, int face, int program) { + const CPDF_DocumentFonts::Program* entry = ProgramAt(faces, face, program); + return entry ? entry->objnum : 0; +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontFaces_GetProgramFormat(EPDF_FONT_FACES faces, int face, int program) { + const CPDF_DocumentFonts::Program* entry = ProgramAt(faces, face, program); + return entry ? FormatCode(entry->format) : -1; +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontFaces_GetProgramEmbedded(EPDF_FONT_FACES faces, int face, int program) { + const CPDF_DocumentFonts::Program* entry = ProgramAt(faces, face, program); + return entry ? EmbeddedCode(entry->embedded) : -1; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontFaces_GetProgramPostScriptName(EPDF_FONT_FACES faces, + int face, + int program, + char* buffer, + unsigned long buflen) { + const CPDF_DocumentFonts::Program* entry = ProgramAt(faces, face, program); + if (!entry) { + return 0; + } + // SAFETY: required from caller. + return NulTerminateMaybeCopyAndReturnLength( + entry->postscript_name, + UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontFaces_GetProgramEmbeddingPermission(EPDF_FONT_FACES faces, + int face, + int program) { + const CPDF_DocumentFonts::Program* entry = ProgramAt(faces, face, program); + if (!entry || !entry->permission.has_value()) { + return -1; + } + return static_cast(*entry->permission); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_ProgramMapsUnicode(EPDF_FONT_FACES faces, int face, int program) { + const CPDF_DocumentFonts::Program* entry = ProgramAt(faces, face, program); + return entry && entry->maps_unicode; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_GetProgramHash(EPDF_FONT_FACES faces, + int face, + int program, + unsigned char* sha256) { + const CPDF_DocumentFonts::Program* entry = ProgramAt(faces, face, program); + return entry && CopySha256(entry->sha256, sha256); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_GetProgramDisplayHash(EPDF_FONT_FACES faces, + int face, + int program, + unsigned char* sha256) { + const CPDF_DocumentFonts::Program* entry = ProgramAt(faces, face, program); + return entry && CopySha256(entry->display_sha256, sha256); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_GetProgramMetrics(EPDF_FONT_FACES faces, + int face, + int program, + float* ascent, + float* descent) { + const CPDF_DocumentFonts::Program* entry = ProgramAt(faces, face, program); + if (!entry || !ascent || !descent || entry->ascent <= 0) { + return false; + } + *ascent = entry->ascent; + *descent = entry->descent; + return true; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetFontProgramData(FPDF_DOCUMENT document, + unsigned long program_objnum, + void* buffer, + unsigned long buflen) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + if (!doc || program_objnum == 0) { + return 0; + } + const DataVector data = CPDF_DocumentFonts::For(doc)->ProgramData( + static_cast(program_objnum)); + return CopyBytesAndReturnLength(data, buffer, buflen); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetFontDisplayProgramData(FPDF_DOCUMENT document, + const unsigned char* display_sha256, + void* buffer, + unsigned long buflen) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + if (!doc || !display_sha256) { + return 0; + } + // SAFETY: required from caller: |display_sha256| holds 32 bytes. + const DataVector data = + CPDF_DocumentFonts::For(doc)->DisplayProgramData( + UNSAFE_BUFFERS(pdfium::span(display_sha256, 32u))); + return CopyBytesAndReturnLength(data, buffer, buflen); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetFontDisplayProgramCoverage(FPDF_DOCUMENT document, + const unsigned char* display_sha256, + uint32_t* ranges, + unsigned long range_capacity) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + if (!doc || !display_sha256) { + return 0; + } + // SAFETY: required from caller: |display_sha256| holds 32 bytes. + return CopyRangesAndReturnCount( + CPDF_DocumentFonts::For(doc)->DisplayProgramCoverage( + UNSAFE_BUFFERS(pdfium::span(display_sha256, 32u))), + ranges, range_capacity); +} + +// ---- The fonts a write would use +// --------------------------------------------- + +struct epdf_font_resolution_t__ { + CPDF_FontResolver::Result result; +}; + +namespace { + +const CPDF_FontResolver::ResolvedRun* RunAt(EPDF_FONT_RESOLUTION resolution, + int run) { + if (!resolution || !fxcrt::IndexInBounds(resolution->result.runs, run)) { + return nullptr; + } + return &resolution->result.runs[run]; +} + +} // namespace + +FPDF_EXPORT EPDF_FONT_RESOLUTION FPDF_CALLCONV +EPDFDoc_ResolveFonts(FPDF_DOCUMENT document, + FPDF_ANNOTATION annot, + int run_count, + const FPDF_WIDESTRING* texts, + const FPDF_WIDESTRING* families, + const int* weights, + const FPDF_BOOL* italics, + const int* programs) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + if (!doc || run_count < 0 || + (run_count > 0 && (!texts || !families || !weights || !italics))) { + return nullptr; + } + const CPDF_Dictionary* annot_dict = nullptr; + if (annot) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + annot_dict = context ? context->GetAnnotDict() : nullptr; + } + const size_t count = static_cast(run_count); + std::vector runs; + runs.reserve(count); + // SAFETY: required from caller: each array holds |run_count| entries. + auto text_span = UNSAFE_BUFFERS(pdfium::span(texts, count)); + auto family_span = UNSAFE_BUFFERS(pdfium::span(families, count)); + auto weight_span = UNSAFE_BUFFERS(pdfium::span(weights, count)); + auto italic_span = UNSAFE_BUFFERS(pdfium::span(italics, count)); + pdfium::span program_span; + if (programs) { + program_span = UNSAFE_BUFFERS(pdfium::span(programs, count)); + } + for (size_t i = 0; i < count; ++i) { + CPDF_FontResolver::Run run; + run.text = text_span[i] ? WideStringFromFPDFWideString(text_span[i]) + : WideString(); + run.family = family_span[i] ? WideStringFromFPDFWideString(family_span[i]) + : WideString(); + run.weight = weight_span[i]; + run.italic = !!italic_span[i]; + if (!program_span.empty() && program_span[i] > 0) { + run.program = static_cast(program_span[i]); + } + runs.push_back(std::move(run)); + } + auto resolution = std::make_unique(); + resolution->result = CPDF_FontResolver::Resolve(doc, annot_dict, runs); + return resolution.release(); +} + +FPDF_EXPORT void FPDF_CALLCONV +EPDFFontResolution_Close(EPDF_FONT_RESOLUTION resolution) { + delete resolution; +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontResolution_GetSource(EPDF_FONT_RESOLUTION resolution, int run) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + if (!entry) { + return -1; + } + switch (entry->face.source) { + case CPDF_AnnotFontMap::Source::kStandard14: + return EPDF_FONT_SOURCE_STANDARD; + case CPDF_AnnotFontMap::Source::kRegistered: + return EPDF_FONT_SOURCE_REGISTERED; + case CPDF_AnnotFontMap::Source::kDocumentProgram: + return EPDF_FONT_SOURCE_DOCUMENT_PROGRAM; + case CPDF_AnnotFontMap::Source::kDocumentFontDict: + return EPDF_FONT_SOURCE_DOCUMENT_FONT; + } +} + +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV +EPDFFontResolution_GetRegisteredFont(EPDF_FONT_RESOLUTION resolution, int run) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + return entry ? entry->face.registered_font_id + : CFX_FontRegistry::kInvalidFontId; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetProgramObjNum(EPDF_FONT_RESOLUTION resolution, int run) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + return entry ? entry->face.program_objnum : 0; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontResolution_GetProgramDisplayHash(EPDF_FONT_RESOLUTION resolution, + int run, + unsigned char* sha256) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + return entry && CopySha256(entry->display_sha256, sha256); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetBaseFontName(EPDF_FONT_RESOLUTION resolution, + int run, + char* buffer, + unsigned long buflen) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + if (!entry || entry->face.base_font_name.IsEmpty()) { + return 0; + } + // SAFETY: required from caller. + return NulTerminateMaybeCopyAndReturnLength( + entry->face.base_font_name, + UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetFamily(EPDF_FONT_RESOLUTION resolution, + int run, + FPDF_WCHAR* buffer, + unsigned long buflen) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + if (!entry) { + return 0; + } + // SAFETY: required from caller. + return Utf16EncodeMaybeCopyAndReturnLength( + entry->face.family, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontResolution_GetWeight(EPDF_FONT_RESOLUTION resolution, int run) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + return entry ? entry->face.weight : 0; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontResolution_IsItalic(EPDF_FONT_RESOLUTION resolution, int run) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + return entry && entry->face.italic; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontResolution_IsDegraded(EPDF_FONT_RESOLUTION resolution, int run) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + return entry && entry->face.degraded; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetFallbacks(EPDF_FONT_RESOLUTION resolution, + int run, + EPDF_FONT_ID* ids, + unsigned long capacity) { + const CPDF_FontResolver::ResolvedRun* entry = RunAt(resolution, run); + if (!entry) { + return 0; + } + return CopyFontIdsAndReturnCount(entry->fallbacks, ids, capacity); +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontResolution_CountMissingCharacters(EPDF_FONT_RESOLUTION resolution) { + return resolution + ? fxcrt::CollectionSize(resolution->result.missing_characters) + : -1; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetMissingCharacter(EPDF_FONT_RESOLUTION resolution, + int index, + FPDF_WCHAR* buffer, + unsigned long buflen) { + if (!resolution || + !fxcrt::IndexInBounds(resolution->result.missing_characters, index)) { + return 0; + } + // SAFETY: required from caller. + return Utf16EncodeMaybeCopyAndReturnLength( + resolution->result.missing_characters[index], + UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +// ---- Characters a write drew with no font +// ------------------------------------- + +FPDF_EXPORT void FPDF_CALLCONV +EPDFDoc_ClearMissingCharacters(FPDF_DOCUMENT document) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (doc) { + CPDF_DocumentFonts::For(doc)->ClearMissingCharacters(); + } +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_CountMissingCharacters(FPDF_DOCUMENT document) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc) { + return -1; + } + return fxcrt::CollectionSize( + CPDF_DocumentFonts::For(doc)->missing_characters()); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetMissingCharacter(FPDF_DOCUMENT document, + int index, + FPDF_WCHAR* buffer, + unsigned long buflen) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc) { + return 0; + } + const std::vector& missing = + CPDF_DocumentFonts::For(doc)->missing_characters(); + if (!fxcrt::IndexInBounds(missing, index)) { + return 0; + } + // SAFETY: required from caller. + return Utf16EncodeMaybeCopyAndReturnLength( + missing[index], UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} diff --git a/fpdfsdk/epdf_font_embeddertest.cpp b/fpdfsdk/epdf_font_embeddertest.cpp new file mode 100644 index 0000000000..6dd7910be6 --- /dev/null +++ b/fpdfsdk/epdf_font_embeddertest.cpp @@ -0,0 +1,1709 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "public/epdf_font.h" + +#include + +#include +#include +#include +#include +#include + +#include "core/fdrm/fx_crypt_sha.h" +#include "core/fpdfapi/font/cpdf_font.h" +#include "core/fpdfapi/page/cpdf_annotcontext.h" +#include "core/fpdfapi/page/cpdf_docpagedata.h" +#include "core/fpdfapi/parser/cpdf_array.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_name.h" +#include "core/fpdfapi/parser/cpdf_reference.h" +#include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fpdfapi/parser/cpdf_stream_acc.h" +#include "core/fpdfdoc/cpdf_documentfonts.h" +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/widestring.h" +#include "core/fxge/cfx_face.h" +#include "core/fxge/cfx_font.h" +#include "core/fxge/cfx_glyphoutline.h" +#include "core/fxge/cfx_sfnt.h" +#include "core/fxge/fx_fontencoding.h" +#include "fpdfsdk/cpdfsdk_helpers.h" +#include "fpdfsdk/epdf_edit_test_util.h" +#include "public/cpp/fpdf_scopers.h" +#include "public/epdf_annot.h" +#include "public/epdf_form.h" +#include "public/epdf_layer.h" +#include "public/fpdf_annot.h" +#include "public/fpdf_edit.h" +#include "testing/embedder_test.h" +#include "testing/embedpdf_form_writes.h" +#include "testing/fx_string_testhelpers.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/utils/file_util.h" +#include "testing/utils/path_service.h" + +namespace { + +class ScopedRegisteredFonts { + public: + ScopedRegisteredFonts() { EPDFFont_ClearRegisteredFonts(); } + ~ScopedRegisteredFonts() { EPDFFont_ClearRegisteredFonts(); } +}; + +std::vector LoadThirdPartyFont(const char* path) { + std::string font_path = PathService::GetThirdPartyFilePath(path); + if (font_path.empty()) { + ADD_FAILURE() << "Failed to find " << path; + return {}; + } + return GetFileContents(font_path.c_str()); +} + +std::vector LoadRoboto() { + return LoadThirdPartyFont("harfbuzz/src/perf/fonts/Roboto-Regular.ttf"); +} + +// |font| with every table one byte further on, as a font whose tables were +// written back to back is: FreeType reads it, a browser refuses it. +std::vector WithTablesOffByOne(const std::vector& font) { + const size_t count = (size_t{font[4]} << 8) | font[5]; + const size_t tables_start = 12 + count * 16; + std::vector moved(font.begin(), font.begin() + tables_start); + for (size_t i = 0; i < count; ++i) { + const size_t offset_at = 12 + i * 16 + 8; + uint32_t offset = 0; + for (size_t j = 0; j < 4; ++j) { + offset = (offset << 8) | moved[offset_at + j]; + } + ++offset; + for (size_t j = 0; j < 4; ++j) { + moved[offset_at + j] = static_cast(offset >> (24 - 8 * j)); + } + } + moved.push_back(0); + moved.insert(moved.end(), font.begin() + tables_start, font.end()); + return moved; +} + +// The tables of an sfnt by tag, as written. +std::map> TablesOf( + const std::vector& font) { + auto u32 = [&](size_t at) { + return (uint32_t{font[at]} << 24) | (uint32_t{font[at + 1]} << 16) | + (uint32_t{font[at + 2]} << 8) | font[at + 3]; + }; + std::map> tables; + const size_t count = (size_t{font[4]} << 8) | font[5]; + for (size_t i = 0; i < count; ++i) { + const size_t record = 12 + i * 16; + const std::string tag(font.begin() + record, font.begin() + record + 4); + const uint32_t offset = u32(record + 8); + const uint32_t length = u32(record + 12); + std::vector table(font.begin() + offset, + font.begin() + offset + length); + // checkSumAdjustment covers the whole font: it moves with the layout. + if (tag == "head") { + std::fill(table.begin() + 8, table.begin() + 12, 0); + } + tables[tag] = std::move(table); + } + return tables; +} + +std::vector LoadDroidSansFallback() { + std::string font_path = + PathService::GetTestFilePath("fonts/DroidSansFallbackFull.ttf"); + if (font_path.empty()) { + ADD_FAILURE() << "Failed to find DroidSansFallbackFull"; + return {}; + } + return GetFileContents(font_path.c_str()); +} + +// Roboto with its OS/2 fsType set to preview-and-print. +std::vector RobotoPreviewAndPrint() { + std::vector font = LoadRoboto(); + if (font.size() < 12) { + return {}; + } + const size_t tables = (font[4] << 8) | font[5]; + for (size_t i = 0; i < tables; ++i) { + const size_t record = 12 + 16 * i; + if (record + 16 > font.size()) { + break; + } + if (font[record] == 'O' && font[record + 1] == 'S' && + font[record + 2] == '/' && font[record + 3] == '2') { + const size_t offset = (static_cast(font[record + 8]) << 24) | + (font[record + 9] << 16) | + (font[record + 10] << 8) | font[record + 11]; + font[offset + 8] = 0x00; + font[offset + 9] = 0x04; // fsType: preview-and-print + return font; + } + } + return {}; +} + +std::wstring Utf16Of(std::vector buffer) { + return GetPlatformWString(buffer.data()); +} + +std::wstring FaceFamily(EPDF_FONT_FACES faces, int face) { + const unsigned long length = EPDFFontFaces_GetFamily(faces, face, nullptr, 0); + if (length == 0) { + return L""; + } + std::vector buffer = GetFPDFWideStringBuffer(length); + EPDFFontFaces_GetFamily(faces, face, buffer.data(), length); + return Utf16Of(std::move(buffer)); +} + +std::string ProgramPostScriptName(EPDF_FONT_FACES faces, + int face, + int program) { + const unsigned long length = + EPDFFontFaces_GetProgramPostScriptName(faces, face, program, nullptr, 0); + if (length == 0) { + return ""; + } + std::vector buffer(length); + EPDFFontFaces_GetProgramPostScriptName(faces, face, program, buffer.data(), + length); + return std::string(buffer.data()); +} + +struct FoundProgram { + int face = -1; + int program = -1; +}; + +// The first program of the first face named |family|. +std::optional FindProgram(EPDF_FONT_FACES faces, + const std::wstring& family) { + for (int face = 0; face < EPDFFontFaces_Count(faces); ++face) { + if (FaceFamily(faces, face) != family) { + continue; + } + if (EPDFFontFaces_CountPrograms(faces, face) > 0) { + return FoundProgram{face, 0}; + } + } + return std::nullopt; +} + +bool HasFace(EPDF_FONT_FACES faces, const std::wstring& family) { + for (int face = 0; face < EPDFFontFaces_Count(faces); ++face) { + if (FaceFamily(faces, face) == family) { + return true; + } + } + return false; +} + +std::vector MissingCharacters(FPDF_DOCUMENT document) { + std::vector missing; + for (int i = 0; i < EPDFDoc_CountMissingCharacters(document); ++i) { + const unsigned long length = + EPDFDoc_GetMissingCharacter(document, i, nullptr, 0); + std::vector buffer = GetFPDFWideStringBuffer(length); + EPDFDoc_GetMissingCharacter(document, i, buffer.data(), length); + missing.push_back(Utf16Of(std::move(buffer))); + } + return missing; +} + +// The widget's normal appearance stream, decoded. +std::string NormalAppearanceOf(FPDF_ANNOTATION annot) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + RetainPtr ap = + context->GetAnnotDict()->GetDictFor("AP"); + RetainPtr normal = ap ? ap->GetStreamFor("N") : nullptr; + if (!normal) { + return std::string(); + } + auto access = pdfium::MakeRetain(normal); + access->LoadAllDataFiltered(); + const pdfium::span data = access->GetSpan(); + return std::string(data.begin(), data.end()); +} + +// Whether the widget's appearance names a registered font among its fonts. +bool AppearanceUsesRegisteredFont(FPDF_ANNOTATION annot, EPDF_FONT_ID id) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + RetainPtr ap = + context->GetAnnotDict()->GetDictFor("AP"); + RetainPtr normal = ap ? ap->GetStreamFor("N") : nullptr; + RetainPtr resources = + normal ? normal->GetDict()->GetDictFor("Resources") : nullptr; + RetainPtr fonts = + resources ? resources->GetDictFor("Font") : nullptr; + return fonts && + fonts->KeyExist(ByteString::Format("ERegF%u", id).AsStringView()); +} + +ByteString DefaultAppearanceFontName(FPDF_ANNOTATION annot) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + const ByteString da = context->GetAnnotDict()->GetByteStringFor("DA"); + // "0 g /EDocF12 12 Tf": the name after the slash. + std::optional slash = da.Find('/'); + if (!slash.has_value()) { + return ByteString(); + } + ByteString rest = da.Substr(*slash + 1); + std::optional space = rest.Find(' '); + return space.has_value() ? rest.First(*space) : rest; +} + +// The object number of the program the appearance's |alias| font embeds. +uint32_t AppearanceFontProgram(FPDF_ANNOTATION annot, const ByteString& alias) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + RetainPtr ap = + context->GetAnnotDict()->GetDictFor("AP"); + RetainPtr normal = ap ? ap->GetStreamFor("N") : nullptr; + RetainPtr resources = + normal ? normal->GetDict()->GetDictFor("Resources") : nullptr; + RetainPtr fonts = + resources ? resources->GetDictFor("Font") : nullptr; + RetainPtr font = + fonts ? fonts->GetDictFor(alias.AsStringView()) : nullptr; + if (!font || font->GetNameFor("Subtype") != "Type0") { + return 0; + } + RetainPtr descendants = + font->GetArrayFor("DescendantFonts"); + RetainPtr descendant = + descendants ? descendants->GetDictAt(0) : nullptr; + RetainPtr descriptor = + descendant ? descendant->GetDictFor("FontDescriptor") : nullptr; + RetainPtr program = + descriptor ? descriptor->GetStreamFor("FontFile2") : nullptr; + return program ? program->GetObjNum() : 0; +} + +class EPDFFontEmbedderTest : public EmbedderTest { + protected: + // A page with a text field whose widget writes with |font_id|, filled + // with |value|: under the default embedding policy, the font's whole + // program goes into the document. + uint32_t AddRegisteredFontField(FPDF_PAGE page, + EPDF_FONT_ID font_id, + const wchar_t* name, + float top, + const std::wstring& value) { + ScopedFPDFAnnotation annot(EPDFPage_CreateAnnot(page, FPDF_ANNOT_WIDGET)); + EXPECT_TRUE(annot); + const FS_RECTF rect{50.0f, top, 350.0f, top - 30.0f}; + EXPECT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + EXPECT_TRUE(EPDFAnnot_SetDefaultAppearanceRegisteredFont( + annot.get(), font_id, 12.0f, 0, 0, 0)); + return AttachAndFill(annot.get(), name, value); + } + + uint32_t AttachAndFill(FPDF_ANNOTATION annot, + const wchar_t* name, + const std::wstring& value) { + ScopedFPDFWideString field_name = GetFPDFWideString(name); + const uint32_t field = EPDFForm_CreateField( + document(), 4 /* EPDF_FORMFIELD_FAMILY_TEXT */, field_name.get(), 0); + EXPECT_GT(field, 0u); + EXPECT_TRUE(EPDFForm_AttachWidget( + document(), field, EPDFAnnot_GetObjectNumber(annot), nullptr, 0)); + EXPECT_TRUE(embedpdf_test::FillTextField(document(), field, value)); + EXPECT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot)); + return field; + } +}; + +} // namespace + +// The registry hands out a font's program as the writer embeds it, its +// hash, the characters it maps and the order fallbacks are tried in. +TEST_F(EPDFFontEmbedderTest, RegisteredFontProgramCoverageAndFallbackOrder) { + ScopedRegisteredFonts scoped_fonts; + std::vector roboto = LoadRoboto(); + ASSERT_FALSE(roboto.empty()); + const EPDF_FONT_ID regular = EPDFFont_RegisterMemFont64( + "Roboto", /*weight=*/400, /*italic=*/0, roboto.data(), roboto.size()); + const EPDF_FONT_ID bold = + EPDFFont_RegisterMemFont64("Roboto Heavy", /*weight=*/700, /*italic=*/0, + roboto.data(), roboto.size()); + ASSERT_NE(0u, regular); + ASSERT_NE(0u, bold); + + ASSERT_EQ(roboto.size(), EPDFFont_GetProgramData(regular, nullptr, 0)); + std::vector bytes(roboto.size()); + ASSERT_EQ(roboto.size(), + EPDFFont_GetProgramData(regular, bytes.data(), bytes.size())); + EXPECT_EQ(roboto, bytes); + + unsigned char hash[32] = {}; + ASSERT_TRUE(EPDFFont_GetProgramHash(regular, hash)); + const DataVector expected = CryptSha256Generate(roboto); + EXPECT_EQ(std::vector(expected.begin(), expected.end()), + std::vector(hash, hash + 32)); + EXPECT_FALSE(EPDFFont_GetProgramHash(9999, hash)); + + const unsigned long range_count = EPDFFont_GetCoverage(regular, nullptr, 0); + ASSERT_GT(range_count, 0u); + std::vector ranges(range_count * 2); + ASSERT_EQ(range_count, + EPDFFont_GetCoverage(regular, ranges.data(), range_count)); + bool covers_a = false; + for (unsigned long i = 0; i < range_count; ++i) { + covers_a |= ranges[2 * i] <= 'A' && 'A' < ranges[2 * i] + ranges[2 * i + 1]; + } + EXPECT_TRUE(covers_a); + + float ascent = 0; + float descent = 0; + ASSERT_TRUE(EPDFFont_GetMetrics(regular, &ascent, &descent)); + EXPECT_GT(ascent, 0.5f); + EXPECT_GT(descent, 0.0f); + EXPECT_GT(EPDFFont_GetPostScriptName(regular, nullptr, 0), 1u); + EXPECT_EQ(EPDF_FONT_FORMAT_TRUETYPE, EPDFFont_GetProgramFormat(regular)); + EXPECT_EQ(-1, EPDFFont_GetProgramFormat(9999)); + + ASSERT_TRUE(EPDFFont_AddFallbackFont(regular, /*tier=*/0)); + ASSERT_TRUE(EPDFFont_AddFallbackFont(bold, /*tier=*/0)); + EPDF_FONT_ID order[2] = {}; + ASSERT_EQ(2u, EPDFFont_GetFallbackOrder(700, false, order, 2)); + EXPECT_EQ(bold, order[0]); + EXPECT_EQ(regular, order[1]); + ASSERT_EQ(2u, EPDFFont_GetFallbackOrder(400, false, order, 2)); + EXPECT_EQ(regular, order[0]); + EXPECT_EQ(bold, order[1]); + + // A lower tier comes first, whatever the style: moved to tier 1, the + // regular font follows the bold one for regular text too. + ASSERT_TRUE(EPDFFont_AddFallbackFont(regular, /*tier=*/1)); + ASSERT_EQ(2u, EPDFFont_GetFallbackOrder(400, false, order, 2)); + EXPECT_EQ(bold, order[0]); + EXPECT_EQ(regular, order[1]); +} + +// The last resort maps every code point but the controls and the surrogates +// to one box: two ranges, however many code points they hold. It isn't a +// fallback, and goes with the registered fonts. +TEST_F(EPDFFontEmbedderTest, LastResortFont) { + ScopedRegisteredFonts scoped_fonts; + std::vector box = GetFileContents( + PathService::GetTestFilePath("fonts/last_resort.ttf").c_str()); + ASSERT_FALSE(box.empty()); + const EPDF_FONT_ID last_resort = EPDFFont_RegisterMemFont64( + "Last Resort", /*weight=*/400, /*italic=*/0, box.data(), box.size()); + ASSERT_NE(0u, last_resort); + uint32_t ranges[4] = {}; + ASSERT_EQ(2u, EPDFFont_GetCoverage(last_resort, ranges, 2)); + EXPECT_EQ(0x20u, ranges[0]); + EXPECT_EQ(0xD800u - 0x20u, ranges[1]); + EXPECT_EQ(0xE000u, ranges[2]); + EXPECT_EQ(0x110000u - 0xE000u, ranges[3]); + + EXPECT_EQ(0u, EPDFFont_GetLastResortFont()); + EXPECT_FALSE(EPDFFont_SetLastResortFont(9999)); + ASSERT_TRUE(EPDFFont_SetLastResortFont(last_resort)); + EXPECT_EQ(last_resort, EPDFFont_GetLastResortFont()); + EPDFFont_ClearFallbackFonts(); + EXPECT_EQ(last_resort, EPDFFont_GetLastResortFont()); + EXPECT_EQ(0u, EPDFFont_GetFallbackOrder(400, false, nullptr, 0)); + ASSERT_TRUE(EPDFFont_SetLastResortFont(0)); + EXPECT_EQ(0u, EPDFFont_GetLastResortFont()); + ASSERT_TRUE(EPDFFont_SetLastResortFont(last_resort)); + EPDFFont_ClearRegisteredFonts(); + EXPECT_EQ(0u, EPDFFont_GetLastResortFont()); +} + +// A whole program the document holds is listed with its facts, and its bytes +// and coverage are handed out by object number; nothing else is. +TEST_F(EPDFFontEmbedderTest, ListsADocumentProgramAndHandsOutItsBytes) { + ScopedRegisteredFonts scoped_fonts; + std::vector roboto = LoadRoboto(); + ASSERT_FALSE(roboto.empty()); + const EPDF_FONT_ID roboto_id = EPDFFont_RegisterMemFont64( + "Roboto", /*weight=*/400, /*italic=*/0, roboto.data(), roboto.size()); + ASSERT_NE(0u, roboto_id); + + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ASSERT_TRUE(page); + AddRegisteredFontField(page.get(), roboto_id, L"name", 320.0f, L"hello"); + + EPDF_FONT_FACES faces = EPDFFontFaces_Load(document()); + ASSERT_TRUE(faces); + std::optional found = FindProgram(faces, L"Roboto"); + ASSERT_TRUE(found.has_value()); + const unsigned long objnum = + EPDFFontFaces_GetProgramObjNum(faces, found->face, found->program); + EXPECT_NE(0u, objnum); + EXPECT_EQ(400, EPDFFontFaces_GetWeight(faces, found->face)); + EXPECT_FALSE(EPDFFontFaces_IsItalic(faces, found->face)); + EXPECT_EQ(EPDF_FONT_FORMAT_TRUETYPE, + EPDFFontFaces_GetProgramFormat(faces, found->face, found->program)); + EXPECT_EQ(EPDF_FONT_EMBEDDED_WHOLE, EPDFFontFaces_GetProgramEmbedded( + faces, found->face, found->program)); + EXPECT_EQ(EPDF_FONT_EMBEDDING_INSTALLABLE, + EPDFFontFaces_GetProgramEmbeddingPermission(faces, found->face, + found->program)); + EXPECT_TRUE( + EPDFFontFaces_ProgramMapsUnicode(faces, found->face, found->program)); + EXPECT_FALSE( + ProgramPostScriptName(faces, found->face, found->program).empty()); + unsigned char hash[32] = {}; + unsigned char registered_hash[32] = {}; + ASSERT_TRUE( + EPDFFontFaces_GetProgramHash(faces, found->face, found->program, hash)); + ASSERT_TRUE(EPDFFont_GetProgramHash(roboto_id, registered_hash)); + EXPECT_EQ(std::vector(registered_hash, registered_hash + 32), + std::vector(hash, hash + 32)); + // A browser loads it as it is. + unsigned char display_hash[32] = {}; + ASSERT_TRUE(EPDFFontFaces_GetProgramDisplayHash( + faces, found->face, found->program, display_hash)); + EXPECT_EQ(std::vector(hash, hash + 32), + std::vector(display_hash, display_hash + 32)); + float ascent = 0; + float descent = 0; + EXPECT_TRUE(EPDFFontFaces_GetProgramMetrics( + faces, found->face, found->program, &ascent, &descent)); + EPDFFontFaces_Close(faces); + + ASSERT_EQ(roboto.size(), + EPDFDoc_GetFontProgramData(document(), objnum, nullptr, 0)); + std::vector bytes(roboto.size()); + ASSERT_EQ(roboto.size(), EPDFDoc_GetFontProgramData( + document(), objnum, bytes.data(), bytes.size())); + EXPECT_EQ(roboto, bytes); + ASSERT_EQ(roboto.size(), + EPDFDoc_GetFontDisplayProgramData(document(), display_hash, + bytes.data(), bytes.size())); + EXPECT_EQ(roboto, bytes); + EXPECT_GT(EPDFDoc_GetFontDisplayProgramCoverage(document(), display_hash, + nullptr, 0), + 0u); + // The catalog is an object, not a font program. + EXPECT_EQ(0u, EPDFDoc_GetFontProgramData(document(), 1, nullptr, 0)); +} + +// A program whose tables don't start on four-byte boundaries goes to a +// browser realigned: the same tables, where the font format puts them, under +// a hash of their own. The engine writes with the program as it is. +TEST_F(EPDFFontEmbedderTest, RealignsAProgramABrowserWouldRefuse) { + ScopedRegisteredFonts scoped_fonts; + std::vector roboto = LoadRoboto(); + ASSERT_FALSE(roboto.empty()); + ASSERT_TRUE(SfntTablesAligned(roboto)); + const std::vector misaligned = WithTablesOffByOne(roboto); + ASSERT_FALSE(SfntTablesAligned(misaligned)); + const EPDF_FONT_ID font_id = + EPDFFont_RegisterMemFont64("Roboto", /*weight=*/400, /*italic=*/0, + misaligned.data(), misaligned.size()); + ASSERT_NE(0u, font_id); + + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ASSERT_TRUE(page); + AddRegisteredFontField(page.get(), font_id, L"name", 320.0f, L"hello"); + + EPDF_FONT_FACES faces = EPDFFontFaces_Load(document()); + ASSERT_TRUE(faces); + std::optional found = FindProgram(faces, L"Roboto"); + ASSERT_TRUE(found.has_value()); + const unsigned long objnum = + EPDFFontFaces_GetProgramObjNum(faces, found->face, found->program); + ASSERT_NE(0u, objnum); + EXPECT_TRUE( + EPDFFontFaces_ProgramMapsUnicode(faces, found->face, found->program)); + unsigned char hash[32] = {}; + unsigned char display_hash[32] = {}; + ASSERT_TRUE( + EPDFFontFaces_GetProgramHash(faces, found->face, found->program, hash)); + ASSERT_TRUE(EPDFFontFaces_GetProgramDisplayHash( + faces, found->face, found->program, display_hash)); + EPDFFontFaces_Close(faces); + + // The document holds the program as it was registered. + std::vector program(misaligned.size()); + ASSERT_EQ(misaligned.size(), + EPDFDoc_GetFontProgramData(document(), objnum, program.data(), + program.size())); + EXPECT_EQ(misaligned, program); + + const unsigned long length = + EPDFDoc_GetFontDisplayProgramData(document(), display_hash, nullptr, 0); + ASSERT_GT(length, 0u); + std::vector display(length); + ASSERT_EQ(length, + EPDFDoc_GetFontDisplayProgramData(document(), display_hash, + display.data(), display.size())); + EXPECT_TRUE(SfntTablesAligned(display)); + EXPECT_EQ(TablesOf(misaligned), TablesOf(display)); + // Named by its own bytes, not the program's. + const DataVector display_sha256 = CryptSha256Generate(display); + EXPECT_EQ(std::vector(display_sha256.begin(), display_sha256.end()), + std::vector(display_hash, display_hash + 32)); + EXPECT_NE(std::vector(hash, hash + 32), + std::vector(display_hash, display_hash + 32)); + // The whole font, padded to whole words, sums to the constant head's + // adjustment makes it. + ASSERT_EQ(0u, display.size() % 4); + uint32_t sum = 0; + for (size_t i = 0; i < display.size(); i += 4) { + sum += (uint32_t{display[i]} << 24) | (uint32_t{display[i + 1]} << 16) | + (uint32_t{display[i + 2]} << 8) | display[i + 3]; + } + EXPECT_EQ(0xb1b0afbau, sum); + // A browser draws the characters the program's own cmap maps. + const unsigned long range_count = EPDFDoc_GetFontDisplayProgramCoverage( + document(), display_hash, nullptr, 0); + std::vector ranges(range_count * 2); + EPDFDoc_GetFontDisplayProgramCoverage(document(), display_hash, ranges.data(), + range_count); + auto covers = [&](uint32_t scalar) { + for (size_t i = 0; i + 1 < ranges.size(); i += 2) { + if (scalar >= ranges[i] && scalar < ranges[i] + ranges[i + 1]) { + return true; + } + } + return false; + }; + EXPECT_TRUE(covers('A')); + EXPECT_TRUE(covers(0xe9)); +} + +// A widget writes with a document program by its object number: /DA names a +// session alias, the appearance embeds that very program, characters it +// lacks are reported, and the box's font reads back as that program. +TEST_F(EPDFFontEmbedderTest, WidgetWritesWithADocumentProgram) { + ScopedRegisteredFonts scoped_fonts; + std::vector roboto = LoadRoboto(); + ASSERT_FALSE(roboto.empty()); + const EPDF_FONT_ID roboto_id = EPDFFont_RegisterMemFont64( + "Roboto", /*weight=*/400, /*italic=*/0, roboto.data(), roboto.size()); + ASSERT_NE(0u, roboto_id); + + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ASSERT_TRUE(page); + AddRegisteredFontField(page.get(), roboto_id, L"first", 320.0f, L"hello"); + EPDF_FONT_FACES faces = EPDFFontFaces_Load(document()); + std::optional found = FindProgram(faces, L"Roboto"); + ASSERT_TRUE(found.has_value()); + const unsigned long objnum = + EPDFFontFaces_GetProgramObjNum(faces, found->face, found->program); + const std::string postscript_name = + ProgramPostScriptName(faces, found->face, found->program); + EPDFFontFaces_Close(faces); + + // The font is the document's alone now. + EPDFFont_ClearRegisteredFonts(); + + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_WIDGET)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 250.0f, 350.0f, 220.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + EXPECT_FALSE( + EPDFAnnot_SetDefaultAppearanceDocumentFont(annot.get(), 1, 12, 0, 0, 0)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearanceDocumentFont(annot.get(), objnum, + 12, 0, 0, 0)); + const ByteString alias = DefaultAppearanceFontName(annot.get()); + EXPECT_EQ(ByteString::Format("EDocF%lu", objnum), alias); + + EPDFDoc_ClearMissingCharacters(document()); + const uint32_t second = + AttachAndFill(annot.get(), L"second", L"\x03A9 and \x4E2D"); + EXPECT_EQ(objnum, AppearanceFontProgram(annot.get(), alias)); + EXPECT_EQ(std::vector{L"\x4E2D"}, + MissingCharacters(document())); + + const unsigned long name_length = + EPDFAnnot_GetDefaultAppearanceFontPostScriptName(annot.get(), nullptr, 0); + ASSERT_GT(name_length, 1u); + std::vector name(name_length); + EPDFAnnot_GetDefaultAppearanceFontPostScriptName(annot.get(), name.data(), + name_length); + EXPECT_EQ(postscript_name, std::string(name.data())); + + int weight = 0; + FPDF_BOOL italic = true; + const unsigned long family_length = EPDFAnnot_GetDefaultAppearanceFontFace( + annot.get(), nullptr, 0, &weight, &italic); + std::vector family = GetFPDFWideStringBuffer(family_length); + EPDFAnnot_GetDefaultAppearanceFontFace(annot.get(), family.data(), + family_length, &weight, &italic); + EXPECT_EQ(L"Roboto", Utf16Of(std::move(family))); + EXPECT_EQ(400, weight); + EXPECT_FALSE(italic); + + // Once the appearance exists, /DR holds the composite font over the + // program, and a new value with a character no earlier one used lays out + // with the program's own cmap. + RetainPtr acroform = + CPDFDocumentFromFPDFDocument(document()) + ->GetRoot() + ->GetDictFor("AcroForm"); + RetainPtr dr_font = + acroform->GetDictFor("DR")->GetDictFor("Font")->GetDictFor( + alias.AsStringView()); + ASSERT_TRUE(dr_font); + EXPECT_EQ("Type0", dr_font->GetNameFor("Subtype")); + EPDFDoc_ClearMissingCharacters(document()); + ASSERT_TRUE(embedpdf_test::FillTextField(document(), second, L"\x03A8")); + ASSERT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot.get())); + EXPECT_EQ(objnum, AppearanceFontProgram(annot.get(), alias)); + EXPECT_TRUE(MissingCharacters(document()).empty()); +} + +// What a write would draw with, without writing: the standard face, a +// registered font, a substitute, the fallbacks in order, and the characters +// no font has. +TEST_F(EPDFFontEmbedderTest, ResolvesTheFontsAWriteWouldUse) { + ScopedRegisteredFonts scoped_fonts; + std::vector roboto = LoadRoboto(); + std::vector droid = LoadDroidSansFallback(); + ASSERT_FALSE(roboto.empty()); + ASSERT_FALSE(droid.empty()); + const EPDF_FONT_ID roboto_id = EPDFFont_RegisterMemFont64( + "Roboto", /*weight=*/400, /*italic=*/0, roboto.data(), roboto.size()); + const EPDF_FONT_ID droid_id = + EPDFFont_RegisterMemFont64("DroidSansFallbackFull", /*weight=*/400, + /*italic=*/0, droid.data(), droid.size()); + ASSERT_NE(0u, roboto_id); + ASSERT_NE(0u, droid_id); + ASSERT_TRUE(EPDFFont_AddFallbackFont(droid_id, /*tier=*/0)); + + CreateEmptyDocument(); + ScopedFPDFWideString text0 = GetFPDFWideString(L"Hello \x4E2D"); + ScopedFPDFWideString text1 = GetFPDFWideString(L"Roboto here"); + ScopedFPDFWideString text2 = GetFPDFWideString(L"X"); + ScopedFPDFWideString family0 = GetFPDFWideString(L"Helvetica"); + ScopedFPDFWideString family1 = GetFPDFWideString(L"Roboto"); + ScopedFPDFWideString family2 = GetFPDFWideString(L"NoSuchFamily"); + FPDF_WIDESTRING texts[] = {text0.get(), text1.get(), text2.get()}; + FPDF_WIDESTRING families[] = {family0.get(), family1.get(), family2.get()}; + const int weights[] = {400, 400, 700}; + const FPDF_BOOL italics[] = {false, false, false}; + + EPDF_FONT_RESOLUTION resolution = EPDFDoc_ResolveFonts( + document(), nullptr, 3, texts, families, weights, italics, nullptr); + ASSERT_TRUE(resolution); + EXPECT_EQ(EPDF_FONT_SOURCE_STANDARD, + EPDFFontResolution_GetSource(resolution, 0)); + char base_font[64] = {}; + EPDFFontResolution_GetBaseFontName(resolution, 0, base_font, + sizeof(base_font)); + EXPECT_STREQ("Helvetica", base_font); + EXPECT_FALSE(EPDFFontResolution_IsDegraded(resolution, 0)); + EPDF_FONT_ID fallbacks[2] = {}; + ASSERT_EQ(1u, EPDFFontResolution_GetFallbacks(resolution, 0, fallbacks, 2)); + EXPECT_EQ(droid_id, fallbacks[0]); + + EXPECT_EQ(EPDF_FONT_SOURCE_REGISTERED, + EPDFFontResolution_GetSource(resolution, 1)); + EXPECT_EQ(roboto_id, EPDFFontResolution_GetRegisteredFont(resolution, 1)); + + EXPECT_EQ(EPDF_FONT_SOURCE_STANDARD, + EPDFFontResolution_GetSource(resolution, 2)); + EXPECT_TRUE(EPDFFontResolution_IsDegraded(resolution, 2)); + EXPECT_EQ(700, EPDFFontResolution_GetWeight(resolution, 2)); + EXPECT_EQ(0, EPDFFontResolution_CountMissingCharacters(resolution)); + EPDFFontResolution_Close(resolution); + + // Without the fallback, the CJK character has no font. + EPDFFont_ClearFallbackFonts(); + resolution = EPDFDoc_ResolveFonts(document(), nullptr, 1, texts, families, + weights, italics, nullptr); + ASSERT_TRUE(resolution); + EXPECT_EQ(0u, EPDFFontResolution_GetFallbacks(resolution, 0, nullptr, 0)); + ASSERT_EQ(1, EPDFFontResolution_CountMissingCharacters(resolution)); + const unsigned long length = + EPDFFontResolution_GetMissingCharacter(resolution, 0, nullptr, 0); + std::vector missing = GetFPDFWideStringBuffer(length); + EPDFFontResolution_GetMissingCharacter(resolution, 0, missing.data(), length); + EXPECT_EQ(L"\x4E2D", Utf16Of(std::move(missing))); + EPDFFontResolution_Close(resolution); +} + +// A family the document holds as a whole program, and nobody registered, +// resolves to that program, which a browser loads under the hash the faces +// list for it. +TEST_F(EPDFFontEmbedderTest, ResolvesADocumentProgram) { + ScopedRegisteredFonts scoped_fonts; + std::vector roboto = LoadRoboto(); + ASSERT_FALSE(roboto.empty()); + const EPDF_FONT_ID roboto_id = EPDFFont_RegisterMemFont64( + "Roboto", /*weight=*/400, /*italic=*/0, roboto.data(), roboto.size()); + ASSERT_NE(0u, roboto_id); + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ASSERT_TRUE(page); + AddRegisteredFontField(page.get(), roboto_id, L"name", 320.0f, L"hello"); + EPDF_FONT_FACES faces = EPDFFontFaces_Load(document()); + std::optional found = FindProgram(faces, L"Roboto"); + ASSERT_TRUE(found.has_value()); + const unsigned long objnum = + EPDFFontFaces_GetProgramObjNum(faces, found->face, found->program); + unsigned char listed_hash[32] = {}; + ASSERT_TRUE(EPDFFontFaces_GetProgramDisplayHash(faces, found->face, + found->program, listed_hash)); + EPDFFontFaces_Close(faces); + EPDFFont_ClearRegisteredFonts(); + + ScopedFPDFWideString text = GetFPDFWideString(L"Hi"); + ScopedFPDFWideString family = GetFPDFWideString(L"Roboto"); + FPDF_WIDESTRING texts[] = {text.get()}; + FPDF_WIDESTRING families[] = {family.get()}; + const int weights[] = {400}; + const FPDF_BOOL italics[] = {false}; + EPDF_FONT_RESOLUTION resolution = EPDFDoc_ResolveFonts( + document(), nullptr, 1, texts, families, weights, italics, nullptr); + ASSERT_TRUE(resolution); + EXPECT_EQ(EPDF_FONT_SOURCE_DOCUMENT_PROGRAM, + EPDFFontResolution_GetSource(resolution, 0)); + EXPECT_EQ(objnum, EPDFFontResolution_GetProgramObjNum(resolution, 0)); + unsigned char hash[32] = {}; + ASSERT_TRUE(EPDFFontResolution_GetProgramDisplayHash(resolution, 0, hash)); + EXPECT_EQ(std::vector(listed_hash, listed_hash + 32), + std::vector(hash, hash + 32)); + EPDFFontResolution_Close(resolution); +} + +// The walk finds fonts wherever a drawing can use them: inherited page +// resources, a form XObject (that names itself), every appearance state, a +// field's own /DR, the form's /DR, and a Type 3 font's resources. +TEST_F(EPDFFontEmbedderTest, FindsFontsWhereverADrawingUsesThem) { + const std::string pdf = MakePdf({ + "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [8 0 R] " + "/DR << /Font << /FDR 9 0 R >> >> >> >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 /Resources << /Font << /FInh 10 " + "0 R /FT3 14 0 R >> /XObject << /X1 4 0 R >> >> >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Annots [5 0 R] " + ">>", + "<< /Type /XObject /Subtype /Form /BBox [0 0 10 10] /Resources << /Font " + "<< /FX 11 0 R >> /XObject << /Self 4 0 R >> >> /Length 0 >>\n" + "stream\n\nendstream", + "<< /Type /Annot /Subtype /Square /Rect [10 10 50 50] /AP << /N << /On " + "6 0 R /Off 7 0 R >> >> >>", + "<< /BBox [0 0 10 10] /Resources << /Font << /FOn 12 0 R >> >> /Length " + "0 >>\nstream\n\nendstream", + Stream(""), + "<< /T (field) /FT /Tx /DR << /Font << /FField 13 0 R >> >> >>", + "<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>", + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", + "<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman >>", + "<< /Type /Font /Subtype /Type1 /BaseFont /Symbol >>", + "<< /Type /Font /Subtype /Type1 /BaseFont /ZapfDingbats >>", + "<< /Type /Font /Subtype /Type3 /FontBBox [0 0 1 1] /FontMatrix [1 0 0 1 " + "0 0] /CharProcs << >> /Encoding << /Differences [] >> /FirstChar 0 " + "/LastChar 0 /Widths [0] /Resources << /Font << /FG 15 0 R >> >> >>", + "<< /Type /Font /Subtype /TrueType /BaseFont /Georgia >>", + }); + ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(doc); + EPDF_FONT_FACES faces = EPDFFontFaces_Load(doc.get()); + ASSERT_TRUE(faces); + for (const wchar_t* family : {L"Courier", L"Helvetica", L"Times", L"Symbol", + L"ZapfDingbats", L"Georgia"}) { + EXPECT_TRUE(HasFace(faces, family)) << family; + } + std::optional georgia = FindProgram(faces, L"Georgia"); + ASSERT_TRUE(georgia.has_value()); + EXPECT_EQ(EPDF_FONT_FORMAT_NONE, EPDFFontFaces_GetProgramFormat( + faces, georgia->face, georgia->program)); + EXPECT_EQ( + EPDF_FONT_EMBEDDED_NONE, + EPDFFontFaces_GetProgramEmbedded(faces, georgia->face, georgia->program)); + EXPECT_EQ(0u, EPDFFontFaces_GetProgramObjNum(faces, georgia->face, + georgia->program)); + EXPECT_EQ(-1, EPDFFontFaces_GetProgramEmbeddingPermission( + faces, georgia->face, georgia->program)); + unsigned char hash[32] = {}; + EXPECT_FALSE(EPDFFontFaces_GetProgramHash(faces, georgia->face, + georgia->program, hash)); + EXPECT_FALSE(EPDFFontFaces_GetProgramDisplayHash(faces, georgia->face, + georgia->program, hash)); + bool type3 = false; + for (int face = 0; face < EPDFFontFaces_Count(faces); ++face) { + for (int program = 0; program < EPDFFontFaces_CountPrograms(faces, face); + ++program) { + type3 |= EPDFFontFaces_GetProgramFormat(faces, face, program) == + EPDF_FONT_FORMAT_TYPE3; + } + } + EXPECT_TRUE(type3); + EPDFFontFaces_Close(faces); +} + +namespace { + +// Three pages: the first two share a resource dictionary with Helvetica, the +// third has its own with Times. +std::string ThreePagesOfFonts() { + return MakePdf({ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources 6 0 R " + ">>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources 6 0 R " + ">>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << " + "/Font << /F1 8 0 R >> >> >>", + "<< /Font << /F1 7 0 R >> >>", + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", + "<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman >>", + }); +} + +// A fresh layer over |pdf|, which must outlive it. +ScopedFPDFDocument OpenLayerOver(const std::string& pdf) { + EPDF_BASE_DOCUMENT base = + EPDF_LoadMemBaseDocument(pdf.data(), pdf.size(), nullptr); + EXPECT_TRUE(base); + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, nullptr)); + EPDF_ReleaseBaseDocument(base); + return layer; +} + +std::vector FontsFingerprint(FPDF_DOCUMENT document) { + std::vector fingerprint(32); + EXPECT_TRUE(EPDFDoc_GetFontsFingerprint(document, fingerprint.data())); + return fingerprint; +} + +bool ListsFace(FPDF_DOCUMENT document, const std::wstring& family) { + EPDF_FONT_FACES faces = EPDFFontFaces_Load(document); + const bool listed = HasFace(faces, family); + EPDFFontFaces_Close(faces); + return listed; +} + +// Gives page |page_objnum|'s resources a Courier font, as a write that adds +// text in it would. +void AddCourierTo(FPDF_DOCUMENT document, uint32_t page_objnum) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + auto font = doc->NewIndirect(); + font->SetNewFor("Type", "Font"); + font->SetNewFor("Subtype", "Type1"); + font->SetNewFor("BaseFont", "Courier"); + RetainPtr page = + ToDictionary(doc->GetMutableIndirectObject(page_objnum)); + ASSERT_TRUE(page); + page->GetMutableDictFor("Resources") + ->GetOrCreateDictFor("Font") + ->SetNewFor("F9", doc, font->GetObjNum()); +} + +} // namespace + +// On a layer, a list after a write walks only the places the write touched: +// a page, or every page that reads a dictionary it changed. +TEST_F(EPDFFontEmbedderTest, AListAfterAWriteWalksOnlyWhatItTouched) { + const std::string pdf = ThreePagesOfFonts(); + ScopedFPDFDocument layer = OpenLayerOver(pdf); + ASSERT_TRUE(layer); + const CPDF_DocumentFonts* index = + CPDF_DocumentFonts::For(CPDFDocumentFromFPDFDocument(layer.get())); + + const std::vector before = FontsFingerprint(layer.get()); + EXPECT_EQ(4u, index->places_walked()); // the AcroForm and three pages + EXPECT_EQ(before, FontsFingerprint(layer.get())); + EXPECT_EQ(4u, index->places_walked()); + + // Courier on the third page: that page is walked again, nothing else. + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + AddCourierTo(layer.get(), 5); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + const std::vector with_courier = FontsFingerprint(layer.get()); + EXPECT_NE(before, with_courier); + EXPECT_EQ(5u, index->places_walked()); + EXPECT_TRUE(ListsFace(layer.get(), L"Courier")); + + // A write that changes no font keeps the fingerprint. + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + { + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), 0)); + FPDFPage_SetRotation(page.get(), 1); + } + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_EQ(with_courier, FontsFingerprint(layer.get())); + EXPECT_EQ(6u, index->places_walked()); + + // The shared resource dictionary changed: both pages that read it. + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(layer.get()); + RetainPtr shared = + ToDictionary(doc->GetMutableIndirectObject(6)); + ASSERT_TRUE(shared); + shared->GetMutableDictFor("Font")->RemoveFor("F1"); + } + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_NE(with_courier, FontsFingerprint(layer.get())); + EXPECT_EQ(8u, index->places_walked()); + EXPECT_FALSE(ListsFace(layer.get(), L"Helvetica")); +} + +// A write rolled back leaves the fonts as they were, though a list during it +// saw its font. +TEST_F(EPDFFontEmbedderTest, AWriteRolledBackChangesNoFonts) { + const std::string pdf = ThreePagesOfFonts(); + ScopedFPDFDocument layer = OpenLayerOver(pdf); + ASSERT_TRUE(layer); + const std::vector before = FontsFingerprint(layer.get()); + + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + AddCourierTo(layer.get(), 3); + EXPECT_NE(before, FontsFingerprint(layer.get())); + EXPECT_TRUE(ListsFace(layer.get(), L"Courier")); + ASSERT_TRUE(EPDFLayer_AbortTransaction(layer.get())); + + EXPECT_EQ(before, FontsFingerprint(layer.get())); + EXPECT_FALSE(ListsFace(layer.get(), L"Courier")); +} + +// A change made outside any transaction is seen: everything is walked again. +TEST_F(EPDFFontEmbedderTest, AChangeOutsideATransactionIsSeen) { + const std::string pdf = ThreePagesOfFonts(); + ScopedFPDFDocument layer = OpenLayerOver(pdf); + ASSERT_TRUE(layer); + const std::vector before = FontsFingerprint(layer.get()); + + AddCourierTo(layer.get(), 4); + EXPECT_NE(before, FontsFingerprint(layer.get())); + EXPECT_TRUE(ListsFace(layer.get(), L"Courier")); +} + +// A plain document says nothing of what changed: it is walked whole each +// time. +TEST_F(EPDFFontEmbedderTest, APlainDocumentIsWalkedWholeEachTime) { + const std::string pdf = ThreePagesOfFonts(); + ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(doc); + const CPDF_DocumentFonts* index = + CPDF_DocumentFonts::For(CPDFDocumentFromFPDFDocument(doc.get())); + const std::vector before = FontsFingerprint(doc.get()); + EXPECT_EQ(before, FontsFingerprint(doc.get())); + EXPECT_EQ(2u, index->places_walked()); + AddCourierTo(doc.get(), 5); + EXPECT_NE(before, FontsFingerprint(doc.get())); +} + +// A free text drawn with no font for a character reports it once; marks, +// line breaks and zero width characters are never reported. +TEST_F(EPDFFontEmbedderTest, FreeTextReportsCharactersNoFontHas) { + ScopedRegisteredFonts scoped_fonts; + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FREETEXT)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 250.0f, 350.0f, 320.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ScopedFPDFWideString contents = GetFPDFWideString(L"a\x4E2D\x200B b\r\x4E2D"); + ASSERT_TRUE( + FPDFAnnot_SetStringValue(annot.get(), "Contents", contents.get())); + EPDFDoc_ClearMissingCharacters(document()); + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + EXPECT_EQ(std::vector{L"\x4E2D"}, + MissingCharacters(document())); + EPDFDoc_ClearMissingCharacters(document()); + EXPECT_EQ(0, EPDFDoc_CountMissingCharacters(document())); +} + +// A registered font not authorized for editing never lays out a box's new +// text, even when the box's /DR font was written with it: the box keeps the +// font dictionary it has. +TEST_F(EPDFFontEmbedderTest, UnauthorizedRegisteredFontDoesNotTakeOverABox) { + ScopedRegisteredFonts scoped_fonts; + std::vector font = RobotoPreviewAndPrint(); + ASSERT_FALSE(font.empty()); + EPDF_FONT_ID font_id = EPDFFont_RegisterMemFont64( + "RobotoPP", /*weight=*/400, /*italic=*/0, font.data(), font.size()); + ASSERT_NE(0u, font_id); + ASSERT_EQ(EPDF_FONT_EMBEDDING_PREVIEW_AND_PRINT, + EPDFFont_GetEmbeddingPermission(font_id)); + ASSERT_TRUE(EPDFFont_AuthorizeEditing(font_id)); + + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_WIDGET)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 320.0f, 350.0f, 290.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearanceRegisteredFont(annot.get(), font_id, + 12, 0, 0, 0)); + AttachAndFill(annot.get(), L"name", L"hello"); + + // The same font again, this time not authorized. + EPDFFont_ClearRegisteredFonts(); + font_id = EPDFFont_RegisterMemFont64("RobotoPP", /*weight=*/400, + /*italic=*/0, font.data(), font.size()); + ASSERT_NE(0u, font_id); + ASSERT_FALSE(EPDFFont_IsEditingAuthorized(font_id)); + + ScopedFPDFWideString text = GetFPDFWideString(L"hello"); + ScopedFPDFWideString family = GetFPDFWideString(L"RobotoPP"); + FPDF_WIDESTRING texts[] = {text.get()}; + FPDF_WIDESTRING families[] = {family.get()}; + const int weights[] = {400}; + const FPDF_BOOL italics[] = {false}; + EPDF_FONT_RESOLUTION resolution = EPDFDoc_ResolveFonts( + document(), annot.get(), 1, texts, families, weights, italics, nullptr); + ASSERT_TRUE(resolution); + EXPECT_EQ(EPDF_FONT_SOURCE_DOCUMENT_FONT, + EPDFFontResolution_GetSource(resolution, 0)); + EPDFFontResolution_Close(resolution); +} + +namespace { + +std::vector LoadTestFont(const char* path) { + std::string font_path = PathService::GetTestFilePath(path); + if (font_path.empty()) { + ADD_FAILURE() << "Failed to find " << path; + return {}; + } + return GetFileContents(font_path.c_str()); +} + +// A form whose field writes with a simple font (a |font_subtype| font) over +// |program| (embedded under |font_file|, with |file_subtype| as the stream's +// /Subtype when it has one), WinAnsi-encoded, every character 600 wide but +// "D" 700. +std::string FormWithFieldFont(const std::vector& program, + const char* font_file, + const char* file_subtype, + const char* base_font, + const char* font_subtype = "Type1") { + std::string widths; + for (int code = 32; code <= 126; ++code) { + widths += code == 'D' ? "700 " : "600 "; + } + const std::string data(program.begin(), program.end()); + std::string file = "<< "; + if (file_subtype) { + file += std::string("/Subtype /") + file_subtype + " "; + } + file += "/Length " + std::to_string(data.size()) + " >>\nstream\n" + data + + "\nendstream"; + return MakePdf({ + "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [5 0 R] /DR << " + "/Font << /FKept 6 0 R >> >> >> >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Annots [5 0 R] " + ">>", + file, + "<< /Type /Annot /Subtype /Widget /FT /Tx /T (kept) /P 3 0 R /Rect [20 " + "200 280 230] /DA (/FKept 12 Tf 0 g) /V (AB) >>", + std::string("<< /Type /Font /Subtype /") + font_subtype + " /BaseFont /" + + base_font + " /FirstChar 32 /LastChar 126 /Widths [" + widths + + "] /Encoding /WinAnsiEncoding /FontDescriptor 7 0 R >>", + std::string("<< /Type /FontDescriptor /FontName /") + base_font + + " /Flags 32 /FontBBox [0 -250 1000 900] /ItalicAngle 0 /Ascent 900 " + "/Descent -250 /CapHeight 700 /StemV 80 /" + + font_file + " 4 0 R >>", + }); +} + +// An outline with zero-length lines left out: a contour's closing line is +// written by one program and implied by another. +std::vector Drawn(const CFX_GlyphOutline& outline) { + std::vector drawn; + CFX_PointF pen; + for (const CFX_GlyphOutline::Segment& segment : outline.segments) { + const CFX_PointF end = segment.op == CFX_GlyphOutline::Op::kCubic + ? segment.points[2] + : segment.points[0]; + if (segment.op == CFX_GlyphOutline::Op::kLine && end == pen) { + continue; + } + pen = end; + drawn.push_back(segment); + } + return drawn; +} + +void ExpectSameOutline(const CFX_GlyphOutline& expected, + const CFX_GlyphOutline& actual) { + const std::vector left = Drawn(expected); + const std::vector right = Drawn(actual); + ASSERT_EQ(left.size(), right.size()); + ASSERT_FALSE(left.empty()); + for (size_t i = 0; i < left.size(); ++i) { + ASSERT_EQ(left[i].op, right[i].op) << i; + const size_t points = left[i].op == CFX_GlyphOutline::Op::kCubic ? 3 : 1; + for (size_t j = 0; j < points; ++j) { + EXPECT_NEAR(left[i].points[j].x, right[i].points[j].x, 0.01) << i; + EXPECT_NEAR(left[i].points[j].y, right[i].points[j].y, 0.01) << i; + } + } +} + +bool Covers(const std::vector& ranges, uint32_t scalar) { + for (size_t i = 0; i + 1 < ranges.size(); i += 2) { + if (ranges[i] <= scalar && scalar < ranges[i] + ranges[i + 1]) { + return true; + } + } + return false; +} + +// The field font of a form from FormWithFieldFont(), as the engine sees it, +// and its display program: an OpenType font a browser loads, with the same +// glyphs, reached by exactly the characters the engine writes, at the +// advances it lays them out with. +void ExpectDisplayProgramDrawsTheKeptFont(const std::string& pdf, + int expected_format) { + ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(doc); + EPDF_FONT_FACES faces = EPDFFontFaces_Load(doc.get()); + ASSERT_TRUE(faces); + std::optional found; + for (int face = 0; face < EPDFFontFaces_Count(faces) && !found; ++face) { + for (int program = 0; program < EPDFFontFaces_CountPrograms(faces, face); + ++program) { + if (EPDFFontFaces_GetProgramFormat(faces, face, program) == + expected_format) { + found = FoundProgram{face, program}; + break; + } + } + } + ASSERT_TRUE(found.has_value()); + EXPECT_FALSE( + EPDFFontFaces_ProgramMapsUnicode(faces, found->face, found->program)); + unsigned char source_hash[32] = {}; + unsigned char display_hash[32] = {}; + ASSERT_TRUE(EPDFFontFaces_GetProgramHash(faces, found->face, found->program, + source_hash)); + ASSERT_TRUE(EPDFFontFaces_GetProgramDisplayHash( + faces, found->face, found->program, display_hash)); + EXPECT_NE(std::vector(source_hash, source_hash + 32), + std::vector(display_hash, display_hash + 32)); + EPDFFontFaces_Close(faces); + + const unsigned long length = + EPDFDoc_GetFontDisplayProgramData(doc.get(), display_hash, nullptr, 0); + ASSERT_GT(length, 0u); + std::vector display(length); + ASSERT_EQ(length, EPDFDoc_GetFontDisplayProgramData(doc.get(), display_hash, + display.data(), length)); + const DataVector hashed = CryptSha256Generate(display); + EXPECT_EQ(std::vector(display_hash, display_hash + 32), + std::vector(hashed.begin(), hashed.end())); + ASSERT_EQ('O', display[0]); // 'OTTO': OpenType with CFF outlines + + auto display_font = std::make_unique(); + ASSERT_TRUE(display_font->LoadFaceZeroFromSpan( + display, /*force_vertical=*/false, /*object_tag=*/0)); + RetainPtr display_face = display_font->GetFace(); + ASSERT_TRUE(display_face->SelectCharMap(fxge::FontEncoding::kUnicode)); + + // The engine's view of the same font. + CPDF_Document* pdf_doc = CPDFDocumentFromFPDFDocument(doc.get()); + RetainPtr font_dict = pdf_doc->GetMutableRoot() + ->GetMutableDictFor("AcroForm") + ->GetMutableDictFor("DR") + ->GetMutableDictFor("Font") + ->GetMutableDictFor("FKept"); + RetainPtr font = + CPDF_DocPageData::FromDocument(pdf_doc)->GetFont(font_dict); + ASSERT_TRUE(font); + RetainPtr source_face = font->GetFont()->GetFace(); + + // Every printable character the font has. + int compared = 0; + for (int character = 0x21; character <= 0x7e; ++character) { + const uint32_t code = + font->CharCodeFromUnicode(static_cast(character)); + const int source_glyph = font->GlyphFromCharCode(code, nullptr); + if (source_glyph <= 0) { + EXPECT_EQ(0, display_face->GetCharIndex(character)) << character; + continue; + } + ++compared; + const int display_glyph = display_face->GetCharIndex(character); + ASSERT_GT(display_glyph, 0) << character; + std::optional expected = + source_face->LoadUnscaledGlyphOutline(source_glyph); + std::optional actual = + display_face->LoadUnscaledGlyphOutline(display_glyph); + ASSERT_TRUE(expected.has_value()); + ASSERT_TRUE(actual.has_value()); + ExpectSameOutline(*expected, *actual); + ASSERT_EQ(0, display_face->LoadGlyph(display_glyph, /*scale=*/false)); + EXPECT_EQ(character == 'D' ? 700 : 600, + display_face->GetGlyphTTWidth(display_glyph)) + << character; + } + EXPECT_GE(compared, 5); + + // The characters it draws are exactly the ones the engine writes. + const unsigned long range_count = EPDFDoc_GetFontDisplayProgramCoverage( + doc.get(), display_hash, nullptr, 0); + ASSERT_GT(range_count, 0u); + std::vector ranges(range_count * 2); + EPDFDoc_GetFontDisplayProgramCoverage(doc.get(), display_hash, ranges.data(), + range_count); + for (uint32_t scalar = 1; scalar < 0x3000; ++scalar) { + const uint32_t code = + font->CharCodeFromUnicode(static_cast(scalar)); + const bool written = code != CPDF_Font::kInvalidCharCode && code != 0 && + font->GlyphFromCharCode(code, nullptr) > 0 && + !font->UnicodeFromCharCode(code).IsEmpty(); + EXPECT_EQ(written, Covers(ranges, scalar)) << scalar; + EXPECT_EQ(written, display_face->GetCharIndex(scalar) > 0) << scalar; + } + + // The same bytes from another opening of the same file, by the hash alone: + // the document is walked to find it. + ScopedFPDFDocument again( + FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(again); + std::vector rebuilt(length); + ASSERT_EQ(length, EPDFDoc_GetFontDisplayProgramData(again.get(), display_hash, + rebuilt.data(), length)); + EXPECT_EQ(display, rebuilt); +} + +} // namespace + +// A field whose /DA font is a simple font over a program that can author +// text writes new text with that program, through its own cmap: the font's +// WinAnsi encoding can't reach "Ω", the program has it. +TEST_F(EPDFFontEmbedderTest, SimpleFieldFontWritesThroughItsProgram) { + std::vector roboto = LoadRoboto(); + ASSERT_FALSE(roboto.empty()); + const std::string pdf = FormWithFieldFont(roboto, "FontFile2", nullptr, + "Roboto-Regular", "TrueType"); + ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(annot); + const uint32_t field = EPDFAnnot_GetObjectNumber(annot.get()); + ASSERT_EQ(5u, field); + + EPDFDoc_ClearMissingCharacters(doc.get()); + ASSERT_TRUE(embedpdf_test::FillTextField(doc.get(), field, L"A\u03A9B")); + ASSERT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot.get())); + EXPECT_EQ(0, EPDFDoc_CountMissingCharacters(doc.get())); + // The appearance draws with the field's own program, the one /DR embeds. + EXPECT_EQ(4u, AppearanceFontProgram(annot.get(), + DefaultAppearanceFontName(annot.get()))); +} + +// A form's field font that is bare CFF, which a browser can't load, gets a +// display program it can. +TEST_F(EPDFFontEmbedderTest, BuildsADisplayProgramForABareCffFont) { + std::vector cff = LoadTestFont("fonts/foxit_fixed.cff"); + ASSERT_FALSE(cff.empty()); + ExpectDisplayProgramDrawsTheKeptFont( + FormWithFieldFont(cff, "FontFile3", "Type1C", "FoxitFixed"), + EPDF_FONT_FORMAT_CFF); +} + +// So does one that is Type 1. +TEST_F(EPDFFontEmbedderTest, BuildsADisplayProgramForAType1Font) { + std::vector type1 = + LoadThirdPartyFont("skia/resources/fonts/Roboto2-Regular.pfb"); + ASSERT_FALSE(type1.empty()); + ExpectDisplayProgramDrawsTheKeptFont( + FormWithFieldFont(type1, "FontFile", nullptr, "Roboto2-Regular"), + EPDF_FONT_FORMAT_TYPE1); +} + +// Two font dictionaries that share one bare CFF program but give it other +// widths are two display programs: each draws at its own dictionary's +// advances, under a hash of its own. The program is still one stream. A field +// in either font resolves to the display program its own dictionary reads. +TEST_F(EPDFFontEmbedderTest, + ADisplayProgramIsTheProgramAsItsDictionaryReadsIt) { + std::vector cff = LoadTestFont("fonts/foxit_fixed.cff"); + ASSERT_FALSE(cff.empty()); + auto font = [](int width, int descriptor) { + std::string widths; + for (int code = 32; code <= 126; ++code) { + widths += std::to_string(width) + " "; + } + return "<< /Type /Font /Subtype /Type1 /BaseFont /FoxitFixed /FirstChar 32 " + "/LastChar 126 /Widths [" + + widths + "] /Encoding /WinAnsiEncoding /FontDescriptor " + + std::to_string(descriptor) + " 0 R >>"; + }; + const char kDescriptor[] = + "<< /Type /FontDescriptor /FontName /FoxitFixed /Flags 32 /FontBBox [0 " + "-250 1000 900] /ItalicAngle 0 /Ascent 900 /Descent -250 /CapHeight 700 " + "/StemV 80 /FontFile3 4 0 R >>"; + auto field = [](const char* name, const char* font_alias, int bottom) { + return std::string("<< /Type /Annot /Subtype /Widget /FT /Tx /T (") + name + + ") /Rect [10 " + std::to_string(bottom) + " 200 " + + std::to_string(bottom + 30) + "] /DA (/" + font_alias + + " 12 Tf 0 g) /P 3 0 R >>"; + }; + const std::string pdf = MakePdf({ + "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [9 0 R 10 0 R] " + "/DR << /Font << /FNarrow 5 0 R /FWide 6 0 R >> >> >> >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Annots [9 0 R 10 " + "0 R] >>", + Stream("/Subtype /Type1C", std::string(cff.begin(), cff.end())), + font(600, 7), + font(900, 8), + kDescriptor, + kDescriptor, + field("narrow", "FNarrow", 200), + field("wide", "FWide", 100), + }); + ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(doc); + + EPDF_FONT_FACES faces = EPDFFontFaces_Load(doc.get()); + ASSERT_TRUE(faces); + std::vector> display_hashes; + std::vector> source_hashes; + for (int face = 0; face < EPDFFontFaces_Count(faces); ++face) { + for (int program = 0; program < EPDFFontFaces_CountPrograms(faces, face); + ++program) { + if (EPDFFontFaces_GetProgramFormat(faces, face, program) != + EPDF_FONT_FORMAT_CFF) { + continue; + } + EXPECT_EQ(4u, EPDFFontFaces_GetProgramObjNum(faces, face, program)); + unsigned char hash[32] = {}; + ASSERT_TRUE(EPDFFontFaces_GetProgramHash(faces, face, program, hash)); + source_hashes.emplace_back(hash, hash + 32); + ASSERT_TRUE( + EPDFFontFaces_GetProgramDisplayHash(faces, face, program, hash)); + display_hashes.emplace_back(hash, hash + 32); + } + } + EPDFFontFaces_Close(faces); + ASSERT_EQ(2u, display_hashes.size()); + EXPECT_EQ(source_hashes[0], source_hashes[1]); + EXPECT_NE(display_hashes[0], display_hashes[1]); + + // Each draws "A" at the advance its dictionary gives it, in walk order. + const int expected_advance[] = {600, 900}; + for (size_t i = 0; i < 2; ++i) { + const unsigned long length = EPDFDoc_GetFontDisplayProgramData( + doc.get(), display_hashes[i].data(), nullptr, 0); + ASSERT_GT(length, 0u); + std::vector display(length); + ASSERT_EQ(length, + EPDFDoc_GetFontDisplayProgramData( + doc.get(), display_hashes[i].data(), display.data(), length)); + const DataVector hashed = CryptSha256Generate(display); + EXPECT_EQ(display_hashes[i], + std::vector(hashed.begin(), hashed.end())); + auto display_font = std::make_unique(); + ASSERT_TRUE(display_font->LoadFaceZeroFromSpan( + display, /*force_vertical=*/false, /*object_tag=*/0)); + RetainPtr display_face = display_font->GetFace(); + ASSERT_TRUE(display_face->SelectCharMap(fxge::FontEncoding::kUnicode)); + const int glyph = display_face->GetCharIndex('A'); + ASSERT_GT(glyph, 0); + ASSERT_EQ(0, display_face->LoadGlyph(glyph, /*scale=*/false)); + EXPECT_EQ(expected_advance[i], display_face->GetGlyphTTWidth(glyph)) << i; + EXPECT_GT(EPDFDoc_GetFontDisplayProgramCoverage( + doc.get(), display_hashes[i].data(), nullptr, 0), + 0u); + } + + // A hash no program has finds nothing. + const unsigned char unknown[32] = {}; + EXPECT_EQ(0u, + EPDFDoc_GetFontDisplayProgramData(doc.get(), unknown, nullptr, 0)); + + // Each field draws its text in its own font: the same stream, read by its + // own dictionary. + ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); + ASSERT_TRUE(page); + ScopedFPDFWideString text = GetFPDFWideString(L"Hello"); + ScopedFPDFWideString family = GetFPDFWideString(L"FoxitFixed"); + FPDF_WIDESTRING texts[] = {text.get()}; + FPDF_WIDESTRING families[] = {family.get()}; + const int weights[] = {400}; + const FPDF_BOOL italics[] = {false}; + const int programs[] = {0}; + for (int i = 0; i < 2; ++i) { + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), i)); + ASSERT_TRUE(annot); + EPDF_FONT_RESOLUTION resolution = EPDFDoc_ResolveFonts( + doc.get(), annot.get(), 1, texts, families, weights, italics, programs); + ASSERT_TRUE(resolution); + EXPECT_EQ(EPDF_FONT_SOURCE_DOCUMENT_FONT, + EPDFFontResolution_GetSource(resolution, 0)) + << i; + EXPECT_EQ(4u, EPDFFontResolution_GetProgramObjNum(resolution, 0)) << i; + unsigned char hash[32] = {}; + ASSERT_TRUE(EPDFFontResolution_GetProgramDisplayHash(resolution, 0, hash)) + << i; + EXPECT_EQ(display_hashes[i], std::vector(hash, hash + 32)) << i; + EPDFFontResolution_Close(resolution); + } +} + +// A form field draws each cluster in the first font that has it: Hebrew +// typed into a Helvetica field comes from the fallback, in bidi order, under +// ActualText, and nothing is reported missing. +TEST_F(EPDFFontEmbedderTest, WidgetDrawsEachClusterInAFontThatHasIt) { + ScopedRegisteredFonts scoped_fonts; + std::vector arimo = + LoadThirdPartyFont("test_fonts/test_fonts/Arimo-Regular.ttf"); + ASSERT_FALSE(arimo.empty()); + const EPDF_FONT_ID arimo_id = EPDFFont_RegisterMemFont64( + "Arimo", /*weight=*/400, /*italic=*/0, arimo.data(), arimo.size()); + ASSERT_NE(0u, arimo_id); + ASSERT_TRUE(EPDFFont_AddFallbackFont(arimo_id, /*tier=*/0)); + + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_WIDGET)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 300.0f, 350.0f, 270.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), FPDF_FONT_HELVETICA, + 12.0f, 0, 0, 0)); + EPDFDoc_ClearMissingCharacters(document()); + AttachAndFill(annot.get(), L"hebrew", L"Hello \x05E9\x05DC\x05D5\x05DD 123"); + + EXPECT_TRUE(AppearanceUsesRegisteredFont(annot.get(), arimo_id)); + const std::string stream = NormalAppearanceOf(annot.get()); + EXPECT_NE(std::string::npos, + stream.find("/Span <>> BDC")) + << stream; + // Left to right: the Latin, the digits, then the Hebrew. + const size_t hello = stream.find("(Hello ) Tj"); + const size_t digits = stream.find("(123) Tj"); + const size_t hebrew = stream.find("/ActualText"); + ASSERT_NE(std::string::npos, hello) << stream; + ASSERT_NE(std::string::npos, digits) << stream; + EXPECT_LT(hello, digits); + EXPECT_LT(digits, hebrew); + EXPECT_EQ(0, EPDFDoc_CountMissingCharacters(document())); +} + +// A character no font has is drawn with the last resort, as its box, and +// reported; a supplementary character stays whole. +TEST_F(EPDFFontEmbedderTest, WidgetDrawsWhatNoFontHasWithTheLastResort) { + ScopedRegisteredFonts scoped_fonts; + std::vector box = GetFileContents( + PathService::GetTestFilePath("fonts/last_resort.ttf").c_str()); + ASSERT_FALSE(box.empty()); + const EPDF_FONT_ID box_id = EPDFFont_RegisterMemFont64( + "Last Resort", /*weight=*/400, /*italic=*/0, box.data(), box.size()); + ASSERT_NE(0u, box_id); + ASSERT_TRUE(EPDFFont_SetLastResortFont(box_id)); + + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_WIDGET)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 300.0f, 350.0f, 270.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), FPDF_FONT_HELVETICA, + 12.0f, 0, 0, 0)); + EPDFDoc_ClearMissingCharacters(document()); + // As UTF-16 code units: the test's string helper writes one per wchar_t. + AttachAndFill(annot.get(), L"boxes", + L"A\x4E2D" + L"B\xD83D\xDE00"); + + EXPECT_TRUE(AppearanceUsesRegisteredFont(annot.get(), box_id)); + const std::string stream = NormalAppearanceOf(annot.get()); + EXPECT_NE(std::string::npos, stream.find("(A) Tj")) << stream; + EXPECT_NE(std::string::npos, stream.find("(B) Tj")) << stream; + EXPECT_NE(std::string::npos, stream.find("")) << stream; + // UTF-16, as the report hands them out. + EXPECT_EQ((std::vector{L"\x4E2D", L"\xD83D\xDE00"}), + MissingCharacters(document())); +} + +// A password field writes stars and nothing else: not the value, and no +// ActualText that would carry it. +TEST_F(EPDFFontEmbedderTest, PasswordFieldWritesOnlyStars) { + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_WIDGET)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 300.0f, 350.0f, 270.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), FPDF_FONT_HELVETICA, + 12.0f, 0, 0, 0)); + ScopedFPDFWideString name = GetFPDFWideString(L"secret"); + const uint32_t field = EPDFForm_CreateField( + document(), 4 /* EPDF_FORMFIELD_FAMILY_TEXT */, name.get(), 0); + ASSERT_GT(field, 0u); + ASSERT_TRUE(EPDFForm_AttachWidget( + document(), field, EPDFAnnot_GetObjectNumber(annot.get()), nullptr, 0)); + ASSERT_TRUE(EPDFForm_SetFieldFlags(document(), field, 1u << 13, 0)); + ASSERT_TRUE(embedpdf_test::FillTextField(document(), field, L"hunter2")); + ASSERT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot.get())); + + const std::string stream = NormalAppearanceOf(annot.get()); + EXPECT_NE(std::string::npos, stream.find("(*******) Tj")) << stream; + EXPECT_EQ(std::string::npos, stream.find("hunter")) << stream; + EXPECT_EQ(std::string::npos, stream.find("ActualText")) << stream; +} + +namespace { + +// One page holding two whole programs that describe themselves alike: neither +// font dictionary has /FontFamily or /FontWeight, and "Thin" isn't a weight +// the name is read for, so both are Mono, 400, upright. Object 5 is +// Mono-Regular's program (Roboto), object 8 Mono-Thin's (Arimo). +constexpr uint32_t kMonoRegular = 5; +constexpr uint32_t kMonoThin = 8; + +std::string TwoProgramsDescribedAlike() { + const std::vector regular = LoadRoboto(); + const std::vector thin = + LoadThirdPartyFont("test_fonts/test_fonts/Arimo-Regular.ttf"); + auto program = [](const std::vector& bytes) { + return Stream("/Length1 " + std::to_string(bytes.size()), + std::string(bytes.begin(), bytes.end())); + }; + auto descriptor = [](const std::string& name, uint32_t file) { + return "<< /Type /FontDescriptor /FontName /" + name + + " /Flags 32 /FontBBox [0 -300 1000 900] /ItalicAngle 0 /Ascent 900 " + "/Descent -300 /CapHeight 700 /StemV 80 /FontFile2 " + + std::to_string(file) + " 0 R >>"; + }; + auto font = [](const std::string& name, uint32_t descriptor) { + return "<< /Type /Font /Subtype /TrueType /BaseFont /" + name + + " /FirstChar 32 /LastChar 32 /Widths [250] /Encoding " + "/WinAnsiEncoding /FontDescriptor " + + std::to_string(descriptor) + " 0 R >>"; + }; + return MakePdf({ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 400 400] /Contents 4 0 R " + "/Resources << /Font << /F1 7 0 R /F2 10 0 R >> >> >>", + Stream("BT /F1 12 Tf 40 300 Td ( ) Tj /F2 12 Tf ( ) Tj ET"), + program(regular), + descriptor("Mono-Regular", kMonoRegular), + font("Mono-Regular", 6), + program(thin), + descriptor("Mono-Thin", kMonoThin), + font("Mono-Thin", 9), + }); +} + +// The programs of the Type 0 fonts |annot|'s appearance draws with. +std::vector AppearanceFontPrograms(FPDF_ANNOTATION annot) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + RetainPtr ap = + context->GetAnnotDict()->GetDictFor("AP"); + RetainPtr normal = ap ? ap->GetStreamFor("N") : nullptr; + RetainPtr resources = + normal ? normal->GetDict()->GetDictFor("Resources") : nullptr; + RetainPtr fonts = + resources ? resources->GetDictFor("Font") : nullptr; + std::vector programs; + if (!fonts) { + return programs; + } + CPDF_DictionaryLocker locker(fonts); + for (const auto& [alias, value] : locker) { + const uint32_t program = AppearanceFontProgram(annot, alias); + if (program != 0) { + programs.push_back(program); + } + } + std::ranges::sort(programs); + return programs; +} + +} // namespace + +// A run can name the program a write would use: two programs that describe +// themselves alike are one face to their family, never to their program. +TEST_F(EPDFFontEmbedderTest, ResolvesTheProgramARunNames) { + ScopedRegisteredFonts scoped_fonts; + const std::string pdf = TwoProgramsDescribedAlike(); + ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(doc); + ScopedFPDFWideString text = GetFPDFWideString(L"Hello"); + ScopedFPDFWideString family = GetFPDFWideString(L"Mono"); + FPDF_WIDESTRING texts[] = {text.get(), text.get()}; + FPDF_WIDESTRING families[] = {family.get(), family.get()}; + const int weights[] = {400, 400}; + const FPDF_BOOL italics[] = {false, false}; + const int programs[] = {0, static_cast(kMonoThin)}; + + EPDF_FONT_RESOLUTION resolution = EPDFDoc_ResolveFonts( + doc.get(), nullptr, 2, texts, families, weights, italics, programs); + ASSERT_TRUE(resolution); + // By its family, the first of the two the document holds. + EXPECT_EQ(EPDF_FONT_SOURCE_DOCUMENT_PROGRAM, + EPDFFontResolution_GetSource(resolution, 0)); + EXPECT_EQ(kMonoRegular, EPDFFontResolution_GetProgramObjNum(resolution, 0)); + // By its program, the one named. + EXPECT_EQ(EPDF_FONT_SOURCE_DOCUMENT_PROGRAM, + EPDFFontResolution_GetSource(resolution, 1)); + EXPECT_EQ(kMonoThin, EPDFFontResolution_GetProgramObjNum(resolution, 1)); + EPDFFontResolution_Close(resolution); +} + +// A rich text body that names its program draws with it and writes it as the +// /DA font; a run that restates the body's face keeps it, and a run in +// another face finds that face by its family. +TEST_F(EPDFFontEmbedderTest, RichTextDrawsTheProgramItsStyleNames) { + ScopedRegisteredFonts scoped_fonts; + const std::string pdf = TwoProgramsDescribedAlike(); + ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); + ASSERT_TRUE(page); + auto box = [&](const std::string& json) { + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FREETEXT)); + const FS_RECTF rect{50.0f, 320.0f, 350.0f, 250.0f}; + EXPECT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + EXPECT_TRUE(EPDFAnnot_SetRichTextJSON(annot.get(), json.c_str())); + return annot; + }; + const std::string mono = + "\"family\":\"Mono\",\"weight\":400,\"italic\":false"; + + // First, while the document holds only its own two: by the family, the + // first of them (a write adds its /DR entry, which a later one finds first). + ScopedFPDFAnnotation by_family = + box("{\"body\":{" + mono + + "},\"paragraphs\":[{\"runs\":[{\"text\":\"Hello\"}]}]}"); + EXPECT_EQ(kMonoRegular, + AppearanceFontProgram(by_family.get(), + DefaultAppearanceFontName(by_family.get()))); + + ScopedFPDFAnnotation named = box( + "{\"body\":{" + mono + + ",\"program\":8},\"paragraphs\":[{\"runs\":[{\"text\":\"Hello\"}]}]}"); + EXPECT_EQ(kMonoThin, + AppearanceFontProgram(named.get(), + DefaultAppearanceFontName(named.get()))); + + ScopedFPDFAnnotation runs = + box("{\"body\":{" + mono + + ",\"program\":8},\"paragraphs\":[{\"runs\":[{\"text\":\"A\"}," + "{\"text\":\"B\",\"style\":{\"family\":\"Mono\"}}," + "{\"text\":\"C\",\"style\":{\"family\":\"Helvetica\"}}]}]}"); + // Only Mono-Thin among the programs: "B" restates the body's face, and + // "C" draws in Helvetica. + EXPECT_EQ(std::vector{kMonoThin}, + AppearanceFontPrograms(runs.get())); + EXPECT_NE(std::string::npos, NormalAppearanceOf(runs.get()).find("/Helv")); +} diff --git a/fpdfsdk/epdf_form_embeddertest.cpp b/fpdfsdk/epdf_form_embeddertest.cpp index 7de04007c5..177a7a53c2 100644 --- a/fpdfsdk/epdf_form_embeddertest.cpp +++ b/fpdfsdk/epdf_form_embeddertest.cpp @@ -2167,6 +2167,37 @@ TEST_F(EPDFFormEmbedderTest, CalculationOrderSetReplaceAndRemove) { // A push button: created, given a widget and a caption, and drawn with it. // Its value can't be written. +// A field's appearance written where there was none is a form XObject that +// says so: readers look for /Subtype /Form. +TEST_F(EPDFFormEmbedderTest, NewFieldAppearanceIsAFormXObject) { + ASSERT_TRUE(OpenDocument("hello_world.pdf")); + FPDF_PAGE page = LoadPage(0); + ASSERT_TRUE(page); + const uint32_t field = + EPDFForm_CreateField(document(), EPDF_FORMFIELD_FAMILY_TEXT, + GetFPDFWideString(L"name").get(), 0); + ASSERT_GT(field, 0u); + const uint32_t widget = CreateWidgetAnnot(page, 20, 200, 180, 230); + ASSERT_GT(widget, 0u); + ASSERT_TRUE(EPDFForm_AttachWidget(document(), field, widget, nullptr, 0)); + ASSERT_TRUE(SetOneValue(document(), field, L"Hello")); + unsigned long changed_count = 0; + ASSERT_TRUE(EPDFForm_RedrawFieldWidgets(document(), field, nullptr, 0, + &changed_count)); + + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document()); + RetainPtr annot = + ToDictionary(doc->GetIndirectObject(widget)); + ASSERT_TRUE(annot); + RetainPtr ap = annot->GetDictFor("AP"); + ASSERT_TRUE(ap); + RetainPtr normal = ap->GetStreamFor("N"); + ASSERT_TRUE(normal); + EXPECT_EQ("XObject", normal->GetDict()->GetNameFor("Type")); + EXPECT_EQ("Form", normal->GetDict()->GetNameFor("Subtype")); + UnloadPage(page); +} + TEST_F(EPDFFormEmbedderTest, PushButtonIsAuthorableAndShowsItsCaption) { ASSERT_TRUE(OpenDocument("hello_world.pdf")); FPDF_PAGE page = LoadPage(0); diff --git a/fpdfsdk/epdf_richtextlayout_embeddertest.cpp b/fpdfsdk/epdf_richtextlayout_embeddertest.cpp new file mode 100644 index 0000000000..47e6b98463 --- /dev/null +++ b/fpdfsdk/epdf_richtextlayout_embeddertest.cpp @@ -0,0 +1,522 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fpdfdoc/cpdf_richtextlayout.h" + +#include + +#include +#include +#include + +#include "core/fpdfapi/font/cpdf_font.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfdoc/cpdf_annotfontmap.h" +#include "core/fpdfdoc/cpdf_documentfonts.h" +#include "core/fpdfdoc/cpdf_richtext.h" +#include "core/fpdfdoc/cpvt_fontmap.h" +#include "core/fpdfdoc/cpvt_variabletext.h" +#include "core/fpdfdoc/cpvt_word.h" +#include "core/fxcrt/fx_coordinates.h" +#include "core/fxcrt/widestring.h" +#include "fpdfsdk/cpdfsdk_helpers.h" +#include "public/cpp/fpdf_scopers.h" +#include "public/epdf_font.h" +#include "public/fpdf_edit.h" +#include "testing/embedder_test.h" +#include "testing/fx_string_testhelpers.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/utils/file_util.h" +#include "testing/utils/path_service.h" + +namespace { + +using Line = CPDF_RichTextLayout::Line; +using GlyphRun = CPDF_RichTextLayout::GlyphRun; +using Align = CPDF_RichTextParagraphProps::Align; + +constexpr float kBoxWidth = 400.0f; +constexpr float kSize = 12.0f; + +// A line's runs as drawn, left to right: "[text]level" each. +std::wstring Describe(const Line& line) { + std::wstring description; + for (const GlyphRun& run : line.runs) { + description += L"["; + description += run.text.c_str(); + description += L"]"; + description += std::to_wstring(run.bidi_level); + } + return description; +} + +} // namespace + +// The rich text layout with real fonts. Arimo covers Latin, digits and Hebrew +// with its points; Helvetica is a standard font, drawn without HarfBuzz. +class EPDFRichTextLayoutEmbedderTest : public EmbedderTest { + protected: + void SetUp() override { + EmbedderTest::SetUp(); + EPDFFont_ClearRegisteredFonts(); + arimo_ = Register("Arimo", TestFont("Arimo-Regular.ttf")); + ASSERT_NE(0u, arimo_); + doc_.reset(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc_); + } + + void TearDown() override { + fonts_.reset(); + doc_.reset(); + EPDFFont_ClearRegisteredFonts(); + EmbedderTest::TearDown(); + } + + static std::string TestFont(const char* name) { + return PathService::GetThirdPartyFilePath( + std::string("test_fonts/test_fonts/") + name); + } + + static EPDF_FONT_ID Register(const char* family, const std::string& path) { + const std::vector data = GetFileContents(path.c_str()); + EXPECT_FALSE(data.empty()) << path; + return EPDFFont_RegisterMemFont64(family, /*weight=*/400, /*italic=*/0, + data.data(), data.size()); + } + + // The registered font |run| draws with; 0 for any other. + EPDF_FONT_ID FontOf(const GlyphRun& run) const { + return fonts_->DescribeEntry(run.font_entry).registered_font_id; + } + + CPDF_DocumentFonts* DocumentFonts() { + return CPDF_DocumentFonts::For(CPDFDocumentFromFPDFDocument(doc_.get())); + } + + // One paragraph of |text| in |family| at 12 pt, in a box |width| wide. + std::vector Arrange(const WideString& text, + bool rtl, + float width = kBoxWidth, + const char* family = "Arimo", + CPDF_RichTextLayout::Options options = {}) { + CPDF_RichTextParagraphProps props; + props.rtl = rtl; + return Arrange(text, props, width, family, options); + } + + std::vector Arrange(const WideString& text, + const CPDF_RichTextParagraphProps& props, + float width = kBoxWidth, + const char* family = "Arimo", + CPDF_RichTextLayout::Options options = {}) { + CPDF_RichTextDocument document; + document.body.family = WideString::FromASCII(family); + document.body.size = kSize; + CPDF_RichTextParagraph paragraph; + paragraph.props = props; + paragraph.runs.push_back({text, {}}); + document.paragraphs.push_back(std::move(paragraph)); + fonts_ = std::make_unique( + CPDFDocumentFromFPDFDocument(doc_.get()), + /*default_font=*/nullptr, ByteString(), + /*allow_registered_fallbacks=*/true); + CPDF_RichTextLayout layout(fonts_.get(), options); + return layout + .Arrange(document, CFX_FloatRect(0, 0, width, 300), + CPDF_Annot::VerticalAlignment::kTop) + .lines; + } + + EPDF_FONT_ID arimo_ = 0; + ScopedFPDFDocument doc_; + // The fonts of the last Arrange(). + std::unique_ptr fonts_; +}; + +// Hebrew and digits in a left-to-right paragraph: the digits take the +// Hebrew's side (they read as right to left between strong characters), and +// the Hebrew with its space and digits reads from the right. +TEST_F(EPDFRichTextLayoutEmbedderTest, LeftToRightParagraphOrdersHebrew) { + const std::vector lines = + Arrange(L"Hello \x05E9\x05DC\x05D5\x05DD 123", /*rtl=*/false); + ASSERT_EQ(1u, lines.size()); + EXPECT_EQ(L"[Hello ]0[123]2[\x05E9\x05DC\x05D5\x05DD ]1", Describe(lines[0])); + // Drawn edge to edge from the left. + float x = 0; + for (const GlyphRun& run : lines[0].runs) { + EXPECT_FLOAT_EQ(x, run.x); + x += run.width; + } + EXPECT_TRUE(lines[0].runs[2].rtl); + EXPECT_TRUE(lines[0].runs[2].needs_actual_text); + EXPECT_FALSE(lines[0].runs[1].rtl); +} + +// A right-to-left paragraph starting with digits: they still read left to +// right, at its right end; the line is right-aligned. +TEST_F(EPDFRichTextLayoutEmbedderTest, RightToLeftParagraphStartingWithDigits) { + const std::vector lines = + Arrange(L"123 \x05E9\x05DC\x05D5\x05DD", /*rtl=*/true); + ASSERT_EQ(1u, lines.size()); + EXPECT_EQ(L"[ \x05E9\x05DC\x05D5\x05DD]1[123]2", Describe(lines[0])); + EXPECT_NEAR(kBoxWidth, lines[0].start_x + lines[0].width, 0.01f); + EXPECT_NEAR(kBoxWidth, lines[0].runs.back().x + lines[0].runs.back().width, + 0.01f); +} + +// A form field's paragraph reads in the direction of its first strong +// character, whatever its dir says. +TEST_F(EPDFRichTextLayoutEmbedderTest, DirectionFromText) { + CPDF_RichTextLayout::Options options; + options.direction_from_text = true; + std::vector lines = Arrange(L"123 \x05E9\x05DC\x05D5\x05DD", + /*rtl=*/false, kBoxWidth, "Arimo", options); + ASSERT_EQ(1u, lines.size()); + EXPECT_EQ(L"[ \x05E9\x05DC\x05D5\x05DD]1[123]2", Describe(lines[0])); + + lines = Arrange(L"abc \x05E9\x05DC\x05D5\x05DD", /*rtl=*/true, kBoxWidth, + "Arimo", options); + ASSERT_EQ(1u, lines.size()); + EXPECT_EQ(L"[abc ]0[\x05E9\x05DC\x05D5\x05DD]1", Describe(lines[0])); +} + +// Breaking a right-to-left paragraph: the space that ends a line goes to the +// paragraph's level, so it trails at the left, past the edge the line is +// aligned to, whether it ran left to right before the break or not. +TEST_F(EPDFRichTextLayoutEmbedderTest, TrailingSpaceHangsAtTheLeft) { + // Two Hebrew words, one per line. + std::vector lines = + Arrange(L"\x05E9\x05DC\x05D5\x05DD \x05E2\x05D5\x05DC\x05DD", + /*rtl=*/true, /*width=*/30); + ASSERT_EQ(2u, lines.size()); + EXPECT_EQ(L"[\x05E9\x05DC\x05D5\x05DD ]1", Describe(lines[0])); + const GlyphRun& hebrew = lines[0].runs[0]; + EXPECT_LT(hebrew.x, lines[0].start_x); + EXPECT_NEAR(30.0f, hebrew.x + hebrew.width, 0.01f); + + // Two Latin words: the space ran at the words' level, 2. + lines = Arrange(L"abc def", /*rtl=*/true, /*width=*/25); + ASSERT_EQ(2u, lines.size()); + EXPECT_EQ(L"[ ]1[abc]2", Describe(lines[0])); + EXPECT_NEAR(lines[0].start_x - lines[0].runs[0].width, lines[0].runs[0].x, + 0.01f); + EXPECT_NEAR(lines[0].start_x, lines[0].runs[1].x, 0.01f); + EXPECT_NEAR(25.0f, lines[0].start_x + lines[0].width, 0.01f); + EXPECT_EQ(L"[def]2", Describe(lines[1])); +} + +// Hebrew points are marks: they stay with their letters, in one run. +TEST_F(EPDFRichTextLayoutEmbedderTest, HebrewPointsStayWithTheirLetters) { + const std::vector lines = + Arrange(L"\x05E9\x05C1\x05B8\x05DC\x05D5\x05B9\x05DD", /*rtl=*/true); + ASSERT_EQ(1u, lines.size()); + ASSERT_EQ(1u, lines[0].runs.size()); + EXPECT_EQ(1, lines[0].runs[0].bidi_level); + EXPECT_FALSE(lines[0].runs[0].degraded); +} + +// Brackets read right to left are mirrored: by HarfBuzz for a font program, +// by the layout for a standard font. +TEST_F(EPDFRichTextLayoutEmbedderTest, RightToLeftBracketsAreMirrored) { + for (const char* family : {"Arimo", "Helvetica"}) { + SCOPED_TRACE(family); + std::vector lines = Arrange(L"()", /*rtl=*/false, kBoxWidth, family); + ASSERT_EQ(1u, lines.size()); + ASSERT_EQ(1u, lines[0].runs.size()); + ASSERT_EQ(2u, lines[0].runs[0].glyphs.size()); + const uint32_t open = lines[0].runs[0].glyphs[0].gid; + const uint32_t close = lines[0].runs[0].glyphs[1].gid; + ASSERT_NE(open, close); + + // "(abc)" in a right-to-left paragraph: the brackets at level 1 around + // the Latin at level 2. Logical ")" is drawn first, as "(". + lines = Arrange(L"(abc)", /*rtl=*/true, kBoxWidth, family); + ASSERT_EQ(1u, lines.size()); + EXPECT_EQ(L"[)]1[abc]2[(]1", Describe(lines[0])); + ASSERT_EQ(1u, lines[0].runs[0].glyphs.size()); + EXPECT_EQ(open, lines[0].runs[0].glyphs[0].gid); + ASSERT_EQ(1u, lines[0].runs[2].glyphs.size()); + EXPECT_EQ(close, lines[0].runs[2].glyphs[0].gid); + } +} + +// The first line's indent is on the side the paragraph starts from. +TEST_F(EPDFRichTextLayoutEmbedderTest, IndentOnTheStartSide) { + CPDF_RichTextParagraphProps props; + props.text_indent = 10; + std::vector lines = Arrange(L"abc", props); + ASSERT_EQ(1u, lines.size()); + EXPECT_NEAR(10.0f, lines[0].start_x, 0.01f); + + props.rtl = true; + lines = Arrange(L"\x05E9\x05DC\x05D5\x05DD", props); + ASSERT_EQ(1u, lines.size()); + EXPECT_NEAR(kBoxWidth - 10.0f, lines[0].start_x + lines[0].width, 0.01f); +} + +// A justified line's ink reaches both edges: the spaces between its words +// stretch, the space it ends with doesn't. +TEST_F(EPDFRichTextLayoutEmbedderTest, JustifiedInkReachesBothEdges) { + CPDF_RichTextParagraphProps props; + props.align = Align::kJustify; + const std::vector lines = Arrange(L"aaa bbb ccc", props, /*width=*/50); + ASSERT_EQ(2u, lines.size()); + ASSERT_EQ(1u, lines[0].runs.size()); + const GlyphRun& run = lines[0].runs[0]; + EXPECT_EQ(L"aaa bbb ", std::wstring(run.text.c_str())); + EXPECT_NEAR(0.0f, run.x, 0.01f); + float x = run.x; + float ink_end = x; + for (const CPDF_RichTextLayout::ShapedGlyph& glyph : run.glyphs) { + x += glyph.x_advance; + if (run.text[glyph.cluster] != L' ') { + ink_end = x; + } + } + EXPECT_NEAR(50.0f, ink_end, 0.01f); +} + +// A cluster goes to the first fallback, in order, that has all of it. Gelasio +// has "e" but not the combining acute, so "e" + U+0301 goes to Arimo, added +// after it; Helvetica, the face, has neither whole. +TEST_F(EPDFRichTextLayoutEmbedderTest, FallbackHasTheWholeCluster) { + const EPDF_FONT_ID gelasio = + Register("Gelasio", TestFont("Gelasio-Regular.ttf")); + ASSERT_NE(0u, gelasio); + ASSERT_TRUE(EPDFFont_AddFallbackFont(gelasio, /*tier=*/0)); + ASSERT_TRUE(EPDFFont_AddFallbackFont(arimo_, /*tier=*/0)); + const std::vector lines = + Arrange(L"e\x0301", /*rtl=*/false, kBoxWidth, "Helvetica"); + ASSERT_EQ(1u, lines.size()); + ASSERT_EQ(1u, lines[0].runs.size()); + EXPECT_EQ(arimo_, FontOf(lines[0].runs[0])); + EXPECT_TRUE(DocumentFonts()->missing_characters().empty()); +} + +// Fallbacks are tried tier by tier: Tinos, added after Arimo in a lower tier, +// draws the Hebrew both have. +TEST_F(EPDFRichTextLayoutEmbedderTest, FallbackTiers) { + const EPDF_FONT_ID tinos = Register("Tinos", TestFont("Tinos-Regular.ttf")); + ASSERT_NE(0u, tinos); + ASSERT_TRUE(EPDFFont_AddFallbackFont(arimo_, /*tier=*/1)); + ASSERT_TRUE(EPDFFont_AddFallbackFont(tinos, /*tier=*/0)); + const std::vector lines = + Arrange(L"\x05E9\x05DC", /*rtl=*/false, kBoxWidth, "Helvetica"); + ASSERT_EQ(1u, lines.size()); + ASSERT_EQ(1u, lines[0].runs.size()); + EXPECT_EQ(tinos, FontOf(lines[0].runs[0])); +} + +// A character no font has is drawn with the last resort, as its box, and is +// still reported; one that draws nothing stays with the face. +TEST_F(EPDFRichTextLayoutEmbedderTest, LastResortDrawsWhatNoFontHas) { + // Without a last resort, the face draws its missing glyph (a run of its + // own: another script). + std::vector lines = Arrange( + L"A\x4E2D" + L"B", + /*rtl=*/false); + ASSERT_EQ(1u, lines.size()); + ASSERT_EQ(3u, lines[0].runs.size()); + for (const GlyphRun& run : lines[0].runs) { + EXPECT_EQ(arimo_, FontOf(run)); + } + + const EPDF_FONT_ID box = Register( + "Last Resort", PathService::GetTestFilePath("fonts/last_resort.ttf")); + ASSERT_NE(0u, box); + ASSERT_TRUE(EPDFFont_SetLastResortFont(box)); + DocumentFonts()->ClearMissingCharacters(); + lines = Arrange( + L"A\x4E2D" + L"B", + /*rtl=*/false); + ASSERT_EQ(1u, lines.size()); + ASSERT_EQ(3u, lines[0].runs.size()); + EXPECT_EQ(arimo_, FontOf(lines[0].runs[0])); + EXPECT_EQ(box, FontOf(lines[0].runs[1])); + EXPECT_EQ(arimo_, FontOf(lines[0].runs[2])); + ASSERT_EQ(1u, lines[0].runs[1].glyphs.size()); + EXPECT_NE(0u, lines[0].runs[1].glyphs[0].gid); + EXPECT_EQ(std::vector{L"\x4E2D"}, + DocumentFonts()->missing_characters()); + + // An invisible separator Arimo lacks is hidden by it, not boxed. + lines = Arrange( + L"A\x2063" + L"B", + /*rtl=*/false); + ASSERT_EQ(1u, lines.size()); + ASSERT_EQ(1u, lines[0].runs.size()); + EXPECT_EQ(arimo_, FontOf(lines[0].runs[0])); +} + +namespace { + +// A character a field draws, where its baseline starts. +struct PlacedCharacter { + wchar_t ch; + float x; + float y; +}; + +} // namespace + +// The field layout keeps the geometry of PDFium's form filler (CPVT) where +// CPVT drew correctly: the same characters at the same places, for the +// standard fonts, the sizes, fitted sizes, alignments, several lines, long +// words, passwords, limits and combs the forms parity gate draws. +TEST_F(EPDFRichTextLayoutEmbedderTest, FieldMatchesCpvt) { + struct Case { + const char* font; + const wchar_t* text; + float size; + int alignment; + bool multiline; + bool password; + size_t limit; + size_t cells; + CFX_FloatRect plate; + }; + const CFX_FloatRect line(20, 20, 180, 44); + const CFX_FloatRect block(20, 20, 170, 110); + const wchar_t* const kParagraphs = + L"The quick brown fox jumps over the lazy dog, again and again.\n" + L"Second paragraph here\n\nAfter a blank line"; + std::vector cases; + for (const char* font : {"Helvetica", "Helvetica-Bold", "Times-Roman", + "Times-BoldItalic", "Courier-BoldOblique"}) { + for (float size : {0.0f, 8.0f, 11.0f, 14.0f}) { + cases.push_back({font, + L"H\x00E9g, quick \x20AC" + L"12.50 \x201C" + L"fox\x201D", + size, 0, false, false, 0, 0, line}); + } + } + for (int alignment : {0, 1, 2}) { + for (const wchar_t* text : {L"Wide WWW text", L"i", L""}) { + cases.push_back( + {"Helvetica", text, 10, alignment, false, false, 0, 0, line}); + } + for (float size : {0.0f, 9.0f}) { + cases.push_back({"Helvetica", kParagraphs, size, alignment, true, false, + 0, 0, block}); + } + for (size_t cells : {6u, 10u}) { + cases.push_back({"Courier", L"AiW1\x00E9", 12, alignment, false, false, 0, + cells, line}); + } + } + cases.push_back({"Helvetica", + L"A long value that will not fit in this box at all", 0, 0, + false, false, 0, 0, line}); + cases.push_back({"Times-Roman", L"Tall", 0, 0, false, false, 0, 0, + CFX_FloatRect(20, 20, 140, 90)}); + cases.push_back({"Times-Roman", L"Supercalifragilisticexpialidocious word", + 12, 0, true, false, 0, 0, CFX_FloatRect(20, 20, 100, 80)}); + cases.push_back({"Helvetica", + L"Words (in brackets) and [more] text {here} to (wrap)", 11, + 0, true, false, 0, 0, CFX_FloatRect(20, 20, 95, 110)}); + cases.push_back({"Helvetica", L"secret", 10, 0, false, true, 0, 0, line}); + cases.push_back( + {"Helvetica", L"truncate me please", 10, 0, false, false, 8, 0, line}); + cases.push_back({"Helvetica", L"AiWm.", 12, 0, false, false, 0, 7, line}); + cases.push_back({"Helvetica", L"1234", 0, 0, false, false, 0, 4, line}); + + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(doc_.get()); + for (const Case& c : cases) { + SCOPED_TRACE(testing::Message() + << c.font << " " << c.size << " q" << c.alignment + << (c.multiline ? " multiline" : "") + << (c.password ? " password" : "") << " limit " << c.limit + << " cells " << c.cells << " \"" << WideString(c.text).ToUTF8() + << "\""); + RetainPtr font = CPDF_Font::GetStockFont(doc, c.font); + ASSERT_TRUE(font); + const bool centered = !c.multiline; + + // CPVT, as the form generator drove it. + std::vector expected; + { + CPVT_FontMap cpvt_fonts(doc, /*resources=*/nullptr, font, "F"); + CPVT_VariableText::Provider provider(&cpvt_fonts); + CPVT_VariableText vt(&provider); + vt.SetPlateRect(c.plate); + vt.SetAlignment(CPVT_VariableText::ToAlignment(c.alignment)); + if (c.size == 0) { + vt.SetAutoFontSize(true); + } else { + vt.SetFontSize(c.size); + } + if (c.multiline) { + vt.SetMultiLine(true); + vt.SetAutoReturn(true); + } + if (c.password) { + vt.SetPasswordChar('*'); + } + if (c.cells > 0) { + vt.SetCharArray(static_cast(c.cells)); + } else { + vt.SetLimitChar(static_cast(c.limit)); + } + vt.Initialize(); + vt.SetText(c.text); + vt.RearrangeAll(); + const CFX_PointF offset( + 0, centered ? (vt.GetContentRect().Height() - c.plate.Height()) / 2.0f + : 0); + CPVT_VariableText::Iterator* it = vt.GetIterator(); + it->SetAt(0); + while (it->NextWord()) { + CPVT_Word word; + if (it->GetWord(word)) { + const CFX_PointF at = word.location() + offset; + expected.push_back( + {c.password ? L'*' : static_cast(word.word()), at.x, + at.y}); + } + } + } + + // The field layout. + std::vector actual; + { + CPDF_AnnotFontMap map(doc, font, "F", + /*allow_registered_fallbacks=*/false); + ASSERT_TRUE(map.HasDefaultFont()); + CPDF_RichTextLayout::Field field; + field.text = c.text; + field.face_entry = 0; + field.size = c.size; + field.alignment = c.alignment; + field.multiline = c.multiline; + field.password = c.password; + field.limit = c.limit; + field.cells = c.cells; + field.centered = centered; + CPDF_RichTextLayout layout(&map, {}); + const CPDF_RichTextLayout::Result laid = + layout.ArrangeField(field, c.plate); + for (const Line& laid_line : laid.lines) { + const float y = c.plate.top - laid_line.baseline_y; + for (const GlyphRun& run : laid_line.runs) { + float x = c.plate.left + run.x; + for (const CPDF_RichTextLayout::ShapedGlyph& glyph : run.glyphs) { + actual.push_back({run.text[glyph.cluster], x, y}); + x += glyph.x_advance; + } + } + } + } + + ASSERT_EQ(expected.size(), actual.size()); + for (size_t i = 0; i < expected.size(); ++i) { + SCOPED_TRACE(i); + EXPECT_EQ(expected[i].ch, actual[i].ch); + EXPECT_NEAR(expected[i].x, actual[i].x, 0.001f); + EXPECT_NEAR(expected[i].y, actual[i].y, 0.001f); + } + } +} diff --git a/fpdfsdk/epdf_text_embeddertest.cpp b/fpdfsdk/epdf_text_embeddertest.cpp index b8c64a5815..86da683286 100644 --- a/fpdfsdk/epdf_text_embeddertest.cpp +++ b/fpdfsdk/epdf_text_embeddertest.cpp @@ -6,14 +6,18 @@ #include "public/epdf_text.h" #include +#include +#include #include "core/fpdfapi/parser/cpdf_document.h" #include "core/fpdfapi/parser/cpdf_read_only_graph_guard.h" #include "core/fxcrt/notreached.h" #include "fpdfsdk/cpdfsdk_helpers.h" +#include "fpdfsdk/epdf_edit_test_util.h" #include "public/cpp/fpdf_scopers.h" #include "public/fpdf_text.h" #include "testing/embedder_test.h" +#include "testing/fx_string_testhelpers.h" #include "testing/gmock/include/gmock/gmock.h" using ::testing::ElementsAreArray; @@ -161,6 +165,128 @@ TEST_F(EPDFTextEmbedderTest, ChromeType3TextLooseBoxesShareTheLine) { } } +// /ActualText is boxed and placed in page space wherever it is drawn. The same +// span drawn on the page, and through a form that doubles its size and moves +// it 200 points down, has the same boxes and origins, 200 points lower. +TEST_F(EPDFTextEmbedderTest, ActualTextInAFormIsInPageSpace) { + static constexpr char kSpan[] = + "/Span << /ActualText >> BDC (abcd) Tj EMC"; + const std::string pdf = MakePdf({ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 400 400] /Contents 4 0 R " + "/Resources << /Font << /F1 5 0 R >> /XObject << /Fm 6 0 R >> >> >>", + Stream(std::string("BT /F1 24 Tf 40 300 Td ") + kSpan + + " ET q 1 0 0 1 20 50 cm /Fm Do Q"), + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", + Stream("/Type /XObject /Subtype /Form /BBox [0 0 200 100] " + "/Matrix [2 0 0 2 0 0] /Resources << /Font << /F1 5 0 R >> >>", + std::string("BT /F1 12 Tf 10 25 Td ") + kSpan + " ET"), + }); + ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); + ASSERT_TRUE(page); + ScopedFPDFTextPage text_page(FPDFText_LoadPage(page.get())); + ASSERT_TRUE(text_page); + + // The span's letters, on the page and then in the form. + std::vector letters; + for (int i = 0; i < FPDFText_CountChars(text_page.get()); ++i) { + if (FPDFText_GetUnicode(text_page.get(), i) >= 0x05D0) { + letters.push_back(i); + } + } + ASSERT_EQ(8u, letters.size()); + for (size_t k = 0; k < 4; ++k) { + SCOPED_TRACE(k); + const int on_page = letters[k]; + const int in_form = letters[k + 4]; + EXPECT_EQ(FPDFText_GetUnicode(text_page.get(), on_page), + FPDFText_GetUnicode(text_page.get(), in_form)); + + double page_box[4]; + double form_box[4]; + ASSERT_TRUE(FPDFText_GetCharBox(text_page.get(), on_page, &page_box[0], + &page_box[1], &page_box[2], &page_box[3])); + ASSERT_TRUE(FPDFText_GetCharBox(text_page.get(), in_form, &form_box[0], + &form_box[1], &form_box[2], &form_box[3])); + // Left, right, bottom, top. + EXPECT_NEAR(page_box[0], form_box[0], 0.01); + EXPECT_NEAR(page_box[1], form_box[1], 0.01); + EXPECT_NEAR(page_box[2] - 200, form_box[2], 0.01); + EXPECT_NEAR(page_box[3] - 200, form_box[3], 0.01); + + FS_RECTF page_loose; + FS_RECTF form_loose; + ASSERT_TRUE( + FPDFText_GetLooseCharBox(text_page.get(), on_page, &page_loose)); + ASSERT_TRUE( + FPDFText_GetLooseCharBox(text_page.get(), in_form, &form_loose)); + EXPECT_NEAR(page_loose.left, form_loose.left, 0.01); + EXPECT_NEAR(page_loose.right, form_loose.right, 0.01); + EXPECT_NEAR(page_loose.top - 200, form_loose.top, 0.01); + EXPECT_NEAR(page_loose.bottom - 200, form_loose.bottom, 0.01); + + double page_x; + double page_y; + double form_x; + double form_y; + ASSERT_TRUE( + FPDFText_GetCharOrigin(text_page.get(), on_page, &page_x, &page_y)); + ASSERT_TRUE( + FPDFText_GetCharOrigin(text_page.get(), in_form, &form_x, &form_y)); + EXPECT_NEAR(40, form_x, 0.01); + EXPECT_NEAR(100, form_y, 0.01); + EXPECT_NEAR(page_x, form_x, 0.01); + EXPECT_NEAR(page_y - 200, form_y, 0.01); + } +} + +// /ActualText is logical text: a span reads back as written, whatever marks +// and spaces it holds, on a line of its own or after left-to-right text. +TEST_F(EPDFTextEmbedderTest, ActualTextReadsAsWritten) { + // Pointed Hebrew, two Hebrew words with digits, Arabic with harakat, an + // Arabic word ending in one, and pointed Hebrew after Latin text, each span + // over Latin glyphs. + static constexpr char kPointed[] = ""; + const std::string content = + std::string("BT /F1 12 Tf 20 350 Td /Span << /ActualText ") + kPointed + + " >> BDC (abcd) Tj EMC 0 -40 Td /Span << /ActualText " + " >> BDC " + "(abcd efg hij) Tj EMC 0 -40 Td /Span << /ActualText " + " >> BDC (abcd) Tj EMC " + "0 -40 Td /Span << /ActualText >> BDC " + "(abcd) Tj EMC 0 -40 Td (Hello ) Tj /Span << /ActualText " + + kPointed + " >> BDC (abcd) Tj EMC ET"; + const std::string pdf = MakePdf({ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 400 400] /Contents 4 0 R " + "/Resources << /Font << /F1 5 0 R >> >> >>", + Stream(content), + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", + }); + ScopedFPDFDocument doc(FPDF_LoadMemDocument(pdf.data(), pdf.size(), nullptr)); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); + ASSERT_TRUE(page); + ScopedFPDFTextPage text_page(FPDFText_LoadPage(page.get())); + ASSERT_TRUE(text_page); + + unsigned short buffer[128] = {}; + ASSERT_GT(FPDFText_GetText(text_page.get(), 0, + FPDFText_CountChars(text_page.get()), buffer), + 0); + EXPECT_EQ( + L"\x05e9\x05b8\x05c1\x05dc\x05d5\x05b9\x05dd\r\n" + L"\x05e9\x05dc\x05d5\x05dd 123 \x05e2\x05d5\x05dc\x05dd\r\n" + L"\x0645\x064e\x0631\x0652\x062d\x064e\x0628\x064b\x0627\r\n" + L"\x0643\x064e\x062a\x064e\x0628\x064e\r\n" + L"Hello \x05e9\x05b8\x05c1\x05dc\x05d5\x05b9\x05dd", + GetPlatformWString(reinterpret_cast(buffer))); +} + TEST_F(EPDFTextEmbedderTest, GetRotatedCharGeometry) { ASSERT_TRUE(OpenDocument("rotated_text.pdf")); ScopedPage page = LoadScopedPage(0); diff --git a/public/epdf_annot.h b/public/epdf_annot.h index 684ce09072..a6848ed52a 100644 --- a/public/epdf_annot.h +++ b/public/epdf_annot.h @@ -620,6 +620,28 @@ EPDFAnnot_SetDefaultAppearanceRegisteredFont(FPDF_ANNOTATION annot, unsigned int G, unsigned int B); +// Experimental EmbedPDF Extension API. +// Set the default appearance of a FreeText, Redact or Widget annotation to a +// font program the document already holds, by the object number of its +// stream (EPDFFontFaces_GetProgramObjNum()). /DA names an alias for it this +// session; the appearance generated next installs its /DR entry. Fails for +// a program the layout can't write new text with: not TrueType or OpenType, +// no Unicode cmap, or a licence (OS/2 fsType) that doesn't allow editing. +// +// annot - handle to an annotation. +// program_objnum - the font program stream's object number. +// font_size - the font size to be set. +// R, G, B - the color to be set. +// +// Returns true on success. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetDefaultAppearanceDocumentFont(FPDF_ANNOTATION annot, + unsigned long program_objnum, + float font_size, + unsigned int R, + unsigned int G, + unsigned int B); + // Experimental EmbedPDF Extension API. // Get the default appearance of a FreeText annotation. // @@ -661,6 +683,26 @@ EPDFAnnot_GetDefaultAppearanceFontFace(FPDF_ANNOTATION annot, int* weight, FPDF_BOOL* italic); +// Experimental EmbedPDF Extension API. +// Get the PostScript name of the font a FreeText, Redact or Widget +// annotation's default appearance (/DA) names: the /BaseFont of the font +// dictionary /DR holds for it (a Type0 font's descendant's), without its +// subset tag, or the program's for an alias reserved by +// EPDFAnnot_SetDefaultAppearanceDocumentFont(). +// +// annot - handle to an annotation. +// buffer - receives the name's bytes and a trailing NUL; may be NULL to +// query the length. +// buflen - the size of |buffer| in bytes. +// +// Returns the name's length in bytes, including the trailing NUL, or 0 when +// /DA names no font dictionary (a standard alias, a registered font's alias) +// or the dictionary has no /BaseFont. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAnnot_GetDefaultAppearanceFontPostScriptName(FPDF_ANNOTATION annot, + char* buffer, + unsigned long buflen); + // Experimental EmbedPDF Extension API. // Set the text alignment (/Q) of a FreeText, Redact or Widget annotation. A // widget's own /Q wins over its field's. diff --git a/public/epdf_font.h b/public/epdf_font.h index ef73518bd1..745116ddde 100644 --- a/public/epdf_font.h +++ b/public/epdf_font.h @@ -71,7 +71,7 @@ EPDFFont_RegisterMemFont64(FPDF_BYTESTRING family_name, size_t size); // Experimental EmbedPDF Extension API. -// Clear all registered fonts and the fallback font order. +// Clear all registered fonts, the fallback fonts and the last resort. // // Existing documents may still contain registered-font DA marker resources // after this call; those markers are invalid until their fonts are registered @@ -143,15 +143,380 @@ FPDF_EXPORT int FPDF_CALLCONV EPDFDoc_GetFontEmbeddingPolicy(FPDF_DOCUMENT document); // Experimental EmbedPDF Extension API. -// Add a registered font to the ordered fallback list used when the selected -// font does not contain a glyph or a PDF page needs a substitute font. +// Add a registered font to the fallback fonts: the fonts that draw a +// character the selected font doesn't have, in new text and where a PDF page +// needs a substitute font. They are tried tier by tier, lowest |tier| first. +// Adding a font that is already one changes its tier only. FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFFont_AddFallbackFont(EPDF_FONT_ID font_id); +EPDFFont_AddFallbackFont(EPDF_FONT_ID font_id, int tier); // Experimental EmbedPDF Extension API. -// Clear the ordered fallback font list without unregistering fonts. +// Clear the fallback fonts without unregistering them. FPDF_EXPORT void FPDF_CALLCONV EPDFFont_ClearFallbackFonts(void); +// Experimental EmbedPDF Extension API. +// Make a registered font the last resort: new text draws a character that +// neither its font nor any fallback has with it, so the character shows as +// that font's box and the text keeps it. Such a character is still reported +// as missing (EPDFDoc_CountMissingCharacters()). A page's own text never uses +// it. Pass 0 to clear it. Returns false for an unknown id. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFont_SetLastResortFont(EPDF_FONT_ID font_id); + +// Experimental EmbedPDF Extension API. +// The last-resort font; 0 when there is none. +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV EPDFFont_GetLastResortFont(void); + +// Experimental EmbedPDF Extension API. +// The registered fallback fonts in the order new text tries them for a +// style: tier by tier, lowest first; within a tier the closest weight and +// italic first, then the order they were added in. Fonts not authorized for +// editing are left out. The first of them that has a whole character +// (a base character with its marks) is the one that draws it. +// +// weight, italic - the style. +// ids - receives up to |capacity| font ids; may be NULL. +// capacity - the number of ids |ids| holds. +// +// Returns the number of fonts in the order, whatever |capacity| is. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFont_GetFallbackOrder(int weight, + FPDF_BOOL italic, + EPDF_FONT_ID* ids, + unsigned long capacity); + +// Experimental EmbedPDF Extension API. +// A registered font's program: the bytes the writer embeds (the static +// instance for a variable font). Returns the byte length, 0 for an unknown +// id; pass NULL/0 to query it. Nothing is copied when |buflen| is too small. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFont_GetProgramData(EPDF_FONT_ID font_id, + void* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// What a registered font's program is: EPDF_FONT_FORMAT_TRUETYPE or +// EPDF_FONT_FORMAT_OPENTYPE_CFF; -1 for an unknown id. +FPDF_EXPORT int FPDF_CALLCONV EPDFFont_GetProgramFormat(EPDF_FONT_ID font_id); + +// Experimental EmbedPDF Extension API. +// The SHA-256 of EPDFFont_GetProgramData()'s bytes into |sha256| (32 +// bytes). Returns false for an unknown id. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFont_GetProgramHash(EPDF_FONT_ID font_id, unsigned char* sha256); + +// Experimental EmbedPDF Extension API. +// The characters a registered font maps, as ranges: |ranges| receives up to +// |range_capacity| pairs (first code point, count). Returns the number of +// ranges, 0 for an unknown id; pass NULL/0 to query it. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFont_GetCoverage(EPDF_FONT_ID font_id, + uint32_t* ranges, + unsigned long range_capacity); + +// Experimental EmbedPDF Extension API. +// A registered font's line metrics as the layout reads them: its hhea +// ascender and descender over its units per em (descent positive). Returns +// false for an unknown id. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFFont_GetMetrics(EPDF_FONT_ID font_id, + float* ascent, + float* descent); + +// Experimental EmbedPDF Extension API. +// A registered font's PostScript name, as its embedded /BaseFont spells it +// (without a subset tag). Bytes and a trailing NUL; returns the length +// including the NUL, 0 for an unknown id; pass NULL/0 to query it. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFont_GetPostScriptName(EPDF_FONT_ID font_id, + char* buffer, + unsigned long buflen); + +// ---- A document's fonts ----------------------------------------------------- + +// Experimental EmbedPDF Extension API. +// The fonts a document uses, as faces (family, weight, italic) and the +// programs behind each: a detached snapshot, read with the EPDFFontFaces_* +// getters and freed with EPDFFontFaces_Close(). Found in the document's +// dictionaries without loading a page: /AcroForm /DR, fields' own /DR, every +// page's resources (inherited ones too), form XObjects, tiling patterns, +// soft masks, Type 3 glyph resources and every state of every annotation +// appearance. Facts only: what may be written with a font is the caller's +// decision. A snapshot goes stale with the next write. +typedef struct epdf_font_faces_t__* EPDF_FONT_FACES; + +// What a program is (EPDFFontFaces_GetProgramFormat). +#define EPDF_FONT_FORMAT_NONE 0 // the PDF only names the font +#define EPDF_FONT_FORMAT_TRUETYPE 1 // TrueType outlines (sfnt) +#define EPDF_FONT_FORMAT_OPENTYPE_CFF 2 // OpenType with CFF outlines (sfnt) +#define EPDF_FONT_FORMAT_CFF 3 // bare CFF (/FontFile3 /Type1C ...) +#define EPDF_FONT_FORMAT_TYPE1 4 // Type 1 (/FontFile) +#define EPDF_FONT_FORMAT_TYPE3 5 // glyphs drawn by content streams + +// How much of the font the PDF holds (EPDFFontFaces_GetProgramEmbedded). +#define EPDF_FONT_EMBEDDED_NONE 0 +#define EPDF_FONT_EMBEDDED_WHOLE 1 +#define EPDF_FONT_EMBEDDED_SUBSET 2 // the /BaseFont carries a subset tag + +// Returns NULL for a NULL document. +FPDF_EXPORT EPDF_FONT_FACES FPDF_CALLCONV +EPDFFontFaces_Load(FPDF_DOCUMENT document); +FPDF_EXPORT void FPDF_CALLCONV EPDFFontFaces_Close(EPDF_FONT_FACES faces); + +// Experimental EmbedPDF Extension API. +// The SHA-256 of every fact EPDFFontFaces_Load() lists for |document|, in +// order, into |sha256| (32 bytes): equal fingerprints, the same faces. Read +// it before and after a write to learn whether the write changed the +// document's fonts. It lists them to tell; on a layer, a list after a +// transaction walks only the places it touched. Returns false for a NULL +// document. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_GetFontsFingerprint(FPDF_DOCUMENT document, unsigned char* sha256); + +// The number of faces; -1 for a NULL snapshot. +FPDF_EXPORT int FPDF_CALLCONV EPDFFontFaces_Count(EPDF_FONT_FACES faces); + +// A face's family as UTF-16LE with a trailing NUL. Returns the length in +// bytes including the NUL, 0 for an index out of range; pass NULL/0 to +// query it. Empty families (some Type 3 fonts) return 2. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontFaces_GetFamily(EPDF_FONT_FACES faces, + int face, + FPDF_WCHAR* buffer, + unsigned long buflen); +// 100 to 900; 0 for an index out of range. +FPDF_EXPORT int FPDF_CALLCONV EPDFFontFaces_GetWeight(EPDF_FONT_FACES faces, + int face); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_IsItalic(EPDF_FONT_FACES faces, int face); + +// The programs behind a face: one per program stream, in the order found, +// plus one per kind for fonts without a program. -1 out of range. +FPDF_EXPORT int FPDF_CALLCONV EPDFFontFaces_CountPrograms(EPDF_FONT_FACES faces, + int face); + +// The program stream's object number: what EPDFDoc_GetFontProgramData() and +// EPDFAnnot_SetDefaultAppearanceDocumentFont() take. 0 for a font without a +// program, an inline program stream, or an index out of range. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontFaces_GetProgramObjNum(EPDF_FONT_FACES faces, int face, int program); +// One of EPDF_FONT_FORMAT_*; -1 out of range. +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontFaces_GetProgramFormat(EPDF_FONT_FACES faces, int face, int program); +// One of EPDF_FONT_EMBEDDED_*; -1 out of range. +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontFaces_GetProgramEmbedded(EPDF_FONT_FACES faces, int face, int program); +// The /BaseFont without its subset tag (a Type0 font's descendant's): bytes +// and a trailing NUL. Returns the length including the NUL, 0 out of range; +// pass NULL/0 to query it. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontFaces_GetProgramPostScriptName(EPDF_FONT_FACES faces, + int face, + int program, + char* buffer, + unsigned long buflen); +// What the program's OS/2 fsType grants: one of EPDF_FONT_EMBEDDING_*; -1 +// when it has no OS/2 table to say (Type 1, bare CFF, no program) or out of +// range. +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontFaces_GetProgramEmbeddingPermission(EPDF_FONT_FACES faces, + int face, + int program); +// Whether the program loads, has glyphs and maps characters through a +// Unicode cmap: whether the layout can write any character it maps. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_ProgramMapsUnicode(EPDF_FONT_FACES faces, int face, int program); +// The SHA-256 of the decoded program into |sha256| (32 bytes). Returns +// false for a font without a program or an index out of range. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_GetProgramHash(EPDF_FONT_FACES faces, + int face, + int program, + unsigned char* sha256); +// The SHA-256 of what a browser loads for the program into |sha256| (32 +// bytes): the program itself when it is TrueType or OpenType with a Unicode +// cmap and its tables where the font format puts them, else the display +// program built for it (EPDFDoc_GetFontDisplayProgramData()). Returns false +// when a browser can't draw it (Type 3, nothing embedded, a program that +// doesn't load) or for an index out of range. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_GetProgramDisplayHash(EPDF_FONT_FACES faces, + int face, + int program, + unsigned char* sha256); +// The program's line metrics as the layout reads them (em, descent +// positive). Returns false when it doesn't load or out of range. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontFaces_GetProgramMetrics(EPDF_FONT_FACES faces, + int face, + int program, + float* ascent, + float* descent); + +// Experimental EmbedPDF Extension API. +// A program the document's fonts use, by its stream's object number: its +// decoded bytes. Returns the length, 0 when |program_objnum| isn't such a +// program; pass NULL/0 to query it. Nothing is copied when |buflen| is too +// small. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetFontProgramData(FPDF_DOCUMENT document, + unsigned long program_objnum, + void* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// What a browser loads, by the SHA-256 EPDFFontFaces_GetProgramDisplayHash() +// gives (|display_sha256|, 32 bytes): the program itself when it is +// TrueType or OpenType with a Unicode cmap. When such a program's tables +// don't each start on a four-byte boundary (a browser refuses it, FreeType +// doesn't), the same tables realigned. Otherwise (Type 1, bare CFF, a +// program without a Unicode cmap) an OpenType font built from the program as +// one font dictionary reads it: its glyphs, a Unicode cmap holding exactly +// the characters the engine writes with that font (each through its +// encoding to the glyph it draws), and the advances the engine lays them out +// with (its /Widths). Two dictionaries that read one stream differently have +// two. The same bytes every time. Returns the length, 0 when no program the +// document's fonts use has that hash; pass NULL/0 to query it. Nothing is +// copied when |buflen| is too small. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetFontDisplayProgramData(FPDF_DOCUMENT document, + const unsigned char* display_sha256, + void* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// The characters a browser draws with that program (by |display_sha256|, as +// EPDFDoc_GetFontDisplayProgramData() takes it), which are the ones the +// engine writes with it, as ranges: |ranges| receives up to +// |range_capacity| pairs (first code point, count). Returns the number of +// ranges, 0 when it maps none or no program has that hash; pass NULL/0 to +// query it. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetFontDisplayProgramCoverage(FPDF_DOCUMENT document, + const unsigned char* display_sha256, + uint32_t* ranges, + unsigned long range_capacity); + +// ---- The fonts a write would use -------------------------------------------- + +// Experimental EmbedPDF Extension API. +// Which font each run of text would be drawn with, found the way a write +// finds it, without writing: the same /DA resolution, each face resolved +// once over all of its text, the same per-character fallback. A detached +// snapshot, read with the EPDFFontResolution_* getters and freed with +// EPDFFontResolution_Close(). +typedef struct epdf_font_resolution_t__* EPDF_FONT_RESOLUTION; + +// Where a run's font comes from (EPDFFontResolution_GetSource). +#define EPDF_FONT_SOURCE_STANDARD 0 // one of the standard 14 +#define EPDF_FONT_SOURCE_REGISTERED 1 // a registered font +#define EPDF_FONT_SOURCE_DOCUMENT_PROGRAM 2 // a program the document holds +#define EPDF_FONT_SOURCE_DOCUMENT_FONT 3 // the box's /DA font as it is + +// document - the document. +// annot - the box being edited (its own font counts first), or NULL +// for a new one. +// run_count - the number of runs. +// texts - each run's text, UTF-16LE, NUL-terminated. +// families - each run's family (a family name or a registered font's). +// weights - each run's weight, 100 to 900. +// italics - whether each run is italic. +// programs - each run's document program, the object number of the font +// program stream a write would name for it (the face exactly, +// whatever its family says), or 0 to resolve the family; NULL +// when no run names one. +// +// Returns NULL for a NULL document or arrays. +FPDF_EXPORT EPDF_FONT_RESOLUTION FPDF_CALLCONV +EPDFDoc_ResolveFonts(FPDF_DOCUMENT document, + FPDF_ANNOTATION annot, + int run_count, + const FPDF_WIDESTRING* texts, + const FPDF_WIDESTRING* families, + const int* weights, + const FPDF_BOOL* italics, + const int* programs); +FPDF_EXPORT void FPDF_CALLCONV +EPDFFontResolution_Close(EPDF_FONT_RESOLUTION resolution); + +// One of EPDF_FONT_SOURCE_*; -1 out of range. +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontResolution_GetSource(EPDF_FONT_RESOLUTION resolution, int run); +// The registered font for EPDF_FONT_SOURCE_REGISTERED; 0 otherwise. +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV +EPDFFontResolution_GetRegisteredFont(EPDF_FONT_RESOLUTION resolution, int run); +// The program stream's object number for EPDF_FONT_SOURCE_DOCUMENT_PROGRAM, +// and for EPDF_FONT_SOURCE_DOCUMENT_FONT when it embeds one; 0 otherwise. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetProgramObjNum(EPDF_FONT_RESOLUTION resolution, int run); +// The SHA-256 of what a browser loads for that program into |sha256| (32 +// bytes), as the font dictionary the run draws with reads it: the hash +// EPDFFontFaces_GetProgramDisplayHash() lists for it, which +// EPDFDoc_GetFontDisplayProgramData() takes. Two dictionaries that read one +// program stream differently give two. Returns false when the run draws with +// no document program or a browser can't draw it. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontResolution_GetProgramDisplayHash(EPDF_FONT_RESOLUTION resolution, + int run, + unsigned char* sha256); +// The font's /BaseFont: the standard name for EPDF_FONT_SOURCE_STANDARD +// ("Helvetica-Bold"). Bytes and a trailing NUL; returns the length +// including the NUL, 0 when there is none; pass NULL/0 to query it. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetBaseFontName(EPDF_FONT_RESOLUTION resolution, + int run, + char* buffer, + unsigned long buflen); +// The face the run is drawn in: its family (UTF-16LE with a trailing NUL; +// the length in bytes, 0 out of range), weight and italic. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetFamily(EPDF_FONT_RESOLUTION resolution, + int run, + FPDF_WCHAR* buffer, + unsigned long buflen); +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontResolution_GetWeight(EPDF_FONT_RESOLUTION resolution, int run); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontResolution_IsItalic(EPDF_FONT_RESOLUTION resolution, int run); +// Whether the face isn't the one the run asked for (a substitute). +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFontResolution_IsDegraded(EPDF_FONT_RESOLUTION resolution, int run); +// The fallback fonts for the run's style, in the order they are tried +// (EPDFFont_GetFallbackOrder()). Returns their number; pass NULL/0 to +// query it. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetFallbacks(EPDF_FONT_RESOLUTION resolution, + int run, + EPDF_FONT_ID* ids, + unsigned long capacity); +// The characters (graphemes) no font has: they would be drawn as missing +// glyphs. Count, then each as UTF-16LE with a trailing NUL (the length in +// bytes, 0 out of range). +FPDF_EXPORT int FPDF_CALLCONV +EPDFFontResolution_CountMissingCharacters(EPDF_FONT_RESOLUTION resolution); +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFFontResolution_GetMissingCharacter(EPDF_FONT_RESOLUTION resolution, + int index, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// ---- Characters a write drew with no font ----------------------------------- + +// Experimental EmbedPDF Extension API. +// The characters (graphemes) appearances generated for |document| drew with +// no font that has them, first seen first, since the list was last cleared: +// clear it before a write and read it after. At most 256 are kept. +FPDF_EXPORT void FPDF_CALLCONV +EPDFDoc_ClearMissingCharacters(FPDF_DOCUMENT document); +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_CountMissingCharacters(FPDF_DOCUMENT document); +// Each as UTF-16LE with a trailing NUL: the length in bytes, 0 out of +// range; pass NULL/0 to query it. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetMissingCharacter(FPDF_DOCUMENT document, + int index, + FPDF_WCHAR* buffer, + unsigned long buflen); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/testing/resources/fonts/last_resort.ttf b/testing/resources/fonts/last_resort.ttf new file mode 100644 index 0000000000..189b5bca0f Binary files /dev/null and b/testing/resources/fonts/last_resort.ttf differ