| /* |
| * Copyright © 2025 Google, Inc. |
| * |
| * This is part of HarfBuzz, a text shaping library. |
| * |
| * Permission is hereby granted, without written agreement and without |
| * license or royalty fees, to use, copy, modify, and distribute this |
| * software and its documentation for any purpose, provided that the |
| * above copyright notice and the following two paragraphs appear in |
| * all copies of this software. |
| * |
| * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR |
| * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES |
| * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN |
| * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH |
| * DAMAGE. |
| * |
| * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, |
| * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND |
| * FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS |
| * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO |
| * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. |
| * |
| * Google Author(s): Garret Rieger |
| */ |
| |
| #ifndef GRAPH_LIGATURE_GRAPH_HH |
| #define GRAPH_LIGATURE_GRAPH_HH |
| |
| #include "graph.hh" |
| #include "../OT/Layout/GSUB/LigatureSubst.hh" |
| #include "../OT/Layout/GSUB/LigatureSubstFormat1.hh" |
| #include "../OT/Layout/GSUB/LigatureSet.hh" |
| #include "../OT/Layout/types.hh" |
| #include <algorithm> |
| #include <utility> |
| |
| namespace graph { |
| |
| struct LigatureSet : public OT::Layout::GSUB_impl::LigatureSet<SmallTypes> |
| { |
| bool sanitize (graph_t::vertex_t& vertex) const |
| { |
| int64_t vertex_len = vertex.obj.tail - vertex.obj.head; |
| if (vertex_len < OT::Layout::GSUB_impl::LigatureSet<SmallTypes>::min_size) return false; |
| hb_barrier (); |
| |
| int64_t total_len = ligature.get_size() + OT::Layout::GSUB_impl::LigatureSet<SmallTypes>::min_size - ligature.len.get_size(); |
| if (vertex_len < total_len) { |
| return false; |
| } |
| return true; |
| } |
| }; |
| |
| struct LigatureSubstFormat1 : public OT::Layout::GSUB_impl::LigatureSubstFormat1_2<SmallTypes> |
| { |
| bool sanitize (graph_t::vertex_t& vertex) const |
| { |
| int64_t vertex_len = vertex.obj.tail - vertex.obj.head; |
| unsigned min_size = OT::Layout::GSUB_impl::LigatureSubstFormat1_2<SmallTypes>::min_size; |
| if (vertex_len < min_size) return false; |
| hb_barrier (); |
| |
| return vertex_len >= |
| min_size + ligatureSet.get_size() - ligatureSet.len.get_size(); |
| } |
| |
| hb_vector_t<unsigned> split_subtables (gsubgpos_graph_context_t& c, |
| unsigned this_index) |
| { |
| auto split_points = compute_split_points(c, this_index); |
| split_context_t split_context { |
| c, |
| this, |
| this_index, |
| total_number_ligas(c, this_index), |
| liga_counts(c, this_index), |
| }; |
| return actuate_subtable_split<split_context_t> (split_context, split_points); |
| } |
| |
| private: |
| unsigned total_number_ligas(gsubgpos_graph_context_t& c, unsigned this_index) const { |
| unsigned total = 0; |
| for (unsigned i = 0; i < ligatureSet.len; i++) |
| { |
| auto liga_set = c.graph.as_table<LigatureSet>(this_index, &ligatureSet[i]); |
| if (!liga_set.table) { |
| return 0; |
| } |
| total += liga_set.table->ligature.len; |
| } |
| return total; |
| } |
| |
| hb_vector_t<unsigned> liga_counts(gsubgpos_graph_context_t& c, unsigned this_index) const { |
| hb_vector_t<unsigned> result; |
| for (unsigned i = 0; i < ligatureSet.len; i++) |
| { |
| auto liga_set = c.graph.as_table<LigatureSet>(this_index, &ligatureSet[i]); |
| result.push(!liga_set.table ? 0 : liga_set.table->ligature.len); |
| } |
| return result; |
| } |
| |
| hb_vector_t<unsigned> ligature_index_to_object_id(const graph_t::vertex_and_table_t<LigatureSet>& liga_set) const { |
| hb_vector_t<unsigned> map; |
| map.resize_exact(liga_set.table->ligature.len); |
| if (map.in_error()) return map; |
| |
| for (unsigned i = 0; i < map.length; i++) { |
| map[i] = (unsigned) -1; |
| } |
| |
| for (const auto& l : liga_set.vertex->obj.real_links) { |
| if (l.position < 2) continue; |
| unsigned array_index = (l.position - 2) / 2; |
| map[array_index] = l.objidx; |
| } |
| return map; |
| } |
| |
| hb_vector_t<unsigned> compute_split_points(gsubgpos_graph_context_t& c, |
| unsigned this_index) const |
| { |
| // For ligature subst coverage is always packed last, and as a result is where an overflow |
| // will happen if there is one, so we can check the estimate length of the |
| // LigatureSubstFormat1 -> Coverage offset length which is the sum of all data in the |
| // retained sub graph except for the coverage table itself. |
| const unsigned base_size = OT::Layout::GSUB_impl::LigatureSubstFormat1_2<SmallTypes>::min_size; |
| unsigned accumulated = base_size; |
| |
| unsigned ligature_index = 0; |
| hb_vector_t<unsigned> split_points; |
| for (unsigned i = 0; i < ligatureSet.len; i++) |
| { |
| accumulated += OT::HBUINT16::static_size; // for ligature set offset |
| accumulated += OT::Layout::GSUB_impl::LigatureSet<SmallTypes>::min_size; // for ligature set table |
| |
| auto liga_set = c.graph.as_table<LigatureSet>(this_index, &ligatureSet[i]); |
| if (!liga_set.table) { |
| return hb_vector_t<unsigned> {}; |
| } |
| |
| // Finding the object id associated with an array index is O(n) |
| // so to avoid O(n^2), precompute the mapping by scanning through |
| // all links |
| auto index_to_id = ligature_index_to_object_id(liga_set); |
| if (index_to_id.in_error()) return hb_vector_t<unsigned>(); |
| |
| for (unsigned j = 0; j < liga_set.table->ligature.len; j++) |
| { |
| const unsigned liga_id = index_to_id[j]; |
| if (liga_id == (unsigned) -1) continue; // no outgoing link, ignore |
| const unsigned liga_size = c.graph.vertices_[liga_id].table_size (); |
| |
| accumulated += OT::HBUINT16::static_size; // for ligature offset |
| accumulated += liga_size; // for the ligature table |
| |
| if (accumulated >= (1 << 16)) |
| { |
| split_points.push(ligature_index); |
| // We're going to split such that the current ligature will be in the new sub table. |
| // That means we'll have one ligature subst (base_base), one ligature set, and one liga table |
| accumulated = base_size + // for liga subst subtable |
| (OT::HBUINT16::static_size * 2) + // for liga set and liga offset |
| OT::Layout::GSUB_impl::LigatureSet<SmallTypes>::min_size + // for liga set subtable |
| liga_size; // for liga sub table |
| } |
| |
| ligature_index++; |
| } |
| } |
| |
| return split_points; |
| } |
| |
| struct split_context_t |
| { |
| gsubgpos_graph_context_t& c; |
| LigatureSubstFormat1* thiz; |
| unsigned this_index; |
| unsigned original_count_; |
| hb_vector_t<unsigned> liga_counts; |
| |
| unsigned original_count () |
| { |
| return original_count_; |
| } |
| |
| unsigned clone_range (unsigned start, unsigned end) |
| { |
| return thiz->clone_range (c, this_index, liga_counts, start, end); |
| } |
| |
| bool shrink (unsigned count) |
| { |
| return thiz->shrink (c, this_index, original_count(), liga_counts, count); |
| } |
| }; |
| |
| hb_pair_t<unsigned, LigatureSet*> new_liga_set(gsubgpos_graph_context_t& c, unsigned count) const { |
| unsigned prime_size = OT::Layout::GSUB_impl::LigatureSet<SmallTypes>::min_size |
| + count * SmallTypes::size; |
| |
| unsigned prime_id = c.create_node (prime_size); |
| if (prime_id == (unsigned) -1) return hb_pair(-1, nullptr); |
| |
| LigatureSet* prime = (LigatureSet*) c.graph.object (prime_id).head; |
| prime->ligature.len = count; |
| return hb_pair(prime_id, prime); |
| } |
| |
| void clear_virtual_links (gsubgpos_graph_context_t& c, unsigned node_index) const |
| { |
| auto& obj = c.graph.vertices_[node_index].obj; |
| for (const auto& l : obj.virtual_links) |
| { |
| auto& child = c.graph.vertices_[l.objidx]; |
| child.remove_parent(node_index); |
| } |
| obj.virtual_links.clear(); |
| } |
| |
| void add_virtual_link(gsubgpos_graph_context_t& c, unsigned from, unsigned to) const { |
| auto& from_obj = c.graph.vertices_[from].obj; |
| c.graph.vertices_[to].add_parent(from, true); |
| auto& link = *from_obj.virtual_links.push (); |
| link.objidx = to; |
| } |
| |
| hb_pair_t<unsigned, unsigned> current_liga_set_bounds (gsubgpos_graph_context_t& c, |
| unsigned liga_set_index, |
| const hb_serialize_context_t::object_t& liga_set) const |
| { |
| // Finds the actual liga indices present in the liga set currently. Takes |
| // into account those that have been removed by processing. |
| unsigned min_index = (unsigned) -1; |
| unsigned max_index = 0; |
| for (const auto& l : liga_set.real_links) { |
| if (l.position < 2) continue; |
| |
| unsigned liga_index = (l.position - 2) / 2; |
| min_index = hb_min(min_index, liga_index); |
| max_index = hb_max(max_index, liga_index); |
| } |
| return hb_pair(min_index, max_index + 1); |
| } |
| |
| void compact_liga_set (gsubgpos_graph_context_t& c, LigatureSet* table, hb_serialize_context_t::object_t& obj) const |
| { |
| if (table->ligature.len <= obj.real_links.length) return; |
| |
| // compact the remaining linked liga offsets into a continous array and shrink the node as needed. |
| unsigned to_remove = table->ligature.len - obj.real_links.length; |
| unsigned new_position = SmallTypes::size; |
| obj.real_links.qsort(); // for this to work we need to process links in order of position. |
| for (auto& l : obj.real_links) |
| { |
| l.position = new_position; |
| new_position += SmallTypes::size; |
| } |
| |
| table->ligature.len = obj.real_links.length; |
| obj.tail -= to_remove * SmallTypes::size; |
| } |
| |
| unsigned clone_range (gsubgpos_graph_context_t& c, |
| unsigned this_index, |
| hb_vector_t<unsigned> liga_counts, |
| unsigned start, unsigned end) const |
| { |
| DEBUG_MSG (SUBSET_REPACK, nullptr, |
| " Cloning LigatureSubstFormat1 (%u) range [%u, %u).", this_index, start, end); |
| |
| // Create an oversized new liga subst, we'll adjust the size down later. We don't know |
| // the final size until we process it but we also need it to exist while we're processing |
| // so that nodes can be moved to it as needed. |
| unsigned prime_size = OT::Layout::GSUB_impl::LigatureSubstFormat1_2<SmallTypes>::min_size |
| + ligatureSet.get_size() - ligatureSet.len.get_size(); |
| |
| unsigned liga_subst_prime_id = c.create_node (prime_size); |
| if (liga_subst_prime_id == (unsigned) -1) return -1; |
| |
| LigatureSubstFormat1* liga_subst_prime = (LigatureSubstFormat1*) c.graph.object (liga_subst_prime_id).head; |
| liga_subst_prime->format = this->format; |
| liga_subst_prime->ligatureSet.len = this->ligatureSet.len; |
| |
| // Create a place holder coverage prime id since we need to add virtual links to it while |
| // generating liga and liga sets. Afterwards it will be updated to have the correct coverage. |
| unsigned coverage_id = c.graph.index_for_offset (this_index, &coverage); |
| unsigned coverage_prime_id = c.graph.duplicate(coverage_id); |
| auto& coverage_prime_vertex = c.graph.vertices_[coverage_prime_id]; |
| auto* coverage_prime_link = c.graph.vertices_[liga_subst_prime_id].obj.real_links.push (); |
| coverage_prime_link->width = SmallTypes::size; |
| coverage_prime_link->objidx = coverage_prime_id; |
| coverage_prime_link->position = 2; |
| coverage_prime_vertex.add_parent (liga_subst_prime_id, false); |
| |
| // Locate all liga sets with ligas between start and end. |
| // Clone or move them as needed. |
| unsigned count = 0; |
| unsigned liga_set_count = 0; |
| unsigned liga_set_start = -1; |
| unsigned liga_set_end = 0; // inclusive |
| for (unsigned i = 0; i < liga_counts.length; i++) |
| { |
| unsigned num_ligas = liga_counts[i]; |
| |
| unsigned current_start = count; |
| unsigned current_end = count + num_ligas; |
| |
| if (current_start >= end || start >= current_end) { |
| // No intersection, so just skip |
| count += num_ligas; |
| continue; |
| } |
| |
| auto liga_set_index = c.graph.index_for_offset(this_index, &ligatureSet[i]); |
| auto liga_set = c.graph.as_table<LigatureSet>(this_index, &ligatureSet[i]); |
| if (!liga_set.table) { |
| return -1; |
| } |
| |
| // Bounds may need to be adjusted if some ligas have been previously removed. |
| hb_pair_t<unsigned, unsigned> liga_bounds = current_liga_set_bounds(c, liga_set_index, liga_set.vertex->obj); |
| current_start = hb_max(count + liga_bounds.first, current_start); |
| current_end = hb_min(count + liga_bounds.second, current_end); |
| |
| unsigned liga_set_prime_id; |
| if (current_start >= start && current_end <= end) { |
| // This liga set is fully contined within [start, end) |
| // We can move the entire ligaset to the new liga subset object. |
| liga_set_end = i; |
| if (i < liga_set_start) liga_set_start = i; |
| liga_set_prime_id = c.graph.move_child<> (this_index, |
| &ligatureSet[i], |
| liga_subst_prime_id, |
| &liga_subst_prime->ligatureSet[liga_set_count++]); |
| compact_liga_set(c, liga_set.table, liga_set.vertex->obj); |
| } |
| else |
| { |
| // This liga set partially overlaps [start, end). We'll need to create |
| // a new liga set sub table and move the intersecting ligas to it. |
| unsigned start_index = hb_max(start, current_start) - count; |
| unsigned end_index = hb_min(end, current_end) - count; |
| unsigned liga_count = end_index - start_index; |
| auto result = new_liga_set(c, liga_count); |
| liga_set_prime_id = result.first; |
| if (liga_set_prime_id == (unsigned) -1) return -1; |
| |
| c.graph.move_children<OT::Offset16>( |
| liga_set_index, |
| 2 + start_index * 2, |
| 2 + end_index * 2, |
| liga_set_prime_id, |
| 2); |
| |
| liga_set_end = i; |
| if (i < liga_set_start) liga_set_start = i; |
| c.graph.add_link(&liga_subst_prime->ligatureSet[liga_set_count++], liga_subst_prime_id, liga_set_prime_id); |
| } |
| |
| // The new liga and all children set needs to have a virtual link to the new coverage table: |
| auto& liga_set_prime = c.graph.vertices_[liga_set_prime_id].obj; |
| clear_virtual_links(c, liga_set_prime_id); |
| add_virtual_link(c, liga_set_prime_id, coverage_prime_id); |
| for (const auto& l : liga_set_prime.real_links) { |
| clear_virtual_links(c, l.objidx); |
| add_virtual_link(c, l.objidx, coverage_prime_id); |
| } |
| |
| count += num_ligas; |
| } |
| |
| c.graph.vertices_[liga_subst_prime_id].obj.tail -= (liga_subst_prime->ligatureSet.len - liga_set_count) * SmallTypes::size; |
| liga_subst_prime->ligatureSet.len = liga_set_count; |
| |
| if (!Coverage::filter_coverage (c, |
| coverage_prime_id, |
| liga_set_start, liga_set_end + 1)) |
| return -1; |
| |
| return liga_subst_prime_id; |
| } |
| |
| bool shrink (gsubgpos_graph_context_t& c, |
| unsigned this_index, |
| unsigned old_count, |
| hb_vector_t<unsigned> liga_counts, |
| unsigned count) |
| { |
| DEBUG_MSG (SUBSET_REPACK, nullptr, |
| " Shrinking LigatureSubstFormat1 (%u) to [0, %u).", |
| this_index, |
| count); |
| if (count >= old_count) |
| return true; |
| |
| hb_set_t retained_indices; |
| unsigned new_liga_set_count = 0; |
| for (unsigned i = 0; i < liga_counts.length; i++) |
| { |
| auto liga_set = c.graph.as_table<LigatureSet>(this_index, &ligatureSet[i]); |
| if (!liga_set.table) { |
| return false; |
| } |
| |
| // We need the virtual links to coverage removed from all descendants on this liga subst. |
| // If any are left when we try to mutate the coverage table later it will be unnessecarily |
| // duplicated. Code later on will re-add the virtual links as needed (via retained_indices). |
| clear_virtual_links(c, liga_set.index); |
| retained_indices.add(liga_set.index); |
| |
| auto index_to_id = ligature_index_to_object_id(liga_set); |
| if (index_to_id.in_error()) return false; |
| |
| for (unsigned i = 0; i < liga_set.table->ligature.len; i++) { |
| unsigned liga_index = index_to_id[i]; |
| if (liga_index != (unsigned) -1) { |
| clear_virtual_links(c, liga_index); |
| retained_indices.add(liga_index); |
| } |
| } |
| |
| unsigned num_ligas = liga_counts[i]; |
| if (num_ligas >= count) { |
| // drop the trailing liga's from this set and all subsequent liga sets |
| unsigned num_ligas_to_remove = num_ligas - count; |
| new_liga_set_count = i + 1; |
| c.graph.vertices_[liga_set.index].obj.tail -= num_ligas_to_remove * SmallTypes::size; |
| liga_set.table->ligature.len = count; |
| break; |
| } else { |
| count -= num_ligas; |
| } |
| } |
| |
| // Adjust liga set array |
| auto& this_vertex = c.graph.vertices_[this_index]; |
| this_vertex.obj.tail -= (ligatureSet.len - new_liga_set_count) * SmallTypes::size; |
| ligatureSet.len = new_liga_set_count; |
| |
| // Coverage matches the number of liga sets so rebuild as needed |
| unsigned coverage_idx = c.graph.index_for_offset (this_index, &this->coverage); |
| if (coverage_idx == (unsigned) -1) return false; |
| |
| auto& coverage_v = c.graph.vertices_[coverage_idx]; |
| if (coverage_v.is_shared ()) |
| { |
| coverage_idx = c.graph.remap_child (this_index, coverage_idx); |
| if (coverage_idx == (unsigned) -1) return false; |
| } |
| |
| for (unsigned i : retained_indices.iter()) |
| add_virtual_link(c, i, coverage_idx); |
| |
| unsigned coverage_size = coverage_v.table_size (); |
| Coverage* coverage_table = (Coverage*) coverage_v.obj.head; |
| auto new_coverage = |
| + hb_zip (coverage_table->iter (), hb_range ()) |
| | hb_filter ([&] (hb_pair_t<unsigned, unsigned> p) { |
| return p.second < new_liga_set_count; |
| }) |
| | hb_map_retains_sorting (hb_first) |
| ; |
| |
| return Coverage::make_coverage (c, new_coverage, coverage_idx, coverage_size); |
| } |
| }; |
| |
| struct LigatureSubst : public OT::Layout::GSUB_impl::LigatureSubst |
| { |
| |
| hb_vector_t<unsigned> split_subtables (gsubgpos_graph_context_t& c, |
| unsigned this_index) |
| { |
| switch (u.format.v) { |
| case 1: |
| return ((LigatureSubstFormat1*)(&u.format1))->split_subtables (c, this_index); |
| #ifndef HB_NO_BEYOND_64K |
| case 2: HB_FALLTHROUGH; |
| // Don't split 24bit Ligature Subs |
| #endif |
| default: |
| return hb_vector_t<unsigned> (); |
| } |
| } |
| |
| bool sanitize (graph_t::vertex_t& vertex) const |
| { |
| int64_t vertex_len = vertex.obj.tail - vertex.obj.head; |
| if (vertex_len < u.format.v.get_size ()) return false; |
| hb_barrier (); |
| |
| switch (u.format.v) { |
| case 1: |
| return ((LigatureSubstFormat1*)(&u.format1))->sanitize (vertex); |
| #ifndef HB_NO_BEYOND_64K |
| case 2: HB_FALLTHROUGH; |
| #endif |
| default: |
| // We don't handle format 2 here. |
| return false; |
| } |
| } |
| }; |
| |
| } |
| |
| #endif // GRAPH_LIGATURE_GRAPH_HH |