From 2821dbab58a47cb47a27b0bd7b583c754d05129f Mon Sep 17 00:00:00 2001 From: Jehan Date: Wed, 13 Aug 2025 14:49:12 +0200 Subject: [PATCH] app: add generic support for less destructive transform tools. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit will make all transform tools run on a link layer cumulate their transform matrix on top of the previous transform steps. It means that as long as you don't edit the pixels another way (e.g. with a paint tool), all your transformations will apply as a single transformation. For instance it means that applying several transform tool steps on a monitored link layer will be less destructive than applying the exact same transformations on the exact same "normal" layer contents. Even scaling an image to 1x1 then back to a big size will work very fine! Note nevertheless the following limitations in current implementation: * The link layer with transformations will still show as a standard link layer. Nothing says it has transformation applied on it right now. * To drop transformations applied on a link layer, you have to discard the link info, then monitor the link again. A specific action in the contextual menu could be worth it. * This should work with all transform tools (scale, rotation, unified, perspective, 3D, even Warp…) but it won't work for the Flip tool, nor will it work for the Transform actions. I will need to implement GimpItem's rotate(), flip() and resize() methods next. * The layer mask would still be destructively transformed (I have not made any tests with layer masks yet, but this should be done next too). * I think that the "scaled-only" property is now meaningless. It is now being replaced by the presence of the GimpMatrix3. Nevertheless I have still not removed this property. * The load/save code has not been redone yet to include all these changes. The kind of caveats we'd have to know about (and which are not planned for change, because it's just how it is): * Any intermediate interpolation methods are dropped when cumulating transform steps. Only the last interpolation is stored. This is because anyway the interpolation is only there as the best algorithm where we visually see the less quality loss. Applying several transformations as a single matrix will always be visually better than applying several matrices (whatever the intermediate interpolation methods chosen). * This only works with the "Adjust" clipping method (basically no clipping) because 2 transform steps with clipping won't produce the same result as the multiplied matrix with clipping. It means that applying a transform with clipping will downgrade your link layer to being a normal layer. The only issue I have with this is how to best convey that clipping is a major setting setting here, which disables our less-destructive abilities. Right now, people will just have to "know" it. * Vector link layers in particular will have 2 levels of non/less-destructivity transforms. In particular any scaling (both through "Scale Image", "Scale Layers" and transform tools — since I added code to detect a matrix doing only scaling and optionally translation) done **first** will be completely non-destructive since we will simply reload the original vector source at the right dimensions. Any other kind of transforms will be appended through the cumulative matrix, as raster link layers. This also includes scaling done **after** other transforms, since we cannot easily move the scaling first (matrix multiplication is not commutative). This second level of scaling will therefore be *less* destructive, but still destructive. It is possible eventually to improve the whole thing if we add some day the ability to request loading a vector image with a transform matrix (it will then be up to each vector format plug-in to support this feature or not). Note: it could be argued that this whole implementation could in fact be moved over to base layers, since it would allow also less-destructivity when applying multiple transformations in a row. We would only merge results once we edit pixels more directly. But I think that it's not a bad idea to experiment with this new code in the link layer. Eventually I may likely move this to the parent GimpLayer if no blocking issues are found. --- app/core/gimpdrawable-transform.c | 38 +-- app/core/gimpdrawable-transform.h | 3 +- app/core/gimpdrawable.c | 12 +- app/core/gimpgrouplayer.c | 6 +- app/core/gimpitem.c | 2 +- app/core/gimpitem.h | 3 +- app/core/gimplayer.c | 16 +- app/core/gimplayer.h | 3 +- app/core/gimplinklayer.c | 407 ++++++++++++++++++++++++++---- app/core/gimplinklayer.h | 44 +++- app/core/gimplinklayerundo.c | 30 ++- app/core/gimplinklayerundo.h | 10 +- app/path/gimppath.c | 13 +- app/path/gimpvectorlayer.c | 8 +- app/tools/gimptransformtool.c | 2 +- 15 files changed, 487 insertions(+), 110 deletions(-) diff --git a/app/core/gimpdrawable-transform.c b/app/core/gimpdrawable-transform.c index 5d0295e2dc..e84ab2da0e 100644 --- a/app/core/gimpdrawable-transform.c +++ b/app/core/gimpdrawable-transform.c @@ -766,7 +766,7 @@ gimp_drawable_transform_affine (GimpDrawable *drawable, { result = gimp_drawable_transform_paste (drawable, new_buffer, profile, new_offset_x, new_offset_y, - new_layer); + new_layer, TRUE); g_object_unref (new_buffer); } } @@ -848,7 +848,7 @@ gimp_drawable_transform_flip (GimpDrawable *drawable, { result = gimp_drawable_transform_paste (drawable, new_buffer, profile, new_offset_x, new_offset_y, - new_layer); + new_layer, TRUE); g_object_unref (new_buffer); } } @@ -933,7 +933,7 @@ gimp_drawable_transform_rotate (GimpDrawable *drawable, { result = gimp_drawable_transform_paste (drawable, new_buffer, profile, new_offset_x, new_offset_y, - new_layer); + new_layer, TRUE); g_object_unref (new_buffer); } } @@ -1026,11 +1026,11 @@ gimp_drawable_transform_paste (GimpDrawable *drawable, GimpColorProfile *buffer_profile, gint offset_x, gint offset_y, - gboolean new_layer) + gboolean new_layer, + gboolean push_undo) { - GimpImage *image; - GimpLayer *layer = NULL; - const gchar *undo_desc = NULL; + GimpImage *image; + GimpLayer *layer = NULL; g_return_val_if_fail (GIMP_IS_DRAWABLE (drawable), NULL); g_return_val_if_fail (gimp_item_is_attached (GIMP_ITEM (drawable)), NULL); @@ -1039,14 +1039,19 @@ gimp_drawable_transform_paste (GimpDrawable *drawable, image = gimp_item_get_image (GIMP_ITEM (drawable)); - if (GIMP_IS_LAYER (drawable)) - undo_desc = C_("undo-type", "Transform Layer"); - else if (GIMP_IS_CHANNEL (drawable)) - undo_desc = C_("undo-type", "Transform Channel"); - else - return NULL; + if (push_undo) + { + const gchar *undo_desc = NULL; - gimp_image_undo_group_start (image, GIMP_UNDO_GROUP_EDIT_PASTE, undo_desc); + if (GIMP_IS_LAYER (drawable)) + undo_desc = C_("undo-type", "Transform Layer"); + else if (GIMP_IS_CHANNEL (drawable)) + undo_desc = C_("undo-type", "Transform Channel"); + else + return NULL; + + gimp_image_undo_group_start (image, GIMP_UNDO_GROUP_EDIT_PASTE, undo_desc); + } if (new_layer) { @@ -1066,13 +1071,14 @@ gimp_drawable_transform_paste (GimpDrawable *drawable, } else { - gimp_drawable_set_buffer_full (drawable, TRUE, NULL, + gimp_drawable_set_buffer_full (drawable, push_undo, NULL, buffer, GEGL_RECTANGLE (offset_x, offset_y, 0, 0), TRUE); } - gimp_image_undo_group_end (image); + if (push_undo) + gimp_image_undo_group_end (image); return drawable; } diff --git a/app/core/gimpdrawable-transform.h b/app/core/gimpdrawable-transform.h index 176e8b3130..612124cd81 100644 --- a/app/core/gimpdrawable-transform.h +++ b/app/core/gimpdrawable-transform.h @@ -87,4 +87,5 @@ GimpDrawable * gimp_drawable_transform_paste (GimpDrawable GimpColorProfile *buffer_profile, gint offset_x, gint offset_y, - gboolean new_layer); + gboolean new_layer, + gboolean push_undo); diff --git a/app/core/gimpdrawable.c b/app/core/gimpdrawable.c index 7d0e1abbb9..fbcc5a1a1f 100644 --- a/app/core/gimpdrawable.c +++ b/app/core/gimpdrawable.c @@ -142,7 +142,8 @@ static void gimp_drawable_transform (GimpItem *item, GimpTransformDirection direction, GimpInterpolationType interpolation_type, GimpTransformResize clip_result, - GimpProgress *progress); + GimpProgress *progress, + gboolean push_undo); static const guint8 * gimp_drawable_get_icc_profile (GimpColorManaged *managed, @@ -742,7 +743,7 @@ gimp_drawable_flip (GimpItem *item, if (buffer) { gimp_drawable_transform_paste (drawable, buffer, buffer_profile, - new_off_x, new_off_y, FALSE); + new_off_x, new_off_y, FALSE, TRUE); g_object_unref (buffer); } } @@ -774,7 +775,7 @@ gimp_drawable_rotate (GimpItem *item, if (buffer) { gimp_drawable_transform_paste (drawable, buffer, buffer_profile, - new_off_x, new_off_y, FALSE); + new_off_x, new_off_y, FALSE, TRUE); g_object_unref (buffer); } } @@ -786,7 +787,8 @@ gimp_drawable_transform (GimpItem *item, GimpTransformDirection direction, GimpInterpolationType interpolation_type, GimpTransformResize clip_result, - GimpProgress *progress) + GimpProgress *progress, + gboolean push_undo) { GimpDrawable *drawable = GIMP_DRAWABLE (item); GeglBuffer *buffer; @@ -809,7 +811,7 @@ gimp_drawable_transform (GimpItem *item, if (buffer) { gimp_drawable_transform_paste (drawable, buffer, buffer_profile, - new_off_x, new_off_y, FALSE); + new_off_x, new_off_y, FALSE, push_undo); g_object_unref (buffer); } } diff --git a/app/core/gimpgrouplayer.c b/app/core/gimpgrouplayer.c index 60660c097c..3fcf7e26ae 100644 --- a/app/core/gimpgrouplayer.c +++ b/app/core/gimpgrouplayer.c @@ -159,7 +159,8 @@ static void gimp_group_layer_transform (GimpLayer *layer, GimpTransformDirection direction, GimpInterpolationType interpolation_type, GimpTransformResize clip_result, - GimpProgress *progress); + GimpProgress *progress, + gboolean push_undo); static void gimp_group_layer_convert_type (GimpLayer *layer, GimpImage *dest_image, const Babl *new_format, @@ -1016,7 +1017,8 @@ gimp_group_layer_transform (GimpLayer *layer, GimpTransformDirection direction, GimpInterpolationType interpolation_type, GimpTransformResize clip_result, - GimpProgress *progress) + GimpProgress *progress, + gboolean push_undo) { GimpGroupLayer *group = GIMP_GROUP_LAYER (layer); GimpGroupLayerPrivate *private = GET_PRIVATE (layer); diff --git a/app/core/gimpitem.c b/app/core/gimpitem.c index 331e6a02bd..29f684005d 100644 --- a/app/core/gimpitem.c +++ b/app/core/gimpitem.c @@ -1777,7 +1777,7 @@ gimp_item_transform (GimpItem *item, g_object_freeze_notify (G_OBJECT (item)); item_class->transform (item, context, matrix, direction, interpolation, - clip_result, progress); + clip_result, progress, TRUE); g_object_thaw_notify (G_OBJECT (item)); diff --git a/app/core/gimpitem.h b/app/core/gimpitem.h index 53e89f6a4d..b35637b662 100644 --- a/app/core/gimpitem.h +++ b/app/core/gimpitem.h @@ -107,7 +107,8 @@ struct _GimpItemClass GimpTransformDirection direction, GimpInterpolationType interpolation_type, GimpTransformResize clip_result, - GimpProgress *progress); + GimpProgress *progress, + gboolean push_undo); GimpTransformResize (* get_clip) (GimpItem *item, GimpTransformResize clip_result); gboolean (* fill) (GimpItem *item, diff --git a/app/core/gimplayer.c b/app/core/gimplayer.c index 0fd1a8e198..678c34aaf9 100644 --- a/app/core/gimplayer.c +++ b/app/core/gimplayer.c @@ -169,7 +169,8 @@ static void gimp_layer_transform (GimpItem *item, GimpTransformDirection direction, GimpInterpolationType interpolation_type, GimpTransformResize clip_result, - GimpProgress *progress); + GimpProgress *progress, + gboolean push_undo); static void gimp_layer_to_selection (GimpItem *item, GimpChannelOps op, gboolean antialias, @@ -248,7 +249,8 @@ static void gimp_layer_real_transform (GimpLayer *layer, GimpTransformDirection direction, GimpInterpolationType interpolation_type, GimpTransformResize clip_result, - GimpProgress *progress); + GimpProgress *progress, + gboolean push_undo); static void gimp_layer_real_convert_type (GimpLayer *layer, GimpImage *dest_image, const Babl *new_format, @@ -1298,7 +1300,8 @@ gimp_layer_transform (GimpItem *item, GimpTransformDirection direction, GimpInterpolationType interpolation_type, GimpTransformResize clip_result, - GimpProgress *progress) + GimpProgress *progress, + gboolean push_undo) { GimpLayer *layer = GIMP_LAYER (item); GimpObjectQueue *queue = NULL; @@ -1328,7 +1331,7 @@ gimp_layer_transform (GimpItem *item, GIMP_LAYER_GET_CLASS (layer)->transform (layer, context, matrix, direction, interpolation_type, clip_result, - progress); + progress, push_undo); if (layer->mask) { @@ -1743,7 +1746,8 @@ gimp_layer_real_transform (GimpLayer *layer, GimpTransformDirection direction, GimpInterpolationType interpolation_type, GimpTransformResize clip_result, - GimpProgress *progress) + GimpProgress *progress, + gboolean push_undo) { if (! gimp_matrix3_is_simple (matrix) && ! gimp_drawable_has_alpha (GIMP_DRAWABLE (layer))) @@ -1753,7 +1757,7 @@ gimp_layer_real_transform (GimpLayer *layer, context, matrix, direction, interpolation_type, clip_result, - progress); + progress, push_undo); } static void diff --git a/app/core/gimplayer.h b/app/core/gimplayer.h index f02d6038bf..790814e41a 100644 --- a/app/core/gimplayer.h +++ b/app/core/gimplayer.h @@ -122,7 +122,8 @@ struct _GimpLayerClass GimpTransformDirection direction, GimpInterpolationType interpolation_type, GimpTransformResize clip_result, - GimpProgress *progress); + GimpProgress *progress, + gboolean push_undo); void (* convert_type) (GimpLayer *layer, GimpImage *dest_image, const Babl *new_format, diff --git a/app/core/gimplinklayer.c b/app/core/gimplinklayer.c index ebee4f7256..495db0115b 100644 --- a/app/core/gimplinklayer.c +++ b/app/core/gimplinklayer.c @@ -29,6 +29,7 @@ #include "libgimpbase/gimpbase.h" #include "libgimpcolor/gimpcolor.h" #include "libgimpconfig/gimpconfig.h" +#include "libgimpmath/gimpmath.h" #include "core-types.h" @@ -69,9 +70,19 @@ enum struct _GimpLinkLayerPrivate { - GimpLink *link; - gboolean scaled_only; - gboolean auto_rename; + GimpLink *link; + gboolean scaled_only; + gboolean auto_rename; + + GimpMatrix3 matrix; + gint offset_x; + gint offset_y; + GimpInterpolationType interpolation; + + /* A transient value only useful to know when to drop monitoring after + * a buffer update. + */ + gboolean keep_monitoring; }; static void gimp_link_layer_finalize (GObject *object); @@ -93,6 +104,11 @@ static gboolean gimp_link_layer_rename (GimpItem *item, const gchar *new_name, const gchar *undo_desc, GError **error); + +static void gimp_link_layer_translate (GimpItem *item, + gdouble offset_x, + gdouble offset_y, + gboolean push_undo); static void gimp_link_layer_scale (GimpItem *item, gint new_width, gint new_height, @@ -100,6 +116,14 @@ static void gimp_link_layer_scale (GimpItem *item, gint new_offset_y, GimpInterpolationType interpolation_type, GimpProgress *progress); +static void gimp_link_layer_transform (GimpItem *item, + GimpContext *context, + const GimpMatrix3 *matrix, + GimpTransformDirection direction, + GimpInterpolationType interpolation_type, + GimpTransformResize clip_result, + GimpProgress *progress, + gboolean push_undo); static void gimp_link_layer_set_buffer (GimpDrawable *drawable, gboolean push_undo, @@ -124,10 +148,18 @@ static void gimp_link_layer_convert_type (GimpLayer *layer, gboolean push_undo, GimpProgress *progress); -static gboolean gimp_link_layer_render (GimpLinkLayer *layer); +static gboolean gimp_link_layer_render_link (GimpLinkLayer *layer); static void gimp_link_layer_set_xcf_flags (GimpLinkLayer *layer, guint32 flags); +static gboolean + gimp_link_layer_is_scaling_matrix (GimpLinkLayer *layer, + const GimpMatrix3 *matrix, + gint *new_width, + gint *new_height, + gint *new_offset_x, + gint *new_offset_y); + G_DEFINE_TYPE_WITH_PRIVATE (GimpLinkLayer, gimp_link_layer, GIMP_TYPE_LAYER) @@ -157,7 +189,9 @@ gimp_link_layer_class_init (GimpLinkLayerClass *klass) item_class->duplicate = gimp_link_layer_duplicate; item_class->rename = gimp_link_layer_rename; + item_class->translate = gimp_link_layer_translate; item_class->scale = gimp_link_layer_scale; + item_class->transform = gimp_link_layer_transform; item_class->rename_desc = _("Rename Link Layer"); item_class->translate_desc = _("Move Link Layer"); @@ -196,8 +230,17 @@ gimp_link_layer_class_init (GimpLinkLayerClass *klass) static void gimp_link_layer_init (GimpLinkLayer *layer) { - layer->p = gimp_link_layer_get_instance_private (layer); - layer->p->link = NULL; + layer->p = gimp_link_layer_get_instance_private (layer); + layer->p->link = NULL; + gimp_matrix3_identity (&layer->p->matrix); + + layer->p->scaled_only = FALSE; + layer->p->auto_rename = FALSE; + layer->p->offset_x = 0; + layer->p->offset_y = 0; + layer->p->interpolation = GIMP_INTERPOLATION_NONE; + + layer->p->keep_monitoring = FALSE; } static void @@ -313,11 +356,18 @@ gimp_link_layer_duplicate (GimpItem *item, if (width != gimp_item_get_width (GIMP_ITEM (new_layer)) || height != gimp_item_get_height (GIMP_ITEM (new_layer))) { - GeglBuffer *new_buffer; + GeglBuffer *new_buffer; + GeglRectangle bounds; + + gimp_item_get_offset (GIMP_ITEM (new_layer), &bounds.x, &bounds.y); + bounds.width = 0; + bounds.height = 0; new_buffer = gegl_buffer_new (GEGL_RECTANGLE (0, 0, width, height), gimp_drawable_get_format (GIMP_DRAWABLE (new_layer))); - gimp_drawable_set_buffer (GIMP_DRAWABLE (new_layer), FALSE, NULL, new_buffer); + GIMP_DRAWABLE_CLASS (parent_class)->set_buffer (GIMP_DRAWABLE (new_layer), + FALSE, NULL, + new_buffer, &bounds); g_object_unref (new_buffer); } @@ -325,8 +375,14 @@ gimp_link_layer_duplicate (GimpItem *item, gimp_drawable_get_buffer (GIMP_DRAWABLE (new_layer)), NULL); } - new_layer->p->scaled_only = layer->p->scaled_only; - new_layer->p->auto_rename = layer->p->auto_rename; + new_layer->p->scaled_only = layer->p->scaled_only; + new_layer->p->auto_rename = layer->p->auto_rename; + new_layer->p->matrix = layer->p->matrix; + new_layer->p->offset_x = layer->p->offset_x; + new_layer->p->offset_y = layer->p->offset_y; + new_layer->p->interpolation = layer->p->interpolation; + + new_layer->p->keep_monitoring = FALSE; g_clear_object (&link); } @@ -350,6 +406,20 @@ gimp_link_layer_rename (GimpItem *item, return FALSE; } +static void +gimp_link_layer_translate (GimpItem *item, + gdouble offset_x, + gdouble offset_y, + gboolean push_undo) +{ + GimpLinkLayer *layer = GIMP_LINK_LAYER (item); + + if (! gimp_matrix3_is_identity (&layer->p->matrix)) + gimp_matrix3_translate (&layer->p->matrix, offset_x, offset_y); + + GIMP_ITEM_CLASS (parent_class)->translate (item, offset_x, offset_y, push_undo); +} + static void gimp_link_layer_scale (GimpItem *item, gint new_width, @@ -385,37 +455,70 @@ gimp_link_layer_scale (GimpItem *item, if (queue) gimp_object_queue_pop (queue); - if (gimp_link_is_vector (link_layer->p->link) && - gimp_link_is_monitored (link_layer->p->link)) + if (gimp_link_is_vector (link_layer->p->link) && + gimp_link_is_monitored (link_layer->p->link) && + gimp_matrix3_is_identity (&link_layer->p->matrix)) { /* Non-modified vector images are always recomputed from the * source file and therefore are always sharp. */ gimp_link_set_size (link_layer->p->link, new_width, new_height); - gimp_link_layer_render (link_layer); + gimp_item_set_offset (item, new_offset_x, new_offset_y); + gimp_link_layer_render_link (link_layer); + } + else if (! gimp_matrix3_is_identity (&link_layer->p->matrix)) + { + GimpMatrix3 matrix = link_layer->p->matrix; + gdouble x_scale_factor; + gdouble y_scale_factor; + gint offset_x; + gint offset_y; + + x_scale_factor = (gdouble) new_width / gimp_item_get_width (item); + y_scale_factor = (gdouble) new_height / gimp_item_get_height (item); + + gimp_matrix3_scale (&matrix, x_scale_factor, y_scale_factor); + + gimp_link_layer_set_transform (link_layer, &matrix, interpolation_type, TRUE); + + /* Unfortunately we can't know the proper translation offset to + * set before we replay the full matrix (the offsets may have been + * changed in previous transformations). + * To avoid re-loading the source image twice though, I don't call + * gimp_link_layer_set_transform() again but directly edit the + * matrix and set the offset. + */ + gimp_item_get_offset (item, &offset_x, &offset_y); + gimp_matrix3_translate (&link_layer->p->matrix, + new_offset_x - offset_x, + new_offset_y - offset_y); + gimp_item_set_offset (item, new_offset_x, new_offset_y); + gimp_drawable_update_all (GIMP_DRAWABLE (layer)); + } + else if (gimp_link_is_monitored (link_layer->p->link)) + { + GimpMatrix3 matrix = link_layer->p->matrix; + gdouble x_scale_factor; + gdouble y_scale_factor; + gint offset_x; + gint offset_y; + + x_scale_factor = (gdouble) new_width / gimp_item_get_width (item); + y_scale_factor = (gdouble) new_height / gimp_item_get_height (item); + + gimp_matrix3_scale (&matrix, x_scale_factor, y_scale_factor); + gimp_item_get_offset (item, &offset_x, &offset_y); + gimp_matrix3_translate (&matrix, + new_offset_x - offset_x, + new_offset_y - offset_y); + gimp_link_layer_set_transform (link_layer, &matrix, interpolation_type, TRUE); } else { - gboolean scaled_only = FALSE; - - if (gimp_link_is_monitored (link_layer->p->link) || link_layer->p->scaled_only) - { - /* Raster images whose only modification are previous scaling - * are scaled back from the source file. Though they are still - * considered "modified" after a scaling (unlike vector - * images) and therefore are demoted to work like normal - * layers, scaling is still special-cased for better image - * quality. - */ - gimp_link_layer_render (link_layer); - scaled_only = TRUE; - } - GIMP_ITEM_CLASS (parent_class)->scale (GIMP_ITEM (layer), new_width, new_height, new_offset_x, new_offset_y, interpolation_type, progress); - g_object_set (layer, "scaled-only", scaled_only, NULL); } if (layer->mask) @@ -432,6 +535,81 @@ gimp_link_layer_scale (GimpItem *item, g_clear_object (&queue); } +static void +gimp_link_layer_transform (GimpItem *item, + GimpContext *context, + const GimpMatrix3 *matrix, + GimpTransformDirection direction, + GimpInterpolationType interpolation_type, + GimpTransformResize clip_result, + GimpProgress *progress, + gboolean push_undo) +{ + GimpLinkLayer *layer = GIMP_LINK_LAYER (item); + gboolean keep_monitoring; + + if (gimp_matrix3_is_identity (matrix)) + return; + + if (gimp_link_is_vector (layer->p->link) && + gimp_link_is_monitored (layer->p->link) && + gimp_matrix3_is_identity (&layer->p->matrix)) + { + gint new_width; + gint new_height; + gint new_offset_x; + gint new_offset_y; + + if (gimp_link_layer_is_scaling_matrix (layer, matrix, + &new_width, &new_height, + &new_offset_x, &new_offset_y)) + { + /* Scaling when no other transformation has happened yet is + * special-case for vector links, because we can do even + * better by reloading from source. + */ + gimp_link_layer_scale (item, new_width, new_height, + new_offset_x, new_offset_y, + interpolation_type, progress); + return; + } + } + + /* Clipping would produce different results when applied on a single + * step of multiple transformations. So only "adjust" transformations + * are stored non-destructively. Any clip/crop transformation triggers + * a destructive transform. + * + * Note: we could also store non-destructively a single clipped + * transformation, but that would be harder to document and explain. + */ + keep_monitoring = (gimp_link_is_monitored (layer->p->link) && + clip_result == GIMP_TRANSFORM_RESIZE_ADJUST); + + if (keep_monitoring) + { + GimpMatrix3 left = *matrix; + GimpMatrix3 right = layer->p->matrix; + + if (direction == GIMP_TRANSFORM_BACKWARD) + gimp_matrix3_invert (&left); + + gimp_matrix3_mult (&left, &right); + gimp_link_layer_set_transform (layer, &right, interpolation_type, TRUE); + } + else + { + /* Reset the transformation matrix. */ + gimp_matrix3_identity (&layer->p->matrix); + + GIMP_ITEM_CLASS (parent_class)->transform (item, context, + matrix, direction, + interpolation_type, + clip_result, progress, + push_undo); + } +} + static void gimp_link_layer_set_buffer (GimpDrawable *drawable, gboolean push_undo, @@ -442,21 +620,23 @@ gimp_link_layer_set_buffer (GimpDrawable *drawable, GimpLinkLayer *layer = GIMP_LINK_LAYER (drawable); GimpImage *image = gimp_item_get_image (GIMP_ITEM (layer)); - if (push_undo && gimp_link_is_monitored (layer->p->link)) - gimp_image_undo_group_start (image, GIMP_UNDO_GROUP_DRAWABLE_MOD, - undo_desc); + if (push_undo) + { + gimp_image_undo_group_start (image, GIMP_UNDO_GROUP_DRAWABLE_MOD, undo_desc); + gimp_image_undo_push_link_layer (image, NULL, layer); + } GIMP_DRAWABLE_CLASS (parent_class)->set_buffer (drawable, - push_undo, undo_desc, + FALSE, undo_desc, buffer, bounds); - if (push_undo && gimp_link_is_monitored (layer->p->link)) + if (push_undo) { - gimp_image_undo_push_link_layer (image, NULL, layer); - - gimp_link_freeze (layer->p->link); - g_object_set (drawable, "scaled-only", FALSE, NULL); + if (layer->p->link && + gimp_link_is_monitored (layer->p->link) && + ! layer->p->keep_monitoring) + gimp_link_freeze (layer->p->link); gimp_image_undo_group_end (image); } @@ -527,7 +707,7 @@ gimp_link_layer_convert_type (GimpLayer *layer, if (push_undo) gimp_image_undo_push_link_layer (image, NULL, link_layer); - gimp_link_layer_render (link_layer); + gimp_link_layer_render_link (link_layer); } } @@ -579,6 +759,20 @@ gboolean gimp_link_layer_set_link (GimpLinkLayer *layer, GimpLink *link, gboolean push_undo) +{ + return gimp_link_layer_set_link_with_matrix (layer, link, NULL, + GIMP_INTERPOLATION_NONE, + 0, 0, push_undo); +} + +gboolean +gimp_link_layer_set_link_with_matrix (GimpLinkLayer *layer, + GimpLink *link, + GimpMatrix3 *matrix, + GimpInterpolationType interpolation_type, + gint offset_x, + gint offset_y, + gboolean push_undo) { gboolean rendered = FALSE; @@ -592,7 +786,7 @@ gimp_link_layer_set_link (GimpLinkLayer *layer, if (layer->p->link) { g_signal_handlers_disconnect_by_func (layer->p->link, - G_CALLBACK (gimp_link_layer_render), + G_CALLBACK (gimp_link_layer_render_link), layer); } @@ -602,11 +796,25 @@ gimp_link_layer_set_link (GimpLinkLayer *layer, if (link) { g_signal_connect_object (link, "changed", - G_CALLBACK (gimp_link_layer_render), + G_CALLBACK (gimp_link_layer_render_link), layer, G_CONNECT_SWAPPED); if (gimp_link_is_monitored (link)) - rendered = gimp_link_layer_render (layer); + { + if (matrix == NULL) + { + rendered = gimp_link_layer_render_link (layer); + } + else + { + if (! gimp_matrix3_is_identity (matrix)) + { + layer->p->offset_x = offset_x; + layer->p->offset_y = offset_y; + } + rendered = gimp_link_layer_set_transform (layer, matrix, interpolation_type, push_undo); + } + } } g_object_notify (G_OBJECT (layer), "link"); @@ -649,7 +857,7 @@ gimp_link_layer_monitor (GimpLinkLayer *layer) _("Monitor Link"), layer); gimp_link_thaw (layer->p->link); - gimp_link_layer_render (layer); + gimp_link_layer_render_link (layer); } gboolean @@ -661,6 +869,69 @@ gimp_link_layer_is_monitored (GimpLinkLayer *layer) gimp_link_is_monitored (GIMP_LINK_LAYER (layer)->p->link)); } +gboolean +gimp_link_layer_get_transform (GimpLinkLayer *layer, + GimpMatrix3 *matrix, + gint *offset_x, + gint *offset_y, + GimpInterpolationType *interpolation) +{ + g_return_val_if_fail (GIMP_IS_LINK_LAYER (layer), FALSE); + + *matrix = layer->p->matrix; + *offset_x = layer->p->offset_x; + *offset_y = layer->p->offset_y; + *interpolation = layer->p->interpolation; + + return ! gimp_matrix3_is_identity (matrix); +} + +gboolean +gimp_link_layer_set_transform (GimpLinkLayer *layer, + GimpMatrix3 *matrix, + GimpInterpolationType interpolation_type, + gboolean push_undo) +{ + Gimp *gimp; + gboolean rendered; + + g_return_val_if_fail (GIMP_IS_LINK_LAYER (layer), FALSE); + g_return_val_if_fail (gimp_link_is_monitored (layer->p->link), FALSE); + + gimp = (gimp_item_get_image (GIMP_ITEM (layer)))->gimp; + + if (gimp_matrix3_is_identity (&layer->p->matrix)) + /* First transformation: store the initial offset. */ + gimp_item_get_offset (GIMP_ITEM (layer), &layer->p->offset_x, &layer->p->offset_y); + + gimp_item_set_offset (GIMP_ITEM (layer), layer->p->offset_x, layer->p->offset_y); + + rendered = gimp_link_layer_render_link (layer); + if (! gimp_matrix3_is_identity (matrix)) + { + layer->p->keep_monitoring = TRUE; + GIMP_ITEM_CLASS (parent_class)->transform (GIMP_ITEM (layer), + gimp_get_user_context (gimp), + matrix, + GIMP_TRANSFORM_FORWARD, + interpolation_type, + GIMP_TRANSFORM_RESIZE_ADJUST, + NULL, push_undo); + layer->p->keep_monitoring = FALSE; + } + + layer->p->matrix = *matrix; + + /* Interpolations are used to obtain reasonable quality. Considering + * that doing a single transform will always be better than several + * transforms in a row, it's OK to just drop the interpolation + * algorithm of previous transforms. We just store the last one. + */ + layer->p->interpolation = interpolation_type; + + return rendered; +} + guint32 gimp_link_layer_get_xcf_flags (GimpLinkLayer *link_layer) { @@ -738,7 +1009,7 @@ gimp_link_layer_from_layer (GimpLayer **layer, /* private functions */ static gboolean -gimp_link_layer_render (GimpLinkLayer *layer) +gimp_link_layer_render_link (GimpLinkLayer *layer) { GimpDrawable *drawable; GimpItem *item; @@ -773,11 +1044,19 @@ gimp_link_layer_render (GimpLinkLayer *layer) if ((width != gimp_item_get_width (item) || height != gimp_item_get_height (item))) { - GeglBuffer *new_buffer; + GeglBuffer *new_buffer; + GeglRectangle bounds; + + gimp_item_get_offset (GIMP_ITEM (drawable), + &bounds.x, &bounds.y); + bounds.width = 0; + bounds.height = 0; new_buffer = gegl_buffer_new (GEGL_RECTANGLE (0, 0, width, height), gimp_drawable_get_format (drawable)); - gimp_drawable_set_buffer (drawable, FALSE, NULL, new_buffer); + GIMP_DRAWABLE_CLASS (parent_class)->set_buffer (drawable, + FALSE, NULL, + new_buffer, &bounds); g_object_unref (new_buffer); if (gimp_layer_get_mask (GIMP_LAYER (layer))) @@ -849,3 +1128,39 @@ gimp_link_layer_set_xcf_flags (GimpLinkLayer *layer, else gimp_link_thaw (layer->p->link); } + +static gboolean +gimp_link_layer_is_scaling_matrix (GimpLinkLayer *layer, + const GimpMatrix3 *matrix, + gint *new_width, + gint *new_height, + gint *new_offset_x, + gint *new_offset_y) +{ + gboolean is_scaling; + + /* Scaling 3x3 matrix on a 2D plane (with optional translation). */ + is_scaling = (matrix->coeff[0][0] > 0.0 && matrix->coeff[0][1] == 0.0 && + matrix->coeff[1][0] == 0.0 && matrix->coeff[1][1] > 0.0 && + matrix->coeff[2][0] == 0.0 && matrix->coeff[2][1] == 0.0 && matrix->coeff[2][2] == 1.0); + + if (is_scaling) + { + gint width; + gint height; + gint offset_x; + gint offset_y; + + width = gimp_item_get_width (GIMP_ITEM (layer)); + height = gimp_item_get_height (GIMP_ITEM (layer)); + gimp_item_get_offset (GIMP_ITEM (layer), &offset_x, &offset_y); + + *new_width = (gint) (width * matrix->coeff[0][0]); + *new_height = (gint) (height * matrix->coeff[0][0]); + + *new_offset_x = (gint) (offset_x + matrix->coeff[0][2]); + *new_offset_y = (gint) (offset_y + matrix->coeff[1][2]); + } + + return is_scaling; +} diff --git a/app/core/gimplinklayer.h b/app/core/gimplinklayer.h index f643ed7087..886cd9f1c0 100644 --- a/app/core/gimplinklayer.h +++ b/app/core/gimplinklayer.h @@ -47,24 +47,40 @@ struct _GimpLinkLayerClass }; -GType gimp_link_layer_get_type (void) G_GNUC_CONST; +GType gimp_link_layer_get_type (void) G_GNUC_CONST; -GimpLayer * gimp_link_layer_new (GimpImage *image, - GimpLink *link); +GimpLayer * gimp_link_layer_new (GimpImage *image, + GimpLink *link); -GimpLink * gimp_link_layer_get_link (GimpLinkLayer *layer); -gboolean gimp_link_layer_set_link (GimpLinkLayer *layer, - GimpLink *link, - gboolean push_undo); +GimpLink * gimp_link_layer_get_link (GimpLinkLayer *layer); +gboolean gimp_link_layer_set_link (GimpLinkLayer *layer, + GimpLink *link, + gboolean push_undo); +gboolean gimp_link_layer_set_link_with_matrix (GimpLinkLayer *layer, + GimpLink *link, + GimpMatrix3 *matrix, + GimpInterpolationType interpolation_type, + gint offset_x, + gint offset_y, + gboolean push_undo); -void gimp_link_layer_discard (GimpLinkLayer *layer); -void gimp_link_layer_monitor (GimpLinkLayer *layer); -gboolean gimp_link_layer_is_monitored (GimpLinkLayer *layer); +void gimp_link_layer_discard (GimpLinkLayer *layer); +void gimp_link_layer_monitor (GimpLinkLayer *layer); +gboolean gimp_link_layer_is_monitored (GimpLinkLayer *layer); +gboolean gimp_link_layer_get_transform (GimpLinkLayer *layer, + GimpMatrix3 *matrix, + gint *offset_x, + gint *offset_y, + GimpInterpolationType *interpolation); +gboolean gimp_link_layer_set_transform (GimpLinkLayer *layer, + GimpMatrix3 *matrix, + GimpInterpolationType interpolation_type, + gboolean push_undo); /* Only to be used for XCF loading/saving. */ -guint32 gimp_link_layer_get_xcf_flags (GimpLinkLayer *layer); -void gimp_link_layer_from_layer (GimpLayer **layer, - GimpLink *link, - guint32 flags); +guint32 gimp_link_layer_get_xcf_flags (GimpLinkLayer *layer); +void gimp_link_layer_from_layer (GimpLayer **layer, + GimpLink *link, + guint32 flags); diff --git a/app/core/gimplinklayerundo.c b/app/core/gimplinklayerundo.c index fbe0fb87f0..b90ca80f49 100644 --- a/app/core/gimplinklayerundo.c +++ b/app/core/gimplinklayerundo.c @@ -108,6 +108,11 @@ gimp_link_layer_undo_constructed (GObject *object) link = gimp_link_layer_get_link (layer); undo->link = link ? gimp_link_duplicate (link) : NULL; + gimp_link_layer_get_transform (layer, + &undo->matrix, + &undo->offset_x, + &undo->offset_y, + &undo->interpolation); } static void @@ -181,18 +186,33 @@ gimp_link_layer_undo_pop (GimpUndo *undo, GimpUndoMode undo_mode, GimpUndoAccumulator *accum) { - GimpLinkLayerUndo *layer_undo = GIMP_LINK_LAYER_UNDO (undo); - GimpLinkLayer *layer = GIMP_LINK_LAYER (GIMP_ITEM_UNDO (undo)->item); - GimpLink *link; + GimpLinkLayerUndo *layer_undo = GIMP_LINK_LAYER_UNDO (undo); + GimpLinkLayer *layer = GIMP_LINK_LAYER (GIMP_ITEM_UNDO (undo)->item); + GimpLink *link; + GimpMatrix3 matrix; + gint offset_x; + gint offset_y; + GimpInterpolationType interpolation; GIMP_UNDO_CLASS (parent_class)->pop (undo, undo_mode, accum); link = gimp_link_layer_get_link (layer); link = link ? g_object_ref (link) : NULL; - gimp_link_layer_set_link (layer, layer_undo->link, FALSE); + gimp_link_layer_get_transform (layer, &matrix, &offset_x, &offset_y, &interpolation); + gimp_link_layer_set_link_with_matrix (layer, layer_undo->link, + &layer_undo->matrix, + layer_undo->interpolation, + layer_undo->offset_x, + layer_undo->offset_y, + FALSE); + + + layer_undo->matrix = matrix; + layer_undo->interpolation = interpolation; + layer_undo->offset_x = offset_x; + layer_undo->offset_y = offset_y; g_clear_object (&layer_undo->link); - layer_undo->link = link; } diff --git a/app/core/gimplinklayerundo.h b/app/core/gimplinklayerundo.h index ced5d63483..98fd536a03 100644 --- a/app/core/gimplinklayerundo.h +++ b/app/core/gimplinklayerundo.h @@ -35,14 +35,18 @@ typedef struct _GimpLinkLayerUndoClass GimpLinkLayerUndoClass; struct _GimpLinkLayerUndo { - GimpItemUndo parent_instance; + GimpItemUndo parent_instance; - GimpLink *link; + GimpLink *link; + GimpMatrix3 matrix; + gint offset_x; + gint offset_y; + GimpInterpolationType interpolation; }; struct _GimpLinkLayerUndoClass { - GimpItemUndoClass parent_class; + GimpItemUndoClass parent_class; }; diff --git a/app/path/gimppath.c b/app/path/gimppath.c index 2f2a498cc8..95adfe0c74 100644 --- a/app/path/gimppath.c +++ b/app/path/gimppath.c @@ -117,7 +117,8 @@ static void gimp_path_transform (GimpItem *item, GimpTransformDirection direction, GimpInterpolationType interp_type, GimpTransformResize clip_result, - GimpProgress *progress); + GimpProgress *progress, + gboolean push_undo); static GimpTransformResize gimp_path_get_clip (GimpItem *item, GimpTransformResize clip_result); @@ -611,7 +612,8 @@ gimp_path_transform (GimpItem *item, GimpTransformDirection direction, GimpInterpolationType interpolation_type, GimpTransformResize clip_result, - GimpProgress *progress) + GimpProgress *progress, + gboolean push_undo) { GimpPath *path = GIMP_PATH (item); GimpMatrix3 local_matrix; @@ -620,9 +622,10 @@ gimp_path_transform (GimpItem *item, gimp_path_freeze (path); - gimp_image_undo_push_path_mod (gimp_item_get_image (item), - _("Transform Path"), - path); + if (push_undo) + gimp_image_undo_push_path_mod (gimp_item_get_image (item), + _("Transform Path"), + path); local_matrix = *matrix; diff --git a/app/path/gimpvectorlayer.c b/app/path/gimpvectorlayer.c index 470f429154..a31d68232a 100644 --- a/app/path/gimpvectorlayer.c +++ b/app/path/gimpvectorlayer.c @@ -119,7 +119,8 @@ static void gimp_vector_layer_transform (GimpItem *ite GimpTransformDirection direction, GimpInterpolationType interp_type, GimpTransformResize clip_result, - GimpProgress *progress); + GimpProgress *progress, + gboolean push_undo); static gboolean gimp_vector_layer_render (GimpVectorLayer *layer); static void gimp_vector_layer_render_path (GimpVectorLayer *layer); @@ -497,7 +498,8 @@ gimp_vector_layer_transform (GimpItem *item, GimpTransformDirection direction, GimpInterpolationType interp_type, GimpTransformResize clip_result, - GimpProgress *progress) + GimpProgress *progress, + gboolean push_undo) { GimpVectorLayer *vector_layer = GIMP_VECTOR_LAYER (item); @@ -511,7 +513,7 @@ gimp_vector_layer_transform (GimpItem *item, { GIMP_ITEM_CLASS (parent_class)->transform (item, context, matrix, direction, interp_type, - clip_result, progress); + clip_result, progress, push_undo); } } diff --git a/app/tools/gimptransformtool.c b/app/tools/gimptransformtool.c index 704b038c2a..429c79642a 100644 --- a/app/tools/gimptransformtool.c +++ b/app/tools/gimptransformtool.c @@ -924,7 +924,7 @@ gimp_transform_tool_transform (GimpTransformTool *tr_tool, gimp_drawable_transform_paste (GIMP_DRAWABLE (selected_objects->data), new_buffer, buffer_profile, new_offset_x, new_offset_y, - new_layer); + new_layer, TRUE); g_object_unref (new_buffer); } break;