PIKApp/app/text/pikatextlayout.c

812 lines
23 KiB
C

/* PIKA - Photo and Image Kooker Application
* a rebranding of The GNU Image Manipulation Program (created with heckimp)
* A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
*
* Original copyright, applying to most contents (license remains unchanged):
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
*
* PikaText
* 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 "libpikabase/pikabase.h"
#include "libpikacolor/pikacolor.h"
#include "libpikamath/pikamath.h"
#include "text-types.h"
#include "core/pikaerror.h"
#include "pikafont.h"
#include "pikatext.h"
#include "pikatextlayout.h"
#include "pika-intl.h"
struct _PikaTextLayout
{
GObject object;
PikaText *text;
gdouble xres;
gdouble yres;
PangoLayout *layout;
PangoRectangle extents;
};
static void pika_text_layout_finalize (GObject *object);
static void pika_text_layout_position (PikaTextLayout *layout);
static void pika_text_layout_set_markup (PikaTextLayout *layout,
GError **error);
static PangoContext * pika_text_get_pango_context (PikaText *text,
gdouble xres,
gdouble yres);
G_DEFINE_TYPE (PikaTextLayout, pika_text_layout, G_TYPE_OBJECT)
#define parent_class pika_text_layout_parent_class
static void
pika_text_layout_class_init (PikaTextLayoutClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->finalize = pika_text_layout_finalize;
}
static void
pika_text_layout_init (PikaTextLayout *layout)
{
layout->text = NULL;
layout->layout = NULL;
}
static void
pika_text_layout_finalize (GObject *object)
{
PikaTextLayout *layout = PIKA_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);
}
PikaTextLayout *
pika_text_layout_new (PikaText *text,
gdouble xres,
gdouble yres,
GError **error)
{
PikaTextLayout *layout;
PangoContext *context;
PangoFontDescription *font_desc;
PangoAlignment alignment = PANGO_ALIGN_LEFT;
gint size;
g_return_val_if_fail (PIKA_IS_TEXT (text), NULL);
font_desc = pango_font_description_from_string (pika_font_get_lookup_name (text->font));
g_return_val_if_fail (font_desc != NULL, NULL);
size = pango_units_from_double (pika_units_to_points (text->font_size,
text->unit,
yres));
pango_font_description_set_size (font_desc, MAX (1, size));
context = pika_text_get_pango_context (text, xres, yres);
layout = g_object_new (PIKA_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);
pika_text_layout_set_markup (layout, error);
switch (text->justify)
{
case PIKA_TEXT_JUSTIFY_LEFT:
alignment = PANGO_ALIGN_LEFT;
break;
case PIKA_TEXT_JUSTIFY_RIGHT:
alignment = PANGO_ALIGN_RIGHT;
break;
case PIKA_TEXT_JUSTIFY_CENTER:
alignment = PANGO_ALIGN_CENTER;
break;
case PIKA_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 PIKA_TEXT_BOX_DYNAMIC:
break;
case PIKA_TEXT_BOX_FIXED:
if (! PANGO_GRAVITY_IS_VERTICAL (pango_context_get_base_gravity (context)))
pango_layout_set_width (layout->layout,
pango_units_from_double
(pika_units_to_pixels (text->box_width,
text->box_unit,
xres)));
else
pango_layout_set_width (layout->layout,
pango_units_from_double
(pika_units_to_pixels (text->box_height,
text->box_unit,
yres)));
break;
}
pango_layout_set_indent (layout->layout,
pango_units_from_double
(pika_units_to_pixels (text->indent,
text->unit,
xres)));
pango_layout_set_spacing (layout->layout,
pango_units_from_double
(pika_units_to_pixels (text->line_spacing,
text->unit,
yres)));
pika_text_layout_position (layout);
switch (text->box_mode)
{
case PIKA_TEXT_BOX_DYNAMIC:
break;
case PIKA_TEXT_BOX_FIXED:
layout->extents.width = ceil (pika_units_to_pixels (text->box_width,
text->box_unit,
xres));
layout->extents.height = ceil (pika_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
pika_text_layout_get_size (PikaTextLayout *layout,
gint *width,
gint *height)
{
g_return_val_if_fail (PIKA_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
pika_text_layout_get_offsets (PikaTextLayout *layout,
gint *x,
gint *y)
{
g_return_if_fail (PIKA_IS_TEXT_LAYOUT (layout));
if (x)
*x = layout->extents.x;
if (y)
*y = layout->extents.y;
}
void
pika_text_layout_get_resolution (PikaTextLayout *layout,
gdouble *xres,
gdouble *yres)
{
g_return_if_fail (PIKA_IS_TEXT_LAYOUT (layout));
if (xres)
*xres = layout->xres;
if (yres)
*yres = layout->yres;
}
PikaText *
pika_text_layout_get_text (PikaTextLayout *layout)
{
g_return_val_if_fail (PIKA_IS_TEXT_LAYOUT (layout), NULL);
return layout->text;
}
PangoLayout *
pika_text_layout_get_pango_layout (PikaTextLayout *layout)
{
g_return_val_if_fail (PIKA_IS_TEXT_LAYOUT (layout), NULL);
return layout->layout;
}
void
pika_text_layout_get_transform (PikaTextLayout *layout,
cairo_matrix_t *matrix)
{
PikaText *text;
gdouble xres;
gdouble yres;
gdouble norm;
g_return_if_fail (PIKA_IS_TEXT_LAYOUT (layout));
g_return_if_fail (matrix != NULL);
text = pika_text_layout_get_text (layout);
pika_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
pika_text_layout_transform_rect (PikaTextLayout *layout,
PangoRectangle *rect)
{
cairo_matrix_t matrix;
gdouble x, y;
gdouble width, height;
g_return_if_fail (PIKA_IS_TEXT_LAYOUT (layout));
g_return_if_fail (rect != NULL);
x = rect->x;
y = rect->y;
width = rect->width;
height = rect->height;
pika_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
pika_text_layout_transform_point (PikaTextLayout *layout,
gdouble *x,
gdouble *y)
{
cairo_matrix_t matrix;
gdouble _x = 0.0;
gdouble _y = 0.0;
g_return_if_fail (PIKA_IS_TEXT_LAYOUT (layout));
if (x) _x = *x;
if (y) _y = *y;
pika_text_layout_get_transform (layout, &matrix);
cairo_matrix_transform_point (&matrix, &_x, &_y);
if (x) *x = _x;
if (y) *y = _y;
}
void
pika_text_layout_transform_distance (PikaTextLayout *layout,
gdouble *x,
gdouble *y)
{
cairo_matrix_t matrix;
gdouble _x = 0.0;
gdouble _y = 0.0;
g_return_if_fail (PIKA_IS_TEXT_LAYOUT (layout));
if (x) _x = *x;
if (y) _y = *y;
pika_text_layout_get_transform (layout, &matrix);
cairo_matrix_transform_distance (&matrix, &_x, &_y);
if (x) *x = _x;
if (y) *y = _y;
}
void
pika_text_layout_untransform_rect (PikaTextLayout *layout,
PangoRectangle *rect)
{
cairo_matrix_t matrix;
gdouble x, y;
gdouble width, height;
g_return_if_fail (PIKA_IS_TEXT_LAYOUT (layout));
g_return_if_fail (rect != NULL);
x = rect->x;
y = rect->y;
width = rect->width;
height = rect->height;
pika_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
pika_text_layout_untransform_point (PikaTextLayout *layout,
gdouble *x,
gdouble *y)
{
cairo_matrix_t matrix;
gdouble _x = 0.0;
gdouble _y = 0.0;
g_return_if_fail (PIKA_IS_TEXT_LAYOUT (layout));
if (x) _x = *x;
if (y) _y = *y;
pika_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
pika_text_layout_untransform_distance (PikaTextLayout *layout,
gdouble *x,
gdouble *y)
{
cairo_matrix_t matrix;
gdouble _x = 0.0;
gdouble _y = 0.0;
g_return_if_fail (PIKA_IS_TEXT_LAYOUT (layout));
if (x) _x = *x;
if (y) _y = *y;
pika_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
pika_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 *
pika_text_layout_apply_tags (PikaTextLayout *layout,
const gchar *markup)
{
PikaText *text = layout->text;
gchar *result;
{
guchar r, g, b;
pika_rgb_get_uchar (&text->color, &r, &g, &b);
result = g_strdup_printf ("<span color=\"#%02x%02x%02x\">%s</span>",
r, g, b, markup);
}
/* Updating font 'locl' (if supported) with 'lang' feature tag */
if (text->language)
{
gchar *tmp = g_strdup_printf ("<span lang=\"%s\">%s</span>",
text->language,
result);
g_free (result);
result = tmp;
}
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
pika_text_layout_set_markup (PikaTextLayout *layout,
GError **error)
{
PikaText *text = layout->text;
gchar *open_tag = NULL;
gchar *content = NULL;
gchar *close_tag = NULL;
gchar *tagged;
gchar *markup;
if (text->markup)
{
if (! pika_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 = pika_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 PIKA 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 PIKA 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, PIKA_ERROR, PIKA_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
pika_text_layout_position (PikaTextLayout *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);
PikaTextDirection base_dir = layout->text->base_dir;
gint width;
pango_layout_get_pixel_size (layout->layout, &width, NULL);
if ((base_dir == PIKA_TEXT_DIRECTION_LTR && align == PANGO_ALIGN_RIGHT) ||
(base_dir == PIKA_TEXT_DIRECTION_RTL && align == PANGO_ALIGN_RIGHT) ||
(base_dir == PIKA_TEXT_DIRECTION_TTB_RTL && align == PANGO_ALIGN_RIGHT) ||
(base_dir == PIKA_TEXT_DIRECTION_TTB_RTL_UPRIGHT && align == PANGO_ALIGN_RIGHT) ||
(base_dir == PIKA_TEXT_DIRECTION_TTB_LTR && align == PANGO_ALIGN_LEFT) ||
(base_dir == PIKA_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 *
pika_text_get_font_options (PikaText *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 PIKA_TEXT_HINT_STYLE_NONE:
cairo_font_options_set_hint_style (options, CAIRO_HINT_STYLE_NONE);
break;
case PIKA_TEXT_HINT_STYLE_SLIGHT:
cairo_font_options_set_hint_style (options, CAIRO_HINT_STYLE_SLIGHT);
break;
case PIKA_TEXT_HINT_STYLE_MEDIUM:
cairo_font_options_set_hint_style (options, CAIRO_HINT_STYLE_MEDIUM);
break;
case PIKA_TEXT_HINT_STYLE_FULL:
cairo_font_options_set_hint_style (options, CAIRO_HINT_STYLE_FULL);
break;
}
return options;
}
static PangoContext *
pika_text_get_pango_context (PikaText *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 = pika_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 PIKA_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 PIKA_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 PIKA_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 PIKA_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 PIKA_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 PIKA_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;
}