gnome-control-center/panels/user-accounts/cc-crop-area.c
Ondrej Holy 33052abe9c user-accounts: change default size for avatar cropping
Change default size for avatar cropping to correspond with changes in cheese.
The default size should be based on maximal size of the image instead of
minimal size for cropping.

https://bugzilla.gnome.org/show_bug.cgi?id=739871
2014-11-25 15:20:39 +01:00

826 lines
28 KiB
C

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
*
* Copyright 2009 Red Hat, Inc,
*
* 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 2 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/>.
*
* Written by: Matthias Clasen <mclasen@redhat.com>
*/
#include "config.h"
#include <stdlib.h>
#include <glib.h>
#include <glib/gi18n.h>
#include <gtk/gtk.h>
#include "cc-crop-area.h"
struct _CcCropAreaPrivate {
GdkPixbuf *browse_pixbuf;
GdkPixbuf *pixbuf;
GdkPixbuf *color_shifted;
gdouble scale;
GdkRectangle image;
GdkCursorType current_cursor;
GdkRectangle crop;
gint active_region;
gint last_press_x;
gint last_press_y;
gint base_width;
gint base_height;
gdouble aspect;
};
G_DEFINE_TYPE (CcCropArea, cc_crop_area, GTK_TYPE_DRAWING_AREA);
static inline guchar
shift_color_byte (guchar b,
int shift)
{
return CLAMP(b + shift, 0, 255);
}
static void
shift_colors (GdkPixbuf *pixbuf,
gint red,
gint green,
gint blue,
gint alpha)
{
gint x, y, offset, y_offset, rowstride, width, height;
guchar *pixels;
gint channels;
width = gdk_pixbuf_get_width (pixbuf);
height = gdk_pixbuf_get_height (pixbuf);
rowstride = gdk_pixbuf_get_rowstride (pixbuf);
pixels = gdk_pixbuf_get_pixels (pixbuf);
channels = gdk_pixbuf_get_n_channels (pixbuf);
for (y = 0; y < height; y++) {
y_offset = y * rowstride;
for (x = 0; x < width; x++) {
offset = y_offset + x * channels;
if (red != 0)
pixels[offset] = shift_color_byte (pixels[offset], red);
if (green != 0)
pixels[offset + 1] = shift_color_byte (pixels[offset + 1], green);
if (blue != 0)
pixels[offset + 2] = shift_color_byte (pixels[offset + 2], blue);
if (alpha != 0 && channels >= 4)
pixels[offset + 3] = shift_color_byte (pixels[offset + 3], blue);
}
}
}
static void
update_pixbufs (CcCropArea *area)
{
gint width;
gint height;
GtkAllocation allocation;
gdouble scale;
GdkRGBA color;
guint32 pixel;
gint dest_x, dest_y, dest_width, dest_height;
GtkWidget *widget;
GtkStyleContext *context;
widget = GTK_WIDGET (area);
gtk_widget_get_allocation (widget, &allocation);
context = gtk_widget_get_style_context (widget);
if (area->priv->pixbuf == NULL ||
gdk_pixbuf_get_width (area->priv->pixbuf) != allocation.width ||
gdk_pixbuf_get_height (area->priv->pixbuf) != allocation.height) {
if (area->priv->pixbuf != NULL)
g_object_unref (area->priv->pixbuf);
area->priv->pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB,
gdk_pixbuf_get_has_alpha (area->priv->browse_pixbuf),
8,
allocation.width, allocation.height);
gtk_style_context_get_background_color (context, gtk_style_context_get_state (context), &color);
pixel = (((gint)(color.red * 1.0)) << 16) |
(((gint)(color.green * 1.0)) << 8) |
((gint)(color.blue * 1.0));
gdk_pixbuf_fill (area->priv->pixbuf, pixel);
width = gdk_pixbuf_get_width (area->priv->browse_pixbuf);
height = gdk_pixbuf_get_height (area->priv->browse_pixbuf);
scale = allocation.height / (gdouble)height;
if (scale * width > allocation.width)
scale = allocation.width / (gdouble)width;
dest_width = width * scale;
dest_height = height * scale;
dest_x = (allocation.width - dest_width) / 2;
dest_y = (allocation.height - dest_height) / 2,
gdk_pixbuf_scale (area->priv->browse_pixbuf,
area->priv->pixbuf,
dest_x, dest_y,
dest_width, dest_height,
dest_x, dest_y,
scale, scale,
GDK_INTERP_BILINEAR);
if (area->priv->color_shifted)
g_object_unref (area->priv->color_shifted);
area->priv->color_shifted = gdk_pixbuf_copy (area->priv->pixbuf);
shift_colors (area->priv->color_shifted, -32, -32, -32, 0);
if (area->priv->scale == 0.0) {
gdouble scale_to_80, scale_to_image, crop_scale;
/* Scale the crop rectangle to 80% of the area, or less to fit the image */
scale_to_80 = MIN ((gdouble)gdk_pixbuf_get_width (area->priv->pixbuf) * 0.8 / area->priv->base_width,
(gdouble)gdk_pixbuf_get_height (area->priv->pixbuf) * 0.8 / area->priv->base_height);
scale_to_image = MIN ((gdouble)dest_width / area->priv->base_width,
(gdouble)dest_height / area->priv->base_height);
crop_scale = MIN (scale_to_80, scale_to_image);
area->priv->crop.width = crop_scale * area->priv->base_width / scale;
area->priv->crop.height = crop_scale * area->priv->base_height / scale;
area->priv->crop.x = (gdk_pixbuf_get_width (area->priv->browse_pixbuf) - area->priv->crop.width) / 2;
area->priv->crop.y = (gdk_pixbuf_get_height (area->priv->browse_pixbuf) - area->priv->crop.height) / 2;
}
area->priv->scale = scale;
area->priv->image.x = dest_x;
area->priv->image.y = dest_y;
area->priv->image.width = dest_width;
area->priv->image.height = dest_height;
}
}
static void
crop_to_widget (CcCropArea *area,
GdkRectangle *crop)
{
crop->x = area->priv->image.x + area->priv->crop.x * area->priv->scale;
crop->y = area->priv->image.y + area->priv->crop.y * area->priv->scale;
crop->width = area->priv->crop.width * area->priv->scale;
crop->height = area->priv->crop.height * area->priv->scale;
}
typedef enum {
OUTSIDE,
INSIDE,
TOP,
TOP_LEFT,
TOP_RIGHT,
BOTTOM,
BOTTOM_LEFT,
BOTTOM_RIGHT,
LEFT,
RIGHT
} Location;
static gboolean
cc_crop_area_draw (GtkWidget *widget,
cairo_t *cr)
{
GdkRectangle crop;
gint width, height;
CcCropArea *uarea = CC_CROP_AREA (widget);
if (uarea->priv->browse_pixbuf == NULL)
return FALSE;
update_pixbufs (uarea);
width = gdk_pixbuf_get_width (uarea->priv->pixbuf);
height = gdk_pixbuf_get_height (uarea->priv->pixbuf);
crop_to_widget (uarea, &crop);
gdk_cairo_set_source_pixbuf (cr, uarea->priv->color_shifted, 0, 0);
cairo_rectangle (cr, 0, 0, width, crop.y);
cairo_rectangle (cr, 0, crop.y, crop.x, crop.height);
cairo_rectangle (cr, crop.x + crop.width, crop.y, width - crop.x - crop.width, crop.height);
cairo_rectangle (cr, 0, crop.y + crop.height, width, height - crop.y - crop.height);
cairo_fill (cr);
gdk_cairo_set_source_pixbuf (cr, uarea->priv->pixbuf, 0, 0);
cairo_rectangle (cr, crop.x, crop.y, crop.width, crop.height);
cairo_fill (cr);
if (uarea->priv->active_region != OUTSIDE) {
gint x1, x2, y1, y2;
cairo_set_source_rgb (cr, 1, 1, 1);
cairo_set_line_width (cr, 1.0);
x1 = crop.x + crop.width / 3.0;
x2 = crop.x + 2 * crop.width / 3.0;
y1 = crop.y + crop.height / 3.0;
y2 = crop.y + 2 * crop.height / 3.0;
cairo_move_to (cr, x1 + 0.5, crop.y);
cairo_line_to (cr, x1 + 0.5, crop.y + crop.height);
cairo_move_to (cr, x2 + 0.5, crop.y);
cairo_line_to (cr, x2 + 0.5, crop.y + crop.height);
cairo_move_to (cr, crop.x, y1 + 0.5);
cairo_line_to (cr, crop.x + crop.width, y1 + 0.5);
cairo_move_to (cr, crop.x, y2 + 0.5);
cairo_line_to (cr, crop.x + crop.width, y2 + 0.5);
cairo_stroke (cr);
}
cairo_set_source_rgb (cr, 0, 0, 0);
cairo_set_line_width (cr, 1.0);
cairo_rectangle (cr,
crop.x + 0.5,
crop.y + 0.5,
crop.width - 1.0,
crop.height - 1.0);
cairo_stroke (cr);
cairo_set_source_rgb (cr, 1, 1, 1);
cairo_set_line_width (cr, 2.0);
cairo_rectangle (cr,
crop.x + 2.0,
crop.y + 2.0,
crop.width - 4.0,
crop.height - 4.0);
cairo_stroke (cr);
return FALSE;
}
typedef enum {
BELOW,
LOWER,
BETWEEN,
UPPER,
ABOVE
} Range;
static Range
find_range (gint x,
gint min,
gint max)
{
gint tolerance = 12;
if (x < min - tolerance)
return BELOW;
if (x <= min + tolerance)
return LOWER;
if (x < max - tolerance)
return BETWEEN;
if (x <= max + tolerance)
return UPPER;
return ABOVE;
}
static Location
find_location (GdkRectangle *rect,
gint x,
gint y)
{
Range x_range, y_range;
Location location[5][5] = {
{ OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE },
{ OUTSIDE, TOP_LEFT, TOP, TOP_RIGHT, OUTSIDE },
{ OUTSIDE, LEFT, INSIDE, RIGHT, OUTSIDE },
{ OUTSIDE, BOTTOM_LEFT, BOTTOM, BOTTOM_RIGHT, OUTSIDE },
{ OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE }
};
x_range = find_range (x, rect->x, rect->x + rect->width);
y_range = find_range (y, rect->y, rect->y + rect->height);
return location[y_range][x_range];
}
static void
update_cursor (CcCropArea *area,
gint x,
gint y)
{
gint cursor_type;
GdkRectangle crop;
gint region;
region = area->priv->active_region;
if (region == OUTSIDE) {
crop_to_widget (area, &crop);
region = find_location (&crop, x, y);
}
switch (region) {
case OUTSIDE:
cursor_type = GDK_LEFT_PTR;
break;
case TOP_LEFT:
cursor_type = GDK_TOP_LEFT_CORNER;
break;
case TOP:
cursor_type = GDK_TOP_SIDE;
break;
case TOP_RIGHT:
cursor_type = GDK_TOP_RIGHT_CORNER;
break;
case LEFT:
cursor_type = GDK_LEFT_SIDE;
break;
case INSIDE:
cursor_type = GDK_FLEUR;
break;
case RIGHT:
cursor_type = GDK_RIGHT_SIDE;
break;
case BOTTOM_LEFT:
cursor_type = GDK_BOTTOM_LEFT_CORNER;
break;
case BOTTOM:
cursor_type = GDK_BOTTOM_SIDE;
break;
case BOTTOM_RIGHT:
cursor_type = GDK_BOTTOM_RIGHT_CORNER;
break;
default:
g_assert_not_reached ();
}
if (cursor_type != area->priv->current_cursor) {
GdkCursor *cursor = gdk_cursor_new (cursor_type);
gdk_window_set_cursor (gtk_widget_get_window (GTK_WIDGET (area)), cursor);
g_object_unref (cursor);
area->priv->current_cursor = cursor_type;
}
}
static int
eval_radial_line (gdouble center_x, gdouble center_y,
gdouble bounds_x, gdouble bounds_y,
gdouble user_x)
{
gdouble decision_slope;
gdouble decision_intercept;
decision_slope = (bounds_y - center_y) / (bounds_x - center_x);
decision_intercept = -(decision_slope * bounds_x);
return (int) (decision_slope * user_x + decision_intercept);
}
static gboolean
cc_crop_area_motion_notify_event (GtkWidget *widget,
GdkEventMotion *event)
{
CcCropArea *area = CC_CROP_AREA (widget);
gint x, y;
gint delta_x, delta_y;
gint width, height;
gint adj_width, adj_height;
gint pb_width, pb_height;
GdkRectangle damage;
gint left, right, top, bottom;
gdouble new_width, new_height;
gdouble center_x, center_y;
gint min_width, min_height;
if (area->priv->browse_pixbuf == NULL)
return FALSE;
update_cursor (area, event->x, event->y);
crop_to_widget (area, &damage);
gtk_widget_queue_draw_area (widget,
damage.x - 1, damage.y - 1,
damage.width + 2, damage.height + 2);
pb_width = gdk_pixbuf_get_width (area->priv->browse_pixbuf);
pb_height = gdk_pixbuf_get_height (area->priv->browse_pixbuf);
x = (event->x - area->priv->image.x) / area->priv->scale;
y = (event->y - area->priv->image.y) / area->priv->scale;
delta_x = x - area->priv->last_press_x;
delta_y = y - area->priv->last_press_y;
area->priv->last_press_x = x;
area->priv->last_press_y = y;
left = area->priv->crop.x;
right = area->priv->crop.x + area->priv->crop.width - 1;
top = area->priv->crop.y;
bottom = area->priv->crop.y + area->priv->crop.height - 1;
center_x = (left + right) / 2.0;
center_y = (top + bottom) / 2.0;
switch (area->priv->active_region) {
case INSIDE:
width = right - left + 1;
height = bottom - top + 1;
left += delta_x;
right += delta_x;
top += delta_y;
bottom += delta_y;
if (left < 0)
left = 0;
if (top < 0)
top = 0;
if (right > pb_width)
right = pb_width;
if (bottom > pb_height)
bottom = pb_height;
adj_width = right - left + 1;
adj_height = bottom - top + 1;
if (adj_width != width) {
if (delta_x < 0)
right = left + width - 1;
else
left = right - width + 1;
}
if (adj_height != height) {
if (delta_y < 0)
bottom = top + height - 1;
else
top = bottom - height + 1;
}
break;
case TOP_LEFT:
if (area->priv->aspect < 0) {
top = y;
left = x;
}
else if (y < eval_radial_line (center_x, center_y, left, top, x)) {
top = y;
new_width = (bottom - top) * area->priv->aspect;
left = right - new_width;
}
else {
left = x;
new_height = (right - left) / area->priv->aspect;
top = bottom - new_height;
}
break;
case TOP:
top = y;
if (area->priv->aspect > 0) {
new_width = (bottom - top) * area->priv->aspect;
right = left + new_width;
}
break;
case TOP_RIGHT:
if (area->priv->aspect < 0) {
top = y;
right = x;
}
else if (y < eval_radial_line (center_x, center_y, right, top, x)) {
top = y;
new_width = (bottom - top) * area->priv->aspect;
right = left + new_width;
}
else {
right = x;
new_height = (right - left) / area->priv->aspect;
top = bottom - new_height;
}
break;
case LEFT:
left = x;
if (area->priv->aspect > 0) {
new_height = (right - left) / area->priv->aspect;
bottom = top + new_height;
}
break;
case BOTTOM_LEFT:
if (area->priv->aspect < 0) {
bottom = y;
left = x;
}
else if (y < eval_radial_line (center_x, center_y, left, bottom, x)) {
left = x;
new_height = (right - left) / area->priv->aspect;
bottom = top + new_height;
}
else {
bottom = y;
new_width = (bottom - top) * area->priv->aspect;
left = right - new_width;
}
break;
case RIGHT:
right = x;
if (area->priv->aspect > 0) {
new_height = (right - left) / area->priv->aspect;
bottom = top + new_height;
}
break;
case BOTTOM_RIGHT:
if (area->priv->aspect < 0) {
bottom = y;
right = x;
}
else if (y < eval_radial_line (center_x, center_y, right, bottom, x)) {
right = x;
new_height = (right - left) / area->priv->aspect;
bottom = top + new_height;
}
else {
bottom = y;
new_width = (bottom - top) * area->priv->aspect;
right = left + new_width;
}
break;
case BOTTOM:
bottom = y;
if (area->priv->aspect > 0) {
new_width = (bottom - top) * area->priv->aspect;
right= left + new_width;
}
break;
default:
return FALSE;
}
min_width = area->priv->base_width / area->priv->scale;
min_height = area->priv->base_height / area->priv->scale;
width = right - left + 1;
height = bottom - top + 1;
if (area->priv->aspect < 0) {
if (left < 0)
left = 0;
if (top < 0)
top = 0;
if (right > pb_width)
right = pb_width;
if (bottom > pb_height)
bottom = pb_height;
width = right - left + 1;
height = bottom - top + 1;
switch (area->priv->active_region) {
case LEFT:
case TOP_LEFT:
case BOTTOM_LEFT:
if (width < min_width)
left = right - min_width;
break;
case RIGHT:
case TOP_RIGHT:
case BOTTOM_RIGHT:
if (width < min_width)
right = left + min_width;
break;
default: ;
}
switch (area->priv->active_region) {
case TOP:
case TOP_LEFT:
case TOP_RIGHT:
if (height < min_height)
top = bottom - min_height;
break;
case BOTTOM:
case BOTTOM_LEFT:
case BOTTOM_RIGHT:
if (height < min_height)
bottom = top + min_height;
break;
default: ;
}
}
else {
if (left < 0 || top < 0 ||
right > pb_width || bottom > pb_height ||
width < min_width || height < min_height) {
left = area->priv->crop.x;
right = area->priv->crop.x + area->priv->crop.width - 1;
top = area->priv->crop.y;
bottom = area->priv->crop.y + area->priv->crop.height - 1;
}
}
area->priv->crop.x = left;
area->priv->crop.y = top;
area->priv->crop.width = right - left + 1;
area->priv->crop.height = bottom - top + 1;
crop_to_widget (area, &damage);
gtk_widget_queue_draw_area (widget,
damage.x - 1, damage.y - 1,
damage.width + 2, damage.height + 2);
return FALSE;
}
static gboolean
cc_crop_area_button_press_event (GtkWidget *widget,
GdkEventButton *event)
{
CcCropArea *area = CC_CROP_AREA (widget);
GdkRectangle crop;
if (area->priv->browse_pixbuf == NULL)
return FALSE;
crop_to_widget (area, &crop);
area->priv->last_press_x = (event->x - area->priv->image.x) / area->priv->scale;
area->priv->last_press_y = (event->y - area->priv->image.y) / area->priv->scale;
area->priv->active_region = find_location (&crop, event->x, event->y);
gtk_widget_queue_draw_area (widget,
crop.x - 1, crop.y - 1,
crop.width + 2, crop.height + 2);
return FALSE;
}
static gboolean
cc_crop_area_button_release_event (GtkWidget *widget,
GdkEventButton *event)
{
CcCropArea *area = CC_CROP_AREA (widget);
GdkRectangle crop;
if (area->priv->browse_pixbuf == NULL)
return FALSE;
crop_to_widget (area, &crop);
area->priv->last_press_x = -1;
area->priv->last_press_y = -1;
area->priv->active_region = OUTSIDE;
gtk_widget_queue_draw_area (widget,
crop.x - 1, crop.y - 1,
crop.width + 2, crop.height + 2);
return FALSE;
}
static void
cc_crop_area_finalize (GObject *object)
{
CcCropArea *area = CC_CROP_AREA (object);
if (area->priv->browse_pixbuf) {
g_object_unref (area->priv->browse_pixbuf);
area->priv->browse_pixbuf = NULL;
}
if (area->priv->pixbuf) {
g_object_unref (area->priv->pixbuf);
area->priv->pixbuf = NULL;
}
if (area->priv->color_shifted) {
g_object_unref (area->priv->color_shifted);
area->priv->color_shifted = NULL;
}
}
static void
cc_crop_area_class_init (CcCropAreaClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
object_class->finalize = cc_crop_area_finalize;
widget_class->draw = cc_crop_area_draw;
widget_class->button_press_event = cc_crop_area_button_press_event;
widget_class->button_release_event = cc_crop_area_button_release_event;
widget_class->motion_notify_event = cc_crop_area_motion_notify_event;
g_type_class_add_private (klass, sizeof (CcCropAreaPrivate));
}
static void
cc_crop_area_init (CcCropArea *area)
{
area->priv = (G_TYPE_INSTANCE_GET_PRIVATE ((area), CC_TYPE_CROP_AREA,
CcCropAreaPrivate));
gtk_widget_add_events (GTK_WIDGET (area), GDK_POINTER_MOTION_MASK |
GDK_BUTTON_PRESS_MASK |
GDK_BUTTON_RELEASE_MASK);
area->priv->scale = 0.0;
area->priv->image.x = 0;
area->priv->image.y = 0;
area->priv->image.width = 0;
area->priv->image.height = 0;
area->priv->active_region = OUTSIDE;
area->priv->base_width = 48;
area->priv->base_height = 48;
area->priv->aspect = 1;
}
GtkWidget *
cc_crop_area_new (void)
{
return g_object_new (CC_TYPE_CROP_AREA, NULL);
}
GdkPixbuf *
cc_crop_area_get_picture (CcCropArea *area)
{
gint width, height;
width = gdk_pixbuf_get_width (area->priv->browse_pixbuf);
height = gdk_pixbuf_get_height (area->priv->browse_pixbuf);
width = MIN (area->priv->crop.width, width - area->priv->crop.x);
height = MIN (area->priv->crop.height, height - area->priv->crop.y);
return gdk_pixbuf_new_subpixbuf (area->priv->browse_pixbuf,
area->priv->crop.x,
area->priv->crop.y,
width, height);
}
void
cc_crop_area_set_picture (CcCropArea *area,
GdkPixbuf *pixbuf)
{
int width;
int height;
if (area->priv->browse_pixbuf) {
g_object_unref (area->priv->browse_pixbuf);
area->priv->browse_pixbuf = NULL;
}
if (pixbuf) {
area->priv->browse_pixbuf = g_object_ref (pixbuf);
width = gdk_pixbuf_get_width (pixbuf);
height = gdk_pixbuf_get_height (pixbuf);
} else {
width = 0;
height = 0;
}
area->priv->crop.width = 2 * area->priv->base_width;
area->priv->crop.height = 2 * area->priv->base_height;
area->priv->crop.x = (width - area->priv->crop.width) / 2;
area->priv->crop.y = (height - area->priv->crop.height) / 2;
area->priv->scale = 0.0;
area->priv->image.x = 0;
area->priv->image.y = 0;
area->priv->image.width = 0;
area->priv->image.height = 0;
gtk_widget_queue_draw (GTK_WIDGET (area));
}
void
cc_crop_area_set_min_size (CcCropArea *area,
gint width,
gint height)
{
area->priv->base_width = width;
area->priv->base_height = height;
if (area->priv->aspect > 0) {
area->priv->aspect = area->priv->base_width / (gdouble)area->priv->base_height;
}
}
void
cc_crop_area_set_constrain_aspect (CcCropArea *area,
gboolean constrain)
{
if (constrain) {
area->priv->aspect = area->priv->base_width / (gdouble)area->priv->base_height;
}
else {
area->priv->aspect = -1;
}
}