diff --git a/core/fpdfdoc/cpdf_annotfontsubset.cpp b/core/fpdfdoc/cpdf_annotfontsubset.cpp index 4b4f730bf..656d9bd81 100644 --- a/core/fpdfdoc/cpdf_annotfontsubset.cpp +++ b/core/fpdfdoc/cpdf_annotfontsubset.cpp @@ -117,11 +117,14 @@ void EnsureMinimumPdfVersion16(CPDF_Document* doc) { ByteString MakeSubsetBaseFontName( const ByteString& base_font_name, + pdfium::span program_hash, const CPDF_AnnotFontSubset::GlyphUnicodeMap& glyph_to_unicode) { - // EmbedPDF: a deterministic six-letter PDF subset tag is enough to keep - // subsets distinguishable for readers/debugging. A theoretical hash collision - // is harmless because each AP resource dictionary still points at its own - // embedded subset font object. + // EmbedPDF: a deterministic six-letter PDF subset tag. Different subsets in + // one file get different tags (ISO 32000-2 9.6.4), so it hashes the program + // as well as the glyphs it holds: two weights that hold the same characters + // number their glyphs alike. A theoretical hash collision is harmless + // because each AP resource dictionary still points at its own embedded + // subset font object. uint32_t hash = 2166136261u; auto mix = [&hash](uint32_t value) { for (int i = 0; i < 4; ++i) { @@ -130,6 +133,9 @@ ByteString MakeSubsetBaseFontName( } }; + for (uint8_t byte : program_hash) { + mix(byte); + } for (const auto& [glyph_id, unicode] : glyph_to_unicode) { mix(glyph_id); mix(unicode); @@ -744,7 +750,9 @@ CPDF_AnnotFontSubset::StageRegisteredFontResource( const ByteString subset_font_name = subset_font_data.empty() ? base_font_name - : MakeSubsetBaseFontName(base_font_name, filtered_glyph_to_unicode); + : MakeSubsetBaseFontName(base_font_name, + CFX_FontRegistry::GetSourceHash(font_id), + filtered_glyph_to_unicode); StagedFontResource staged; if (!BuildCompositeFontParts(font.get(), subset_font_name, font_data, IdentityForRegisteredFont(font_id), widths, diff --git a/core/fxge/cfx_fontregistry.cpp b/core/fxge/cfx_fontregistry.cpp index 820364df1..7c24b2baf 100644 --- a/core/fxge/cfx_fontregistry.cpp +++ b/core/fxge/cfx_fontregistry.cpp @@ -7,8 +7,10 @@ #include "core/fxge/cfx_fontregistry.h" #include +#include #include #include +#include #include #include @@ -79,9 +81,103 @@ CFX_FontRegistry::EmbeddingPermission ClassifyFsTypeImpl(uint16_t fs_type) { // the annotation writer subsets is static. enum class Instancing { kNotVariable, kInstanced, kFailed }; +// A name table entry, in English when it has one; empty when it has none. +ByteString OpenTypeName(hb_face_t* face, hb_ot_name_id_t name_id) { + if (name_id == HB_OT_NAME_ID_INVALID) { + return ByteString(); + } + unsigned int length = + hb_ot_name_get_utf8(face, name_id, HB_LANGUAGE_INVALID, nullptr, nullptr); + if (length == 0) { + return ByteString(); + } + std::vector text(length + 1); + unsigned int size = length + 1; + hb_ot_name_get_utf8(face, name_id, HB_LANGUAGE_INVALID, &size, text.data()); + return ByteString(ByteStringView(text.data(), size)); +} + +// The letters and digits of |name|, as a PostScript name may hold them. +ByteString PostScriptCharacters(const ByteString& name) { + ByteString out; + for (char ch : name) { + if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || + (ch >= '0' && ch <= '9')) { + out += ch; + } + } + return out; +} + +// The named instance at |coords| (one per axis, in fvar order), if the font +// has one there. +std::optional NamedInstanceAt(hb_face_t* face, + const std::vector& coords) { + const unsigned int instances = hb_ot_var_get_named_instance_count(face); + for (unsigned int i = 0; i < instances; ++i) { + std::vector instance(coords.size()); + unsigned int count = pdfium::checked_cast(instance.size()); + if (hb_ot_var_named_instance_get_design_coords( + face, i, &count, instance.data()) == coords.size() && + std::ranges::equal(instance, coords, [](float a, float b) { + return std::abs(a - b) < 0.001f; + })) { + return i; + } + } + return std::nullopt; +} + +// The PostScript name of the instance at |coords|, as Adobe Technical Note +// #5902 names instances: the named instance's own PostScript name; else the +// family prefix (name 25, else the typographic family, else the family) and +// the named instance's style; else the prefix and the weight. The instance +// keeps the variable font's name table, whose name 6 is the default +// instance's, so it can't name itself. Empty when the font names nothing. +ByteString InstancePostScriptName(hb_face_t* face, + const std::vector& coords) { + const std::optional named = NamedInstanceAt(face, coords); + if (named.has_value()) { + ByteString own = OpenTypeName( + face, hb_ot_var_named_instance_get_postscript_name_id(face, *named)); + if (!own.IsEmpty()) { + return own; + } + } + ByteString prefix = PostScriptCharacters( + OpenTypeName(face, static_cast(25))); + if (prefix.IsEmpty()) { + prefix = PostScriptCharacters( + OpenTypeName(face, HB_OT_NAME_ID_TYPOGRAPHIC_FAMILY)); + } + if (prefix.IsEmpty()) { + prefix = + PostScriptCharacters(OpenTypeName(face, HB_OT_NAME_ID_FONT_FAMILY)); + } + if (prefix.IsEmpty()) { + return ByteString(); + } + if (named.has_value()) { + const ByteString style = PostScriptCharacters(OpenTypeName( + face, hb_ot_var_named_instance_get_subfamily_name_id(face, *named))); + return style.IsEmpty() ? prefix : prefix + "-" + style; + } + // No named instance there: only `wght` moves off its default. + std::vector axes(coords.size()); + unsigned int axis_count = pdfium::checked_cast(axes.size()); + hb_ot_var_get_axis_infos(face, 0, &axis_count, axes.data()); + for (unsigned int i = 0; i < axis_count; ++i) { + if (axes[i].tag == HB_TAG('w', 'g', 'h', 't')) { + return prefix + ByteString::Format("_%ldwght", std::lround(coords[i])); + } + } + return prefix; +} + Instancing InstanceVariableFont(pdfium::span data, int requested_weight, - DataVector* result) { + DataVector* result, + ByteString* instance_name) { hb_blob_t* blob = hb_blob_create(reinterpret_cast(data.data()), pdfium::checked_cast(data.size()), @@ -101,14 +197,24 @@ Instancing InstanceVariableFont(pdfium::span data, if (input) { hb_subset_input_keep_everything(input); hb_subset_input_pin_all_axes_to_default(input, face); + // Where the instance sits, one coordinate per axis in fvar order. + std::vector axes(hb_ot_var_get_axis_count(face)); + unsigned int axis_count = pdfium::checked_cast(axes.size()); + hb_ot_var_get_axis_infos(face, 0, &axis_count, axes.data()); + std::vector coords; + for (const hb_ot_var_axis_info_t& axis : axes) { + coords.push_back(axis.default_value); + } if (requested_weight >= 100 && requested_weight <= 900) { - hb_ot_var_axis_info_t axis; - if (hb_ot_var_find_axis_info(face, HB_TAG('w', 'g', 'h', 't'), &axis) && - axis.min_value <= requested_weight && - requested_weight <= axis.max_value) { - hb_subset_input_pin_axis_location( - input, face, HB_TAG('w', 'g', 'h', 't'), - static_cast(requested_weight)); + for (size_t i = 0; i < axes.size(); ++i) { + if (axes[i].tag == HB_TAG('w', 'g', 'h', 't') && + axes[i].min_value <= requested_weight && + requested_weight <= axes[i].max_value) { + hb_subset_input_pin_axis_location( + input, face, HB_TAG('w', 'g', 'h', 't'), + static_cast(requested_weight)); + coords[i] = static_cast(requested_weight); + } } } hb_face_t* instance = hb_subset_or_fail(face, input); @@ -118,6 +224,7 @@ Instancing InstanceVariableFont(pdfium::span data, const char* bytes = hb_blob_get_data(out, &length); if (bytes && length > 0) { result->assign(bytes, bytes + length); + *instance_name = InstancePostScriptName(face, coords); outcome = Instancing::kInstanced; } hb_blob_destroy(out); @@ -269,7 +376,8 @@ CFX_FontRegistry::FontId RegisterLoadedFontSource( // one that cannot be instanced is refused rather than kept variable. bool instanced = false; DataVector instanced_data; - switch (InstanceVariableFont(data, weight, &instanced_data)) { + ByteString instance_name; + switch (InstanceVariableFont(data, weight, &instanced_data, &instance_name)) { case Instancing::kNotVariable: break; case Instancing::kFailed: @@ -301,8 +409,12 @@ CFX_FontRegistry::FontId RegisterLoadedFontSource( registered_font->no_subsetting = (fs_type & kFsTypeNoSubsetting) != 0; registered_font->instanced = instanced; registered_font->source_hash = CryptSha256Generate(data); + // /BaseFont is the program's own PostScript name (ISO 32000-2 9.6.3, + // 9.7.4), never the family it is registered under, so two weights of one + // family are two fonts by name too: the family is its identity in the + // descriptor's /FontFamily. An instance takes the name of the instance. registered_font->base_font_name = NormalizeBaseFontName( - family_name.IsEmpty() ? font->GetBaseFontName() : family_name); + instance_name.IsEmpty() ? font->GetBaseFontName() : instance_name); // The persistent identity (descriptor /FontFamily, rich text families): // the family given, else the font's own family from its name table // ("Roboto", never the PostScript "Roboto-Regular"), so a later session diff --git a/fpdfsdk/epdf_annot_embeddertest.cpp b/fpdfsdk/epdf_annot_embeddertest.cpp index 8a345188e..1da53ee75 100644 --- a/fpdfsdk/epdf_annot_embeddertest.cpp +++ b/fpdfsdk/epdf_annot_embeddertest.cpp @@ -144,6 +144,39 @@ std::vector LoadRobotoVariableAbcFontData() { return GetFileContents(font_path.c_str()); } +// A variable Noto Sans whose named instances have no PostScript names. +std::vector LoadNotoSansVariableAbcFontData() { + std::string font_path = PathService::GetThirdPartyFilePath( + "harfbuzz/src/test/subset/data/fonts/NotoSans-VF.abc.ttf"); + if (font_path.empty()) { + ADD_FAILURE() << "Failed to find NotoSans-VF.abc test font"; + return {}; + } + return GetFileContents(font_path.c_str()); +} + +std::vector LoadTestFontData(const char* name) { + std::string font_path = PathService::GetThirdPartyFilePath( + std::string("test_fonts/test_fonts/") + name); + if (font_path.empty()) { + ADD_FAILURE() << "Failed to find " << name; + return {}; + } + return GetFileContents(font_path.c_str()); +} + +// A registered font's PostScript name, as its embedded /BaseFont spells it. +std::string PostScriptNameOf(EPDF_FONT_ID font_id) { + const unsigned long length = EPDFFont_GetPostScriptName(font_id, nullptr, 0); + if (length == 0) { + return std::string(); + } + std::string name(length, '\0'); + EPDFFont_GetPostScriptName(font_id, name.data(), length); + name.resize(length - 1); + return name; +} + std::vector LoadCffLatinFontData() { // A small OpenType/CFF font (no CIDFont operators) with A, B, C. std::string font_path = PathService::GetThirdPartyFilePath( @@ -2471,7 +2504,9 @@ TEST_F(EPDFAnnotEmbedderTest, NoSubsettingFontEmbedsWholeProgram) { RetainPtr font_dict = GetAppearanceFontDict(annot.get(), RegisteredFontAlias(font_id)); ASSERT_TRUE(font_dict); - EXPECT_EQ("Roboto", font_dict->GetNameFor("BaseFont")); // no subset tag + // Its PostScript name, not the family it was registered under; whole, so + // no subset tag. + EXPECT_EQ("Roboto-Regular", font_dict->GetNameFor("BaseFont")); std::vector program = EmbeddedFontProgram(font_dict.Get()); EXPECT_EQ(no_subsetting.size(), program.size()); EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'A')); @@ -2500,6 +2535,11 @@ TEST_F(EPDFAnnotEmbedderTest, VariableFontIsInstancedAtRegistration) { EXPECT_TRUE(EPDFFont_IsInstanced(regular)); EXPECT_TRUE(EPDFFont_IsInstanced(bold)); EXPECT_FALSE(EPDFFont_IsInstanced(static_font)); + // Each instance is named as the font names that instance: name 6, which an + // instance keeps from the variable font, is the default instance's. + EXPECT_EQ("RobotoRoman-Regular", PostScriptNameOf(regular)); + EXPECT_EQ("RobotoRoman-Bold", PostScriptNameOf(bold)); + EXPECT_EQ("Roboto-Regular", PostScriptNameOf(static_font)); EXPECT_EQ(32u, CFX_FontRegistry::GetSourceHash(regular).size()); EXPECT_NE( std::vector(CFX_FontRegistry::GetSourceHash(regular).begin(), @@ -2553,6 +2593,87 @@ TEST_F(EPDFAnnotEmbedderTest, VariableFontIsInstancedAtRegistration) { GetType0FontDescriptor(font_dict.Get()); ASSERT_TRUE(descriptor); EXPECT_EQ(700, descriptor->GetIntegerFor("FontWeight")); + EXPECT_EQ("+RobotoRoman-Bold", font_dict->GetNameFor("BaseFont").Substr(6)); +} + +// A variable font whose named instances have no PostScript names: an instance +// is the family prefix and the instance's style, and a weight no instance +// sits at is the prefix and the weight (Adobe Technical Note #5902). +TEST_F(EPDFAnnotEmbedderTest, VariableFontInstanceWithoutItsOwnName) { + ScopedRegisteredFonts scoped_fonts; + std::vector vf = LoadNotoSansVariableAbcFontData(); + ASSERT_FALSE(vf.empty()); + EPDF_FONT_ID bold = EPDFFont_RegisterMemFont64( + "Noto Sans", /*weight=*/700, /*italic=*/0, vf.data(), vf.size()); + EPDF_FONT_ID between = EPDFFont_RegisterMemFont64( + "Noto Sans", /*weight=*/450, /*italic=*/0, vf.data(), vf.size()); + ASSERT_NE(0u, bold); + ASSERT_NE(0u, between); + EXPECT_EQ("NotoSans-Bold", PostScriptNameOf(bold)); + EXPECT_EQ("NotoSans_450wght", PostScriptNameOf(between)); +} + +// Two weights of one family are two fonts by name: each /BaseFont is its +// program's PostScript name and each subset has its own tag (ISO 32000-2 +// 9.6.3, 9.6.4), while the family they were registered under stays their +// /FontFamily. Arimo's two files number the glyphs of "Hello" alike, so a tag +// of the glyphs alone would be one tag. +TEST_F(EPDFAnnotEmbedderTest, TwoWeightsOfOneFamilyAreTwoFontsByName) { + ScopedRegisteredFonts scoped_fonts; + std::vector regular_data = LoadTestFontData("Arimo-Regular.ttf"); + std::vector bold_data = LoadTestFontData("Arimo-Bold.ttf"); + ASSERT_FALSE(regular_data.empty()); + ASSERT_FALSE(bold_data.empty()); + EPDF_FONT_ID regular = + EPDFFont_RegisterMemFont64("Arimo", /*weight=*/400, /*italic=*/0, + regular_data.data(), regular_data.size()); + EPDF_FONT_ID bold = + EPDFFont_RegisterMemFont64("Arimo", /*weight=*/700, /*italic=*/0, + bold_data.data(), bold_data.size()); + ASSERT_NE(0u, regular); + ASSERT_NE(0u, bold); + EXPECT_EQ("Arimo-Regular", PostScriptNameOf(regular)); + EXPECT_EQ("Arimo-Bold", PostScriptNameOf(bold)); + + embedpdf_test::ScopedTestDocument doc(NewTestDocument()); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); + auto write = [&](EPDF_FONT_ID font_id, + float top) -> RetainPtr { + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FREETEXT)); + const FS_RECTF rect{50.0f, top, 350.0f, top - 60.0f}; + if (!annot || !FPDFAnnot_SetRect(annot.get(), &rect)) { + return nullptr; + } + ScopedFPDFWideString contents = GetFPDFWideString(L"Hello"); + if (!FPDFAnnot_SetStringValue(annot.get(), "Contents", contents.get()) || + !EPDFAnnot_SetDefaultAppearanceRegisteredFont(annot.get(), font_id, + 18.0f, 0, 0, 0) || + !EPDFAnnot_GenerateAppearance(annot.get())) { + return nullptr; + } + return GetAppearanceFontDict(annot.get(), RegisteredFontAlias(font_id)); + }; + RetainPtr regular_font = write(regular, 350.0f); + RetainPtr bold_font = write(bold, 250.0f); + ASSERT_TRUE(regular_font); + ASSERT_TRUE(bold_font); + + const ByteString regular_name = regular_font->GetNameFor("BaseFont"); + const ByteString bold_name = bold_font->GetNameFor("BaseFont"); + EXPECT_EQ("+Arimo-Regular", regular_name.Substr(6)); + EXPECT_EQ("+Arimo-Bold", bold_name.Substr(6)); + EXPECT_NE(regular_name.First(6), bold_name.First(6)); + RetainPtr regular_descriptor = + GetType0FontDescriptor(regular_font.Get()); + RetainPtr bold_descriptor = + GetType0FontDescriptor(bold_font.Get()); + ASSERT_TRUE(regular_descriptor); + ASSERT_TRUE(bold_descriptor); + EXPECT_EQ(regular_name, regular_descriptor->GetNameFor("FontName")); + EXPECT_EQ(bold_name, bold_descriptor->GetNameFor("FontName")); + EXPECT_EQ(L"Arimo", regular_descriptor->GetUnicodeTextFor("FontFamily")); + EXPECT_EQ(L"Arimo", bold_descriptor->GetUnicodeTextFor("FontFamily")); } namespace { @@ -2791,7 +2912,9 @@ TEST_F(EPDFAnnotEmbedderTest, RegisteredCffFontEmitsCidFontType0) { EXPECT_EQ('T', bytes[2]); EXPECT_EQ('O', bytes[3]); EXPECT_LT(bytes.size(), cff.size()); // CFF subsetting is compiled in - EXPECT_TRUE(font_dict->GetNameFor("BaseFont").Contains("+CffLatin")); + // Its PostScript name, not the family it was registered under. + EXPECT_EQ("gpos1_2_font1", PostScriptNameOf(font_id)); + EXPECT_EQ("+gpos1_2_font1", font_dict->GetNameFor("BaseFont").Substr(6)); EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'A')); ScopedFPDFBitmap bitmap = diff --git a/testing/embedpdf_layer_mode_writers.txt b/testing/embedpdf_layer_mode_writers.txt index 097e782ce..7724c9ad1 100644 --- a/testing/embedpdf_layer_mode_writers.txt +++ b/testing/embedpdf_layer_mode_writers.txt @@ -161,6 +161,7 @@ EPDFAnnotEmbedderTest.TextFieldDrawsItsBorderStyle 2 EPDFAnnotEmbedderTest.TextFieldGenerateAppearanceStreamIsStable 2 EPDFAnnotEmbedderTest.TextFieldKoreanUsesRegisteredDroidFallbackFont 20 EPDFAnnotEmbedderTest.TurnedBoxRectIsTheUprightBoxAroundIt 2 +EPDFAnnotEmbedderTest.TwoWeightsOfOneFamilyAreTwoFontsByName 19 EPDFAnnotEmbedderTest.VariableFontIsInstancedAtRegistration 12 EPDFAnnotEmbedderTest.WidgetRegisteredFontInstallsRealDrEntry 25 EPDFAnnotEmbedderTest.WidgetTextAlignment 2