scale.c revision 14b11b2b
1/*
2 * Copyright 2012, Red Hat, Inc.
3 * Copyright 2012, Soren Sandmann
4 *
5 * Permission is hereby granted, free of charge, to any person obtaining a
6 * copy of this software and associated documentation files (the "Software"),
7 * to deal in the Software without restriction, including without limitation
8 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
9 * and/or sell copies of the Software, and to permit persons to whom the
10 * Software is furnished to do so, subject to the following conditions:
11 *
12 * The above copyright notice and this permission notice (including the next
13 * paragraph) shall be included in all copies or substantial portions of the
14 * Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
19 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 * DEALINGS IN THE SOFTWARE.
23 *
24 * Author: Soren Sandmann <soren.sandmann@gmail.com>
25 */
26#ifdef HAVE_CONFIG_H
27#include "pixman-config.h"
28#endif
29#include <math.h>
30#include <gtk/gtk.h>
31#include <pixman.h>
32#include <stdlib.h>
33#include "gtk-utils.h"
34
35typedef struct
36{
37    GtkBuilder *        builder;
38    pixman_image_t *	original;
39    GtkAdjustment *     scale_x_adjustment;
40    GtkAdjustment *     scale_y_adjustment;
41    GtkAdjustment *     rotate_adjustment;
42    GtkAdjustment *	subsample_adjustment;
43    int                 scaled_width;
44    int                 scaled_height;
45} app_t;
46
47static GtkWidget *
48get_widget (app_t *app, const char *name)
49{
50    GtkWidget *widget = GTK_WIDGET (gtk_builder_get_object (app->builder, name));
51
52    if (!widget)
53        g_error ("Widget %s not found\n", name);
54
55    return widget;
56}
57
58/* Figure out the boundary of a diameter=1 circle transformed into an ellipse
59 * by trans. Proof that this is the correct calculation:
60 *
61 * Transform x,y to u,v by this matrix calculation:
62 *
63 *  |u|   |a c| |x|
64 *  |v| = |b d|*|y|
65 *
66 * Horizontal component:
67 *
68 *  u = ax+cy (1)
69 *
70 * For each x,y on a radius-1 circle (p is angle to the point):
71 *
72 *  x^2+y^2 = 1
73 *  x = cos(p)
74 *  y = sin(p)
75 *  dx/dp = -sin(p) = -y
76 *  dy/dp = cos(p) = x
77 *
78 * Figure out derivative of (1) relative to p:
79 *
80 *  du/dp = a(dx/dp) + c(dy/dp)
81 *        = -ay + cx
82 *
83 * The min and max u are when du/dp is zero:
84 *
85 *  -ay + cx = 0
86 *  cx = ay
87 *  c = ay/x  (2)
88 *  y = cx/a  (3)
89 *
90 * Substitute (2) into (1) and simplify:
91 *
92 *  u = ax + ay^2/x
93 *    = a(x^2+y^2)/x
94 *    = a/x (because x^2+y^2 = 1)
95 *  x = a/u (4)
96 *
97 * Substitute (4) into (3) and simplify:
98 *
99 *  y = c(a/u)/a
100 *  y = c/u (5)
101 *
102 * Square (4) and (5) and add:
103 *
104 *  x^2+y^2 = (a^2+c^2)/u^2
105 *
106 * But x^2+y^2 is 1:
107 *
108 *  1 = (a^2+c^2)/u^2
109 *  u^2 = a^2+c^2
110 *  u = hypot(a,c)
111 *
112 * Similarily the max/min of v is at:
113 *
114 *  v = hypot(b,d)
115 *
116 */
117static void
118compute_extents (pixman_f_transform_t *trans, double *sx, double *sy)
119{
120    *sx = hypot (trans->m[0][0], trans->m[0][1]) / trans->m[2][2];
121    *sy = hypot (trans->m[1][0], trans->m[1][1]) / trans->m[2][2];
122}
123
124typedef struct
125{
126    char	name [20];
127    int		value;
128} named_int_t;
129
130static const named_int_t filters[] =
131{
132    { "Box",			PIXMAN_KERNEL_BOX },
133    { "Impulse",		PIXMAN_KERNEL_IMPULSE },
134    { "Linear",			PIXMAN_KERNEL_LINEAR },
135    { "Cubic",			PIXMAN_KERNEL_CUBIC },
136    { "Lanczos2",		PIXMAN_KERNEL_LANCZOS2 },
137    { "Lanczos3",		PIXMAN_KERNEL_LANCZOS3 },
138    { "Lanczos3 Stretched",	PIXMAN_KERNEL_LANCZOS3_STRETCHED },
139    { "Gaussian",		PIXMAN_KERNEL_GAUSSIAN },
140};
141
142static const named_int_t repeats[] =
143{
144    { "None",                   PIXMAN_REPEAT_NONE },
145    { "Normal",                 PIXMAN_REPEAT_NORMAL },
146    { "Reflect",                PIXMAN_REPEAT_REFLECT },
147    { "Pad",                    PIXMAN_REPEAT_PAD },
148};
149
150static int
151get_value (app_t *app, const named_int_t table[], const char *box_name)
152{
153    GtkComboBox *box = GTK_COMBO_BOX (get_widget (app, box_name));
154
155    return table[gtk_combo_box_get_active (box)].value;
156}
157
158static void
159copy_to_counterpart (app_t *app, GObject *object)
160{
161    static const char *xy_map[] =
162    {
163	"reconstruct_x_combo_box", "reconstruct_y_combo_box",
164	"sample_x_combo_box",      "sample_y_combo_box",
165	"scale_x_adjustment",      "scale_y_adjustment",
166    };
167    GObject *counterpart = NULL;
168    int i;
169
170    for (i = 0; i < G_N_ELEMENTS (xy_map); i += 2)
171    {
172	GObject *x = gtk_builder_get_object (app->builder, xy_map[i]);
173	GObject *y = gtk_builder_get_object (app->builder, xy_map[i + 1]);
174
175	if (object == x)
176	    counterpart = y;
177	if (object == y)
178	    counterpart = x;
179    }
180
181    if (!counterpart)
182	return;
183
184    if (GTK_IS_COMBO_BOX (counterpart))
185    {
186	gtk_combo_box_set_active (
187	    GTK_COMBO_BOX (counterpart),
188	    gtk_combo_box_get_active (
189		GTK_COMBO_BOX (object)));
190    }
191    else if (GTK_IS_ADJUSTMENT (counterpart))
192    {
193	gtk_adjustment_set_value (
194	    GTK_ADJUSTMENT (counterpart),
195	    gtk_adjustment_get_value (
196		GTK_ADJUSTMENT (object)));
197    }
198}
199
200static double
201to_scale (double v)
202{
203    return pow (1.15, v);
204}
205
206static void
207rescale (GtkWidget *may_be_null, app_t *app)
208{
209    pixman_f_transform_t ftransform;
210    pixman_transform_t transform;
211    double new_width, new_height;
212    double fscale_x, fscale_y;
213    double rotation;
214    pixman_fixed_t *params;
215    int n_params;
216    double sx, sy;
217
218    pixman_f_transform_init_identity (&ftransform);
219
220    if (may_be_null && gtk_toggle_button_get_active (
221	    GTK_TOGGLE_BUTTON (get_widget (app, "lock_checkbutton"))))
222    {
223	copy_to_counterpart (app, G_OBJECT (may_be_null));
224    }
225
226    fscale_x = gtk_adjustment_get_value (app->scale_x_adjustment);
227    fscale_y = gtk_adjustment_get_value (app->scale_y_adjustment);
228    rotation = gtk_adjustment_get_value (app->rotate_adjustment);
229
230    fscale_x = to_scale (fscale_x);
231    fscale_y = to_scale (fscale_y);
232
233    new_width = pixman_image_get_width (app->original) * fscale_x;
234    new_height = pixman_image_get_height (app->original) * fscale_y;
235
236    pixman_f_transform_scale (&ftransform, NULL, fscale_x, fscale_y);
237
238    pixman_f_transform_translate (&ftransform, NULL, - new_width / 2.0, - new_height / 2.0);
239
240    rotation = (rotation / 360.0) * 2 * M_PI;
241    pixman_f_transform_rotate (&ftransform, NULL, cos (rotation), sin (rotation));
242
243    pixman_f_transform_translate (&ftransform, NULL, new_width / 2.0, new_height / 2.0);
244
245    pixman_f_transform_invert (&ftransform, &ftransform);
246
247    compute_extents (&ftransform, &sx, &sy);
248
249    pixman_transform_from_pixman_f_transform (&transform, &ftransform);
250    pixman_image_set_transform (app->original, &transform);
251
252    params = pixman_filter_create_separable_convolution (
253        &n_params,
254        sx * 65536.0 + 0.5,
255	sy * 65536.0 + 0.5,
256	get_value (app, filters, "reconstruct_x_combo_box"),
257	get_value (app, filters, "reconstruct_y_combo_box"),
258	get_value (app, filters, "sample_x_combo_box"),
259	get_value (app, filters, "sample_y_combo_box"),
260	gtk_adjustment_get_value (app->subsample_adjustment),
261	gtk_adjustment_get_value (app->subsample_adjustment));
262
263    pixman_image_set_filter (app->original, PIXMAN_FILTER_SEPARABLE_CONVOLUTION, params, n_params);
264
265    pixman_image_set_repeat (
266        app->original, get_value (app, repeats, "repeat_combo_box"));
267
268    free (params);
269
270    app->scaled_width = ceil (new_width);
271    app->scaled_height = ceil (new_height);
272
273    gtk_widget_set_size_request (
274        get_widget (app, "drawing_area"), new_width + 0.5, new_height + 0.5);
275
276    gtk_widget_queue_draw (
277        get_widget (app, "drawing_area"));
278}
279
280static gboolean
281on_draw (GtkWidget *widget, cairo_t *cr, gpointer user_data)
282{
283    app_t *app = user_data;
284    GdkRectangle area;
285    cairo_surface_t *surface;
286    pixman_image_t *tmp;
287    uint32_t *pixels;
288
289    gdk_cairo_get_clip_rectangle(cr, &area);
290
291    pixels = calloc (1, area.width * area.height * 4);
292    tmp = pixman_image_create_bits (
293        PIXMAN_a8r8g8b8, area.width, area.height, pixels, area.width * 4);
294
295    if (area.x < app->scaled_width && area.y < app->scaled_height)
296    {
297        pixman_image_composite (
298            PIXMAN_OP_SRC,
299            app->original, NULL, tmp,
300            area.x, area.y, 0, 0, 0, 0,
301            app->scaled_width - area.x, app->scaled_height - area.y);
302    }
303
304    surface = cairo_image_surface_create_for_data (
305        (uint8_t *)pixels, CAIRO_FORMAT_ARGB32,
306        area.width, area.height, area.width * 4);
307
308    cairo_set_source_surface (cr, surface, area.x, area.y);
309
310    cairo_paint (cr);
311
312    cairo_surface_destroy (surface);
313    free (pixels);
314    pixman_image_unref (tmp);
315
316    return TRUE;
317}
318
319static void
320set_up_combo_box (app_t *app, const char *box_name,
321                  int n_entries, const named_int_t table[])
322{
323    GtkWidget *widget = get_widget (app, box_name);
324    GtkListStore *model;
325    GtkCellRenderer *cell;
326    int i;
327
328    model = gtk_list_store_new (1, G_TYPE_STRING);
329
330    cell = gtk_cell_renderer_text_new ();
331    gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (widget), cell, TRUE);
332    gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (widget), cell,
333				    "text", 0,
334				    NULL);
335
336    gtk_combo_box_set_model (GTK_COMBO_BOX (widget), GTK_TREE_MODEL (model));
337
338    for (i = 0; i < n_entries; ++i)
339    {
340	const named_int_t *info = &(table[i]);
341	GtkTreeIter iter;
342
343	gtk_list_store_append (model, &iter);
344	gtk_list_store_set (model, &iter, 0, info->name, -1);
345    }
346
347    gtk_combo_box_set_active (GTK_COMBO_BOX (widget), 0);
348
349    g_signal_connect (widget, "changed", G_CALLBACK (rescale), app);
350}
351
352static void
353set_up_filter_box (app_t *app, const char *box_name)
354{
355    set_up_combo_box (app, box_name, G_N_ELEMENTS (filters), filters);
356}
357
358static char *
359format_value (GtkWidget *widget, double value)
360{
361    return g_strdup_printf ("%.4f", to_scale (value));
362}
363
364static app_t *
365app_new (pixman_image_t *original)
366{
367    GtkWidget *widget;
368    app_t *app = g_malloc (sizeof *app);
369    GError *err = NULL;
370
371    app->builder = gtk_builder_new ();
372    app->original = original;
373
374    if (!gtk_builder_add_from_file (app->builder, "scale.ui", &err))
375	g_error ("Could not read file scale.ui: %s", err->message);
376
377    app->scale_x_adjustment =
378        GTK_ADJUSTMENT (gtk_builder_get_object (app->builder, "scale_x_adjustment"));
379    app->scale_y_adjustment =
380        GTK_ADJUSTMENT (gtk_builder_get_object (app->builder, "scale_y_adjustment"));
381    app->rotate_adjustment =
382        GTK_ADJUSTMENT (gtk_builder_get_object (app->builder, "rotate_adjustment"));
383    app->subsample_adjustment =
384	GTK_ADJUSTMENT (gtk_builder_get_object (app->builder, "subsample_adjustment"));
385
386    g_signal_connect (app->scale_x_adjustment, "value_changed", G_CALLBACK (rescale), app);
387    g_signal_connect (app->scale_y_adjustment, "value_changed", G_CALLBACK (rescale), app);
388    g_signal_connect (app->rotate_adjustment, "value_changed", G_CALLBACK (rescale), app);
389    g_signal_connect (app->subsample_adjustment, "value_changed", G_CALLBACK (rescale), app);
390
391    widget = get_widget (app, "scale_x_scale");
392    gtk_scale_add_mark (GTK_SCALE (widget), 0.0, GTK_POS_LEFT, NULL);
393    g_signal_connect (widget, "format_value", G_CALLBACK (format_value), app);
394    widget = get_widget (app, "scale_y_scale");
395    gtk_scale_add_mark (GTK_SCALE (widget), 0.0, GTK_POS_LEFT, NULL);
396    g_signal_connect (widget, "format_value", G_CALLBACK (format_value), app);
397    widget = get_widget (app, "rotate_scale");
398    gtk_scale_add_mark (GTK_SCALE (widget), 0.0, GTK_POS_LEFT, NULL);
399
400    widget = get_widget (app, "drawing_area");
401    g_signal_connect (widget, "draw", G_CALLBACK (on_draw), app);
402
403    set_up_filter_box (app, "reconstruct_x_combo_box");
404    set_up_filter_box (app, "reconstruct_y_combo_box");
405    set_up_filter_box (app, "sample_x_combo_box");
406    set_up_filter_box (app, "sample_y_combo_box");
407
408    set_up_combo_box (
409        app, "repeat_combo_box", G_N_ELEMENTS (repeats), repeats);
410
411    g_signal_connect (
412	gtk_builder_get_object (app->builder, "lock_checkbutton"),
413	"toggled", G_CALLBACK (rescale), app);
414
415    rescale (NULL, app);
416
417    return app;
418}
419
420int
421main (int argc, char **argv)
422{
423    GtkWidget *window;
424    pixman_image_t *image;
425    app_t *app;
426
427    gtk_init (&argc, &argv);
428
429    if (argc < 2)
430    {
431	printf ("%s <image file>\n", argv[0]);
432	return -1;
433    }
434
435    if (!(image = pixman_image_from_file (argv[1], PIXMAN_a8r8g8b8)))
436    {
437	printf ("Could not load image \"%s\"\n", argv[1]);
438	return -1;
439    }
440
441    app = app_new (image);
442
443    window = get_widget (app, "main");
444
445    g_signal_connect (window, "delete_event", G_CALLBACK (gtk_main_quit), NULL);
446
447    gtk_window_set_default_size (GTK_WINDOW (window), 1024, 768);
448
449    gtk_widget_show_all (window);
450
451    gtk_main ();
452
453    return 0;
454}
455