options.cc revision 30874b4819a99cc84fa39e794266685e1b8735d2
1/*
2 * Copyright © 2011  Google, Inc.
3 *
4 *  This is part of HarfBuzz, a text shaping library.
5 *
6 * Permission is hereby granted, without written agreement and without
7 * license or royalty fees, to use, copy, modify, and distribute this
8 * software and its documentation for any purpose, provided that the
9 * above copyright notice and the following two paragraphs appear in
10 * all copies of this software.
11 *
12 * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
13 * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
14 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
15 * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
16 * DAMAGE.
17 *
18 * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
19 * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
20 * FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS
21 * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
22 * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
23 *
24 * Google Author(s): Behdad Esfahbod
25 */
26
27#include "options.hh"
28
29#ifdef HAVE_FREETYPE
30#include <hb-ft.h>
31#endif
32
33
34void
35fail (hb_bool_t suggest_help, const char *format, ...)
36{
37  const char *msg;
38
39  va_list vap;
40  va_start (vap, format);
41  msg = g_strdup_vprintf (format, vap);
42  const char *prgname = g_get_prgname ();
43  g_printerr ("%s: %s\n", prgname, msg);
44  if (suggest_help)
45    g_printerr ("Try `%s --help' for more information.\n", prgname);
46
47  exit (1);
48}
49
50
51hb_bool_t debug = FALSE;
52
53static gchar *
54shapers_to_string (void)
55{
56  GString *shapers = g_string_new (NULL);
57  const char **shaper_list = hb_shape_list_shapers ();
58
59  for (; *shaper_list; shaper_list++) {
60    g_string_append (shapers, *shaper_list);
61    g_string_append_c (shapers, ',');
62  }
63  g_string_truncate (shapers, MAX (0, (gint)shapers->len - 1));
64
65  return g_string_free (shapers, FALSE);
66}
67
68static G_GNUC_NORETURN gboolean
69show_version (const char *name G_GNUC_UNUSED,
70	      const char *arg G_GNUC_UNUSED,
71	      gpointer    data G_GNUC_UNUSED,
72	      GError    **error G_GNUC_UNUSED)
73{
74  g_printf ("%s (%s) %s\n", g_get_prgname (), PACKAGE_NAME, PACKAGE_VERSION);
75
76  char *shapers = shapers_to_string ();
77  g_printf ("Available shapers: %s\n", shapers);
78  g_free (shapers);
79  if (strcmp (HB_VERSION_STRING, hb_version_string ()))
80    g_printf ("Linked HarfBuzz library has a different version: %s\n", hb_version_string ());
81
82  exit(0);
83}
84
85
86void
87option_parser_t::add_main_options (void)
88{
89  GOptionEntry entries[] =
90  {
91    {"version",		0, G_OPTION_FLAG_NO_ARG,
92			      G_OPTION_ARG_CALLBACK,	(gpointer) &show_version,	"Show version numbers",			NULL},
93    {"debug",		0, 0, G_OPTION_ARG_NONE,	&debug,				"Free all resources before exit",	NULL},
94    {NULL}
95  };
96  g_option_context_add_main_entries (context, entries, NULL);
97}
98
99static gboolean
100pre_parse (GOptionContext *context G_GNUC_UNUSED,
101	   GOptionGroup *group G_GNUC_UNUSED,
102	   gpointer data,
103	   GError **error)
104{
105  option_group_t *option_group = (option_group_t *) data;
106  option_group->pre_parse (error);
107  return *error == NULL;
108}
109
110static gboolean
111post_parse (GOptionContext *context G_GNUC_UNUSED,
112	    GOptionGroup *group G_GNUC_UNUSED,
113	    gpointer data,
114	    GError **error)
115{
116  option_group_t *option_group = static_cast<option_group_t *>(data);
117  option_group->post_parse (error);
118  return *error == NULL;
119}
120
121void
122option_parser_t::add_group (GOptionEntry   *entries,
123			    const gchar    *name,
124			    const gchar    *description,
125			    const gchar    *help_description,
126			    option_group_t *option_group)
127{
128  GOptionGroup *group = g_option_group_new (name, description, help_description,
129					    static_cast<gpointer>(option_group), NULL);
130  g_option_group_add_entries (group, entries);
131  g_option_group_set_parse_hooks (group, pre_parse, post_parse);
132  g_option_context_add_group (context, group);
133}
134
135void
136option_parser_t::parse (int *argc, char ***argv)
137{
138  setlocale (LC_ALL, "");
139
140  GError *parse_error = NULL;
141  if (!g_option_context_parse (context, argc, argv, &parse_error))
142  {
143    if (parse_error != NULL) {
144      fail (TRUE, "%s", parse_error->message);
145      //g_error_free (parse_error);
146    } else
147      fail (TRUE, "Option parse error");
148  }
149}
150
151
152static gboolean
153parse_margin (const char *name G_GNUC_UNUSED,
154	      const char *arg,
155	      gpointer    data,
156	      GError    **error G_GNUC_UNUSED)
157{
158  view_options_t *view_opts = (view_options_t *) data;
159  view_options_t::margin_t &m = view_opts->margin;
160  switch (sscanf (arg, "%lf %lf %lf %lf", &m.t, &m.r, &m.b, &m.l)) {
161    case 1: m.r = m.t;
162    case 2: m.b = m.t;
163    case 3: m.l = m.r;
164    case 4: return TRUE;
165    default:
166      g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE,
167		   "%s argument should be one to four space-separated numbers",
168		   name);
169      return FALSE;
170  }
171}
172
173
174static gboolean
175parse_shapers (const char *name G_GNUC_UNUSED,
176	       const char *arg,
177	       gpointer    data,
178	       GError    **error G_GNUC_UNUSED)
179{
180  shape_options_t *shape_opts = (shape_options_t *) data;
181  shape_opts->shapers = g_strsplit (arg, ",", 0);
182  return TRUE;
183}
184
185static G_GNUC_NORETURN gboolean
186list_shapers (const char *name G_GNUC_UNUSED,
187	      const char *arg G_GNUC_UNUSED,
188	      gpointer    data G_GNUC_UNUSED,
189	      GError    **error G_GNUC_UNUSED)
190{
191  for (const char **shaper = hb_shape_list_shapers (); *shaper; shaper++)
192    g_printf ("%s\n", *shaper);
193
194  exit(0);
195}
196
197
198
199static void
200parse_space (char **pp)
201{
202  char c;
203#define ISSPACE(c) ((c)==' '||(c)=='\f'||(c)=='\n'||(c)=='\r'||(c)=='\t'||(c)=='\v')
204  while (c = **pp, ISSPACE (c))
205    (*pp)++;
206#undef ISSPACE
207}
208
209static hb_bool_t
210parse_char (char **pp, char c)
211{
212  parse_space (pp);
213
214  if (**pp != c)
215    return FALSE;
216
217  (*pp)++;
218  return TRUE;
219}
220
221static hb_bool_t
222parse_uint (char **pp, unsigned int *pv)
223{
224  char *p = *pp;
225  unsigned int v;
226
227  v = strtol (p, pp, 0);
228
229  if (p == *pp)
230    return FALSE;
231
232  *pv = v;
233  return TRUE;
234}
235
236
237static hb_bool_t
238parse_feature_value_prefix (char **pp, hb_feature_t *feature)
239{
240  if (parse_char (pp, '-'))
241    feature->value = 0;
242  else {
243    parse_char (pp, '+');
244    feature->value = 1;
245  }
246
247  return TRUE;
248}
249
250static hb_bool_t
251parse_feature_tag (char **pp, hb_feature_t *feature)
252{
253  char *p = *pp, c;
254
255  parse_space (pp);
256
257#define ISALNUM(c) (('a' <= (c) && (c) <= 'z') || ('A' <= (c) && (c) <= 'Z') || ('0' <= (c) && (c) <= '9'))
258  while (c = **pp, ISALNUM(c))
259    (*pp)++;
260#undef ISALNUM
261
262  if (p == *pp)
263    return FALSE;
264
265  feature->tag = hb_tag_from_string (p, *pp - p);
266  return TRUE;
267}
268
269static hb_bool_t
270parse_feature_indices (char **pp, hb_feature_t *feature)
271{
272  parse_space (pp);
273
274  hb_bool_t has_start;
275
276  feature->start = 0;
277  feature->end = (unsigned int) -1;
278
279  if (!parse_char (pp, '['))
280    return TRUE;
281
282  has_start = parse_uint (pp, &feature->start);
283
284  if (parse_char (pp, ':')) {
285    parse_uint (pp, &feature->end);
286  } else {
287    if (has_start)
288      feature->end = feature->start + 1;
289  }
290
291  return parse_char (pp, ']');
292}
293
294static hb_bool_t
295parse_feature_value_postfix (char **pp, hb_feature_t *feature)
296{
297  return !parse_char (pp, '=') || parse_uint (pp, &feature->value);
298}
299
300
301static hb_bool_t
302parse_one_feature (char **pp, hb_feature_t *feature)
303{
304  return parse_feature_value_prefix (pp, feature) &&
305	 parse_feature_tag (pp, feature) &&
306	 parse_feature_indices (pp, feature) &&
307	 parse_feature_value_postfix (pp, feature) &&
308	 (parse_char (pp, ',') || **pp == '\0');
309}
310
311static void
312skip_one_feature (char **pp)
313{
314  char *e;
315  e = strchr (*pp, ',');
316  if (e)
317    *pp = e + 1;
318  else
319    *pp = *pp + strlen (*pp);
320}
321
322static gboolean
323parse_features (const char *name G_GNUC_UNUSED,
324	        const char *arg,
325	        gpointer    data,
326	        GError    **error G_GNUC_UNUSED)
327{
328  shape_options_t *shape_opts = (shape_options_t *) data;
329  char *s = (char *) arg;
330  char *p;
331
332  shape_opts->num_features = 0;
333  shape_opts->features = NULL;
334
335  if (!*s)
336    return TRUE;
337
338  /* count the features first, so we can allocate memory */
339  p = s;
340  do {
341    shape_opts->num_features++;
342    p = strchr (p, ',');
343    if (p)
344      p++;
345  } while (p);
346
347  shape_opts->features = (hb_feature_t *) calloc (shape_opts->num_features, sizeof (*shape_opts->features));
348
349  /* now do the actual parsing */
350  p = s;
351  shape_opts->num_features = 0;
352  while (*p) {
353    if (parse_one_feature (&p, &shape_opts->features[shape_opts->num_features]))
354      shape_opts->num_features++;
355    else
356      skip_one_feature (&p);
357  }
358
359  return TRUE;
360}
361
362
363void
364view_options_t::add_options (option_parser_t *parser)
365{
366  GOptionEntry entries[] =
367  {
368    {"annotate",	0, 0, G_OPTION_ARG_NONE,	&this->annotate,		"Annotate output rendering",				NULL},
369    {"background",	0, 0, G_OPTION_ARG_STRING,	&this->back,			"Set background color (default: "DEFAULT_BACK")",	"red/#rrggbb/#rrggbbaa"},
370    {"foreground",	0, 0, G_OPTION_ARG_STRING,	&this->fore,			"Set foreground color (default: "DEFAULT_FORE")",	"red/#rrggbb/#rrggbbaa"},
371    {"line-space",	0, 0, G_OPTION_ARG_DOUBLE,	&this->line_space,		"Set space between lines (default: 0)",			"units"},
372    {"margin",		0, 0, G_OPTION_ARG_CALLBACK,	(gpointer) &parse_margin,	"Margin around output (default: "G_STRINGIFY(DEFAULT_MARGIN)")","one to four numbers"},
373    {"font-size",	0, 0, G_OPTION_ARG_DOUBLE,	&this->font_size,		"Font size (default: "G_STRINGIFY(DEFAULT_FONT_SIZE)")","size"},
374    {NULL}
375  };
376  parser->add_group (entries,
377		     "view",
378		     "View options:",
379		     "Options controlling output rendering",
380		     this);
381}
382
383void
384shape_options_t::add_options (option_parser_t *parser)
385{
386  GOptionEntry entries[] =
387  {
388    {"list-shapers",	0, G_OPTION_FLAG_NO_ARG,
389			      G_OPTION_ARG_CALLBACK,	(gpointer) &list_shapers,	"List available shapers and quit",	NULL},
390    {"shapers",		0, 0, G_OPTION_ARG_CALLBACK,	(gpointer) &parse_shapers,	"Comma-separated list of shapers to try","list"},
391    {"direction",	0, 0, G_OPTION_ARG_STRING,	&this->direction,		"Set text direction (default: auto)",	"ltr/rtl/ttb/btt"},
392    {"language",	0, 0, G_OPTION_ARG_STRING,	&this->language,		"Set text language (default: $LANG)",	"langstr"},
393    {"script",		0, 0, G_OPTION_ARG_STRING,	&this->script,			"Set text script (default: auto)",	"ISO-15924 tag"},
394    {"utf8-clusters",	0, 0, G_OPTION_ARG_NONE,	&this->utf8_clusters,		"Use UTF8 byte indices, not char indices",	NULL},
395    {NULL}
396  };
397  parser->add_group (entries,
398		     "shape",
399		     "Shape options:",
400		     "Options controlling the shaping process",
401		     this);
402
403  const gchar *features_help = "Comma-separated list of font features\n"
404    "\n"
405    "    Features can be enabled or disabled, either globally or limited to\n"
406    "    specific character ranges.\n"
407    "\n"
408    "    The range indices refer to the positions between Unicode characters,\n"
409    "    unless the --utf8-clusters is provided, in which case range indices\n"
410    "    refer to UTF-8 byte indices. The position before the first character\n"
411    "    is always 0.\n"
412    "\n"
413    "    The format is Python-esque.  Here is how it all works:\n"
414    "\n"
415    "      Syntax:       Value:    Start:    End:\n"
416    "\n"
417    "    Setting value:\n"
418    "      \"kern\"        1         0         ∞         # Turn feature on\n"
419    "      \"+kern\"       1         0         ∞         # Turn feature on\n"
420    "      \"-kern\"       0         0         ∞         # Turn feature off\n"
421    "      \"kern=0\"      0         0         ∞         # Turn feature off\n"
422    "      \"kern=1\"      1         0         ∞         # Turn feature on\n"
423    "      \"aalt=2\"      2         0         ∞         # Choose 2nd alternate\n"
424    "\n"
425    "    Setting index:\n"
426    "      \"kern[]\"      1         0         ∞         # Turn feature on\n"
427    "      \"kern[:]\"     1         0         ∞         # Turn feature on\n"
428    "      \"kern[5:]\"    1         5         ∞         # Turn feature on, partial\n"
429    "      \"kern[:5]\"    1         0         5         # Turn feature on, partial\n"
430    "      \"kern[3:5]\"   1         3         5         # Turn feature on, range\n"
431    "      \"kern[3]\"     1         3         3+1       # Turn feature on, single char\n"
432    "\n"
433    "    Mixing it all:\n"
434    "\n"
435    "      \"kern[3:5]=0\" 1         3         5         # Turn feature off for range";
436
437  GOptionEntry entries2[] =
438  {
439    {"features",	0, 0, G_OPTION_ARG_CALLBACK,	(gpointer) &parse_features,	features_help,	"list"},
440    {NULL}
441  };
442  parser->add_group (entries2,
443		     "features",
444		     "Features options:",
445		     "Options controlling font features used",
446		     this);
447}
448
449void
450font_options_t::add_options (option_parser_t *parser)
451{
452  GOptionEntry entries[] =
453  {
454    {"font-file",	0, 0, G_OPTION_ARG_STRING,	&this->font_file,		"Font file-name",					"filename"},
455    {"face-index",	0, 0, G_OPTION_ARG_INT,		&this->face_index,		"Face index (default: 0)",                              "index"},
456    {NULL}
457  };
458  parser->add_group (entries,
459		     "font",
460		     "Font options:",
461		     "Options controlling the font",
462		     this);
463}
464
465void
466text_options_t::add_options (option_parser_t *parser)
467{
468  GOptionEntry entries[] =
469  {
470    {"text",		0, 0, G_OPTION_ARG_STRING,	&this->text,			"Set input text",			"string"},
471    {"text-file",	0, 0, G_OPTION_ARG_STRING,	&this->text_file,		"Set input text file-name\n\n    If no text is provided, standard input is used for input.",		"filename"},
472    {NULL}
473  };
474  parser->add_group (entries,
475		     "text",
476		     "Text options:",
477		     "Options controlling the input text",
478		     this);
479}
480
481void
482output_options_t::add_options (option_parser_t *parser)
483{
484  GOptionEntry entries[] =
485  {
486    {"output-file",	0, 0, G_OPTION_ARG_STRING,	&this->output_file,		"Set output file-name (default: stdout)","filename"},
487    {"output-format",	0, 0, G_OPTION_ARG_STRING,	&this->output_format,		"Set output format",			"format"},
488    {NULL}
489  };
490  parser->add_group (entries,
491		     "output",
492		     "Output options:",
493		     "Options controlling the output",
494		     this);
495}
496
497
498
499hb_font_t *
500font_options_t::get_font (void) const
501{
502  if (font)
503    return font;
504
505  hb_blob_t *blob = NULL;
506
507  /* Create the blob */
508  {
509    char *font_data;
510    unsigned int len = 0;
511    hb_destroy_func_t destroy;
512    void *user_data;
513    hb_memory_mode_t mm;
514
515    /* This is a hell of a lot of code for just reading a file! */
516    if (!font_file)
517      fail (TRUE, "No font file set");
518
519    if (0 == strcmp (font_file, "-")) {
520      /* read it */
521      GString *gs = g_string_new (NULL);
522      char buf[BUFSIZ];
523#ifdef HAVE__SETMODE
524      _setmode (fileno (stdin), _O_BINARY);
525#endif
526      while (!feof (stdin)) {
527	size_t ret = fread (buf, 1, sizeof (buf), stdin);
528	if (ferror (stdin))
529	  fail (FALSE, "Failed reading font from standard input: %s",
530		strerror (errno));
531	g_string_append_len (gs, buf, ret);
532      }
533      len = gs->len;
534      font_data = g_string_free (gs, FALSE);
535      user_data = font_data;
536      destroy = (hb_destroy_func_t) g_free;
537      mm = HB_MEMORY_MODE_WRITABLE;
538    } else {
539      GError *error = NULL;
540      GMappedFile *mf = g_mapped_file_new (font_file, FALSE, &error);
541      if (mf) {
542	font_data = g_mapped_file_get_contents (mf);
543	len = g_mapped_file_get_length (mf);
544	if (len) {
545	  destroy = (hb_destroy_func_t) g_mapped_file_unref;
546	  user_data = (void *) mf;
547	  mm = HB_MEMORY_MODE_READONLY_MAY_MAKE_WRITABLE;
548	} else
549	  g_mapped_file_unref (mf);
550      } else {
551	fail (FALSE, "%s", error->message);
552	//g_error_free (error);
553      }
554      if (!len) {
555	/* GMappedFile is buggy, it doesn't fail if file isn't regular.
556	 * Try reading.
557	 * https://bugzilla.gnome.org/show_bug.cgi?id=659212 */
558        GError *error = NULL;
559	gsize l;
560	if (g_file_get_contents (font_file, &font_data, &l, &error)) {
561	  len = l;
562	  destroy = (hb_destroy_func_t) g_free;
563	  user_data = (void *) font_data;
564	  mm = HB_MEMORY_MODE_WRITABLE;
565	} else {
566	  fail (FALSE, "%s", error->message);
567	  //g_error_free (error);
568	}
569      }
570    }
571
572    blob = hb_blob_create (font_data, len, mm, user_data, destroy);
573  }
574
575  /* Create the face */
576  hb_face_t *face = hb_face_create (blob, face_index);
577  hb_blob_destroy (blob);
578
579
580  font = hb_font_create (face);
581
582  unsigned int upem = hb_face_get_upem (face);
583  hb_font_set_scale (font, upem, upem);
584  hb_face_destroy (face);
585
586#ifdef HAVE_FREETYPE
587  hb_ft_font_set_funcs (font);
588#endif
589
590  return font;
591}
592
593
594const char *
595text_options_t::get_line (unsigned int *len)
596{
597  if (text) {
598    if (text_len == (unsigned int) -1)
599      text_len = strlen (text);
600
601    if (!text_len) {
602      *len = 0;
603      return NULL;
604    }
605
606    const char *ret = text;
607    const char *p = (const char *) memchr (text, '\n', text_len);
608    unsigned int ret_len;
609    if (!p) {
610      ret_len = text_len;
611      text += ret_len;
612      text_len = 0;
613    } else {
614      ret_len = p - ret;
615      text += ret_len + 1;
616      text_len -= ret_len + 1;
617    }
618
619    *len = ret_len;
620    return ret;
621  }
622
623  if (!fp) {
624    if (!text_file)
625      fail (TRUE, "At least one of text or text-file must be set");
626
627    if (0 != strcmp (text_file, "-"))
628      fp = fopen (text_file, "r");
629    else
630      fp = stdin;
631
632    if (!fp)
633      fail (FALSE, "Failed opening text file `%s': %s",
634	    text_file, strerror (errno));
635
636    gs = g_string_new (NULL);
637  }
638
639  g_string_set_size (gs, 0);
640  char buf[BUFSIZ];
641  while (fgets (buf, sizeof (buf), fp)) {
642    unsigned int bytes = strlen (buf);
643    if (bytes && buf[bytes - 1] == '\n') {
644      bytes--;
645      g_string_append_len (gs, buf, bytes);
646      break;
647    }
648      g_string_append_len (gs, buf, bytes);
649  }
650  if (ferror (fp))
651    fail (FALSE, "Failed reading text: %s",
652	  strerror (errno));
653  *len = gs->len;
654  return !*len && feof (fp) ? NULL : gs->str;
655}
656
657
658FILE *
659output_options_t::get_file_handle (void)
660{
661  if (fp)
662    return fp;
663
664  if (output_file)
665    fp = fopen (output_file, "wb");
666  else {
667#ifdef HAVE__SETMODE
668    _setmode (fileno (stdout), _O_BINARY);
669#endif
670    fp = stdout;
671  }
672  if (!fp)
673    fail (FALSE, "Cannot open output file `%s': %s",
674	  g_filename_display_name (output_file), strerror (errno));
675
676  return fp;
677}
678
679
680void
681format_options_t::add_options (option_parser_t *parser)
682{
683  GOptionEntry entries[] =
684  {
685    {"no-glyph-names",	0, G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE,	&this->show_glyph_names,	"Use glyph indices instead of names",	NULL},
686    {"no-positions",	0, G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE,	&this->show_positions,		"Do not show glyph positions",		NULL},
687    {"no-clusters",	0, G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE,	&this->show_clusters,		"Do not show cluster mapping",		NULL},
688    {"show-text",	0, 0,			  G_OPTION_ARG_NONE,	&this->show_text,		"Show input text",			NULL},
689    {"show-unicode",	0, 0,			  G_OPTION_ARG_NONE,	&this->show_unicode,		"Show input Unicode codepoints",	NULL},
690    {"show-line-num",	0, 0,			  G_OPTION_ARG_NONE,	&this->show_line_num,		"Show line numbers",			NULL},
691    {NULL}
692  };
693  parser->add_group (entries,
694		     "format",
695		     "Format options:",
696		     "Options controlling the formatting of buffer contents",
697		     this);
698}
699
700void
701format_options_t::serialize_unicode (hb_buffer_t *buffer,
702				     GString     *gs)
703{
704  unsigned int num_glyphs = hb_buffer_get_length (buffer);
705  hb_glyph_info_t *info = hb_buffer_get_glyph_infos (buffer, NULL);
706
707  g_string_append_c (gs, '<');
708  for (unsigned int i = 0; i < num_glyphs; i++)
709  {
710    if (i)
711      g_string_append_c (gs, ',');
712    g_string_append_printf (gs, "U+%04X", info->codepoint);
713    info++;
714  }
715  g_string_append_c (gs, '>');
716}
717
718void
719format_options_t::serialize_glyphs (hb_buffer_t *buffer,
720				    hb_font_t   *font,
721				    hb_bool_t    utf8_clusters,
722				    GString     *gs)
723{
724  FT_Face ft_face = show_glyph_names ? hb_ft_font_get_face (font) : NULL;
725
726  unsigned int num_glyphs = hb_buffer_get_length (buffer);
727  hb_glyph_info_t *info = hb_buffer_get_glyph_infos (buffer, NULL);
728  hb_glyph_position_t *pos = hb_buffer_get_glyph_positions (buffer, NULL);
729
730  g_string_append_c (gs, '[');
731  for (unsigned int i = 0; i < num_glyphs; i++)
732  {
733    if (i)
734      g_string_append_c (gs, '|');
735
736    char glyph_name[30];
737    if (show_glyph_names) {
738      if (!FT_Get_Glyph_Name (ft_face, info->codepoint, glyph_name, sizeof (glyph_name)))
739	g_string_append_printf (gs, "%s", glyph_name);
740      else
741	g_string_append_printf (gs, "gid%u", info->codepoint);
742    } else
743      g_string_append_printf (gs, "%u", info->codepoint);
744
745    if (show_clusters) {
746      g_string_append_printf (gs, "=%u", info->cluster);
747      if (utf8_clusters)
748	g_string_append (gs, "u8");
749    }
750
751    if (show_positions && (pos->x_offset || pos->y_offset)) {
752      g_string_append_c (gs, '@');
753      if (pos->x_offset) g_string_append_printf (gs, "%d", pos->x_offset);
754      if (pos->y_offset) g_string_append_printf (gs, ",%d", pos->y_offset);
755    }
756    if (show_positions && (pos->x_advance || pos->y_advance)) {
757      g_string_append_c (gs, '+');
758      if (pos->x_advance) g_string_append_printf (gs, "%d", pos->x_advance);
759      if (pos->y_advance) g_string_append_printf (gs, ",%d", pos->y_advance);
760    }
761
762    info++;
763    pos++;
764  }
765  g_string_append_c (gs, ']');
766}
767void
768format_options_t::serialize_line_no (unsigned int  line_no,
769				     GString      *gs)
770{
771  if (show_line_num)
772    g_string_append_printf (gs, "%d: ", line_no);
773}
774void
775format_options_t::serialize_line (hb_buffer_t  *buffer,
776				  unsigned int  line_no,
777				  const char   *text,
778				  unsigned int  text_len,
779				  hb_font_t    *font,
780				  hb_bool_t     utf8_clusters,
781				  GString      *gs)
782{
783  if (show_text) {
784    serialize_line_no (line_no, gs);
785    g_string_append_c (gs, '(');
786    g_string_append_len (gs, text, text_len);
787    g_string_append_c (gs, ')');
788    g_string_append_c (gs, '\n');
789  }
790
791  if (show_unicode) {
792    serialize_line_no (line_no, gs);
793    hb_buffer_reset (scratch);
794    hb_buffer_add_utf8 (scratch, text, text_len, 0, -1);
795    serialize_unicode (scratch, gs);
796    g_string_append_c (gs, '\n');
797  }
798
799  serialize_line_no (line_no, gs);
800  serialize_glyphs (buffer, font, utf8_clusters, gs);
801  g_string_append_c (gs, '\n');
802}
803