2013-06-07 11:18:00 -06:00
|
|
|
/*
|
|
|
|
* Copyright 2012, Red Hat, Inc.
|
|
|
|
* Copyright 2012, Soren Sandmann
|
|
|
|
*
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a
|
|
|
|
* copy of this software and associated documentation files (the "Software"),
|
|
|
|
* to deal in the Software without restriction, including without limitation
|
|
|
|
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
|
|
* and/or sell copies of the Software, and to permit persons to whom the
|
|
|
|
* Software is furnished to do so, subject to the following conditions:
|
|
|
|
*
|
|
|
|
* The above copyright notice and this permission notice (including the next
|
|
|
|
* paragraph) shall be included in all copies or substantial portions of the
|
|
|
|
* Software.
|
|
|
|
*
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
|
|
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
|
|
* DEALINGS IN THE SOFTWARE.
|
|
|
|
*
|
|
|
|
* Author: Soren Sandmann <soren.sandmann@gmail.com>
|
|
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
|
|
#include "config.h"
|
|
|
|
#endif
|
|
|
|
#include <math.h>
|
|
|
|
#include <gtk/gtk.h>
|
|
|
|
#include <pixman.h>
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include "gtk-utils.h"
|
|
|
|
|
|
|
|
typedef struct
|
|
|
|
{
|
|
|
|
GtkBuilder * builder;
|
|
|
|
pixman_image_t * original;
|
|
|
|
GtkAdjustment * scale_x_adjustment;
|
|
|
|
GtkAdjustment * scale_y_adjustment;
|
|
|
|
GtkAdjustment * rotate_adjustment;
|
|
|
|
GtkAdjustment * subsample_adjustment;
|
|
|
|
int scaled_width;
|
|
|
|
int scaled_height;
|
|
|
|
} app_t;
|
|
|
|
|
|
|
|
static GtkWidget *
|
|
|
|
get_widget (app_t *app, const char *name)
|
|
|
|
{
|
|
|
|
GtkWidget *widget = GTK_WIDGET (gtk_builder_get_object (app->builder, name));
|
|
|
|
|
|
|
|
if (!widget)
|
|
|
|
g_error ("Widget %s not found\n", name);
|
|
|
|
|
|
|
|
return widget;
|
|
|
|
}
|
|
|
|
|
2019-01-18 07:34:01 -07:00
|
|
|
/* Figure out the boundary of a diameter=1 circle transformed into an ellipse
|
|
|
|
* by trans. Proof that this is the correct calculation:
|
|
|
|
*
|
|
|
|
* Transform x,y to u,v by this matrix calculation:
|
|
|
|
*
|
|
|
|
* |u| |a c| |x|
|
|
|
|
* |v| = |b d|*|y|
|
|
|
|
*
|
|
|
|
* Horizontal component:
|
|
|
|
*
|
|
|
|
* u = ax+cy (1)
|
|
|
|
*
|
|
|
|
* For each x,y on a radius-1 circle (p is angle to the point):
|
|
|
|
*
|
|
|
|
* x^2+y^2 = 1
|
|
|
|
* x = cos(p)
|
|
|
|
* y = sin(p)
|
|
|
|
* dx/dp = -sin(p) = -y
|
|
|
|
* dy/dp = cos(p) = x
|
|
|
|
*
|
|
|
|
* Figure out derivative of (1) relative to p:
|
|
|
|
*
|
|
|
|
* du/dp = a(dx/dp) + c(dy/dp)
|
|
|
|
* = -ay + cx
|
|
|
|
*
|
|
|
|
* The min and max u are when du/dp is zero:
|
|
|
|
*
|
|
|
|
* -ay + cx = 0
|
|
|
|
* cx = ay
|
|
|
|
* c = ay/x (2)
|
|
|
|
* y = cx/a (3)
|
|
|
|
*
|
|
|
|
* Substitute (2) into (1) and simplify:
|
|
|
|
*
|
|
|
|
* u = ax + ay^2/x
|
|
|
|
* = a(x^2+y^2)/x
|
|
|
|
* = a/x (because x^2+y^2 = 1)
|
|
|
|
* x = a/u (4)
|
|
|
|
*
|
|
|
|
* Substitute (4) into (3) and simplify:
|
|
|
|
*
|
|
|
|
* y = c(a/u)/a
|
|
|
|
* y = c/u (5)
|
|
|
|
*
|
|
|
|
* Square (4) and (5) and add:
|
|
|
|
*
|
|
|
|
* x^2+y^2 = (a^2+c^2)/u^2
|
|
|
|
*
|
|
|
|
* But x^2+y^2 is 1:
|
|
|
|
*
|
|
|
|
* 1 = (a^2+c^2)/u^2
|
|
|
|
* u^2 = a^2+c^2
|
|
|
|
* u = hypot(a,c)
|
|
|
|
*
|
|
|
|
* Similarily the max/min of v is at:
|
|
|
|
*
|
|
|
|
* v = hypot(b,d)
|
|
|
|
*
|
|
|
|
*/
|
2013-06-07 11:18:00 -06:00
|
|
|
static void
|
|
|
|
compute_extents (pixman_f_transform_t *trans, double *sx, double *sy)
|
|
|
|
{
|
2019-01-18 07:34:01 -07:00
|
|
|
*sx = hypot (trans->m[0][0], trans->m[0][1]) / trans->m[2][2];
|
|
|
|
*sy = hypot (trans->m[1][0], trans->m[1][1]) / trans->m[2][2];
|
2013-06-07 11:18:00 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
typedef struct
|
|
|
|
{
|
2013-12-01 13:34:20 -07:00
|
|
|
char name [20];
|
|
|
|
int value;
|
2013-06-07 11:18:00 -06:00
|
|
|
} named_int_t;
|
|
|
|
|
|
|
|
static const named_int_t filters[] =
|
|
|
|
{
|
|
|
|
{ "Box", PIXMAN_KERNEL_BOX },
|
|
|
|
{ "Impulse", PIXMAN_KERNEL_IMPULSE },
|
|
|
|
{ "Linear", PIXMAN_KERNEL_LINEAR },
|
|
|
|
{ "Cubic", PIXMAN_KERNEL_CUBIC },
|
|
|
|
{ "Lanczos2", PIXMAN_KERNEL_LANCZOS2 },
|
|
|
|
{ "Lanczos3", PIXMAN_KERNEL_LANCZOS3 },
|
|
|
|
{ "Lanczos3 Stretched", PIXMAN_KERNEL_LANCZOS3_STRETCHED },
|
|
|
|
{ "Gaussian", PIXMAN_KERNEL_GAUSSIAN },
|
|
|
|
};
|
|
|
|
|
|
|
|
static const named_int_t repeats[] =
|
|
|
|
{
|
|
|
|
{ "None", PIXMAN_REPEAT_NONE },
|
|
|
|
{ "Normal", PIXMAN_REPEAT_NORMAL },
|
|
|
|
{ "Reflect", PIXMAN_REPEAT_REFLECT },
|
|
|
|
{ "Pad", PIXMAN_REPEAT_PAD },
|
|
|
|
};
|
|
|
|
|
2013-12-01 13:34:20 -07:00
|
|
|
static int
|
2013-06-07 11:18:00 -06:00
|
|
|
get_value (app_t *app, const named_int_t table[], const char *box_name)
|
|
|
|
{
|
|
|
|
GtkComboBox *box = GTK_COMBO_BOX (get_widget (app, box_name));
|
|
|
|
|
|
|
|
return table[gtk_combo_box_get_active (box)].value;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
copy_to_counterpart (app_t *app, GObject *object)
|
|
|
|
{
|
|
|
|
static const char *xy_map[] =
|
|
|
|
{
|
|
|
|
"reconstruct_x_combo_box", "reconstruct_y_combo_box",
|
|
|
|
"sample_x_combo_box", "sample_y_combo_box",
|
|
|
|
"scale_x_adjustment", "scale_y_adjustment",
|
|
|
|
};
|
|
|
|
GObject *counterpart = NULL;
|
|
|
|
int i;
|
|
|
|
|
|
|
|
for (i = 0; i < G_N_ELEMENTS (xy_map); i += 2)
|
|
|
|
{
|
|
|
|
GObject *x = gtk_builder_get_object (app->builder, xy_map[i]);
|
|
|
|
GObject *y = gtk_builder_get_object (app->builder, xy_map[i + 1]);
|
|
|
|
|
|
|
|
if (object == x)
|
|
|
|
counterpart = y;
|
|
|
|
if (object == y)
|
|
|
|
counterpart = x;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!counterpart)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (GTK_IS_COMBO_BOX (counterpart))
|
|
|
|
{
|
|
|
|
gtk_combo_box_set_active (
|
|
|
|
GTK_COMBO_BOX (counterpart),
|
|
|
|
gtk_combo_box_get_active (
|
|
|
|
GTK_COMBO_BOX (object)));
|
|
|
|
}
|
|
|
|
else if (GTK_IS_ADJUSTMENT (counterpart))
|
|
|
|
{
|
|
|
|
gtk_adjustment_set_value (
|
|
|
|
GTK_ADJUSTMENT (counterpart),
|
|
|
|
gtk_adjustment_get_value (
|
|
|
|
GTK_ADJUSTMENT (object)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static double
|
|
|
|
to_scale (double v)
|
|
|
|
{
|
|
|
|
return pow (1.15, v);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
rescale (GtkWidget *may_be_null, app_t *app)
|
|
|
|
{
|
|
|
|
pixman_f_transform_t ftransform;
|
|
|
|
pixman_transform_t transform;
|
|
|
|
double new_width, new_height;
|
|
|
|
double fscale_x, fscale_y;
|
|
|
|
double rotation;
|
|
|
|
pixman_fixed_t *params;
|
|
|
|
int n_params;
|
|
|
|
double sx, sy;
|
|
|
|
|
|
|
|
pixman_f_transform_init_identity (&ftransform);
|
|
|
|
|
|
|
|
if (may_be_null && gtk_toggle_button_get_active (
|
|
|
|
GTK_TOGGLE_BUTTON (get_widget (app, "lock_checkbutton"))))
|
|
|
|
{
|
|
|
|
copy_to_counterpart (app, G_OBJECT (may_be_null));
|
|
|
|
}
|
|
|
|
|
|
|
|
fscale_x = gtk_adjustment_get_value (app->scale_x_adjustment);
|
|
|
|
fscale_y = gtk_adjustment_get_value (app->scale_y_adjustment);
|
|
|
|
rotation = gtk_adjustment_get_value (app->rotate_adjustment);
|
|
|
|
|
|
|
|
fscale_x = to_scale (fscale_x);
|
|
|
|
fscale_y = to_scale (fscale_y);
|
|
|
|
|
|
|
|
new_width = pixman_image_get_width (app->original) * fscale_x;
|
|
|
|
new_height = pixman_image_get_height (app->original) * fscale_y;
|
|
|
|
|
|
|
|
pixman_f_transform_scale (&ftransform, NULL, fscale_x, fscale_y);
|
|
|
|
|
|
|
|
pixman_f_transform_translate (&ftransform, NULL, - new_width / 2.0, - new_height / 2.0);
|
|
|
|
|
|
|
|
rotation = (rotation / 360.0) * 2 * M_PI;
|
|
|
|
pixman_f_transform_rotate (&ftransform, NULL, cos (rotation), sin (rotation));
|
|
|
|
|
|
|
|
pixman_f_transform_translate (&ftransform, NULL, new_width / 2.0, new_height / 2.0);
|
|
|
|
|
|
|
|
pixman_f_transform_invert (&ftransform, &ftransform);
|
|
|
|
|
|
|
|
compute_extents (&ftransform, &sx, &sy);
|
|
|
|
|
|
|
|
pixman_transform_from_pixman_f_transform (&transform, &ftransform);
|
|
|
|
pixman_image_set_transform (app->original, &transform);
|
|
|
|
|
|
|
|
params = pixman_filter_create_separable_convolution (
|
|
|
|
&n_params,
|
|
|
|
sx * 65536.0 + 0.5,
|
|
|
|
sy * 65536.0 + 0.5,
|
|
|
|
get_value (app, filters, "reconstruct_x_combo_box"),
|
|
|
|
get_value (app, filters, "reconstruct_y_combo_box"),
|
|
|
|
get_value (app, filters, "sample_x_combo_box"),
|
|
|
|
get_value (app, filters, "sample_y_combo_box"),
|
|
|
|
gtk_adjustment_get_value (app->subsample_adjustment),
|
|
|
|
gtk_adjustment_get_value (app->subsample_adjustment));
|
|
|
|
|
|
|
|
pixman_image_set_filter (app->original, PIXMAN_FILTER_SEPARABLE_CONVOLUTION, params, n_params);
|
|
|
|
|
|
|
|
pixman_image_set_repeat (
|
|
|
|
app->original, get_value (app, repeats, "repeat_combo_box"));
|
|
|
|
|
|
|
|
free (params);
|
|
|
|
|
|
|
|
app->scaled_width = ceil (new_width);
|
|
|
|
app->scaled_height = ceil (new_height);
|
|
|
|
|
|
|
|
gtk_widget_set_size_request (
|
|
|
|
get_widget (app, "drawing_area"), new_width + 0.5, new_height + 0.5);
|
|
|
|
|
|
|
|
gtk_widget_queue_draw (
|
|
|
|
get_widget (app, "drawing_area"));
|
|
|
|
}
|
|
|
|
|
|
|
|
static gboolean
|
|
|
|
on_expose (GtkWidget *da, GdkEvent *event, gpointer data)
|
|
|
|
{
|
|
|
|
app_t *app = data;
|
|
|
|
GdkRectangle *area = &event->expose.area;
|
|
|
|
cairo_surface_t *surface;
|
|
|
|
pixman_image_t *tmp;
|
|
|
|
cairo_t *cr;
|
|
|
|
uint32_t *pixels;
|
|
|
|
|
|
|
|
pixels = calloc (1, area->width * area->height * 4);
|
|
|
|
tmp = pixman_image_create_bits (
|
|
|
|
PIXMAN_a8r8g8b8, area->width, area->height, pixels, area->width * 4);
|
|
|
|
|
|
|
|
if (area->x < app->scaled_width && area->y < app->scaled_height)
|
|
|
|
{
|
|
|
|
pixman_image_composite (
|
|
|
|
PIXMAN_OP_SRC,
|
|
|
|
app->original, NULL, tmp,
|
|
|
|
area->x, area->y, 0, 0, 0, 0,
|
|
|
|
app->scaled_width - area->x, app->scaled_height - area->y);
|
|
|
|
}
|
|
|
|
|
|
|
|
surface = cairo_image_surface_create_for_data (
|
|
|
|
(uint8_t *)pixels, CAIRO_FORMAT_ARGB32,
|
|
|
|
area->width, area->height, area->width * 4);
|
|
|
|
|
|
|
|
cr = gdk_cairo_create (da->window);
|
|
|
|
|
|
|
|
cairo_set_source_surface (cr, surface, area->x, area->y);
|
|
|
|
|
|
|
|
cairo_paint (cr);
|
|
|
|
|
|
|
|
cairo_destroy (cr);
|
|
|
|
cairo_surface_destroy (surface);
|
|
|
|
free (pixels);
|
|
|
|
pixman_image_unref (tmp);
|
|
|
|
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
set_up_combo_box (app_t *app, const char *box_name,
|
|
|
|
int n_entries, const named_int_t table[])
|
|
|
|
{
|
|
|
|
GtkWidget *widget = get_widget (app, box_name);
|
|
|
|
GtkListStore *model;
|
|
|
|
GtkCellRenderer *cell;
|
|
|
|
int i;
|
|
|
|
|
|
|
|
model = gtk_list_store_new (1, G_TYPE_STRING);
|
|
|
|
|
|
|
|
cell = gtk_cell_renderer_text_new ();
|
|
|
|
gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (widget), cell, TRUE);
|
|
|
|
gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (widget), cell,
|
|
|
|
"text", 0,
|
|
|
|
NULL);
|
|
|
|
|
|
|
|
gtk_combo_box_set_model (GTK_COMBO_BOX (widget), GTK_TREE_MODEL (model));
|
|
|
|
|
|
|
|
for (i = 0; i < n_entries; ++i)
|
|
|
|
{
|
|
|
|
const named_int_t *info = &(table[i]);
|
|
|
|
GtkTreeIter iter;
|
|
|
|
|
|
|
|
gtk_list_store_append (model, &iter);
|
|
|
|
gtk_list_store_set (model, &iter, 0, info->name, -1);
|
|
|
|
}
|
|
|
|
|
|
|
|
gtk_combo_box_set_active (GTK_COMBO_BOX (widget), 0);
|
|
|
|
|
|
|
|
g_signal_connect (widget, "changed", G_CALLBACK (rescale), app);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
set_up_filter_box (app_t *app, const char *box_name)
|
|
|
|
{
|
|
|
|
set_up_combo_box (app, box_name, G_N_ELEMENTS (filters), filters);
|
|
|
|
}
|
|
|
|
|
|
|
|
static char *
|
|
|
|
format_value (GtkWidget *widget, double value)
|
|
|
|
{
|
|
|
|
return g_strdup_printf ("%.4f", to_scale (value));
|
|
|
|
}
|
|
|
|
|
|
|
|
static app_t *
|
|
|
|
app_new (pixman_image_t *original)
|
|
|
|
{
|
|
|
|
GtkWidget *widget;
|
|
|
|
app_t *app = g_malloc (sizeof *app);
|
|
|
|
GError *err = NULL;
|
|
|
|
|
|
|
|
app->builder = gtk_builder_new ();
|
|
|
|
app->original = original;
|
|
|
|
|
|
|
|
if (!gtk_builder_add_from_file (app->builder, "scale.ui", &err))
|
|
|
|
g_error ("Could not read file scale.ui: %s", err->message);
|
|
|
|
|
|
|
|
app->scale_x_adjustment =
|
|
|
|
GTK_ADJUSTMENT (gtk_builder_get_object (app->builder, "scale_x_adjustment"));
|
|
|
|
app->scale_y_adjustment =
|
|
|
|
GTK_ADJUSTMENT (gtk_builder_get_object (app->builder, "scale_y_adjustment"));
|
|
|
|
app->rotate_adjustment =
|
|
|
|
GTK_ADJUSTMENT (gtk_builder_get_object (app->builder, "rotate_adjustment"));
|
|
|
|
app->subsample_adjustment =
|
|
|
|
GTK_ADJUSTMENT (gtk_builder_get_object (app->builder, "subsample_adjustment"));
|
|
|
|
|
|
|
|
g_signal_connect (app->scale_x_adjustment, "value_changed", G_CALLBACK (rescale), app);
|
|
|
|
g_signal_connect (app->scale_y_adjustment, "value_changed", G_CALLBACK (rescale), app);
|
|
|
|
g_signal_connect (app->rotate_adjustment, "value_changed", G_CALLBACK (rescale), app);
|
|
|
|
g_signal_connect (app->subsample_adjustment, "value_changed", G_CALLBACK (rescale), app);
|
|
|
|
|
|
|
|
widget = get_widget (app, "scale_x_scale");
|
|
|
|
gtk_scale_add_mark (GTK_SCALE (widget), 0.0, GTK_POS_LEFT, NULL);
|
|
|
|
g_signal_connect (widget, "format_value", G_CALLBACK (format_value), app);
|
|
|
|
widget = get_widget (app, "scale_y_scale");
|
|
|
|
gtk_scale_add_mark (GTK_SCALE (widget), 0.0, GTK_POS_LEFT, NULL);
|
|
|
|
g_signal_connect (widget, "format_value", G_CALLBACK (format_value), app);
|
|
|
|
widget = get_widget (app, "rotate_scale");
|
|
|
|
gtk_scale_add_mark (GTK_SCALE (widget), 0.0, GTK_POS_LEFT, NULL);
|
|
|
|
|
|
|
|
widget = get_widget (app, "drawing_area");
|
|
|
|
g_signal_connect (widget, "expose_event", G_CALLBACK (on_expose), app);
|
|
|
|
|
|
|
|
set_up_filter_box (app, "reconstruct_x_combo_box");
|
|
|
|
set_up_filter_box (app, "reconstruct_y_combo_box");
|
|
|
|
set_up_filter_box (app, "sample_x_combo_box");
|
|
|
|
set_up_filter_box (app, "sample_y_combo_box");
|
|
|
|
|
|
|
|
set_up_combo_box (
|
|
|
|
app, "repeat_combo_box", G_N_ELEMENTS (repeats), repeats);
|
|
|
|
|
|
|
|
g_signal_connect (
|
|
|
|
gtk_builder_get_object (app->builder, "lock_checkbutton"),
|
|
|
|
"toggled", G_CALLBACK (rescale), app);
|
|
|
|
|
|
|
|
rescale (NULL, app);
|
|
|
|
|
|
|
|
return app;
|
|
|
|
}
|
|
|
|
|
|
|
|
int
|
|
|
|
main (int argc, char **argv)
|
|
|
|
{
|
|
|
|
GtkWidget *window;
|
|
|
|
pixman_image_t *image;
|
|
|
|
app_t *app;
|
|
|
|
|
|
|
|
gtk_init (&argc, &argv);
|
|
|
|
|
|
|
|
if (argc < 2)
|
|
|
|
{
|
|
|
|
printf ("%s <image file>\n", argv[0]);
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!(image = pixman_image_from_file (argv[1], PIXMAN_a8r8g8b8)))
|
|
|
|
{
|
|
|
|
printf ("Could not load image \"%s\"\n", argv[1]);
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
app = app_new (image);
|
|
|
|
|
|
|
|
window = get_widget (app, "main");
|
|
|
|
|
|
|
|
g_signal_connect (window, "delete_event", G_CALLBACK (gtk_main_quit), NULL);
|
|
|
|
|
|
|
|
gtk_window_set_default_size (GTK_WINDOW (window), 1024, 768);
|
|
|
|
|
|
|
|
gtk_widget_show_all (window);
|
|
|
|
|
|
|
|
gtk_main ();
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|