Skip to content

Commit 931f0cb

Browse files
committed
Always desubroutinize CFF fonts in IFT encoder.
This ensures all glyphs are fully self contained. Also add a integration test for a CFF font extension.
1 parent 49b6d13 commit 931f0cb

File tree

4 files changed

+151
-20
lines changed

4 files changed

+151
-20
lines changed

ift/encoder/encoder.cc

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,21 @@ StatusOr<Encoder::Encoding> Encoder::Encode() const {
229229
// extra space in the initial font. However, it would
230230
// require us to examine conditions against each subset
231231
// to determine patch reachability.
232+
//
233+
// TODO(garretrieger): in the mean time we can use the max glyph id from
234+
// fully
235+
// expanded subset instead. this will at least prune
236+
// glyphs not used at any extension level.
232237
uint32_t gid_count = hb_face_get_glyph_count(face_.get());
233238
if (gid_count > 0) context.base_subset_.gids.insert(gid_count - 1);
234239
}
235240

241+
// TODO(garretrieger): when generating the fully expanded subset don't use
242+
// retain
243+
// gids. Save the resulting glyph mapping and use it to
244+
// translate encoder config gids into the space used by
245+
// fully expanded subset. This will optimize for cases
246+
// that don't include the entire original font.
236247
context.force_long_loca_and_gvar_ = false;
237248
auto expanded = FullyExpandedSubset(context);
238249
if (!expanded.ok()) {
@@ -241,6 +252,11 @@ StatusOr<Encoder::Encoding> Encoder::Encode() const {
241252

242253
context.fully_expanded_subset_.shallow_copy(*expanded);
243254
auto expanded_face = expanded->face();
255+
256+
// TODO(garretrieger): we don't need to force long gvar anymore. The client is
257+
// now capable of
258+
// upgrading the offset size as needed. Forcing long loca
259+
// is still needed though.
244260
context.force_long_loca_and_gvar_ =
245261
FontHelper::HasLongLoca(expanded_face.get()) ||
246262
FontHelper::HasWideGvar(expanded_face.get());
@@ -313,8 +329,9 @@ Status Encoder::EnsureGlyphKeyedPatchesPopulated(
313329
instance.shallow_copy(*result);
314330
}
315331

316-
GlyphKeyedDiff differ(instance, compat_id,
317-
{FontHelper::kGlyf, FontHelper::kGvar, FontHelper::kCFF});
332+
GlyphKeyedDiff differ(
333+
instance, compat_id,
334+
{FontHelper::kGlyf, FontHelper::kGvar, FontHelper::kCFF});
318335

319336
for (uint32_t index : reachable_segments) {
320337
auto e = glyph_data_patches_.find(index);
@@ -565,7 +582,7 @@ StatusOr<FontData> Encoder::GenerateBaseGvar(
565582

566583
// Step 3: extract gvar table.
567584
hb_blob_unique_ptr gvar_blob = make_hb_blob(
568-
hb_face_reference_table(face_builder->get(), HB_TAG('g', 'v', 'a', 'r')));
585+
hb_face_reference_table(face_builder->get(), FontHelper::kGvar));
569586
FontData result(gvar_blob.get());
570587
return result;
571588
}
@@ -575,9 +592,18 @@ void Encoder::SetMixedModeSubsettingFlagsIfNeeded(
575592
if (IsMixedMode()) {
576593
// Mixed mode requires stable gids set flags accordingly.
577594
hb_subset_input_set_flags(
578-
input, hb_subset_input_get_flags(input) | HB_SUBSET_FLAGS_RETAIN_GIDS |
579-
HB_SUBSET_FLAGS_NOTDEF_OUTLINE |
580-
HB_SUBSET_FLAGS_PASSTHROUGH_UNRECOGNIZED);
595+
input,
596+
hb_subset_input_get_flags(input) | HB_SUBSET_FLAGS_RETAIN_GIDS |
597+
HB_SUBSET_FLAGS_NOTDEF_OUTLINE |
598+
HB_SUBSET_FLAGS_PASSTHROUGH_UNRECOGNIZED |
599+
// CFF tables are always desubroutinized for mixed mode encoding.
600+
// This ensures that for each glyph all data for that glyph is fully
601+
// self contained. See: https://w3c.github.io/IFT/Overview.html#cff
602+
//
603+
// Note: a non desubroutinized mode could be supported, but a
604+
// special base CFF table would need to be generated in a similar
605+
// style to "GenerateBaseGvar()"
606+
HB_SUBSET_FLAGS_DESUBROUTINIZE);
581607

582608
if (context.force_long_loca_and_gvar_) {
583609
// IFTB requirements flag has the side effect of forcing long loca and
@@ -596,7 +622,9 @@ StatusOr<FontData> Encoder::CutSubset(const ProcessingContext& context,
596622
return result.status();
597623
}
598624

599-
if (IsMixedMode() && def.IsVariable()) {
625+
auto tags = FontHelper::GetTags(font);
626+
if (IsMixedMode() && def.IsVariable() &&
627+
tags.contains(FontHelper::kGvar)) {
600628
// In mixed mode glyph keyed patches handles gvar, except for when design
601629
// space is expanded, in which case a gvar table should be patched in that
602630
// only has coverage of the base (root) subset definition + the current
@@ -605,13 +633,9 @@ StatusOr<FontData> Encoder::CutSubset(const ProcessingContext& context,
605633
// Create such a gvar table here and overwrite the one that was otherwise
606634
// generated by the normal subsetting operation. The patch generation will
607635
// handle including a replacement gvar patch when needed.
608-
auto base_gvar = GenerateBaseGvar(context, font, def.design_space);
609-
if (!base_gvar.ok()) {
610-
return base_gvar.status();
611-
}
612-
613-
hb_blob_unique_ptr gvar_blob = base_gvar->blob();
614-
hb_face_builder_add_table(result->get(), HB_TAG('g', 'v', 'a', 'r'),
636+
auto base_gvar = TRY(GenerateBaseGvar(context, font, def.design_space));
637+
hb_blob_unique_ptr gvar_blob = base_gvar.blob();
638+
hb_face_builder_add_table(result->get(), FontHelper::kGvar,
615639
gvar_blob.get());
616640
}
617641

ift/encoder/encoder.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,8 @@ class Encoder {
211211

212212
static ift::TableKeyedDiff* MixedModeTableKeyedDiff(
213213
common::CompatId base_compat_id) {
214-
return new TableKeyedDiff(base_compat_id, {"IFTX", "glyf", "loca", "gvar", "CFF "});
214+
return new TableKeyedDiff(base_compat_id,
215+
{"IFTX", "glyf", "loca", "gvar", "CFF "});
215216
}
216217

217218
static ift::TableKeyedDiff* ReplaceIftMapTableKeyedDiff(

ift/glyph_keyed_diff_test.cc

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -266,11 +266,10 @@ TEST_F(GlyphKeyedDiffTest, CreatePatch_Cff) {
266266
FontData empty;
267267
FontData compressed_stream(patch->str(29));
268268
FontData uncompressed_stream;
269-
auto status =
270-
unbrotli.Patch(empty, compressed_stream, &uncompressed_stream);
271-
ASSERT_TRUE(status.ok()) << status;
272-
ASSERT_EQ(uncompressed_stream.str(),
273-
absl::string_view((const char*)data_stream_u16_cff, 57));
269+
auto status = unbrotli.Patch(empty, compressed_stream, &uncompressed_stream);
270+
ASSERT_TRUE(status.ok()) << status;
271+
ASSERT_EQ(uncompressed_stream.str(),
272+
absl::string_view((const char*)data_stream_u16_cff, 57));
274273
}
275274

276275
TEST_F(GlyphKeyedDiffTest, CreatePatch_Gvar) {

ift/integration_test.cc

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ class IntegrationTest : public ::testing::Test {
7272
hb_blob_create_from_file("ift/testdata/NotoSansJP-Regular.subset.ttf"));
7373
noto_sans_jp_.set(blob.get());
7474

75+
blob = make_hb_blob(
76+
hb_blob_create_from_file("common/testdata/NotoSansJP-Regular.otf"));
77+
noto_sans_jp_cff_.set(blob.get());
78+
7579
// Noto Sans JP VF
7680
blob = make_hb_blob(
7781
hb_blob_create_from_file("ift/testdata/NotoSansJP[wght].subset.ttf"));
@@ -119,6 +123,20 @@ class IntegrationTest : public ::testing::Test {
119123
return init_segment;
120124
}
121125

126+
Status InitEncoderForMixedModeCff(Encoder& encoder) {
127+
auto face = noto_sans_jp_cff_.face();
128+
encoder.SetFace(face.get());
129+
130+
auto sc = encoder.AddGlyphDataPatch(1, {34, 35, 46, 47}); // A, B, M, N
131+
sc.Update(encoder.AddGlyphDataPatch(2, {41, 42, 43, 59})); // H, I, J, Z
132+
133+
if (!sc.ok()) {
134+
return sc;
135+
}
136+
137+
return absl::OkStatus();
138+
}
139+
122140
StatusOr<btree_set<uint32_t>> InitEncoderForVfMixedMode(Encoder& encoder) {
123141
auto face = noto_sans_vf_.face();
124142
encoder.SetFace(face.get());
@@ -202,6 +220,7 @@ class IntegrationTest : public ::testing::Test {
202220
}
203221

204222
FontData noto_sans_jp_;
223+
FontData noto_sans_jp_cff_;
205224
FontData noto_sans_vf_;
206225

207226
FontData feature_test_;
@@ -1160,4 +1179,92 @@ TEST_F(IntegrationTest, MixedMode_DesignSpaceAugmentation_DropsUnusedPatches) {
11601179
ASSERT_GT(FontHelper::GvarData(extended_face.get(), chunk4_gid)->size(), 0);
11611180
}
11621181

1182+
StatusOr<FontData> desubroutinize(hb_face_t* font) {
1183+
hb_subset_input_t* input = hb_subset_input_create_or_fail();
1184+
if (!input) {
1185+
return absl::InternalError("failed to create subset input.");
1186+
}
1187+
1188+
hb_subset_input_keep_everything(input);
1189+
hb_subset_input_set_flags(
1190+
input, hb_subset_input_get_flags(input) | HB_SUBSET_FLAGS_DESUBROUTINIZE);
1191+
1192+
hb_face_t* subset = hb_subset_or_fail(font, input);
1193+
hb_subset_input_destroy(input);
1194+
1195+
if (!subset) {
1196+
return absl::InternalError("subset operation failed.");
1197+
}
1198+
1199+
FontData result(subset);
1200+
hb_face_destroy(subset);
1201+
1202+
return result;
1203+
}
1204+
1205+
TEST_F(IntegrationTest, MixedMode_Cff) {
1206+
Encoder encoder;
1207+
auto sc = InitEncoderForMixedModeCff(encoder);
1208+
ASSERT_TRUE(sc.ok()) << sc;
1209+
1210+
ASSERT_TRUE(encoder.SetBaseSubset(btree_set<uint32_t>()).ok());
1211+
1212+
auto all_codepoints =
1213+
btree_set<uint32_t>{'A', 'B', 'H', 'I', 'J', 'M', 'N', 'Z'};
1214+
auto face = noto_sans_jp_cff_.face();
1215+
encoder.AddNonGlyphDataSegment(all_codepoints);
1216+
1217+
// Setup activations for patches 1 and 2
1218+
sc.Update(encoder.AddGlyphDataPatchCondition(Condition::SimpleCondition(
1219+
SubsetDefinition::Codepoints(btree_set<uint32_t>{'A', 'B', 'M', 'N'}),
1220+
1)));
1221+
sc.Update(encoder.AddGlyphDataPatchCondition(Condition::SimpleCondition(
1222+
SubsetDefinition::Codepoints(btree_set<uint32_t>{'H', 'I', 'J', 'Z'}),
1223+
2)));
1224+
1225+
auto encoding = encoder.Encode();
1226+
ASSERT_TRUE(encoding.ok()) << encoding.status();
1227+
auto encoded_face = encoding->init_font.face();
1228+
1229+
ASSERT_EQ(FontHelper::CffData(encoded_face.get(), 34).size(),
1230+
1); // empty glyphs in cff are one byte long
1231+
ASSERT_EQ(FontHelper::CffData(encoded_face.get(), 43).size(),
1232+
1); // empty glyphs in cff are one byte long
1233+
1234+
auto codepoints = FontHelper::ToCodepointsSet(encoded_face.get());
1235+
ASSERT_TRUE(codepoints.empty());
1236+
1237+
auto extended = Extend(*encoding, {'M'});
1238+
ASSERT_TRUE(extended.ok()) << extended.status();
1239+
auto extended_face = extended->face();
1240+
1241+
// The encoder desubroutinizes CFF fonts, so generate a desubroutinized
1242+
// copy of the input face to use for comparisons.
1243+
auto desubroutinized = desubroutinize(face.get());
1244+
ASSERT_TRUE(desubroutinized.ok()) << desubroutinized.status();
1245+
auto desubroutinized_face = desubroutinized->face();
1246+
1247+
codepoints = FontHelper::ToCodepointsSet(extended_face.get());
1248+
ASSERT_EQ(codepoints, all_codepoints);
1249+
1250+
// patch 2 gids not present
1251+
ASSERT_EQ(FontHelper::CffData(encoded_face.get(), 43).size(),
1252+
1); // empty glyphs in cff are one byte long
1253+
1254+
// patch 1 gids present and match the desubroutinized face.
1255+
ASSERT_EQ(FontHelper::CffData(extended_face.get(), 34).span(),
1256+
FontHelper::CffData(desubroutinized_face.get(), 34).span());
1257+
1258+
// Second extension
1259+
encoding->init_font.shallow_copy(*extended);
1260+
extended = Extend(*encoding, {'H'});
1261+
ASSERT_TRUE(extended.ok()) << extended.status();
1262+
extended_face = extended->face();
1263+
1264+
ASSERT_EQ(FontHelper::CffData(extended_face.get(), 43).span(),
1265+
FontHelper::CffData(desubroutinized_face.get(), 43).span());
1266+
ASSERT_EQ(FontHelper::CffData(extended_face.get(), 34).span(),
1267+
FontHelper::CffData(desubroutinized_face.get(), 34).span());
1268+
}
1269+
11631270
} // namespace ift

0 commit comments

Comments
 (0)