Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions core/fpdfdoc/cpdf_annotfontsubset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,14 @@ void EnsureMinimumPdfVersion16(CPDF_Document* doc) {

ByteString MakeSubsetBaseFontName(
const ByteString& base_font_name,
pdfium::span<const uint8_t> 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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
132 changes: 122 additions & 10 deletions core/fxge/cfx_fontregistry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
#include "core/fxge/cfx_fontregistry.h"

#include <algorithm>
#include <cmath>
#include <limits>
#include <memory>
#include <optional>
#include <utility>
#include <vector>

Expand Down Expand Up @@ -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<char> 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<unsigned int> NamedInstanceAt(hb_face_t* face,
const std::vector<float>& coords) {
const unsigned int instances = hb_ot_var_get_named_instance_count(face);
for (unsigned int i = 0; i < instances; ++i) {
std::vector<float> instance(coords.size());
unsigned int count = pdfium::checked_cast<unsigned int>(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<float>& coords) {
const std::optional<unsigned int> 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<hb_ot_name_id_t>(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<hb_ot_var_axis_info_t> axes(coords.size());
unsigned int axis_count = pdfium::checked_cast<unsigned int>(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<const uint8_t> data,
int requested_weight,
DataVector<uint8_t>* result) {
DataVector<uint8_t>* result,
ByteString* instance_name) {
hb_blob_t* blob =
hb_blob_create(reinterpret_cast<const char*>(data.data()),
pdfium::checked_cast<unsigned int>(data.size()),
Expand All @@ -101,14 +197,24 @@ Instancing InstanceVariableFont(pdfium::span<const uint8_t> 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<hb_ot_var_axis_info_t> axes(hb_ot_var_get_axis_count(face));
unsigned int axis_count = pdfium::checked_cast<unsigned int>(axes.size());
hb_ot_var_get_axis_infos(face, 0, &axis_count, axes.data());
std::vector<float> 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<float>(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<float>(requested_weight));
coords[i] = static_cast<float>(requested_weight);
}
}
}
hb_face_t* instance = hb_subset_or_fail(face, input);
Expand All @@ -118,6 +224,7 @@ Instancing InstanceVariableFont(pdfium::span<const uint8_t> 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);
Expand Down Expand Up @@ -269,7 +376,8 @@ CFX_FontRegistry::FontId RegisterLoadedFontSource(
// one that cannot be instanced is refused rather than kept variable.
bool instanced = false;
DataVector<uint8_t> 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:
Expand Down Expand Up @@ -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
Expand Down
127 changes: 125 additions & 2 deletions fpdfsdk/epdf_annot_embeddertest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,39 @@ std::vector<uint8_t> LoadRobotoVariableAbcFontData() {
return GetFileContents(font_path.c_str());
}

// A variable Noto Sans whose named instances have no PostScript names.
std::vector<uint8_t> 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<uint8_t> 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<uint8_t> LoadCffLatinFontData() {
// A small OpenType/CFF font (no CIDFont operators) with A, B, C.
std::string font_path = PathService::GetThirdPartyFilePath(
Expand Down Expand Up @@ -2471,7 +2504,9 @@ TEST_F(EPDFAnnotEmbedderTest, NoSubsettingFontEmbedsWholeProgram) {
RetainPtr<const CPDF_Dictionary> 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<uint8_t> program = EmbeddedFontProgram(font_dict.Get());
EXPECT_EQ(no_subsetting.size(), program.size());
EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'A'));
Expand Down Expand Up @@ -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<uint8_t>(CFX_FontRegistry::GetSourceHash(regular).begin(),
Expand Down Expand Up @@ -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<uint8_t> 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<uint8_t> regular_data = LoadTestFontData("Arimo-Regular.ttf");
std::vector<uint8_t> 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<const CPDF_Dictionary> {
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<const CPDF_Dictionary> regular_font = write(regular, 350.0f);
RetainPtr<const CPDF_Dictionary> 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<const CPDF_Dictionary> regular_descriptor =
GetType0FontDescriptor(regular_font.Get());
RetainPtr<const CPDF_Dictionary> 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 {
Expand Down Expand Up @@ -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 =
Expand Down
1 change: 1 addition & 0 deletions testing/embedpdf_layer_mode_writers.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down