Gimp/app/text/gimptextlayout.c
Adam Fontenot 00bbeabaf4 Issue #6210: Subpixel font rendering system settings should only…
… apply to GIMP GUI not text layer rendering in image.

Reviewer note: this is the theoretical fix, but it won't work right now
because Cairo explicitly bypasses grayscale antialiasing when system set
subpixel one. Still let's push this first patch, but the issue will be
actually fixed when Cairo will merge my MR too:
https://gitlab.freedesktop.org/cairo/cairo/-/merge_requests/114
2021-01-19 13:47:27 +01:00

796 lines
22 KiB
C

/* GIMP - The GNU Image Manipulation Program
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
*
* GimpText
* Copyright (C) 2002-2003 Sven Neumann <sven@gimp.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <string.h>
#include <gegl.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <pango/pangocairo.h>
#include "libgimpbase/gimpbase.h"
#include "libgimpcolor/gimpcolor.h"
#include "libgimpmath/gimpmath.h"
#include "text-types.h"
#include "core/gimperror.h"
#include "gimptext.h"
#include "gimptextlayout.h"
#include "gimp-intl.h"
struct _GimpTextLayout
{
GObject object;
GimpText *text;
gdouble xres;
gdouble yres;
PangoLayout *layout;
PangoRectangle extents;
};
static void gimp_text_layout_finalize (GObject *object);
static void gimp_text_layout_position (GimpTextLayout *layout);
static void gimp_text_layout_set_markup (GimpTextLayout *layout,
GError **error);
static PangoContext * gimp_text_get_pango_context (GimpText *text,
gdouble xres,
gdouble yres);
G_DEFINE_TYPE (GimpTextLayout, gimp_text_layout, G_TYPE_OBJECT)
#define parent_class gimp_text_layout_parent_class
static void
gimp_text_layout_class_init (GimpTextLayoutClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->finalize = gimp_text_layout_finalize;
}
static void
gimp_text_layout_init (GimpTextLayout *layout)
{
layout->text = NULL;
layout->layout = NULL;
}
static void
gimp_text_layout_finalize (GObject *object)
{
GimpTextLayout *layout = GIMP_TEXT_LAYOUT (object);
if (layout->text)
{
g_object_unref (layout->text);
layout->text = NULL;
}
if (layout->layout)
{
g_object_unref (layout->layout);
layout->layout = NULL;
}
G_OBJECT_CLASS (parent_class)->finalize (object);
}
GimpTextLayout *
gimp_text_layout_new (GimpText *text,
gdouble xres,
gdouble yres,
GError **error)
{
GimpTextLayout *layout;
PangoContext *context;
PangoFontDescription *font_desc;
PangoAlignment alignment = PANGO_ALIGN_LEFT;
gint size;
g_return_val_if_fail (GIMP_IS_TEXT (text), NULL);
font_desc = pango_font_description_from_string (text->font);
g_return_val_if_fail (font_desc != NULL, NULL);
size = pango_units_from_double (gimp_units_to_points (text->font_size,
text->unit,
yres));
pango_font_description_set_size (font_desc, MAX (1, size));
context = gimp_text_get_pango_context (text, xres, yres);
layout = g_object_new (GIMP_TYPE_TEXT_LAYOUT, NULL);
layout->text = g_object_ref (text);
layout->layout = pango_layout_new (context);
layout->xres = xres;
layout->yres = yres;
pango_layout_set_wrap (layout->layout, PANGO_WRAP_WORD_CHAR);
pango_layout_set_font_description (layout->layout, font_desc);
pango_font_description_free (font_desc);
gimp_text_layout_set_markup (layout, error);
switch (text->justify)
{
case GIMP_TEXT_JUSTIFY_LEFT:
alignment = PANGO_ALIGN_LEFT;
break;
case GIMP_TEXT_JUSTIFY_RIGHT:
alignment = PANGO_ALIGN_RIGHT;
break;
case GIMP_TEXT_JUSTIFY_CENTER:
alignment = PANGO_ALIGN_CENTER;
break;
case GIMP_TEXT_JUSTIFY_FILL:
alignment = PANGO_ALIGN_LEFT;
pango_layout_set_justify (layout->layout, TRUE);
break;
}
pango_layout_set_alignment (layout->layout, alignment);
switch (text->box_mode)
{
case GIMP_TEXT_BOX_DYNAMIC:
break;
case GIMP_TEXT_BOX_FIXED:
if (! PANGO_GRAVITY_IS_VERTICAL (pango_context_get_base_gravity (context)))
pango_layout_set_width (layout->layout,
pango_units_from_double
(gimp_units_to_pixels (text->box_width,
text->box_unit,
xres)));
else
pango_layout_set_width (layout->layout,
pango_units_from_double
(gimp_units_to_pixels (text->box_height,
text->box_unit,
yres)));
break;
}
pango_layout_set_indent (layout->layout,
pango_units_from_double
(gimp_units_to_pixels (text->indent,
text->unit,
xres)));
pango_layout_set_spacing (layout->layout,
pango_units_from_double
(gimp_units_to_pixels (text->line_spacing,
text->unit,
yres)));
gimp_text_layout_position (layout);
switch (text->box_mode)
{
case GIMP_TEXT_BOX_DYNAMIC:
break;
case GIMP_TEXT_BOX_FIXED:
layout->extents.width = ceil (gimp_units_to_pixels (text->box_width,
text->box_unit,
xres));
layout->extents.height = ceil (gimp_units_to_pixels (text->box_height,
text->box_unit,
yres));
/* #define VERBOSE */
#ifdef VERBOSE
g_printerr ("extents set to %d x %d\n",
layout->extents.width, layout->extents.height);
#endif
break;
}
g_object_unref (context);
return layout;
}
gboolean
gimp_text_layout_get_size (GimpTextLayout *layout,
gint *width,
gint *height)
{
g_return_val_if_fail (GIMP_IS_TEXT_LAYOUT (layout), FALSE);
if (width)
*width = layout->extents.width;
if (height)
*height = layout->extents.height;
return (layout->extents.width > 0 && layout->extents.height > 0);
}
void
gimp_text_layout_get_offsets (GimpTextLayout *layout,
gint *x,
gint *y)
{
g_return_if_fail (GIMP_IS_TEXT_LAYOUT (layout));
if (x)
*x = layout->extents.x;
if (y)
*y = layout->extents.y;
}
void
gimp_text_layout_get_resolution (GimpTextLayout *layout,
gdouble *xres,
gdouble *yres)
{
g_return_if_fail (GIMP_IS_TEXT_LAYOUT (layout));
if (xres)
*xres = layout->xres;
if (yres)
*yres = layout->yres;
}
GimpText *
gimp_text_layout_get_text (GimpTextLayout *layout)
{
g_return_val_if_fail (GIMP_IS_TEXT_LAYOUT (layout), NULL);
return layout->text;
}
PangoLayout *
gimp_text_layout_get_pango_layout (GimpTextLayout *layout)
{
g_return_val_if_fail (GIMP_IS_TEXT_LAYOUT (layout), NULL);
return layout->layout;
}
void
gimp_text_layout_get_transform (GimpTextLayout *layout,
cairo_matrix_t *matrix)
{
GimpText *text;
gdouble xres;
gdouble yres;
gdouble norm;
g_return_if_fail (GIMP_IS_TEXT_LAYOUT (layout));
g_return_if_fail (matrix != NULL);
text = gimp_text_layout_get_text (layout);
gimp_text_layout_get_resolution (layout, &xres, &yres);
norm = 1.0 / yres * xres;
matrix->xx = text->transformation.coeff[0][0] * norm;
matrix->xy = text->transformation.coeff[0][1] * 1.0;
matrix->yx = text->transformation.coeff[1][0] * norm;
matrix->yy = text->transformation.coeff[1][1] * 1.0;
matrix->x0 = 0;
matrix->y0 = 0;
}
void
gimp_text_layout_transform_rect (GimpTextLayout *layout,
PangoRectangle *rect)
{
cairo_matrix_t matrix;
gdouble x, y;
gdouble width, height;
g_return_if_fail (GIMP_IS_TEXT_LAYOUT (layout));
g_return_if_fail (rect != NULL);
x = rect->x;
y = rect->y;
width = rect->width;
height = rect->height;
gimp_text_layout_get_transform (layout, &matrix);
cairo_matrix_transform_point (&matrix, &x, &y);
cairo_matrix_transform_distance (&matrix, &width, &height);
rect->x = ROUND (x);
rect->y = ROUND (y);
rect->width = ROUND (width);
rect->height = ROUND (height);
}
void
gimp_text_layout_transform_point (GimpTextLayout *layout,
gdouble *x,
gdouble *y)
{
cairo_matrix_t matrix;
gdouble _x = 0.0;
gdouble _y = 0.0;
g_return_if_fail (GIMP_IS_TEXT_LAYOUT (layout));
if (x) _x = *x;
if (y) _y = *y;
gimp_text_layout_get_transform (layout, &matrix);
cairo_matrix_transform_point (&matrix, &_x, &_y);
if (x) *x = _x;
if (y) *y = _y;
}
void
gimp_text_layout_transform_distance (GimpTextLayout *layout,
gdouble *x,
gdouble *y)
{
cairo_matrix_t matrix;
gdouble _x = 0.0;
gdouble _y = 0.0;
g_return_if_fail (GIMP_IS_TEXT_LAYOUT (layout));
if (x) _x = *x;
if (y) _y = *y;
gimp_text_layout_get_transform (layout, &matrix);
cairo_matrix_transform_distance (&matrix, &_x, &_y);
if (x) *x = _x;
if (y) *y = _y;
}
void
gimp_text_layout_untransform_rect (GimpTextLayout *layout,
PangoRectangle *rect)
{
cairo_matrix_t matrix;
gdouble x, y;
gdouble width, height;
g_return_if_fail (GIMP_IS_TEXT_LAYOUT (layout));
g_return_if_fail (rect != NULL);
x = rect->x;
y = rect->y;
width = rect->width;
height = rect->height;
gimp_text_layout_get_transform (layout, &matrix);
if (cairo_matrix_invert (&matrix) == CAIRO_STATUS_SUCCESS)
{
cairo_matrix_transform_point (&matrix, &x, &y);
cairo_matrix_transform_distance (&matrix, &width, &height);
rect->x = ROUND (x);
rect->y = ROUND (y);
rect->width = ROUND (width);
rect->height = ROUND (height);
}
}
void
gimp_text_layout_untransform_point (GimpTextLayout *layout,
gdouble *x,
gdouble *y)
{
cairo_matrix_t matrix;
gdouble _x = 0.0;
gdouble _y = 0.0;
g_return_if_fail (GIMP_IS_TEXT_LAYOUT (layout));
if (x) _x = *x;
if (y) _y = *y;
gimp_text_layout_get_transform (layout, &matrix);
if (cairo_matrix_invert (&matrix) == CAIRO_STATUS_SUCCESS)
{
cairo_matrix_transform_point (&matrix, &_x, &_y);
if (x) *x = _x;
if (y) *y = _y;
}
}
void
gimp_text_layout_untransform_distance (GimpTextLayout *layout,
gdouble *x,
gdouble *y)
{
cairo_matrix_t matrix;
gdouble _x = 0.0;
gdouble _y = 0.0;
g_return_if_fail (GIMP_IS_TEXT_LAYOUT (layout));
if (x) _x = *x;
if (y) _y = *y;
gimp_text_layout_get_transform (layout, &matrix);
if (cairo_matrix_invert (&matrix) == CAIRO_STATUS_SUCCESS)
{
cairo_matrix_transform_distance (&matrix, &_x, &_y);
if (x) *x = _x;
if (y) *y = _y;
}
}
static gboolean
gimp_text_layout_split_markup (const gchar *markup,
gchar **open_tag,
gchar **content,
gchar **close_tag)
{
gchar *p_open;
gchar *p_close;
p_open = strstr (markup, "<markup>");
if (! p_open)
return FALSE;
*open_tag = g_strndup (markup, p_open - markup + strlen ("<markup>"));
p_close = g_strrstr (markup, "</markup>");
if (! p_close)
{
g_free (*open_tag);
return FALSE;
}
*close_tag = g_strdup (p_close);
if (p_open + strlen ("<markup>") < p_close)
{
*content = g_strndup (p_open + strlen ("<markup>"),
p_close - p_open - strlen ("<markup>"));
}
else
{
*content = g_strdup ("");
}
return TRUE;
}
static gchar *
gimp_text_layout_apply_tags (GimpTextLayout *layout,
const gchar *markup)
{
GimpText *text = layout->text;
gchar *result;
{
guchar r, g, b;
gimp_rgb_get_uchar (&text->color, &r, &g, &b);
result = g_strdup_printf ("<span color=\"#%02x%02x%02x\">%s</span>",
r, g, b, markup);
}
if (fabs (text->letter_spacing) > 0.1)
{
gchar *tmp = g_strdup_printf ("<span letter_spacing=\"%d\">%s</span>",
(gint) (text->letter_spacing * PANGO_SCALE),
result);
g_free (result);
result = tmp;
}
return result;
}
static void
gimp_text_layout_set_markup (GimpTextLayout *layout,
GError **error)
{
GimpText *text = layout->text;
gchar *open_tag = NULL;
gchar *content = NULL;
gchar *close_tag = NULL;
gchar *tagged;
gchar *markup;
if (text->markup)
{
if (! gimp_text_layout_split_markup (text->markup,
&open_tag, &content, &close_tag))
{
open_tag = g_strdup ("<markup>");
content = g_strdup ("");
close_tag = g_strdup ("</markup>");
}
}
else
{
open_tag = g_strdup ("<markup>");
close_tag = g_strdup ("</markup>");
if (text->text)
content = g_markup_escape_text (text->text, -1);
else
content = g_strdup ("");
}
tagged = gimp_text_layout_apply_tags (layout, content);
g_free (content);
markup = g_strconcat (open_tag, tagged, close_tag, NULL);
g_free (open_tag);
g_free (tagged);
g_free (close_tag);
if (pango_parse_markup (markup, -1, 0, NULL, NULL, NULL, error) == FALSE)
{
if (error && *error &&
(*error)->domain == G_MARKUP_ERROR &&
(*error)->code == G_MARKUP_ERROR_INVALID_CONTENT)
{
/* Errors from pango lib are not accurate enough.
* Other possible error codes are: G_MARKUP_ERROR_UNKNOWN_ELEMENT
* and G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE, which likely indicate a bug
* in GIMP code or a pango library version issue.
* G_MARKUP_ERROR_INVALID_CONTENT on the other hand likely indicates
* size/color/style/weight/variant/etc. value issue. Font size is the
* only free text in GIMP GUI so we assume that must be it.
* Also we output a custom message because pango's error->message is
* too technical (telling of <span> tags, not using user's font size
* unit, and such). */
g_error_free (*error);
*error = NULL;
g_set_error_literal (error, GIMP_ERROR, GIMP_FAILED,
_("The new text layout cannot be generated. "
"Most likely the font size is too big."));
}
}
else
pango_layout_set_markup (layout->layout, markup, -1);
g_free (markup);
}
static void
gimp_text_layout_position (GimpTextLayout *layout)
{
PangoRectangle ink;
PangoRectangle logical;
PangoContext *context;
gint x1, y1;
gint x2, y2;
layout->extents.x = 0;
layout->extents.y = 0;
layout->extents.width = 0;
layout->extents.height = 0;
pango_layout_get_pixel_extents (layout->layout, &ink, &logical);
ink.width = ceil ((gdouble) ink.width * layout->xres / layout->yres);
logical.width = ceil ((gdouble) logical.width * layout->xres / layout->yres);
context = pango_layout_get_context (layout->layout);
#ifdef VERBOSE
g_printerr ("ink rect: %d x %d @ %d, %d\n",
ink.width, ink.height, ink.x, ink.y);
g_printerr ("logical rect: %d x %d @ %d, %d\n",
logical.width, logical.height, logical.x, logical.y);
#endif
if (ink.width < 1 || ink.height < 1)
{
layout->extents.width = 1;
layout->extents.height = logical.height;
return;
}
x1 = MIN (ink.x, logical.x);
y1 = MIN (ink.y, logical.y);
x2 = MAX (ink.x + ink.width, logical.x + logical.width);
y2 = MAX (ink.y + ink.height, logical.y + logical.height);
layout->extents.x = - x1;
layout->extents.y = - y1;
layout->extents.width = x2 - x1;
layout->extents.height = y2 - y1;
/* If the width of the layout is > 0, then the text-box is FIXED and
* the layout position should be offset if the alignment is centered
* or right-aligned, also adjust for RTL text direction.
*/
if (pango_layout_get_width (layout->layout) > 0)
{
PangoAlignment align = pango_layout_get_alignment (layout->layout);
GimpTextDirection base_dir = layout->text->base_dir;
gint width;
pango_layout_get_pixel_size (layout->layout, &width, NULL);
if ((base_dir == GIMP_TEXT_DIRECTION_LTR && align == PANGO_ALIGN_RIGHT) ||
(base_dir == GIMP_TEXT_DIRECTION_RTL && align == PANGO_ALIGN_LEFT) ||
(base_dir == GIMP_TEXT_DIRECTION_TTB_RTL && align == PANGO_ALIGN_RIGHT) ||
(base_dir == GIMP_TEXT_DIRECTION_TTB_RTL_UPRIGHT && align == PANGO_ALIGN_RIGHT) ||
(base_dir == GIMP_TEXT_DIRECTION_TTB_LTR && align == PANGO_ALIGN_LEFT) ||
(base_dir == GIMP_TEXT_DIRECTION_TTB_LTR_UPRIGHT && align == PANGO_ALIGN_LEFT))
{
layout->extents.x +=
PANGO_PIXELS (pango_layout_get_width (layout->layout)) - width;
}
else if (align == PANGO_ALIGN_CENTER)
{
layout->extents.x +=
(PANGO_PIXELS (pango_layout_get_width (layout->layout)) - width) / 2;
}
}
if (layout->text->border > 0)
{
gint border = layout->text->border;
layout->extents.x += border;
layout->extents.y += border;
layout->extents.width += 2 * border;
layout->extents.height += 2 * border;
}
if (PANGO_GRAVITY_IS_VERTICAL (pango_context_get_base_gravity (context)))
{
gint temp;
temp = layout->extents.y;
layout->extents.y = layout->extents.x;
layout->extents.x = temp;
temp = layout->extents.height;
layout->extents.height = layout->extents.width;
layout->extents.width = temp;
}
#ifdef VERBOSE
g_printerr ("layout extents: %d x %d @ %d, %d\n",
layout->extents.width, layout->extents.height,
layout->extents.x, layout->extents.y);
#endif
}
static cairo_font_options_t *
gimp_text_get_font_options (GimpText *text)
{
cairo_font_options_t *options = cairo_font_options_create ();
cairo_font_options_set_antialias (options, (text->antialias ?
CAIRO_ANTIALIAS_GRAY :
CAIRO_ANTIALIAS_NONE));
switch (text->hint_style)
{
case GIMP_TEXT_HINT_STYLE_NONE:
cairo_font_options_set_hint_style (options, CAIRO_HINT_STYLE_NONE);
break;
case GIMP_TEXT_HINT_STYLE_SLIGHT:
cairo_font_options_set_hint_style (options, CAIRO_HINT_STYLE_SLIGHT);
break;
case GIMP_TEXT_HINT_STYLE_MEDIUM:
cairo_font_options_set_hint_style (options, CAIRO_HINT_STYLE_MEDIUM);
break;
case GIMP_TEXT_HINT_STYLE_FULL:
cairo_font_options_set_hint_style (options, CAIRO_HINT_STYLE_FULL);
break;
}
return options;
}
static PangoContext *
gimp_text_get_pango_context (GimpText *text,
gdouble xres,
gdouble yres)
{
PangoContext *context;
PangoFontMap *fontmap;
cairo_font_options_t *options;
fontmap = pango_cairo_font_map_new_for_font_type (CAIRO_FONT_TYPE_FT);
if (! fontmap)
g_error ("You are using a Pango that has been built against a cairo "
"that lacks the Freetype font backend");
pango_cairo_font_map_set_resolution (PANGO_CAIRO_FONT_MAP (fontmap), yres);
context = pango_font_map_create_context (fontmap);
g_object_unref (fontmap);
options = gimp_text_get_font_options (text);
pango_cairo_context_set_font_options (context, options);
cairo_font_options_destroy (options);
if (text->language)
pango_context_set_language (context,
pango_language_from_string (text->language));
switch (text->base_dir)
{
case GIMP_TEXT_DIRECTION_LTR:
pango_context_set_base_dir (context, PANGO_DIRECTION_LTR);
pango_context_set_gravity_hint (context, PANGO_GRAVITY_HINT_NATURAL);
pango_context_set_base_gravity (context, PANGO_GRAVITY_SOUTH);
break;
case GIMP_TEXT_DIRECTION_RTL:
pango_context_set_base_dir (context, PANGO_DIRECTION_RTL);
pango_context_set_gravity_hint (context, PANGO_GRAVITY_HINT_NATURAL);
pango_context_set_base_gravity (context, PANGO_GRAVITY_SOUTH);
break;
case GIMP_TEXT_DIRECTION_TTB_RTL:
pango_context_set_base_dir (context, PANGO_DIRECTION_LTR);
pango_context_set_gravity_hint (context, PANGO_GRAVITY_HINT_LINE);
pango_context_set_base_gravity (context, PANGO_GRAVITY_EAST);
break;
case GIMP_TEXT_DIRECTION_TTB_RTL_UPRIGHT:
pango_context_set_base_dir (context, PANGO_DIRECTION_LTR);
pango_context_set_gravity_hint (context, PANGO_GRAVITY_HINT_STRONG);
pango_context_set_base_gravity (context, PANGO_GRAVITY_EAST);
break;
case GIMP_TEXT_DIRECTION_TTB_LTR:
pango_context_set_base_dir (context, PANGO_DIRECTION_LTR);
pango_context_set_gravity_hint (context, PANGO_GRAVITY_HINT_LINE);
pango_context_set_base_gravity (context, PANGO_GRAVITY_WEST);
break;
case GIMP_TEXT_DIRECTION_TTB_LTR_UPRIGHT:
pango_context_set_base_dir (context, PANGO_DIRECTION_LTR);
pango_context_set_gravity_hint (context, PANGO_GRAVITY_HINT_STRONG);
pango_context_set_base_gravity (context, PANGO_GRAVITY_WEST);
break;
}
return context;
}