diff --git a/ift/config/config_compiler.cc b/ift/config/config_compiler.cc index 54763b83..727d598f 100644 --- a/ift/config/config_compiler.cc +++ b/ift/config/config_compiler.cc @@ -150,6 +150,9 @@ Status ConfigCompiler::Configure(const SegmentationPlan& plan, compiler.SetJumpAhead(plan.jump_ahead()); } compiler.SetUsePrefetchLists(plan.use_prefetch_lists()); + if (plan.max_depth() > 0) { + compiler.SetMaxDepth(plan.max_depth()); + } if (plan.has_advanced_settings()) { const auto& advanced = plan.advanced_settings(); @@ -167,10 +170,6 @@ Status ConfigCompiler::Configure(const SegmentationPlan& plan, "include_all_segment_patches is not yet supported."); } - if (plan.max_depth() > 0) { - return absl::UnimplementedError("max_depth is not yet supported."); - } - return absl::OkStatus(); } diff --git a/ift/config/config_compiler_test.cc b/ift/config/config_compiler_test.cc index 02c24e4e..a36403de 100644 --- a/ift/config/config_compiler_test.cc +++ b/ift/config/config_compiler_test.cc @@ -25,4 +25,15 @@ TEST(ConfigCompilerTest, ConfigureOverrideUrlTemplatePrefix) { EXPECT_EQ(compiler.override_url_template_prefix(), expected); } +TEST(ConfigCompilerTest, ConfigureMaxDepth) { + Compiler compiler; + SegmentationPlan plan; + plan.set_max_depth(3); + + absl::Status status = ConfigCompiler::Configure(plan, compiler); + ASSERT_TRUE(status.ok()) << status; + + EXPECT_EQ(compiler.max_depth(), 3); +} + } // namespace ift::config diff --git a/ift/config/segmentation_plan.proto b/ift/config/segmentation_plan.proto index d7d398f1..48093e35 100644 --- a/ift/config/segmentation_plan.proto +++ b/ift/config/segmentation_plan.proto @@ -104,8 +104,6 @@ message SegmentationPlan { // only a single patch which adds everything remaining. // // Defaults to unlimited depth. - // - // Note: this is currently UNIMPLEMENTED in the encoder. uint32 max_depth = 6; // In the generated table keyed patch graph every node (in addition to the usual patches) will diff --git a/ift/encoder/compiler.cc b/ift/encoder/compiler.cc index 94e9ff48..574be5a5 100644 --- a/ift/encoder/compiler.cc +++ b/ift/encoder/compiler.cc @@ -103,8 +103,8 @@ StatusOr Compiler::FullyExpandedSubset( return CutSubset(context, face_.get(), all, false); } -std::vector Compiler::OutgoingEdges( - const SubsetDefinition& node_subset, uint32_t choose) const { +std::vector Compiler::RemainingSubsets( + const SubsetDefinition& node_subset) const { std::vector remaining_subsets; for (const auto& s : extension_subsets_) { SubsetDefinition filtered = s; @@ -115,6 +115,22 @@ std::vector Compiler::OutgoingEdges( remaining_subsets.push_back(std::move(filtered)); } + return remaining_subsets; +} + +SubsetDefinition Compiler::RemainingSubsetDefinition( + const SubsetDefinition& node_subset) const { + SubsetDefinition remaining; + for (const auto& s : RemainingSubsets(node_subset)) { + remaining.Union(s); + } + return remaining; +} + +std::vector Compiler::OutgoingEdges( + const SubsetDefinition& node_subset, uint32_t choose) const { + std::vector remaining_subsets = + RemainingSubsets(node_subset); std::vector input; for (const auto& s : remaining_subsets) { @@ -129,6 +145,40 @@ std::vector Compiler::OutgoingEdges( return result; } +std::vector Compiler::OutgoingEdgesWithMaxDepth( + const ProcessingContext& context, + const SubsetDefinition& node_subset) const { + std::vector edges {}; + std::vector remaining_subsets = + RemainingSubsets(node_subset); + if (remaining_subsets.empty()) { + return edges; + } + + bool depth_is_limited = max_depth_ > 0; + + // The init font node is considered depth 0, one subset added is depth 1, and so on. + size_t depth = + context.initial_remaining_subsets_count_ - remaining_subsets.size(); + + // Jump ahead needs to be restricted to jump to at most the second last depth + uint32_t choose = jump_ahead_; + if (depth_is_limited && depth + choose >= max_depth_) { + choose = max_depth_ - 1 - depth; + } + + if (choose > 0) { + edges = OutgoingEdges(node_subset, choose); + } + + if (choose != jump_ahead_ && remaining_subsets.size() >= max_depth_ - depth) { + // Lastly if jump ahead can reach the max depth then we should include a jump to add all remaining subsets. + edges.push_back(Edge{ RemainingSubsetDefinition(node_subset) }); + } + + return edges; +} + Status Compiler::AddGlyphDataPatch(uint32_t id, const IntSet& gids) { if (!face_) { return absl::FailedPreconditionError("Encoder must have a face set."); @@ -210,6 +260,8 @@ StatusOr Compiler::Compile() const { ProcessingContext context(next_id_); context.init_subset_ = init_subset_; AddInitSubsetDefaults(context.init_subset_); + context.initial_remaining_subsets_count_ = + RemainingSubsets(context.init_subset_).size(); if (IsMixedMode()) { // Glyph keyed patches can't change the glyph count in the font (and hence // loca len) so always include the last gid in the init subset to force the @@ -540,7 +592,7 @@ StatusOr Compiler::Compile( glyph_keyed_url_template, glyph_keyed_compat_id)); - std::vector edges = OutgoingEdges(node_subset, jump_ahead_); + std::vector edges = OutgoingEdgesWithMaxDepth(context, node_subset); // The first subset forms the base file, the remaining subsets are made // reachable via patches. diff --git a/ift/encoder/compiler.h b/ift/encoder/compiler.h index 9bcc42f8..fa0aaf7f 100644 --- a/ift/encoder/compiler.h +++ b/ift/encoder/compiler.h @@ -50,6 +50,13 @@ class Compiler { */ void SetJumpAhead(uint32_t count) { this->jump_ahead_ = count; } + /* + * Configures the maximum depth of the table keyed patch graph. + * Defaults to 0 (unlimited depth). + */ + void SetMaxDepth(uint32_t max_depth) { this->max_depth_ = max_depth; } + uint32_t max_depth() const { return max_depth_; } + /* * If enabled then for jump ahead entries preload lists will be used instead * of a single patch which jumps multiple levels. @@ -182,6 +189,9 @@ class Compiler { } } + explicit Edge(const SubsetDefinition& value) + : subsets_({value}), combined_(value) {} + void Add(const SubsetDefinition& s) { subsets_.insert(subsets_.begin(), s); combined_.Union(s); @@ -376,6 +386,14 @@ class Compiler { std::vector& url_template, ift::common::CompatId& compat_id) const; + std::vector RemainingSubsets( + const SubsetDefinition& node_subset) const; + SubsetDefinition RemainingSubsetDefinition( + const SubsetDefinition& node_subset) const; + std::vector OutgoingEdgesWithMaxDepth( + const ProcessingContext& context, + const SubsetDefinition& node_subset) const; + ift::common::hb_face_unique_ptr face_; absl::btree_map glyph_data_patches_; std::vector glyph_patch_conditions_; @@ -383,6 +401,7 @@ class Compiler { SubsetDefinition init_subset_; std::vector extension_subsets_; uint32_t jump_ahead_ = 1; + uint32_t max_depth_ = 0; uint32_t next_id_ = 0; bool use_prefetch_lists_ = false; bool woff2_encode_ = false; @@ -400,6 +419,7 @@ class Compiler { ift::common::FontData fully_expanded_subset_; bool force_long_loca_and_gvar_ = false; + size_t initial_remaining_subsets_count_ = 0; uint32_t next_id_ = 0; uint32_t next_patch_set_id_ = 1; // id 0 is reserved for table keyed patches. diff --git a/ift/encoder/compiler_test.cc b/ift/encoder/compiler_test.cc index 214756d6..81d81f77 100644 --- a/ift/encoder/compiler_test.cc +++ b/ift/encoder/compiler_test.cc @@ -42,6 +42,7 @@ using ift::common::CodepointSet; using ift::common::FontData; using ift::common::FontHelper; using ift::common::GlyphSet; +using ift::common::hb_face_unique_ptr; using ift::common::IntSet; using ift::common::make_hb_set; using ift::proto::DEFAULT_ENCODING; @@ -846,6 +847,232 @@ TEST_F(CompilerTest, Encode_FourSubsets_WithJumpAhead_AndPreload) { ASSERT_EQ(g, expected); } +TEST_F(CompilerTest, Encode_FourSubsets_MaxDepth_2) { + IntSet s1 = {'b'}; + IntSet s2 = {'c'}; + IntSet s3 = {'d'}; + Compiler compiler; + compiler.SetMaxDepth(2); + hb_face_unique_ptr face = font.face(); + compiler.SetFace(face.get()); + auto s = compiler.SetInitSubset(IntSet{'a'}); + ASSERT_TRUE(s.ok()) << s; + compiler.AddNonGlyphDataSegment(s1); + compiler.AddNonGlyphDataSegment(s2); + compiler.AddNonGlyphDataSegment(s3); + + auto encoding = compiler.Compile(); + + ASSERT_TRUE(encoding.ok()) << encoding.status(); + + graph g; + auto sc = ToGraph(*encoding, g); + ASSERT_TRUE(sc.ok()) << sc; + + graph expected{ + {"a", {"ab", "ac", "ad"}}, + {"ab", {"abcd"}}, + {"ac", {"abcd"}}, + {"ad", {"abcd"}}, + {"abcd", {}}, + }; + ASSERT_EQ(g, expected); +} + +TEST_F(CompilerTest, Encode_FourSubsets_MaxDepth_1) { + IntSet s1 = {'b'}; + IntSet s2 = {'c'}; + IntSet s3 = {'d'}; + Compiler compiler; + compiler.SetMaxDepth(1); + hb_face_unique_ptr face = font.face(); + compiler.SetFace(face.get()); + auto s = compiler.SetInitSubset(IntSet{'a'}); + ASSERT_TRUE(s.ok()) << s; + compiler.AddNonGlyphDataSegment(s1); + compiler.AddNonGlyphDataSegment(s2); + compiler.AddNonGlyphDataSegment(s3); + + auto encoding = compiler.Compile(); + + ASSERT_TRUE(encoding.ok()) << encoding.status(); + + graph g; + auto sc = ToGraph(*encoding, g); + ASSERT_TRUE(sc.ok()) << sc; + + graph expected{ + {"a", {"abcd"}}, + {"abcd", {}}, + }; + ASSERT_EQ(g, expected); +} + +TEST_F(CompilerTest, Encode_FourSubsets_MaxDepth_3) { + IntSet s1 = {'b'}; + IntSet s2 = {'c'}; + IntSet s3 = {'d'}; + Compiler compiler; + compiler.SetMaxDepth(3); + hb_face_unique_ptr face = font.face(); + compiler.SetFace(face.get()); + auto s = compiler.SetInitSubset(IntSet{'a'}); + ASSERT_TRUE(s.ok()) << s; + compiler.AddNonGlyphDataSegment(s1); + compiler.AddNonGlyphDataSegment(s2); + compiler.AddNonGlyphDataSegment(s3); + + auto encoding = compiler.Compile(); + + ASSERT_TRUE(encoding.ok()) << encoding.status(); + + graph g; + auto sc = ToGraph(*encoding, g); + ASSERT_TRUE(sc.ok()) << sc; + + graph expected{ + {"a", {"ab", "ac", "ad"}}, {"ab", {"abc", "abd"}}, {"ac", {"abc", "acd"}}, + {"ad", {"abd", "acd"}}, {"abc", {"abcd"}}, {"abd", {"abcd"}}, + {"acd", {"abcd"}}, {"abcd", {}}, + }; + ASSERT_EQ(g, expected); +} + +TEST_F(CompilerTest, Encode_FourSubsets_MaxDepth_2_WithJumpAhead) { + IntSet s1 = {'b'}; + IntSet s2 = {'c'}; + IntSet s3 = {'d'}; + Compiler compiler; + compiler.SetJumpAhead(2); + compiler.SetMaxDepth(2); + hb_face_unique_ptr face = font.face(); + compiler.SetFace(face.get()); + auto s = compiler.SetInitSubset(IntSet{'a'}); + ASSERT_TRUE(s.ok()) << s; + compiler.AddNonGlyphDataSegment(s1); + compiler.AddNonGlyphDataSegment(s2); + compiler.AddNonGlyphDataSegment(s3); + + auto encoding = compiler.Compile(); + + ASSERT_TRUE(encoding.ok()) << encoding.status(); + + graph g; + auto sc = ToGraph(*encoding, g); + ASSERT_TRUE(sc.ok()) << sc; + + graph expected{ + {"a", {"ab", "ac", "ad", "abcd"}}, + {"ab", {"abcd"}}, + {"ac", {"abcd"}}, + {"ad", {"abcd"}}, + {"abcd", {}}, + }; + ASSERT_EQ(g, expected); +} + +TEST_F(CompilerTest, Encode_FourSubsets_MaxDepth_2_WithJumpAhead_AndPreload) { + IntSet s1 = {'b'}; + IntSet s2 = {'c'}; + IntSet s3 = {'d'}; + Compiler compiler; + compiler.SetJumpAhead(2); + compiler.SetUsePrefetchLists(true); + compiler.SetMaxDepth(2); + hb_face_unique_ptr face = font.face(); + compiler.SetFace(face.get()); + auto s = compiler.SetInitSubset(IntSet{'a'}); + ASSERT_TRUE(s.ok()) << s; + compiler.AddNonGlyphDataSegment(s1); + compiler.AddNonGlyphDataSegment(s2); + compiler.AddNonGlyphDataSegment(s3); + + auto encoding = compiler.Compile(); + + ASSERT_TRUE(encoding.ok()) << encoding.status(); + + graph g; + auto sc = ToGraph(*encoding, g); + ASSERT_TRUE(sc.ok()) << sc; + + graph expected{ + {"a", {"ab", "ac", "ad", "abcd"}}, + {"ab", {"abcd"}}, + {"ac", {"abcd"}}, + {"ad", {"abcd"}}, + {"abcd", {}}, + }; + ASSERT_EQ(g, expected); +} + +TEST_F(CompilerTest, Encode_FourSubsets_MaxDepth_3_WithJumpAhead) { + IntSet s1 = {'b'}; + IntSet s2 = {'c'}; + IntSet s3 = {'d'}; + Compiler compiler; + compiler.SetJumpAhead(2); + compiler.SetMaxDepth(3); + hb_face_unique_ptr face = font.face(); + compiler.SetFace(face.get()); + auto s = compiler.SetInitSubset(IntSet{'a'}); + ASSERT_TRUE(s.ok()) << s; + compiler.AddNonGlyphDataSegment(s1); + compiler.AddNonGlyphDataSegment(s2); + compiler.AddNonGlyphDataSegment(s3); + + auto encoding = compiler.Compile(); + + ASSERT_TRUE(encoding.ok()) << encoding.status(); + + graph g; + auto sc = ToGraph(*encoding, g); + ASSERT_TRUE(sc.ok()) << sc; + + graph expected{ + {"a", {"ab", "ac", "ad", "abc", "abd", "acd"}}, + {"ab", {"abc", "abd", "abcd"}}, + {"ac", {"abc", "acd", "abcd"}}, + {"ad", {"abd", "acd", "abcd"}}, + {"abc", {"abcd"}}, + {"abd", {"abcd"}}, + {"acd", {"abcd"}}, + {"abcd", {}}, + }; + ASSERT_EQ(g, expected); +} + +TEST_F(CompilerTest, Encode_FourSubsets_MaxDepth_2_ClientExtend) { + IntSet s1 = {'b'}; + IntSet s2 = {'c'}; + IntSet s3 = {'d'}; + Compiler compiler; + compiler.SetMaxDepth(2); + hb_face_unique_ptr face = font.face(); + compiler.SetFace(face.get()); + auto s = compiler.SetInitSubset(IntSet{'a'}); + ASSERT_TRUE(s.ok()) << s; + compiler.AddNonGlyphDataSegment(s1); + compiler.AddNonGlyphDataSegment(s2); + compiler.AddNonGlyphDataSegment(s3); + + auto encoding = compiler.Compile(); + + ASSERT_TRUE(encoding.ok()) << encoding.status(); + + // Client requesting 'b' should take at most 1 round trip + auto extended = ift::client::Extend(*encoding, IntSet{'b'}, 1, UINT32_MAX); + ASSERT_TRUE(extended.ok()) << extended.status(); + + // Client requesting 'b' and 'c' should take at most 2 round trips + extended = ift::client::Extend(*encoding, IntSet{'b', 'c'}, 2, UINT32_MAX); + ASSERT_TRUE(extended.ok()) << extended.status(); + + // Client requesting all 'b', 'c', 'd' should take at most 2 round trips + extended = + ift::client::Extend(*encoding, IntSet{'b', 'c', 'd'}, 2, UINT32_MAX); + ASSERT_TRUE(extended.ok()) << extended.status(); +} + void ClearCompatIdFromFormat2(uint8_t* data) { for (uint32_t index = 5; index < (5 + 16); index++) { data[index] = 0;