Gimp/app/text/gimptextlayer.c
Øyvind Kolås ee97c1f4ff app: correct usage of babl formats
GIMP was doing evil hacks lying to GEGL about it's pixels being in a linear
color space when they are not. This causes incorrect rendering, makes gaussian
blur misbehave etc.

The legacy projection modes should be implemented using the same 2.2 gamma
formats that are correct to specify for sRGB data. (for proper color
management in higher bitdepths; icc backend babl formats should be used.)

For the old image modes correct babl formats are:

R'G'B'A u8  -  8 bit RGB with 2.2 gamma (sRGB) with linear alpha component
R'G'B' u8   -  8 bit RGB with 2.2 gamma (sRGB)
Y'A u8      -  8 bit Grayscale with 2.2 gamma with linear alpha component
Y' u8       -  8 bit Grayscale with 2.2 gamma

Y u8        -  8 bit linear data, used for masks/channels
A u8        -  8 bit linear alpha

-----------------------------------------------

RGBA float  -     32bit floating point linear light RGB
RaGaBaA float  -  32bit floating point linear light RGB, premultiplied alpha
                  to be used for processing that needs to scale by the alpha,
                  (blurs, resampling etc)
R'G'B'A float  -  32bit floating point sRGB with gamma, to be used where
                  the result depends on being closer to perceptual when
                  processing, can be used a cheaper alternative to CIE Lab
                  based modes.

-----------------------------------------------

The legacy layer modes should use the formats with gamma 2.2 only for loading
and rendering legacy XCF files correctly, in the brave new world compositing
should most likely be done in linear light with "RGBA float" and even better
"RaGaBaA float" like GEGL does for porter duff and other compositing modes.

The ability to chose the legacy layer modes should probably be hidden from the
user unless an old .xcf has been opened.
2012-05-02 17:50:38 +02:00

727 lines
22 KiB
C

/* GIMP - The GNU Image Manipulation Program
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
*
* GimpTextLayer
* Copyright (C) 2002-2004 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 <http://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <string.h>
#include <cairo.h>
#include <gegl.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <pango/pangocairo.h>
#include "libgimpbase/gimpbase.h"
#include "libgimpcolor/gimpcolor.h"
#include "libgimpconfig/gimpconfig.h"
#include "text-types.h"
#include "gegl/gimp-gegl-utils.h"
#include "core/gimp.h"
#include "core/gimp-utils.h"
#include "core/gimpcontext.h"
#include "core/gimpcontainer.h"
#include "core/gimpimage.h"
#include "core/gimpimage-undo.h"
#include "core/gimpimage-undo-push.h"
#include "core/gimpitemtree.h"
#include "core/gimpparasitelist.h"
#include "gimptext.h"
#include "gimptextlayer.h"
#include "gimptextlayer-transform.h"
#include "gimptextlayout.h"
#include "gimptextlayout-render.h"
#include "gimp-intl.h"
enum
{
PROP_0,
PROP_TEXT,
PROP_AUTO_RENAME,
PROP_MODIFIED
};
static void gimp_text_layer_finalize (GObject *object);
static void gimp_text_layer_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec);
static void gimp_text_layer_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec);
static gint64 gimp_text_layer_get_memsize (GimpObject *object,
gint64 *gui_size);
static GimpItem * gimp_text_layer_duplicate (GimpItem *item,
GType new_type);
static gboolean gimp_text_layer_rename (GimpItem *item,
const gchar *new_name,
const gchar *undo_desc,
GError **error);
static void gimp_text_layer_set_buffer (GimpDrawable *drawable,
gboolean push_undo,
const gchar *undo_desc,
GeglBuffer *buffer,
gint offset_x,
gint offset_y);
static void gimp_text_layer_push_undo (GimpDrawable *drawable,
const gchar *undo_desc,
GeglBuffer *buffer,
gint x,
gint y,
gint width,
gint height);
static void gimp_text_layer_text_changed (GimpTextLayer *layer);
static gboolean gimp_text_layer_render (GimpTextLayer *layer);
static void gimp_text_layer_render_layout (GimpTextLayer *layer,
GimpTextLayout *layout);
G_DEFINE_TYPE (GimpTextLayer, gimp_text_layer, GIMP_TYPE_LAYER)
#define parent_class gimp_text_layer_parent_class
static void
gimp_text_layer_class_init (GimpTextLayerClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GimpObjectClass *gimp_object_class = GIMP_OBJECT_CLASS (klass);
GimpViewableClass *viewable_class = GIMP_VIEWABLE_CLASS (klass);
GimpItemClass *item_class = GIMP_ITEM_CLASS (klass);
GimpDrawableClass *drawable_class = GIMP_DRAWABLE_CLASS (klass);
object_class->finalize = gimp_text_layer_finalize;
object_class->get_property = gimp_text_layer_get_property;
object_class->set_property = gimp_text_layer_set_property;
gimp_object_class->get_memsize = gimp_text_layer_get_memsize;
viewable_class->default_stock_id = "gimp-text-layer";
item_class->duplicate = gimp_text_layer_duplicate;
item_class->rename = gimp_text_layer_rename;
#if 0
item_class->scale = gimp_text_layer_scale;
item_class->flip = gimp_text_layer_flip;
item_class->rotate = gimp_text_layer_rotate;
item_class->transform = gimp_text_layer_transform;
#endif
item_class->default_name = _("Text Layer");
item_class->rename_desc = _("Rename Text Layer");
item_class->translate_desc = _("Move Text Layer");
item_class->scale_desc = _("Scale Text Layer");
item_class->resize_desc = _("Resize Text Layer");
item_class->flip_desc = _("Flip Text Layer");
item_class->rotate_desc = _("Rotate Text Layer");
item_class->transform_desc = _("Transform Text Layer");
drawable_class->set_buffer = gimp_text_layer_set_buffer;
drawable_class->push_undo = gimp_text_layer_push_undo;
GIMP_CONFIG_INSTALL_PROP_OBJECT (object_class, PROP_TEXT,
"text", NULL,
GIMP_TYPE_TEXT,
GIMP_PARAM_STATIC_STRINGS);
GIMP_CONFIG_INSTALL_PROP_BOOLEAN (object_class, PROP_AUTO_RENAME,
"auto-rename", NULL,
TRUE,
GIMP_PARAM_STATIC_STRINGS);
GIMP_CONFIG_INSTALL_PROP_BOOLEAN (object_class, PROP_MODIFIED,
"modified", NULL,
FALSE,
GIMP_PARAM_STATIC_STRINGS);
}
static void
gimp_text_layer_init (GimpTextLayer *layer)
{
layer->text = NULL;
layer->text_parasite = NULL;
}
static void
gimp_text_layer_finalize (GObject *object)
{
GimpTextLayer *layer = GIMP_TEXT_LAYER (object);
if (layer->text)
{
g_object_unref (layer->text);
layer->text = NULL;
}
G_OBJECT_CLASS (parent_class)->finalize (object);
}
static void
gimp_text_layer_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
GimpTextLayer *text_layer = GIMP_TEXT_LAYER (object);
switch (property_id)
{
case PROP_TEXT:
g_value_set_object (value, text_layer->text);
break;
case PROP_AUTO_RENAME:
g_value_set_boolean (value, text_layer->auto_rename);
break;
case PROP_MODIFIED:
g_value_set_boolean (value, text_layer->modified);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
gimp_text_layer_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
GimpTextLayer *text_layer = GIMP_TEXT_LAYER (object);
switch (property_id)
{
case PROP_TEXT:
gimp_text_layer_set_text (text_layer, g_value_get_object (value));
break;
case PROP_AUTO_RENAME:
text_layer->auto_rename = g_value_get_boolean (value);
break;
case PROP_MODIFIED:
text_layer->modified = g_value_get_boolean (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static gint64
gimp_text_layer_get_memsize (GimpObject *object,
gint64 *gui_size)
{
GimpTextLayer *text_layer = GIMP_TEXT_LAYER (object);
gint64 memsize = 0;
memsize += gimp_object_get_memsize (GIMP_OBJECT (text_layer->text),
gui_size);
return memsize + GIMP_OBJECT_CLASS (parent_class)->get_memsize (object,
gui_size);
}
static GimpItem *
gimp_text_layer_duplicate (GimpItem *item,
GType new_type)
{
GimpItem *new_item;
g_return_val_if_fail (g_type_is_a (new_type, GIMP_TYPE_DRAWABLE), NULL);
new_item = GIMP_ITEM_CLASS (parent_class)->duplicate (item, new_type);
if (GIMP_IS_TEXT_LAYER (new_item))
{
GimpTextLayer *layer = GIMP_TEXT_LAYER (item);
GimpTextLayer *new_layer = GIMP_TEXT_LAYER (new_item);
gimp_config_sync (G_OBJECT (layer), G_OBJECT (new_layer), 0);
if (layer->text)
{
GimpText *text = gimp_config_duplicate (GIMP_CONFIG (layer->text));
gimp_text_layer_set_text (new_layer, text);
g_object_unref (text);
}
/* this is just the parasite name, not a pointer to the parasite */
if (layer->text_parasite)
new_layer->text_parasite = layer->text_parasite;
}
return new_item;
}
static gboolean
gimp_text_layer_rename (GimpItem *item,
const gchar *new_name,
const gchar *undo_desc,
GError **error)
{
if (GIMP_ITEM_CLASS (parent_class)->rename (item, new_name, undo_desc, error))
{
g_object_set (item, "auto-rename", FALSE, NULL);
return TRUE;
}
return FALSE;
}
static void
gimp_text_layer_set_buffer (GimpDrawable *drawable,
gboolean push_undo,
const gchar *undo_desc,
GeglBuffer *buffer,
gint offset_x,
gint offset_y)
{
GimpTextLayer *layer = GIMP_TEXT_LAYER (drawable);
GimpImage *image = gimp_item_get_image (GIMP_ITEM (layer));
if (push_undo && ! layer->modified)
gimp_image_undo_group_start (image, GIMP_UNDO_GROUP_DRAWABLE_MOD,
undo_desc);
GIMP_DRAWABLE_CLASS (parent_class)->set_buffer (drawable,
push_undo, undo_desc,
buffer,
offset_x, offset_y);
if (push_undo && ! layer->modified)
{
gimp_image_undo_push_text_layer_modified (image, NULL, layer);
g_object_set (drawable, "modified", TRUE, NULL);
gimp_image_undo_group_end (image);
}
}
static void
gimp_text_layer_push_undo (GimpDrawable *drawable,
const gchar *undo_desc,
GeglBuffer *buffer,
gint x,
gint y,
gint width,
gint height)
{
GimpTextLayer *layer = GIMP_TEXT_LAYER (drawable);
GimpImage *image = gimp_item_get_image (GIMP_ITEM (layer));
if (! layer->modified)
gimp_image_undo_group_start (image, GIMP_UNDO_GROUP_DRAWABLE, undo_desc);
GIMP_DRAWABLE_CLASS (parent_class)->push_undo (drawable, undo_desc,
buffer,
x, y, width, height);
if (! layer->modified)
{
gimp_image_undo_push_text_layer_modified (image, NULL, layer);
g_object_set (drawable, "modified", TRUE, NULL);
gimp_image_undo_group_end (image);
}
}
/* public functions */
/**
* gimp_text_layer_new:
* @image: the #GimpImage the layer should belong to
* @text: a #GimpText object
*
* Creates a new text layer.
*
* Return value: a new #GimpTextLayer or %NULL in case of a problem
**/
GimpLayer *
gimp_text_layer_new (GimpImage *image,
GimpText *text)
{
GimpTextLayer *layer;
GimpImageType type;
g_return_val_if_fail (GIMP_IS_IMAGE (image), NULL);
g_return_val_if_fail (GIMP_IS_TEXT (text), NULL);
if (! text->text && ! text->markup)
return NULL;
type = gimp_image_base_type_with_alpha (image);
layer = GIMP_TEXT_LAYER (gimp_drawable_new (GIMP_TYPE_TEXT_LAYER,
image, NULL,
0, 0, 1, 1,
gimp_image_get_format (image,
type)));
gimp_text_layer_set_text (layer, text);
if (! gimp_text_layer_render (layer))
{
g_object_unref (layer);
return NULL;
}
return GIMP_LAYER (layer);
}
void
gimp_text_layer_set_text (GimpTextLayer *layer,
GimpText *text)
{
g_return_if_fail (GIMP_IS_TEXT_LAYER (layer));
g_return_if_fail (text == NULL || GIMP_IS_TEXT (text));
if (layer->text == text)
return;
if (layer->text)
{
g_signal_handlers_disconnect_by_func (layer->text,
G_CALLBACK (gimp_text_layer_text_changed),
layer);
g_object_unref (layer->text);
layer->text = NULL;
}
if (text)
{
layer->text = g_object_ref (text);
g_signal_connect_object (text, "changed",
G_CALLBACK (gimp_text_layer_text_changed),
layer, G_CONNECT_SWAPPED);
}
g_object_notify (G_OBJECT (layer), "text");
gimp_viewable_invalidate_preview (GIMP_VIEWABLE (layer));
}
GimpText *
gimp_text_layer_get_text (GimpTextLayer *layer)
{
g_return_val_if_fail (GIMP_IS_TEXT_LAYER (layer), NULL);
return layer->text;
}
void
gimp_text_layer_set (GimpTextLayer *layer,
const gchar *undo_desc,
const gchar *first_property_name,
...)
{
GimpImage *image;
GimpText *text;
va_list var_args;
g_return_if_fail (gimp_item_is_text_layer (GIMP_ITEM (layer)));
g_return_if_fail (gimp_item_is_attached (GIMP_ITEM (layer)));
text = gimp_text_layer_get_text (layer);
if (! text)
return;
image = gimp_item_get_image (GIMP_ITEM (layer));
gimp_image_undo_group_start (image, GIMP_UNDO_GROUP_TEXT, undo_desc);
g_object_freeze_notify (G_OBJECT (layer));
if (layer->modified)
{
gimp_image_undo_push_text_layer_modified (image, NULL, layer);
/* pass copy_tiles = TRUE so we not only ref the tiles; after
* being a text layer again, undo doesn't care about the
* layer's pixels any longer because they are generated, so
* changing the text would happily overwrite the layer's
* pixels, changing the pixels on the undo stack too without
* any chance to ever undo again.
*/
gimp_image_undo_push_drawable_mod (image, NULL,
GIMP_DRAWABLE (layer), TRUE);
}
gimp_image_undo_push_text_layer (image, undo_desc, layer, NULL);
va_start (var_args, first_property_name);
g_object_set_valist (G_OBJECT (text), first_property_name, var_args);
va_end (var_args);
g_object_set (layer, "modified", FALSE, NULL);
g_object_thaw_notify (G_OBJECT (layer));
gimp_image_undo_group_end (image);
}
/**
* gimp_text_layer_discard:
* @layer: a #GimpTextLayer
*
* Discards the text information. This makes @layer behave like a
* normal layer.
*/
void
gimp_text_layer_discard (GimpTextLayer *layer)
{
g_return_if_fail (GIMP_IS_TEXT_LAYER (layer));
g_return_if_fail (gimp_item_is_attached (GIMP_ITEM (layer)));
if (! layer->text)
return;
gimp_image_undo_push_text_layer (gimp_item_get_image (GIMP_ITEM (layer)),
_("Discard Text Information"),
layer, NULL);
gimp_text_layer_set_text (layer, NULL);
}
gboolean
gimp_item_is_text_layer (GimpItem *item)
{
return (GIMP_IS_TEXT_LAYER (item) &&
GIMP_TEXT_LAYER (item)->text &&
GIMP_TEXT_LAYER (item)->modified == FALSE);
}
/* private functions */
static void
gimp_text_layer_text_changed (GimpTextLayer *layer)
{
/* If the text layer was created from a parasite, it's time to
* remove that parasite now.
*/
if (layer->text_parasite)
{
/* Don't push an undo because the parasite only exists temporarily
* while the text layer is loaded from XCF.
*/
gimp_item_parasite_detach (GIMP_ITEM (layer), layer->text_parasite,
FALSE);
layer->text_parasite = NULL;
}
gimp_text_layer_render (layer);
}
static gboolean
gimp_text_layer_render (GimpTextLayer *layer)
{
GimpDrawable *drawable;
GimpItem *item;
GimpImage *image;
GimpTextLayout *layout;
gdouble xres;
gdouble yres;
gint width;
gint height;
if (! layer->text)
return FALSE;
drawable = GIMP_DRAWABLE (layer);
item = GIMP_ITEM (layer);
image = gimp_item_get_image (item);
if (gimp_container_is_empty (image->gimp->fonts))
{
gimp_message_literal (image->gimp, NULL, GIMP_MESSAGE_ERROR,
_("Due to lack of any fonts, "
"text functionality is not available."));
return FALSE;
}
gimp_image_get_resolution (image, &xres, &yres);
layout = gimp_text_layout_new (layer->text, xres, yres);
g_object_freeze_notify (G_OBJECT (drawable));
if (gimp_text_layout_get_size (layout, &width, &height) &&
(width != gimp_item_get_width (item) ||
height != gimp_item_get_height (item)))
{
GeglBuffer *new_buffer;
new_buffer = gimp_gegl_buffer_new (GIMP_GEGL_RECT (0, 0, width, height),
gimp_drawable_get_format (drawable));
gimp_drawable_set_buffer (drawable, FALSE, NULL, new_buffer);
g_object_unref (new_buffer);
if (gimp_layer_get_mask (GIMP_LAYER (layer)))
{
GimpLayerMask *mask = gimp_layer_get_mask (GIMP_LAYER (layer));
static GimpContext *unused_eek = NULL;
if (! unused_eek)
unused_eek = gimp_context_new (image->gimp, "eek", NULL);
gimp_item_resize (GIMP_ITEM (mask), unused_eek, width, height, 0, 0);
}
}
if (layer->auto_rename)
{
GimpItem *item = GIMP_ITEM (layer);
gchar *name = NULL;
if (layer->text->text)
{
name = gimp_utf8_strtrim (layer->text->text, 30);
}
else if (layer->text->markup)
{
gchar *tmp = gimp_markup_extract_text (layer->text->markup);
name = gimp_utf8_strtrim (tmp, 30);
g_free (tmp);
}
if (! name)
name = g_strdup (_("Empty Text Layer"));
if (gimp_item_is_attached (item))
{
gimp_item_tree_rename_item (gimp_item_get_tree (item), item,
name, FALSE, NULL);
g_free (name);
}
else
{
gimp_object_take_name (GIMP_OBJECT (layer), name);
}
}
gimp_text_layer_render_layout (layer, layout);
g_object_unref (layout);
g_object_thaw_notify (G_OBJECT (drawable));
return (width > 0 && height > 0);
}
static void
gimp_text_layer_render_layout (GimpTextLayer *layer,
GimpTextLayout *layout)
{
GimpDrawable *drawable = GIMP_DRAWABLE (layer);
GimpItem *item = GIMP_ITEM (layer);
GeglBuffer *buffer;
const Babl *format;
const Babl *fish;
GeglBufferIterator *iter;
cairo_t *cr;
cairo_surface_t *surface;
const guchar *data;
gint bytes;
gint rowstride;
gint width;
gint height;
g_return_if_fail (gimp_drawable_has_alpha (drawable));
width = gimp_item_get_width (item);
height = gimp_item_get_height (item);
surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
cr = cairo_create (surface);
gimp_text_layout_render (layout, cr, layer->text->base_dir, FALSE);
cairo_destroy (cr);
cairo_surface_flush (surface);
data = cairo_image_surface_get_data (surface);
rowstride = cairo_image_surface_get_stride (surface);
buffer = gimp_drawable_get_buffer (drawable);
format = gimp_drawable_get_format (drawable);
fish = babl_fish (babl_format ("R'G'B'A u8"), format);
bytes = babl_format_get_bytes_per_pixel (format);
iter = gegl_buffer_iterator_new (buffer, NULL, 0, format,
GEGL_BUFFER_WRITE, GEGL_ABYSS_NONE);
while (gegl_buffer_iterator_next (iter))
{
const guchar *src = data + iter->roi[0].y * rowstride + iter->roi[0].x * 4;
guchar *dest = iter->data[0];
gint rows = iter->roi[0].height;
while (rows--)
{
const guchar *s = src;
guchar *d = dest;
gint w = iter->roi[0].width;
while (w--)
{
guchar color[4];
GIMP_CAIRO_ARGB32_GET_PIXEL (s,
color[0],
color[1],
color[2],
color[3]);
babl_process (fish, color, d, 1);
s += 4;
d += bytes;
}
src += rowstride;
dest += iter->roi[0].width * bytes;
}
}
cairo_surface_destroy (surface);
gimp_drawable_update (drawable, 0, 0, width, height);
}