| /* |
| * Copyright © 2025 Adobe, 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. |
| * |
| * Adobe Author(s): Skef Iterum |
| */ |
| |
| #include "batch.hh" |
| #include "face-options.hh" |
| |
| #include <hb-subset.h> |
| |
| #include <set> |
| #include <string> |
| #include <vector> |
| |
| struct depend_t : option_parser_t, face_options_t |
| { |
| gboolean flagged_only = false; |
| gboolean no_context = false; |
| gboolean no_glyph_names = false; |
| gboolean compile_only = false; |
| gboolean quiet = false; |
| char *gids_str = nullptr; |
| |
| void |
| add_options () |
| { |
| set_summary ("Show glyph dependency graph."); |
| set_description ("Prints the glyph dependency graph extracted from a font file.\n" |
| "For each source glyph, lists the glyphs it can produce or depend on\n" |
| "through GSUB substitutions, composite glyphs, COLR layers, and MATH variants."); |
| |
| face_options_t::add_options (this); |
| |
| GOptionEntry depend_entries[] = |
| { |
| {"gids", 0, 0, G_OPTION_ARG_STRING, &this->gids_str, |
| "Filter output to specific glyph IDs (e.g. '1,5,10-20')", "GIDs"}, |
| {"flagged", 0, 0, G_OPTION_ARG_NONE, &this->flagged_only, |
| "Show only edges with over-approximation flags set (C or N)", nullptr}, |
| {"no-context", 0, 0, G_OPTION_ARG_NONE, &this->no_context, |
| "Suppress context set output", nullptr}, |
| {"no-glyph-names", 0, 0, G_OPTION_ARG_NONE, &this->no_glyph_names, |
| "Use numeric glyph IDs instead of names", nullptr}, |
| {"compile", 0, 0, G_OPTION_ARG_NONE, &this->compile_only, |
| "Build the dependency graph without retrieving it", nullptr}, |
| {"quiet", 'q', 0, G_OPTION_ARG_NONE, &this->quiet, |
| "Retrieve the dependency graph without printing it", nullptr}, |
| {nullptr} |
| }; |
| add_group (depend_entries, |
| "depend", |
| "Dependency graph options:", |
| "Options for filtering and formatting the dependency graph output", |
| this, |
| false); |
| |
| GOptionEntry entries[] = |
| { |
| {G_OPTION_REMAINING, 0, G_OPTION_FLAG_IN_MAIN, |
| G_OPTION_ARG_CALLBACK, (gpointer) &collect_rest, nullptr, "[FONT-FILE]"}, |
| {nullptr} |
| }; |
| add_main_group (entries, this); |
| |
| option_parser_t::add_options (); |
| } |
| |
| static gboolean |
| collect_rest (const char *name G_GNUC_UNUSED, |
| const char *arg, |
| gpointer data, |
| GError **error) |
| { |
| depend_t *thiz = (depend_t *) data; |
| |
| if (!thiz->font_file) |
| { |
| thiz->font_file = g_strdup (arg); |
| return true; |
| } |
| |
| g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, |
| "Too many arguments on the command line"); |
| return false; |
| } |
| |
| /* Returns the glyph name if font is non-null and the font has a real name |
| * for this glyph; otherwise returns the glyph ID as a decimal string. */ |
| static std::string |
| glyph_str (hb_font_t *font, hb_codepoint_t gid) |
| { |
| if (font) |
| { |
| char buf[128]; |
| if (hb_font_get_glyph_name (font, gid, buf, sizeof (buf))) |
| return buf; |
| } |
| char buf[32]; |
| snprintf (buf, sizeof (buf), "%u", gid); |
| return buf; |
| } |
| |
| /* Parses a comma-separated list of GID ranges (e.g. "1,5,10-20,42"). */ |
| static std::set<unsigned> |
| parse_gids (const char *str) |
| { |
| std::set<unsigned> result; |
| const char *p = str; |
| while (*p) |
| { |
| char *end; |
| unsigned long start = strtoul (p, &end, 10); |
| if (end == p) break; |
| p = end; |
| if (*p == '-') |
| { |
| p++; |
| unsigned long stop = strtoul (p, &end, 10); |
| if (end == p) break; |
| p = end; |
| for (unsigned long i = start; i <= stop; i++) |
| result.insert ((unsigned) i); |
| } |
| else |
| result.insert ((unsigned) start); |
| if (*p == ',') p++; |
| } |
| return result; |
| } |
| |
| /* Builds a human-readable string representing the context set at ctx_idx. |
| * Elements < 0x80000000 are direct GID refs joined with &. |
| * Elements >= 0x80000000 are indirect set refs (strip high bit for index), |
| * printed as parenthesized |-separated members. */ |
| static std::string |
| build_context_str (hb_subset_depend_t *depend, hb_codepoint_t ctx_idx, |
| hb_font_t *font) |
| { |
| hb_set_t *ctx_set = hb_set_create (); |
| if (!hb_subset_depend_lookup_set (depend, ctx_idx, ctx_set)) |
| { |
| hb_set_destroy (ctx_set); |
| return ""; |
| } |
| |
| std::string result = "("; |
| bool first = true; |
| hb_codepoint_t elem = HB_SET_VALUE_INVALID; |
| while (hb_set_next (ctx_set, &elem)) |
| { |
| if (!first) result += " & "; |
| first = false; |
| if (elem < 0x80000000u) |
| { |
| result += glyph_str (font, elem); |
| } |
| else |
| { |
| hb_codepoint_t set_idx = elem & 0x7FFFFFFFu; |
| hb_set_t *sub_set = hb_set_create (); |
| if (hb_subset_depend_lookup_set (depend, set_idx, sub_set)) |
| { |
| result += "("; |
| bool sub_first = true; |
| hb_codepoint_t sub_elem = HB_SET_VALUE_INVALID; |
| while (hb_set_next (sub_set, &sub_elem)) |
| { |
| if (!sub_first) result += "|"; |
| sub_first = false; |
| result += glyph_str (font, sub_elem); |
| } |
| result += ")"; |
| } |
| hb_set_destroy (sub_set); |
| } |
| } |
| result += ")"; |
| |
| hb_set_destroy (ctx_set); |
| return result; |
| } |
| |
| int |
| operator () (int argc, char **argv) |
| { |
| add_options (); |
| |
| parse (&argc, &argv); |
| |
| hb_subset_depend_t *depend = hb_subset_depend_from_face_or_fail (face); |
| if (!depend) |
| { |
| fprintf (stderr, "Failed to build dependency graph\n"); |
| return 1; |
| } |
| |
| if (compile_only) |
| { |
| hb_subset_depend_destroy (depend); |
| return 0; |
| } |
| |
| hb_font_t *font = no_glyph_names || quiet ? nullptr : hb_font_create (face); |
| |
| std::set<unsigned> gid_filter; |
| bool filter_gids = gids_str != nullptr; |
| if (filter_gids) |
| gid_filter = parse_gids (gids_str); |
| |
| unsigned num_glyphs = hb_face_get_glyph_count (face); |
| std::set<hb_codepoint_t> seen_lig_sets; |
| bool printed_any = false; |
| |
| for (unsigned gid = 0; gid < num_glyphs; gid++) |
| { |
| if (filter_gids && !gid_filter.count (gid)) |
| continue; |
| |
| unsigned total = hb_subset_depend_lookup_glyph (depend, gid, 0, nullptr, nullptr); |
| if (total == 0) continue; |
| |
| /* Collect entries, applying the --flagged filter */ |
| std::vector<hb_subset_depend_entry_t> entries; |
| for (unsigned i = 0; i < total; i++) |
| { |
| hb_subset_depend_entry_t entry; |
| unsigned count = 1; |
| hb_subset_depend_lookup_glyph (depend, gid, i, &count, &entry); |
| if (flagged_only && entry.flags == HB_SUBSET_DEPEND_EDGE_FLAG_NONE) |
| continue; |
| entries.push_back (entry); |
| } |
| |
| if (entries.empty () || quiet) continue; |
| |
| if (printed_any) printf ("\n"); |
| printed_any = true; |
| |
| /* Source GID header: "42 (glyphname):" or "42:" */ |
| { |
| char name_buf[128]; |
| if (font && hb_font_get_glyph_name (font, gid, name_buf, sizeof (name_buf))) |
| printf ("%u (%s):\n", gid, name_buf); |
| else |
| printf ("%u:\n", gid); |
| } |
| |
| for (const auto &entry : entries) |
| { |
| std::string line = " "; |
| |
| /* Table tag, trailing spaces trimmed (e.g. "CFF " -> "CFF") */ |
| char tag_buf[5]; |
| hb_tag_to_string (entry.table_tag, tag_buf); |
| tag_buf[4] = 0; |
| int tag_end = 4; |
| while (tag_end > 0 && tag_buf[tag_end - 1] == ' ') tag_end--; |
| tag_buf[tag_end] = 0; |
| line += tag_buf; |
| |
| /* Feature tag only for GSUB edges */ |
| if (entry.table_tag == HB_OT_TAG_GSUB) |
| { |
| char feat_buf[5]; |
| hb_tag_to_string (entry.layout_tag, feat_buf); |
| feat_buf[4] = 0; |
| line += " "; |
| line += feat_buf; |
| } |
| |
| line += " -> "; |
| line += glyph_str (font, entry.dependent); |
| |
| if (entry.ligature_set_index != HB_CODEPOINT_INVALID) |
| { |
| char buf[32]; |
| snprintf (buf, sizeof (buf), " lig[%u]", entry.ligature_set_index); |
| line += buf; |
| } |
| |
| if (entry.flags) |
| { |
| line += " flags: "; |
| if (entry.flags & HB_SUBSET_DEPEND_EDGE_FLAG_FROM_CONTEXT_POSITION) line += "C"; |
| if (entry.flags & HB_SUBSET_DEPEND_EDGE_FLAG_FROM_NESTED_CONTEXT) line += "N"; |
| } |
| |
| /* Context: append inline if it fits within 80 columns, else wrap */ |
| bool ctx_wraps = false; |
| std::string ctx_str; |
| if (!no_context && entry.context_set_index != HB_CODEPOINT_INVALID) |
| { |
| ctx_str = build_context_str (depend, entry.context_set_index, font); |
| if (!ctx_str.empty ()) |
| { |
| if (line.size () + 2 + ctx_str.size () <= 80) |
| line += " " + ctx_str; |
| else |
| ctx_wraps = true; |
| } |
| } |
| |
| printf ("%s\n", line.c_str ()); |
| |
| if (ctx_wraps) |
| printf (" ctx: %s\n", ctx_str.c_str ()); |
| |
| /* Ligature set contents: printed once (on first occurrence) */ |
| if (entry.ligature_set_index != HB_CODEPOINT_INVALID && |
| !seen_lig_sets.count (entry.ligature_set_index)) |
| { |
| seen_lig_sets.insert (entry.ligature_set_index); |
| hb_set_t *lig_set = hb_set_create (); |
| if (hb_subset_depend_lookup_set (depend, entry.ligature_set_index, lig_set)) |
| { |
| printf (" lig[%u]:", entry.ligature_set_index); |
| hb_codepoint_t lig_gid = HB_SET_VALUE_INVALID; |
| while (hb_set_next (lig_set, &lig_gid)) |
| printf (" %s", glyph_str (font, lig_gid).c_str ()); |
| printf ("\n"); |
| } |
| hb_set_destroy (lig_set); |
| } |
| } |
| } |
| |
| if (font) hb_font_destroy (font); |
| hb_subset_depend_destroy (depend); |
| |
| return 0; |
| } |
| }; |
| |
| int |
| main (int argc, char **argv) |
| { |
| return batch_main<depend_t> (argc, argv); |
| } |