/* viewer-render.c: Common code for rendering in viewers
*
* Copyright (C) 1999, 2004 Red Hat Software
* Copyright (C) 2001 Sun Microsystems
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
#include "config.h"
#include <errno.h>
#include <math.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <glib.h>
#include <glib/gprintf.h>
#include <pango/pango.h>
#include "viewer-render.h"
gboolean opt_display = TRUE;
int opt_dpi = 96;
gboolean opt_pixels = FALSE;
const char *opt_font = "";
gboolean opt_header = FALSE;
const char *opt_output = NULL;
int opt_margin_t = 10;
int opt_margin_r = 10;
int opt_margin_b = 10;
int opt_margin_l = 10;
int opt_markup = FALSE;
gboolean opt_rtl = FALSE;
double opt_rotate = 0;
gboolean opt_auto_dir = TRUE;
const char *opt_text = NULL;
gboolean opt_waterfall = FALSE;
int opt_width = -1;
int opt_height = -1;
int opt_indent = 0;
gboolean opt_justify = 0;
int opt_runs = 1;
PangoAlignment opt_align = PANGO_ALIGN_LEFT;
PangoEllipsizeMode opt_ellipsize = PANGO_ELLIPSIZE_NONE;
PangoGravity opt_gravity = PANGO_GRAVITY_SOUTH;
PangoGravityHint opt_gravity_hint = PANGO_GRAVITY_HINT_NATURAL;
HintMode opt_hinting = HINT_DEFAULT;
PangoWrapMode opt_wrap = PANGO_WRAP_WORD_CHAR;
gboolean opt_wrap_set = FALSE;
static const char *opt_pangorc = NULL; /* Unused */
const PangoViewer *opt_viewer = NULL;
const char *opt_language = NULL;
gboolean opt_single_par = FALSE;
PangoColor opt_fg_color = {0, 0, 0};
guint16 opt_fg_alpha = 65535;
gboolean opt_bg_set = FALSE;
PangoColor opt_bg_color = {65535, 65535, 65535};
guint16 opt_bg_alpha = 65535;
/* Text (or markup) to render */
static char *text;
void
fail (const char *format, ...)
{
const char *msg;
va_list vap;
va_start (vap, format);
msg = g_strdup_vprintf (format, vap);
g_printerr ("%s: %s\n", g_get_prgname (), msg);
exit (1);
}
static PangoLayout *
make_layout(PangoContext *context,
const char *text,
double size)
{
static PangoFontDescription *font_description;
PangoAlignment align;
PangoLayout *layout;
layout = pango_layout_new (context);
if (opt_markup)
pango_layout_set_markup (layout, text, -1);
else
pango_layout_set_text (layout, text, -1);
pango_layout_set_auto_dir (layout, opt_auto_dir);
pango_layout_set_ellipsize (layout, opt_ellipsize);
pango_layout_set_justify (layout, opt_justify);
pango_layout_set_single_paragraph_mode (layout, opt_single_par);
pango_layout_set_wrap (layout, opt_wrap);
font_description = pango_font_description_from_string (opt_font);
if (size > 0)
pango_font_description_set_size (font_description, size * PANGO_SCALE);
if (opt_width > 0)
pango_layout_set_width (layout, (opt_width * opt_dpi * PANGO_SCALE + 36) / 72);
if (opt_height > 0)
pango_layout_set_height (layout, (opt_height * opt_dpi * PANGO_SCALE + 36) / 72);
else
pango_layout_set_height (layout, opt_height);
if (opt_indent != 0)
pango_layout_set_indent (layout, (opt_indent * opt_dpi * PANGO_SCALE + 36) / 72);
align = opt_align;
if (align != PANGO_ALIGN_CENTER &&
pango_context_get_base_dir (context) != PANGO_DIRECTION_LTR) {
/* pango reverses left and right if base dir ir rtl. so we should
* reverse to cancel that. unfortunately it also does that for
* rtl paragraphs, so we cannot really get left/right. all we get
* is default/other-side. */
align = PANGO_ALIGN_LEFT + PANGO_ALIGN_RIGHT - align;
}
pango_layout_set_alignment (layout, align);
pango_layout_set_font_description (layout, font_description);
pango_font_description_free (font_description);
return layout;
}
gchar *
get_options_string (void)
{
PangoFontDescription *font_description = pango_font_description_from_string (opt_font);
gchar *font_name;
gchar *result;
if (opt_waterfall)
pango_font_description_unset_fields (font_description, PANGO_FONT_MASK_SIZE);
font_name = pango_font_description_to_string (font_description);
result = g_strdup_printf ("%s: %s (%d dpi)", opt_viewer->name, font_name, opt_dpi);
pango_font_description_free (font_description);
g_free (font_name);
return result;
}
static void
output_body (PangoLayout *layout,
RenderCallback render_cb,
gpointer cb_context,
gpointer cb_data,
int *width,
int *height,
gboolean supports_matrix)
{
PangoRectangle logical_rect;
int size, start_size, end_size, increment;
int x = 0, y = 0;
if (!supports_matrix)
{
const PangoMatrix* matrix;
const PangoMatrix identity = PANGO_MATRIX_INIT;
PangoContext *context = pango_layout_get_context (layout);
matrix = pango_context_get_matrix (context);
if (matrix)
{
x += matrix->x0;
y += matrix->y0;
}
pango_context_set_matrix (context, &identity);
pango_layout_context_changed (layout);
}
if (opt_waterfall)
{
start_size = 8;
end_size = 48;
increment = 4;
}
else
{
start_size = end_size = -1;
increment = 1;
}
*width = 0;
*height = 0;
for (size = start_size; size <= end_size; size += increment)
{
if (size > 0)
{
PangoFontDescription *desc = pango_font_description_copy (pango_layout_get_font_description (layout));
pango_font_description_set_size (desc, size * PANGO_SCALE);
pango_layout_set_font_description (layout, desc);
pango_font_description_free (desc);
}
pango_layout_get_pixel_extents (layout, NULL, &logical_rect);
if (render_cb)
(*render_cb) (layout, x, y+*height, cb_context, cb_data);
*width = MAX (*width,
MAX (logical_rect.x + logical_rect.width,
PANGO_PIXELS (pango_layout_get_width (layout))));
*height += MAX (logical_rect.y + logical_rect.height,
PANGO_PIXELS (pango_layout_get_height (layout)));
}
}
static void
set_transform (PangoContext *context,
TransformCallback transform_cb,
gpointer cb_context,
gpointer cb_data,
PangoMatrix *matrix)
{
pango_context_set_matrix (context, matrix);
if (transform_cb)
(*transform_cb) (context, matrix, cb_context, cb_data);
}
void
do_output (PangoContext *context,
RenderCallback render_cb,
TransformCallback transform_cb,
gpointer cb_context,
gpointer cb_data,
int *width_out,
int *height_out)
{
PangoLayout *layout;
PangoRectangle rect;
PangoMatrix matrix = PANGO_MATRIX_INIT;
PangoMatrix *orig_matrix;
gboolean supports_matrix;
int rotated_width, rotated_height;
int x = opt_margin_l;
int y = opt_margin_t;
int width, height;
width = 0;
height = 0;
orig_matrix = pango_matrix_copy (pango_context_get_matrix (context));
/* If the backend sets an all-zero matrix on the context,
* means that it doesn't support transformations.
*/
supports_matrix = !orig_matrix ||
(orig_matrix->xx != 0. || orig_matrix->xy != 0. ||
orig_matrix->yx != 0. || orig_matrix->yy != 0. ||
orig_matrix->x0 != 0. || orig_matrix->y0 != 0.);
set_transform (context, transform_cb, cb_context, cb_data, NULL);
pango_context_set_language (context,
opt_language ? pango_language_from_string (opt_language)
: pango_language_get_default ());
pango_context_set_base_dir (context,
opt_rtl ? PANGO_DIRECTION_RTL : PANGO_DIRECTION_LTR);
if (opt_header)
{
char *options_string = get_options_string ();
pango_context_set_base_gravity (context, PANGO_GRAVITY_SOUTH);
layout = make_layout (context, options_string, 10);
pango_layout_get_extents (layout, NULL, &rect);
width = MAX (width, PANGO_PIXELS (rect.width));
height += PANGO_PIXELS (rect.height);
if (render_cb)
(*render_cb) (layout, x, y, cb_context, cb_data);
y += PANGO_PIXELS (rect.height);
g_object_unref (layout);
g_free (options_string);
}
if (opt_rotate != 0)
{
if (supports_matrix)
pango_matrix_rotate (&matrix, opt_rotate);
else
g_printerr ("The backend does not support rotated text\n");
}
pango_context_set_base_gravity (context, opt_gravity);
pango_context_set_gravity_hint (context, opt_gravity_hint);
layout = make_layout (context, text, -1);
set_transform (context, transform_cb, cb_context, cb_data, &matrix);
output_body (layout,
NULL, NULL, NULL,
&rotated_width, &rotated_height,
supports_matrix);
rect.x = rect.y = 0;
rect.width = rotated_width;
rect.height = rotated_height;
pango_matrix_transform_pixel_rectangle (&matrix, &rect);
matrix.x0 = x - rect.x;
matrix.y0 = y - rect.y;
set_transform (context, transform_cb, cb_context, cb_data, &matrix);
if (render_cb)
output_body (layout,
render_cb, cb_context, cb_data,
&rotated_width, &rotated_height,
supports_matrix);
width = MAX (width, rect.width);
height += rect.height;
width += opt_margin_l + opt_margin_r;
height += opt_margin_t + opt_margin_b;
if (width_out)
*width_out = width;
if (height_out)
*height_out = height;
pango_context_set_matrix (context, orig_matrix);
pango_matrix_free (orig_matrix);
g_object_unref (layout);
}
static gboolean
parse_enum (GType type,
int *value,
const char *name,
const char *arg,
gpointer data G_GNUC_UNUSED,
GError **error)
{
char *possible_values = NULL;
gboolean ret;
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
ret = pango_parse_enum (type,
arg,
value,
FALSE,
&possible_values);
G_GNUC_END_IGNORE_DEPRECATIONS
if (!ret && error)
{
g_set_error(error,
G_OPTION_ERROR,
G_OPTION_ERROR_BAD_VALUE,
"Argument for %s must be one of %s",
name,
possible_values);
}
g_free (possible_values);
return ret;
}
static gboolean
parse_align (const char *name,
const char *arg,
gpointer data,
GError **error)
{
return parse_enum (PANGO_TYPE_ALIGNMENT, (int*)(void*)&opt_align,
name, arg, data, error);
}
static gboolean
parse_ellipsis (const char *name,
const char *arg,
gpointer data,
GError **error)
{
return parse_enum (PANGO_TYPE_ELLIPSIZE_MODE, (int*)(void*)&opt_ellipsize,
name, arg, data, error);
}
static gboolean
parse_gravity (const char *name,
const char *arg,
gpointer data,
GError **error)
{
return parse_enum (PANGO_TYPE_GRAVITY, (int*)(void*)&opt_gravity,
name, arg, data, error);
}
static gboolean
parse_gravity_hint (const char *name,
const char *arg,
gpointer data,
GError **error)
{
return parse_enum (PANGO_TYPE_GRAVITY_HINT, (int*)(void*)&opt_gravity_hint,
name, arg, data, error);
}
static gboolean
parse_hinting (const char *name G_GNUC_UNUSED,
const char *arg,
gpointer data G_GNUC_UNUSED,
GError **error)
{
gboolean ret = TRUE;
if (strcmp (arg, "none") == 0)
opt_hinting = HINT_NONE;
else if (strcmp (arg, "auto") == 0)
opt_hinting = HINT_AUTO;
else if (strcmp (arg, "full") == 0)
opt_hinting = HINT_FULL;
else
{
g_set_error(error,
G_OPTION_ERROR,
G_OPTION_ERROR_BAD_VALUE,
"Argument for --hinting must be one of none/auto/full");
ret = FALSE;
}
return ret;
}
static gboolean
parse_wrap (const char *name,
const char *arg,
gpointer data,
GError **error)
{
gboolean ret;
if ((ret = parse_enum (PANGO_TYPE_WRAP_MODE, (int*)(void*)&opt_wrap,
name, arg, data, error)))
{
opt_wrap_set = TRUE;
}
return ret;
}
static gboolean
parse_rgba_color (PangoColor *color,
guint16 *alpha,
const char *name,
const char *arg,
gpointer data G_GNUC_UNUSED,
GError **error)
{
gboolean ret;
char buf[32];
int len;
len = strlen (arg);
/* handle alpha */
if (*arg == '#' && (len == 5 || len == 9 || len == 17))
{
int width, bits;
unsigned int a;
bits = len - 1;
width = bits >> 2;
strcpy (buf, arg);
arg = buf;
if (!sscanf (buf + len - width, "%x", &a))
{
ret = FALSE;
goto err;
}
buf[len - width] = '\0';
a <<= (16 - bits);
while (bits < 16)
{
a |= (a >> bits);
bits *= 2;
}
*alpha = a;
}
else
*alpha = 65535;
ret = pango_color_parse (color, arg);
err:
if (!ret && error)
{
g_set_error(error,
G_OPTION_ERROR,
G_OPTION_ERROR_BAD_VALUE,
"Argument for %s must be a color name like red, or CSS-style #rrggbb / #rrggbbaa",
name);
}
return ret;
}
static gboolean
parse_foreground (const char *name,
const char *arg,
gpointer data,
GError **error)
{
return parse_rgba_color (&opt_fg_color, &opt_fg_alpha,
name, arg, data, error);
}
static gboolean
parse_background (const char *name,
const char *arg,
gpointer data,
GError **error)
{
opt_bg_set = TRUE;
if (0 == strcmp ("transparent", arg))
{
opt_bg_alpha = 0;
return TRUE;
}
return parse_rgba_color (&opt_bg_color, &opt_bg_alpha,
name, arg, data, error);
}
static gboolean
parse_margin (const char *name G_GNUC_UNUSED,
const char *arg,
gpointer data G_GNUC_UNUSED,
GError **error)
{
switch (sscanf (arg, "%d%*[ ,]%d%*[ ,]%d%*[ ,]%d", &opt_margin_t, &opt_margin_r, &opt_margin_b, &opt_margin_l))
{
case 0:
{
g_set_error(error,
G_OPTION_ERROR,
G_OPTION_ERROR_BAD_VALUE,
"Argument for --margin must be one to four space-separated numbers");
return FALSE;
}
case 1: opt_margin_r = opt_margin_t;
case 2: opt_margin_b = opt_margin_t;
case 3: opt_margin_l = opt_margin_r;
}
return TRUE;
}
static gchar *
backends_to_string (void)
{
GString *backends = g_string_new (NULL);
const PangoViewer **viewer;
for (viewer = viewers; *viewer; viewer++)
if ((*viewer)->id)
{
g_string_append (backends, (*viewer)->id);
g_string_append_c (backends, '/');
}
g_string_truncate (backends, MAX (0, (gint)backends->len - 1));
return g_string_free(backends,FALSE);
}
static int
backends_get_count (void)
{
const PangoViewer **viewer;
int i = 0;
for (viewer = viewers; *viewer; viewer++)
if ((*viewer)->id)
i++;
return i;
}
static gchar *
backend_description (void)
{
GString *description = g_string_new("Pango backend to use for rendering ");
int backends_count = backends_get_count ();
if (backends_count > 1)
g_string_append_printf(description,"(default: %s)", (*viewers)->id);
else if (backends_count == 1)
g_string_append_printf(description,"(only available: %s)", (*viewers)->id);
else
g_string_append_printf(description,"(no backends found!)");
return g_string_free(description,FALSE);
}
static gboolean
parse_backend (const char *name G_GNUC_UNUSED,
const char *arg,
gpointer data G_GNUC_UNUSED,
GError **error)
{
gboolean ret = TRUE;
const PangoViewer **viewer;
for (viewer = viewers; *viewer; viewer++)
if (!g_ascii_strcasecmp ((*viewer)->id, arg))
break;
if (*viewer)
opt_viewer = *viewer;
else
{
gchar *backends = backends_to_string ();
g_set_error(error,
G_OPTION_ERROR,
G_OPTION_ERROR_BAD_VALUE,
"Available --backend options are: %s",
backends);
g_free(backends);
ret = FALSE;
}
return ret;
}
static G_GNUC_NORETURN gboolean
show_version(const char *name G_GNUC_UNUSED,
const char *arg G_GNUC_UNUSED,
gpointer data G_GNUC_UNUSED,
GError **error G_GNUC_UNUSED)
{
g_printf("%s (%s) %s\n", g_get_prgname (), PACKAGE_NAME, PACKAGE_VERSION);
if (PANGO_VERSION != pango_version())
g_printf("Linked Pango library has a different version: %s\n", pango_version_string ());
exit(0);
}
void
parse_options (int argc, char *argv[])
{
gchar *backend_options = backends_to_string ();
GOptionFlags backend_flag = backends_get_count () > 1 ? 0 : G_OPTION_FLAG_HIDDEN;
gchar *backend_desc = backend_description ();
GOptionEntry entries[] =
{
{"no-auto-dir", 0, G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_auto_dir,
"No layout direction according to contents", NULL},
{"backend", 0, backend_flag, G_OPTION_ARG_CALLBACK, &parse_backend,
backend_desc, backend_options},
{"background", 0, 0, G_OPTION_ARG_CALLBACK, &parse_background,
"Set the background color", "red/#rrggbb/#rrggbbaa/transparent"},
{"no-display", 'q', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_display,
"Do not display (just write to file or whatever)", NULL},
{"dpi", 0, 0, G_OPTION_ARG_INT, &opt_dpi,
"Set the resolution", "number"},
{"align", 0, 0, G_OPTION_ARG_CALLBACK, &parse_align,
"Text alignment", "left/center/right"},
{"ellipsize", 0, 0, G_OPTION_ARG_CALLBACK, &parse_ellipsis,
"Ellipsization mode", "start/middle/end"},
{"font", 0, 0, G_OPTION_ARG_STRING, &opt_font,
"Set the font description", "description"},
{"foreground", 0, 0, G_OPTION_ARG_CALLBACK, &parse_foreground,
"Set the text color", "red/#rrggbb/#rrggbbaa"},
{"gravity", 0, 0, G_OPTION_ARG_CALLBACK, &parse_gravity,
"Base gravity: glyph rotation", "south/east/north/west/auto"},
{"gravity-hint", 0, 0, G_OPTION_ARG_CALLBACK, &parse_gravity_hint,
"Gravity hint", "natural/strong/line"},
{"header", 0, 0, G_OPTION_ARG_NONE, &opt_header,
"Display the options in the output", NULL},
{"height", 0, 0, G_OPTION_ARG_INT, &opt_height,
"Height in points (positive) or number of lines (negative) for ellipsizing", "+points/-numlines"},
{"hinting", 0, 0, G_OPTION_ARG_CALLBACK, &parse_hinting,
"Hinting style", "none/auto/full"},
{"indent", 0, 0, G_OPTION_ARG_INT, &opt_indent,
"Width in points to indent paragraphs", "points"},
{"justify", 0, 0, G_OPTION_ARG_NONE, &opt_justify,
"Align paragraph lines to be justified", NULL},
{"language", 0, 0, G_OPTION_ARG_STRING, &opt_language,
"Language to use for font selection", "en_US/etc"},
{"margin", 0, 0, G_OPTION_ARG_CALLBACK, &parse_margin,
"Set the margin on the output in pixels", "CSS-style numbers in pixels"},
{"markup", 0, 0, G_OPTION_ARG_NONE, &opt_markup,
"Interpret text as Pango markup", NULL},
{"output", 'o', 0, G_OPTION_ARG_STRING, &opt_output,
"Save rendered image to output file", "file"},
{"pangorc", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &opt_pangorc,
"Deprecated", "file"},
{"pixels", 0, 0, G_OPTION_ARG_NONE, &opt_pixels,
"Use pixel units instead of points (sets dpi to 72)", NULL},
{"rtl", 0, 0, G_OPTION_ARG_NONE, &opt_rtl,
"Set base direction to right-to-left", NULL},
{"rotate", 0, 0, G_OPTION_ARG_DOUBLE, &opt_rotate,
"Angle at which to rotate results", "degrees"},
{"runs", 'n', 0, G_OPTION_ARG_INT, &opt_runs,
"Run Pango layout engine this many times", "integer"},
{"single-par", 0, 0, G_OPTION_ARG_NONE, &opt_single_par,
"Enable single-paragraph mode", NULL},
{"text", 't', 0, G_OPTION_ARG_STRING, &opt_text,
"Text to display (instead of a file)", "string"},
{"version", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, &show_version,
"Show version numbers", NULL},
{"waterfall", 0, 0, G_OPTION_ARG_NONE, &opt_waterfall,
"Create a waterfall display", NULL},
{"width", 'w', 0, G_OPTION_ARG_INT, &opt_width,
"Width in points to which to wrap lines or ellipsize", "points"},
{"wrap", 0, 0, G_OPTION_ARG_CALLBACK, &parse_wrap,
"Text wrapping mode (needs a width to be set)", "word/char/word-char"},
{NULL}
};
GError *error = NULL;
GError *parse_error = NULL;
GOptionContext *context;
size_t len;
const PangoViewer **viewer;
context = g_option_context_new ("- FILE");
g_option_context_add_main_entries (context, entries, NULL);
for (viewer = viewers; *viewer; viewer++)
if ((*viewer)->get_option_group)
{
GOptionGroup *group = (*viewer)->get_option_group (*viewer);
if (group)
g_option_context_add_group (context, group);
}
if (!g_option_context_parse (context, &argc, &argv, &parse_error))
{
if (parse_error != NULL)
fail("%s", parse_error->message);
else
fail("Option parse error");
exit(1);
}
g_option_context_free(context);
g_free(backend_options);
g_free(backend_desc);
if (opt_pixels)
opt_dpi = 72;
if ((opt_text && argc != 1) || (!opt_text && argc != 2))
{
if (opt_text && argc != 1)
fail ("When specifying --text, no file should be given");
g_printerr ("Usage: %s [OPTION...] FILE\n", g_get_prgname ());
exit (1);
}
/* set up the backend */
if (!opt_viewer)
{
opt_viewer = *viewers;
if (!opt_viewer)
fail ("No viewer backend found");
}
/* Get the text
*/
if (opt_text)
{
text = g_strdup (opt_text);
len = strlen (text);
}
else
{
if (!g_file_get_contents (argv[1], &text, &len, &error))
fail ("%s\n", error->message);
}
/* Strip one trailing newline
*/
if (len > 0 && text[len - 1] == '\n')
len--;
if (len > 0 && text[len - 1] == '\r')
len--;
text[len] = '\0';
/* Make sure we have valid markup
*/
if (opt_markup &&
!pango_parse_markup (text, -1, 0, NULL, NULL, NULL, &error))
fail ("Cannot parse input as markup: %s", error->message);
}
void
finalize (void)
{
g_free (text);
}