Search code examples
perlgtk3

Save GooCanvas2 to PNG file


After drawing with GooCanvas2, I'm trying to take a 'screenshot' of the canvas and save it to a .PNG file.

This script provides a very nice example using Gtk2/GooCanvas, but having converted that script to Gtk3/GooCanvas2, I get an error that I don't understand:

Write PNG...
*** unhandled exception in callback:
***   `need' is not a valid cairo_status_t value; valid values are: success, no-memory, invalid-restore, invalid-pop-group, no-current-point, invalid-matrix, invalid-status, null-pointer, invalid-string, invalid-path-data, read-error, write-error, surface-finished, surface-type-mismatch, pattern-type-mismatch, invalid-content, invalid-format, invalid-visual, file-not-found, invalid-dash, invalid-dsc-comment, invalid-index, clip-not-representable, temp-file-error, invalid-stride, font-type-mismatch, user-font-immutable, user-font-error, negative-count, invalid-clusters, invalid-slant, invalid-weight at goopng2.pl line 90.
***  ignoring at /usr/share/perl5/Gtk3.pm line 546.

The error is generated by Gtk3::Gdk::PixbufLoader->write(). I have not modified that function at all:

$surface->write_to_png_stream (sub {
    my ($closure, $data) = @_;
    $loader->write($data);
});

And this is the converted script:

#!/usr/bin/perl -w
use strict;

use warnings;
use GooCanvas2;
use Gtk3 '-init';
use Glib qw(TRUE FALSE);

my $window = Gtk3::Window->new('toplevel');
$window->signal_connect('delete_event' => sub { Gtk3->main_quit; });
$window->set_default_size(640, 600);


my $vbox = Gtk3::VBox->new;
$vbox->set_border_width(4);
$vbox->show;
$window->add($vbox);

my $swin = Gtk3::ScrolledWindow->new;
$swin->set_shadow_type('in');
$vbox->pack_start($swin, 1, 1, 0); 

my $canvas = GooCanvas2::Canvas->new();
$canvas->set_size_request(600, 450);
$canvas->set_bounds(0, 0, 1000, 1000);
$swin->add($canvas);

my $root = $canvas->get_root_item();

my $rect = GooCanvas2::CanvasRect->new(
    parent => $root,
    'x' => 100,
    'y' => 100,
    'width' => 400,
    'height' => 400,
    'line-width' => 10,
    'radius-x' => 20,
    'radius-y' => 10,
    'stroke-color' => 'yellow',
    'fill-color' => 'red'
);

my $text = GooCanvas2::CanvasText->new(
    'parent' => $root,
    'text' => "Hello World",
    'x' => 300,
    'y' => 300,
    'width' => -1,
    'anchor' => 'center',
    'font' => 'Sans 24',
);
$text->rotate(45, 300, 300);

# Create PNG                                                                          
my $sb = Gtk3::Button->new_with_label('Write PNG and JPG');                                       
$vbox->pack_start($sb, FALSE, FALSE, 0);                                               
$sb->show;                                                                             
$sb->signal_connect("clicked", \&write_png_clicked, $canvas);                          

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

sub write_png_clicked {
    my ($but, $canvas) = @_;
    print "Write PNG...\n";

    my $surface = Cairo::ImageSurface->create ('rgb24', 1000, 1000);
    # also argb32 is available
    # my $surface = Cairo::ImageSurface->create ('argb32', 1000, 1000);

    my $cr = Cairo::Context->create($surface);

    # make a background rectangle filled white so saved file looks same as screen
    # otherwise a black background may appear, it's like pdf, if it isn't
    # drawn , it will be a black background, It won't automagically pick up
    # a white background on a canvas
    $cr->rectangle( 0, 0, 1000, 1000 );
    $cr->set_source_rgb( 1, 1, 1 );
    $cr->fill;

    $canvas->render($cr, undef, 1);

    # this works, but see below for way to use pixbuf and jpg
    #    my $status = $surface->write_to_png ("$0.png");
    #    print "$status\n";

    my $loader = Gtk3::Gdk::PixbufLoader->new;
        $surface->write_to_png_stream (sub {
        my ($closure, $data) = @_;
        $loader->write($data);
    });
    $loader->close;
    my $pixbuf = $loader->get_pixbuf;

    print $pixbuf->get_bits_per_sample(),"\n";
    print $pixbuf->get_colorspace(),"\n";

    $pixbuf->save ("$0.png", 'png');
    print "done png\n";
    $pixbuf->save ("$0.jpg", 'jpeg', quality => 100); 
    print "done jpg\n";

    return TRUE;
}

Solution

  • * unhandled exception in callback: * `need' is not a valid cairo_status_t value; valid values are: success, no-memory, [...] at goopng2.pl line 90. *** ignoring at /usr/share/perl5/Gtk3.pm line 546.

    By running the debugger on your code I could see that $loader->write($data) raised an exception:

    need an array ref to convert to GArray
    

    and write_to_png_stream() was not expecting this type of exception and truncated the message to the first word "need" as you can see from Glib error message at the top: `need' is not a valid cairo_status_t value ...

    By some trial and error I found that I could pass the $buffer argument as an array of characters and not as a perl string:

    sub write_png_clicked {
        my ($but, $canvas) = @_;
        print "Write PNG...\n";
    
        my $surface = Cairo::ImageSurface->create ('rgb24', 1000, 1000);
        my $cr = Cairo::Context->create($surface);
        $cr->rectangle( 0, 0, 1000, 1000 );
        $cr->set_source_rgb( 1, 1, 1 );
        $cr->fill;
        $canvas->render($cr, undef, 1);
        my $loader = Gtk3::Gdk::PixbufLoader->new;
        $surface->write_to_png_stream (
            sub {
                my ($loader, $buffer) = @_;
                $loader->write([map ord, split //, $buffer]);
                return TRUE;
            }, $loader
        );
        $loader->close;
        my $pixbuf = $loader->get_pixbuf;
    
        print $pixbuf->get_bits_per_sample(),"\n";
        print $pixbuf->get_colorspace(),"\n";
    
        $pixbuf->save ("test.png", 'png');
        print "done png\n";
        $pixbuf->save ("test.jpg", 'jpeg', quality => 100); 
        print "done jpg\n";
        return TRUE;
    }
    

    Edit:

    To save only a part of the canvas you can pass a GooCanvasBounds parameter to the render() method:

    my $bounds = GooCanvas2::CanvasBounds->new();
    $bounds->x1(50);
    $bounds->x2(250);
    $bounds->y1(50);
    $bounds->y2(250);
    $canvas->render($cr, $bounds, 1);
    

    Edit 2:

    To capture a region at a specific position and a specific width and height:

    my $img_width = 200;
    my $img_height = 200;
    my $img_x0 = 100;
    my $img_y0 = 100;
    my $surface = Cairo::ImageSurface->create ('rgb24', $img_width, $img_height);
    $cr->translate(-$img_x0,-$img_y0);
    $canvas->render($cr, undef, 1);