Image scaling with GTK+ 3 and Perl

Recently, I was writing a program in Perl that uses a GTK+ 3 GUI to display an image file. When I did something similar with Perl and GTK+ 2 a few years ago, there was a great image viewer widget available that provided all kinds of nice things like scaling and zooming out of the box. Sadly, I found nothing like this for GTK+ 3, so I had to resort to the basic GTK+ widgets to display image files.

Simply displaying an image in a GTK+ program window is easy. But I had quite a hard time figuring out how to make a (large) image fit into a (small) window, i.e., how to scale the image. Since the documentation I found concerning this was surprisingly sparse, here’s what I can add to this topic now.

Image display without scaling

Let’s start with the basics: A window that displays an image file when you press a button.

#!/usr/bin/env perl

use strict;
use warnings;
use Gtk3 -init;
use List::Util qw(min); # We'll need this later.

my $file = 'image.png';

my $window = Gtk3::Window->new('toplevel');
$window->set_title('Image scaling');
$window->set_default_size(400, 400);
$window->signal_connect('delete-event' => sub { Gtk3::main_quit });

my $grid = Gtk3::Grid->new();
$window->add($grid);

my $scrolled = Gtk3::ScrolledWindow->new();
$scrolled->set_hexpand(1);
$scrolled->set_vexpand(1);
$grid->attach($scrolled, 1, 1, 1, 1);

my $image = Gtk3::Image->new();
$scrolled->add_with_viewport($image);

my $button = Gtk3::Button->new('Load image');
$button->signal_connect('clicked' => \&on_button_clicked);
$grid->attach($button, 1, 2, 1, 1);

$window->show_all();
Gtk3::main();

sub on_button_clicked {
    $image->set_from_file($file);
}

This is pretty basic GTK+ stuff: We made a window, put a scrollable area and a button inside it and connected the callback function on_button_clicked to the button. That looks all nice and well, except for one thing: The image is too big for the program window (assuming image.png is bigger than 400 x 400 pixels).

Image display without scaling.
Image display without scaling.

(The Tux image used in all the screenshots displayed here is by Larry Ewing via Wikimedia Commons, licensed under GPL.)

Image display with basic scaling

To shrink the image so that it fits inside the window we need to scale it. We can achieve this using Gtk3::Gdk::Pixbuf’s scale_simple method:

sub on_button_clicked {
    $image->set_from_pixbuf(load_image($file));
}

sub load_image {
    my $file = shift;
    my $pixbuf = Gtk3::Gdk::Pixbuf->new_from_file($file);
    my $scaled = $pixbuf->scale_simple(400, 400, 'GDK_INTERP_HYPER');
    return $scaled;
}

Better, but not perfect. There is still one issue with this approach; generally, we cannot know in advance how big the window will be and how much of the space inside the window will be available to the image. In our case, the image has to share the window with the button, so it still doesn’t fit inside the window although we scaled it down to 400 x 400 pixels (the size of the window).

Image display with basic scaling.
Image display with basic scaling.

Image display with advanced scaling

So prior to scaling the image we have to find out how much space will be available and adjust the scaling factor accordingly. This is a bit tricky:

sub on_button_clicked {
    $image->set_from_pixbuf(load_image($file, $scrolled));
}

sub load_image {
    my ($file, $parent) = @_;
    my $pixbuf = Gtk3::Gdk::Pixbuf->new_from_file($file);
    my $scaled = scale_pixbuf($pixbuf, $parent);
    return $scaled;
}

sub scale_pixbuf {
    my ($pixbuf, $parent) = @_;
    my $max_w = $parent->get_allocation()->{width};
    my $max_h = $parent->get_allocation()->{height};
    my $pixb_w = $pixbuf->get_width();
    my $pixb_h = $pixbuf->get_height();
    if (($pixb_w > $max_w) || ($pixb_h > $max_h)) {
        my $sc_factor_w = $max_w / $pixb_w;
        my $sc_factor_h = $max_h / $pixb_h;
        my $sc_factor = min $sc_factor_w, $sc_factor_h;
        my $sc_w = int($pixb_w * $sc_factor);
        my $sc_h = int($pixb_h * $sc_factor);
        my $scaled
            = $pixbuf->scale_simple($sc_w, $sc_h, 'GDK_INTERP_HYPER');
        return $scaled;
    }
    return $pixbuf;
}

The actual work happens in the scale_pixbuf function: First, we get the dimensions of the parent widget holding the image (in our case this is the scrollable area) and of the image itself which is represented by a pixbuf. Then we check whether the width or height of the pixbuf is greater than the parent widget’s width or height. If this is the case, we need to scale down the image so that it fits inside the parent widget (otherwise, it fits inside its parent anyway).

We use the scale_simple method again, but this time we don’t scale the pixbuf to a fixed size (remember, previously we scaled it to 400 x 400 pixels). Instead, the parameters for the scale_simple method are now calculated by multiplying the pixbuf’s width and height with a scaling factor. To get this scaling factor we divide the parent’s width by the pixbuf’s width and the parent’s height by the pixbuf’s height and then choose the smaller of the two results.

This is probably best visualized by an example. Suppose the pixbuf holding the image is 500 pixels wide and 300 pixels high but there are only 400 pixels available for the width (and enough for the height). If we divide 400 by 500 we get 0.8, which is our scaling factor. Multiplying 500 and 300 each by 0.8 we get 400 and 240. Scaling the image to these dimensions will make it fit inside its parent widget while keeping the image’s proportions intact. Play around with this algorithm and several different values if you have problems wrapping your brain around this!

Using this method we finally get the result shown below:

Image display with advanced scaling.
Image display with advanced scaling.

As you can see, the image now fits perfectly inside the window, no matter how big the image or the window is. You can resize the window, reload the image with a click on the button and the image will be resized accordingly. (Probably, one could somehow observe window resize events and automatically reload the image; but I did not need this for my use case.)

You can see a real life application of this image scaling stuff in the code of my image metadata editor Verso.

PS: There is one caveat, though. In our example program the image is loaded with a click on the button after the program window has been completely built up. If you try to load the image while the GUI is being constructed (omit the button and load the image at once after you created the image widget), GTK+ apparently cannot determine the size of the widgets (I didn’t really track down the cause for this) and will produce errors like the following:

gdk_pixbuf_scale_simple: assertion `dest_width > 0' failed