diff --git a/docs/reference/gtk/gtk4-rendernode-tool.rst b/docs/reference/gtk/gtk4-rendernode-tool.rst index 3ebfd0bcf6..6a16424a2a 100644 --- a/docs/reference/gtk/gtk4-rendernode-tool.rst +++ b/docs/reference/gtk/gtk4-rendernode-tool.rst @@ -18,6 +18,7 @@ SYNOPSIS | | **gtk4-rendernode-tool** benchmark [OPTIONS...] | **gtk4-rendernode-tool** compare [OPTIONS...] +| **gtk4-rendernode-tool** extract [OPTIONS...] | **gtk4-rendernode-tool** info [OPTIONS...] | **gtk4-rendernode-tool** render [OPTIONS...] [] | **gtk4-rendernode-tool** show [OPTIONS...] @@ -99,3 +100,15 @@ exit code is 1. If the images are identical, it is 0. ``--quiet`` Don't write results to stdout. + + +Extract +^^^^^^^ + +The ``extract`` command saves all the data urls found in a node file to a given +directory. The file names for the extracted files are derived from the mimetype +of the url. + +``--dir=DIRECTORY`` + + Save extracted files in ``DIRECTORY`` (defaults to the current directory). diff --git a/gsk/gskrendernodeparser.c b/gsk/gskrendernodeparser.c index f3b403f154..f37f194a21 100644 --- a/gsk/gskrendernodeparser.c +++ b/gsk/gskrendernodeparser.c @@ -54,6 +54,8 @@ #include #endif +#include + #include typedef struct _Context Context; @@ -946,23 +948,22 @@ create_ascii_glyphs (PangoFont *font) PangoGlyphString *result, *glyph_string; guint i; - coverage = pango_font_get_coverage (font, language); - for (i = MIN_ASCII_GLYPH; i < MAX_ASCII_GLYPH; i++) - { - if (!pango_coverage_get (coverage, i)) - break; - } - g_object_unref (coverage); - if (i < MAX_ASCII_GLYPH) - return NULL; - result = pango_glyph_string_new (); + + coverage = pango_font_get_coverage (font, language); + pango_glyph_string_set_size (result, N_ASCII_GLYPHS); glyph_string = pango_glyph_string_new (); for (i = MIN_ASCII_GLYPH; i < MAX_ASCII_GLYPH; i++) { const char text[2] = { i, 0 }; + if (!pango_coverage_get (coverage, i)) + { + result->glyphs[i - MIN_ASCII_GLYPH].glyph = PANGO_GLYPH_INVALID_INPUT; + continue; + } + pango_shape_with_flags (text, 1, text, 1, ¬_a_hack, @@ -970,14 +971,13 @@ create_ascii_glyphs (PangoFont *font) PANGO_SHAPE_NONE); if (glyph_string->num_glyphs != 1) - { - pango_glyph_string_free (glyph_string); - pango_glyph_string_free (result); - return NULL; - } - result->glyphs[i - MIN_ASCII_GLYPH] = glyph_string->glyphs[0]; + result->glyphs[i - MIN_ASCII_GLYPH].glyph = PANGO_GLYPH_INVALID_INPUT; + else + result->glyphs[i - MIN_ASCII_GLYPH] = glyph_string->glyphs[0]; } + g_object_unref (coverage); + pango_glyph_string_free (glyph_string); return result; @@ -1113,81 +1113,70 @@ parse_font (GtkCssParser *parser, if (font_name == NULL) return FALSE; - if (context->fontmap) - font = font_from_string (context->fontmap, font_name, FALSE); - if (gtk_css_parser_has_url (parser)) { char *url; + char *scheme; + GBytes *bytes; + GError *error = NULL; + GtkCssLocation start_location; + gboolean success = FALSE; - if (font != NULL) + start_location = *gtk_css_parser_get_start_location (parser); + url = gtk_css_parser_consume_url (parser); + + if (url != NULL) { - gtk_css_parser_error_value (parser, "A font with this name already exists."); - /* consume the url to avoid more errors */ - url = gtk_css_parser_consume_url (parser); - g_free (url); - } - else - { - char *scheme; - GBytes *bytes; - GError *error = NULL; - GtkCssLocation start_location; - gboolean success = FALSE; - - start_location = *gtk_css_parser_get_start_location (parser); - url = gtk_css_parser_consume_url (parser); - - if (url != NULL) + scheme = g_uri_parse_scheme (url); + if (scheme && g_ascii_strcasecmp (scheme, "data") == 0) { - scheme = g_uri_parse_scheme (url); - if (scheme && g_ascii_strcasecmp (scheme, "data") == 0) - { - bytes = gtk_css_data_url_parse (url, NULL, &error); - } - else - { - GFile *file; + bytes = gtk_css_data_url_parse (url, NULL, &error); + } + else + { + GFile *file; - file = g_file_new_for_uri (url); - bytes = g_file_load_bytes (file, NULL, NULL, &error); - g_object_unref (file); - } - - g_free (scheme); - g_free (url); - if (bytes != NULL) - { - success = add_font_from_bytes (context, bytes, &error); - g_bytes_unref (bytes); - } - - if (!success) - { - gtk_css_parser_emit_error (parser, - &start_location, - gtk_css_parser_get_end_location (parser), - error); - } + file = g_file_new_for_uri (url); + bytes = g_file_load_bytes (file, NULL, NULL, &error); + g_object_unref (file); } - if (success) + g_free (scheme); + g_free (url); + if (bytes != NULL) { - font = font_from_string (context->fontmap, font_name, FALSE); - if (!font) - { - gtk_css_parser_error (parser, - GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE, - &start_location, - gtk_css_parser_get_end_location (parser), - "The given url does not define a font named \"%s\"", - font_name); - } + success = add_font_from_bytes (context, bytes, &error); + g_bytes_unref (bytes); + } + + if (!success) + { + gtk_css_parser_emit_error (parser, + &start_location, + gtk_css_parser_get_end_location (parser), + error); + } + } + + if (success) + { + font = font_from_string (context->fontmap, font_name, FALSE); + if (!font) + { + gtk_css_parser_error (parser, + GTK_CSS_PARSER_ERROR_UNKNOWN_VALUE, + &start_location, + gtk_css_parser_get_end_location (parser), + "The given url does not define a font named \"%s\"", + font_name); } } } else { + if (context->fontmap) + font = font_from_string (context->fontmap, font_name, FALSE); + if (!font) font = font_from_string (pango_cairo_font_map_get_default (), font_name, TRUE); @@ -2255,6 +2244,12 @@ unpack_glyphs (PangoFont *font, return FALSE; } + if (ascii->glyphs[idx].glyph == PANGO_GLYPH_INVALID_INPUT) + { + g_clear_pointer (&ascii, pango_glyph_string_free); + return FALSE; + } + gi->glyph = ascii->glyphs[idx].glyph; gi->geometry.width = ascii->glyphs[idx].geometry.width; } @@ -2946,7 +2941,7 @@ typedef struct gsize named_node_counter; GHashTable *named_textures; gsize named_texture_counter; - GHashTable *serialized_fonts; + GHashTable *fonts; } Printer; static void @@ -2961,6 +2956,59 @@ printer_init_check_texture (Printer *printer, g_hash_table_insert (printer->named_textures, texture, g_strdup ("")); } +typedef struct { + hb_face_t *face; + hb_subset_input_t *input; + gboolean serialized; +} FontInfo; + +static void +font_info_free (gpointer data) +{ + FontInfo *info = (FontInfo *) data; + + hb_face_destroy (info->face); + if (info->input) + hb_subset_input_destroy (info->input); + g_free (info); +} + +static void +printer_init_collect_font_info (Printer *printer, + GskRenderNode *node) +{ + PangoFont *font; + FontInfo *info; + + font = gsk_text_node_get_font (node); + + info = (FontInfo *) g_hash_table_lookup (printer->fonts, hb_font_get_face (pango_font_get_hb_font (font))); + if (!info) + { + info = g_new0 (FontInfo, 1); + + info->face = hb_face_reference (hb_font_get_face (pango_font_get_hb_font (font))); + if (!g_object_get_data (G_OBJECT (pango_font_get_font_map (font)), "font-files")) + { + info->input = hb_subset_input_create_or_fail (); + hb_subset_input_set_flags (info->input, HB_SUBSET_FLAGS_RETAIN_GIDS); + } + + g_hash_table_insert (printer->fonts, info->face, info); + } + + if (info->input) + { + const PangoGlyphInfo *glyphs; + guint n_glyphs; + + glyphs = gsk_text_node_get_glyphs (node, &n_glyphs); + + for (guint i = 0; i < n_glyphs; i++) + hb_set_add (hb_subset_input_glyph_set (info->input), glyphs[i].glyph); + } +} + static void printer_init_duplicates_for_node (Printer *printer, GskRenderNode *node) @@ -2976,6 +3024,9 @@ printer_init_duplicates_for_node (Printer *printer, { case GSK_CAIRO_NODE: case GSK_TEXT_NODE: + printer_init_collect_font_info (printer, node); + break; + case GSK_COLOR_NODE: case GSK_LINEAR_GRADIENT_NODE: case GSK_REPEATING_LINEAR_GRADIENT_NODE: @@ -3099,7 +3150,7 @@ printer_init (Printer *self, self->named_node_counter = 0; self->named_textures = g_hash_table_new_full (NULL, NULL, NULL, g_free); self->named_texture_counter = 0; - self->serialized_fonts = g_hash_table_new (g_str_hash, g_str_equal); + self->fonts = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, font_info_free); printer_init_duplicates_for_node (self, node); } @@ -3111,7 +3162,7 @@ printer_clear (Printer *self) g_string_free (self->str, TRUE); g_hash_table_unref (self->named_nodes); g_hash_table_unref (self->named_textures); - g_hash_table_unref (self->serialized_fonts); + g_hash_table_unref (self->fonts); } #define IDENT_LEVEL 2 /* Spaces per level */ @@ -3571,14 +3622,46 @@ append_texture_param (Printer *p, g_bytes_unref (bytes); } +static void +print_font (PangoFont *font) +{ + PangoFontDescription *desc; + char *s; + hb_face_t *face; + hb_blob_t *blob; + const char *data; + unsigned int length; + char *csum; + + desc = pango_font_describe_with_absolute_size (font); + s = pango_font_description_to_string (desc); + + face = hb_font_get_face (pango_font_get_hb_font (font)); + blob = hb_face_reference_blob (face); + + data = hb_blob_get_data (blob, &length); + csum = g_compute_checksum_for_data (G_CHECKSUM_SHA256, (const guchar *)data, length); + + g_print ("%s, face %p, sha %s\n", s, face, csum); + + g_free (csum); + hb_blob_destroy (blob); + g_free (s); +} + static void gsk_text_node_serialize_font (GskRenderNode *node, Printer *p) { PangoFont *font = gsk_text_node_get_font (node); - PangoFontMap *fontmap = pango_font_get_font_map (font); PangoFontDescription *desc; char *s; + FontInfo *info; + hb_face_t *face; + hb_blob_t *blob; + const char *data; + guint length; + char *b64; desc = pango_font_describe_with_absolute_size (font); s = pango_font_description_to_string (desc); @@ -3586,42 +3669,31 @@ gsk_text_node_serialize_font (GskRenderNode *node, g_free (s); pango_font_description_free (desc); - /* Check if this is a custom font that we created from a url */ - if (!g_object_get_data (G_OBJECT (fontmap), "font-files")) + g_print ("serializing "); + print_font (font); + info = g_hash_table_lookup (p->fonts, hb_font_get_face (pango_font_get_hb_font (font))); + if (info->serialized) return; -#ifdef HAVE_PANGOFT - { - FcPattern *pat; - FcResult res; - const char *file; - char *data; - gsize len; - char *b64; + if (info->input) + face = hb_subset_or_fail (info->face, info->input); + else + face = hb_face_reference (info->face); - pat = pango_fc_font_get_pattern (PANGO_FC_FONT (font)); - res = FcPatternGetString (pat, FC_FILE, 0, (FcChar8 **)&file); - if (res != FcResultMatch) - return; + blob = hb_face_reference_blob (face); + data = hb_blob_get_data (blob, &length); - if (g_hash_table_contains (p->serialized_fonts, file)) - return; + b64 = base64_encode_with_linebreaks ((const guchar *) data, length); - if (!g_file_get_contents (file, &data, &len, NULL)) - return; + g_string_append (p->str, " url(\"data:font/ttf;base64,\\\n"); + append_escaping_newlines (p->str, b64); + g_string_append (p->str, "\")"); - g_hash_table_add (p->serialized_fonts, (gpointer) file); + g_free (b64); + hb_blob_destroy (blob); + hb_face_destroy (face); - b64 = base64_encode_with_linebreaks ((const guchar *) data, len); - - g_string_append (p->str, " url(\"data:font/ttf;base64,\\\n"); - append_escaping_newlines (p->str, b64); - g_string_append (p->str, "\")"); - - g_free (b64); - g_free (data); - } -#endif + info->serialized = TRUE; } static void diff --git a/gtk/meson.build b/gtk/meson.build index 2a83114158..4187d50f6e 100644 --- a/gtk/meson.build +++ b/gtk/meson.build @@ -1002,6 +1002,7 @@ gtk_deps = [ platform_gio_dep, pangocairo_dep, harfbuzz_dep, + hb_subset_dep, fribidi_dep, cairogobj_dep, fontconfig_dep, diff --git a/meson.build b/meson.build index c3194846ee..b18c8ca882 100644 --- a/meson.build +++ b/meson.build @@ -399,6 +399,7 @@ fribidi_dep = dependency('fribidi', version: fribidi_req, default_options: ['docs=false']) harfbuzz_dep = dependency('harfbuzz', version: harfbuzz_req, default_options: ['coretext=enabled']) +hb_subset_dep = dependency('harfbuzz-subset', version: harfbuzz_req) # Require PangoFT2 if on X11 or wayland pangoft_dep = dependency('pangoft2', version: pango_req, diff --git a/tools/gtk-rendernode-tool-extract.c b/tools/gtk-rendernode-tool-extract.c new file mode 100644 index 0000000000..c85ea9b88a --- /dev/null +++ b/tools/gtk-rendernode-tool-extract.c @@ -0,0 +1,326 @@ +/* Copyright 2023 Red Hat, Inc. + * + * GTK is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * GTK is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with GTK; see the file COPYING. If not, + * see . + * + * Author: Matthias Clasen + */ + +#include "config.h" + +#include +#include +#include + +#include +#include +#include +#include +#include "gtk-rendernode-tool.h" + +static gboolean verbose; +static char *directory = NULL; + +static guint texture_count; +static guint font_count; + +static GHashTable *fonts; + +static void +extract_texture (GskRenderNode *node) +{ + GdkTexture *texture; + char *filename; + char *path; + + if (gsk_render_node_get_node_type (node) == GSK_TEXTURE_NODE) + texture = gsk_texture_node_get_texture (node); + else + texture = gsk_texture_scale_node_get_texture (node); + + do { + filename = g_strdup_printf ("gtk-texture-%u.ttf", texture_count); + path = g_build_path ("/", directory, filename, NULL); + + if (!g_file_test (path, G_FILE_TEST_EXISTS)) + break; + + g_free (path); + g_free (filename); + texture_count++; + } while (TRUE); + + if (verbose) + g_print ("Writing %dx%d texture to %s\n", + gdk_texture_get_width (texture), + gdk_texture_get_height (texture), + filename); + + if (!gdk_texture_save_to_png (texture, path)) + { + g_printerr (_("Failed to write %s\n"), filename); + } + + g_free (path); + g_free (filename); + + texture_count++; +} + +static void +extract_font (GskRenderNode *node) +{ + PangoFont *font; + hb_font_t *hb_font; + hb_face_t *hb_face; + hb_blob_t *hb_blob; + const char *data; + unsigned int length; + char *filename; + char *path; + char *sum; + + font = gsk_text_node_get_font (node); + hb_font = pango_font_get_hb_font (font); + hb_face = hb_font_get_face (hb_font); + hb_blob = hb_face_reference_blob (hb_face); + + if (hb_blob == hb_blob_get_empty ()) + { + hb_blob_destroy (hb_blob); + g_warning ("Failed to extract font data\n"); + return; + } + + data = hb_blob_get_data (hb_blob, &length); + + sum = g_compute_checksum_for_data (G_CHECKSUM_SHA256, (const guchar *)data, length); + + if (!fonts) + fonts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + if (g_hash_table_contains (fonts, sum)) + { + g_free (sum); + hb_blob_destroy (hb_blob); + return; + } + + g_hash_table_add (fonts, sum); + + do { + filename = g_strdup_printf ("gtk-font-%u.ttf", font_count); + path = g_build_path ("/", directory, filename, NULL); + + if (!g_file_test (path, G_FILE_TEST_EXISTS)) + break; + + g_free (path); + g_free (filename); + font_count++; + } while (TRUE); + + if (verbose) + { + PangoFontDescription *desc; + + desc = pango_font_describe (font); + g_print ("Writing font %s to %s\n", + pango_font_description_get_family (desc), + filename); + pango_font_description_free (desc); + } + + if (!g_file_set_contents (path, data, length, NULL)) + { + g_printerr (_("Failed to write %s\n"), filename); + } + + hb_blob_destroy (hb_blob); + + g_free (path); + g_free (filename); + + font_count++; +} + +#define N_NODE_TYPES (GSK_SUBSURFACE_NODE + 1) +static void +extract_from_node (GskRenderNode *node) +{ + g_assert (gsk_render_node_get_node_type (node) < N_NODE_TYPES); + + switch (gsk_render_node_get_node_type (node)) + { + case GSK_CONTAINER_NODE: + for (unsigned int i = 0; i < gsk_container_node_get_n_children (node); i++) + extract_from_node (gsk_container_node_get_child (node, i)); + break; + + case GSK_CAIRO_NODE: + case GSK_COLOR_NODE: + case GSK_LINEAR_GRADIENT_NODE: + case GSK_REPEATING_LINEAR_GRADIENT_NODE: + case GSK_RADIAL_GRADIENT_NODE: + case GSK_REPEATING_RADIAL_GRADIENT_NODE: + case GSK_CONIC_GRADIENT_NODE: + case GSK_BORDER_NODE: + case GSK_INSET_SHADOW_NODE: + case GSK_OUTSET_SHADOW_NODE: + break; + + case GSK_TEXTURE_NODE: + case GSK_TEXTURE_SCALE_NODE: + extract_texture (node); + break; + + case GSK_TRANSFORM_NODE: + extract_from_node (gsk_transform_node_get_child (node)); + break; + + case GSK_OPACITY_NODE: + extract_from_node (gsk_opacity_node_get_child (node)); + break; + + case GSK_COLOR_MATRIX_NODE: + extract_from_node (gsk_color_matrix_node_get_child (node)); + break; + + case GSK_REPEAT_NODE: + extract_from_node (gsk_repeat_node_get_child (node)); + break; + + case GSK_CLIP_NODE: + extract_from_node (gsk_clip_node_get_child (node)); + break; + + case GSK_ROUNDED_CLIP_NODE: + extract_from_node (gsk_rounded_clip_node_get_child (node)); + break; + + case GSK_SHADOW_NODE: + extract_from_node (gsk_shadow_node_get_child (node)); + break; + + case GSK_BLEND_NODE: + extract_from_node (gsk_blend_node_get_bottom_child (node)); + extract_from_node (gsk_blend_node_get_top_child (node)); + break; + + case GSK_CROSS_FADE_NODE: + extract_from_node (gsk_cross_fade_node_get_start_child (node)); + extract_from_node (gsk_cross_fade_node_get_end_child (node)); + break; + + case GSK_TEXT_NODE: + extract_font (node); + break; + + case GSK_BLUR_NODE: + extract_from_node (gsk_blur_node_get_child (node)); + break; + + case GSK_DEBUG_NODE: + extract_from_node (gsk_debug_node_get_child (node)); + break; + + case GSK_GL_SHADER_NODE: + for (unsigned int i = 0; i < gsk_gl_shader_node_get_n_children (node); i++) + extract_from_node (gsk_gl_shader_node_get_child (node, i)); + break; + + case GSK_MASK_NODE: + extract_from_node (gsk_mask_node_get_source (node)); + extract_from_node (gsk_mask_node_get_mask (node)); + break; + + case GSK_FILL_NODE: + extract_from_node (gsk_fill_node_get_child (node)); + break; + + case GSK_STROKE_NODE: + extract_from_node (gsk_stroke_node_get_child (node)); + break; + + case GSK_SUBSURFACE_NODE: + extract_from_node (gsk_subsurface_node_get_child (node)); + break; + + case GSK_NOT_A_RENDER_NODE: + default: + g_assert_not_reached (); + } +} + +static void +file_extract (const char *filename) +{ + GskRenderNode *node; + + node = load_node_file (filename); + + extract_from_node (node); + + gsk_render_node_unref (node); +} + +void +do_extract (int *argc, + const char ***argv) +{ + GOptionContext *context; + char **filenames = NULL; + const GOptionEntry entries[] = { + { "dir", 0, 0, G_OPTION_ARG_FILENAME, &directory, N_("Directory to use"), N_("DIRECTORY") }, + { "verbose", 0, 0, G_OPTION_ARG_NONE, &verbose, N_("Be verbose"), NULL }, + { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &filenames, NULL, N_("FILE") }, + { NULL, } + }; + GError *error = NULL; + + g_set_prgname ("gtk4-rendernode-tool extract"); + context = g_option_context_new (NULL); + g_option_context_set_translation_domain (context, GETTEXT_PACKAGE); + g_option_context_add_main_entries (context, entries, NULL); + g_option_context_set_summary (context, _("Extract data urls from the render node.")); + + if (!g_option_context_parse (context, argc, (char ***)argv, &error)) + { + g_printerr ("%s\n", error->message); + g_error_free (error); + exit (1); + } + + g_option_context_free (context); + + if (filenames == NULL) + { + g_printerr (_("No .node file specified\n")); + exit (1); + } + + if (g_strv_length (filenames) > 1) + { + g_printerr (_("Can only accept a single .node file\n")); + exit (1); + } + + if (directory == NULL) + directory = g_strdup ("."); + + file_extract (filenames[0]); + + g_strfreev (filenames); + g_free (directory); +} diff --git a/tools/gtk-rendernode-tool.c b/tools/gtk-rendernode-tool.c index 1b7407fada..7a2f0bf81f 100644 --- a/tools/gtk-rendernode-tool.c +++ b/tools/gtk-rendernode-tool.c @@ -40,6 +40,7 @@ usage (void) "Commands:\n" " benchmark Benchmark rendering of a node\n" " compare Compare nodes or images\n" + " extract Extract data urls\n" " info Provide information about the node\n" " show Show the node\n" " render Take a screenshot of the node\n" @@ -119,6 +120,8 @@ main (int argc, const char *argv[]) do_benchmark (&argc, &argv); else if (strcmp (argv[0], "compare") == 0) do_compare (&argc, &argv); + else if (strcmp (argv[0], "extract") == 0) + do_extract (&argc, &argv); else usage (); diff --git a/tools/gtk-rendernode-tool.h b/tools/gtk-rendernode-tool.h index 12f653b837..015082c883 100644 --- a/tools/gtk-rendernode-tool.h +++ b/tools/gtk-rendernode-tool.h @@ -6,6 +6,7 @@ void do_compare (int *argc, const char ***argv); void do_info (int *argc, const char ***argv); void do_show (int *argc, const char ***argv); void do_render (int *argc, const char ***argv); +void do_extract (int *argc, const char ***argv); GskRenderNode *load_node_file (const char *filename); GskRenderer *create_renderer (const char *name, GError **error); diff --git a/tools/meson.build b/tools/meson.build index f259cc5070..c9380f93f7 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -43,6 +43,7 @@ gtk_tools = [ ['gtk4-rendernode-tool', ['gtk-rendernode-tool.c', 'gtk-rendernode-tool-benchmark.c', 'gtk-rendernode-tool-compare.c', + 'gtk-rendernode-tool-extract.c', 'gtk-rendernode-tool-info.c', 'gtk-rendernode-tool-render.c', 'gtk-rendernode-tool-show.c',