Skip to content
This repository
branch: master
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

executable file 256 lines (199 sloc) 7.747 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
#!/usr/bin/env perl

=head1 NAME

screenshot.pl - Take a screenshot

=head1 SYNOPSIS

screenshot.pl [OPTION]... URI

--pause MS number of miliseconds to wait before taking a screenshot
--show show the window where the screenshots are taken from
-x, --xpath XPATH XPath expression of the element to screenshot
-t, --type TYPE format type (svg, ps, pdf, png)
-o, --output FILE the screenshot's file name
-s, --size SIZE the window's size (ex: 1024x800)
-u, --user USER the user name to use
-p, --password PASSWORD the password to use
--proxy PROXY the proxy to use
--transparent if true then the window will be transparent
-h, --help print this help message

Simple usage:

Save a page as an SVG:

screenshot.pl --type svg http://www.google.com/

Save a page as a PDF:

screenshot.pl --output cpan.pdf http://search.cpan.org/

Save an element of a page taken from an XPath query as a PNG:

screenshot.pl --output ba.png --xpath 'id("content")' http://bratislava.pm.org/

=head1 DESCRIPTION

Take a screenshot of a page or part of a page by specifying an XPath query.

=cut

use strict;
use warnings;

use Data::Dumper;
use Getopt::Long qw(:config auto_help);
use Pod::Usage;

use Glib ':constants';
use Gtk3 -init;
use Gtk3::WebKit qw(:xpath_results :node_types);
use Cairo::GObject;

my %TYPES = (
    svg => sub { save_as_vector('Cairo::SvgSurface', @_) },
    ps => sub { save_as_vector('Cairo::PsSurface', @_) },
    pdf => sub { save_as_vector('Cairo::PdfSurface', @_) },
    png => \&save_as_png,
);

sub main {
    GetOptions(
        'pause=i' => \my $pause,
        'show' => \my $show,
        'o|output=s' => \my $filename,
        's|size=s' => \my $geometry,
        'u|user=s' => \my $user,
        'x|xpath=s' => \my $xpath,
        'p|password=s' => \my $password,
        't|type=s' => \my $type,
        'transparent' => \my $transparent,
        'proxy=s' => \my $proxy,
    ) or pod2usage(1);
    my ($url) = @ARGV or pod2usage(1);

    if ($type) {
        $type = lc $type;
        die "Type must be one of: ", join(", ", keys %TYPES) unless exists $TYPES{$type};
    }
    elsif (defined $filename) {
        if ( ($type) = ($filename =~ /\.([^.]+)$/) ) {
            $type = lc $type;
            die "Extension must be one of: ", join(", ", keys %TYPES) unless exists $TYPES{$type};
        }
        else {
            $type = 'pdf';
            $filename .= ".$type";
        }
    }
    else {
        $type = 'pdf';
    }
    $filename ||= "screenshot.$type";


    if (defined $user and defined $password) {
        require HTTP::Soup;

        # Remove the default authentication dialog so that we can provide our
        # own authentication method.
        my $session = Gtk3::WebKit->get_default_session();
        $session->remove_feature_by_type('Gtk3::WebKit::SoupAuthDialog');

        my $count = 0;
        $session->signal_connect('authenticate' => sub {
            my ($session, $message, $auth) = @_;
            if ($count++) {
                print "Too many authentication failures\n";
                Gtk3->main_quit();
            }
            $auth->authenticate($user, $password);
        });
    }


    if (defined $proxy) {
        require HTTP::Soup;

        $proxy = "http://$proxy" unless $proxy =~ m,^https?://,;
        my $proxy_uri = HTTP::Soup::URI->new($proxy);
        my $session = Gtk3::WebKit->get_default_session();
        $session->set(
            'proxy-uri' => $proxy_uri,
        );
    }

    my $save_as_func = $TYPES{$type};

    my $view = Gtk3::WebKit::WebView->new();
    $view->signal_connect('notify::load-status' => sub {
        return unless $view->get_uri and ($view->get_load_status eq 'finished');

        # Sometimes the program dies with:
        # (<unknown>:19092): Gtk-CRITICAL **: gtk_widget_draw: assertion `!widget->priv->alloc_needed' failed
        # This seem to happend is there's a newtwork error and we can't download
        # external stuff (e.g. facebook iframe). This timeout seems to help a bit.
        my $grab_screenshot_cb = sub {
            grab_screenshot($view, $filename, $save_as_func, $xpath);
        };
        if ($pause) {
            Glib::Timeout->add($pause, $grab_screenshot_cb);
        }
        else {
            Glib::Idle->add($grab_screenshot_cb);
        }
    });
    $view->load_uri($url);


    my $window = $show ? Gtk3::Window->new('toplevel') : Gtk3::OffscreenWindow->new();
    if (defined $geometry and $geometry =~ /^ ([0-9]+) x ([0-9]+) $/x) {
        my ($width, $height) = ($1, $2);
        $window->set_default_size($width, $height);
    }

    # Set the main window transparent
    if ($transparent) {
        my $screen = $window->get_screen;
        $window->set_visual($screen->get_rgba_visual || $screen->get_system_visual);
        $view->set_transparent(TRUE);
    }

    $window->add($view);
    $window->show_all();

    Gtk3->main();
    return 0;
}


sub grab_screenshot {
    my ($view, $filename, $save_as_func, $xpath) = @_;

    my ($left, $top, $width, $height) = (0, 0, 0, 0);
    if (defined $xpath) {
        # Get the first element returned by the XPath query and return it's offsets
        my $element = get_xpath_element($view->get_dom_document, $xpath);
        ($left, $top, $width, $height) = get_offsets($element) if $element;
    }
    if (!$width and !$height) {
        ($width, $height) = ($view->get_allocated_width, $view->get_allocated_height);
    }

    $save_as_func->($view, $filename, $left, $top, $width, $height);
    print "$filename has size: $width x $height\n";

    Gtk3->main_quit();
}


sub save_as_vector {
    my ($surface_class, $widget, $filename, $left, $top, $width, $height) = @_;
    my $surface = $surface_class->create($filename, $width, $height);
    my $cr = Cairo::Context->create($surface);
    $cr->translate(-$left, -$top);
    $widget->draw($cr);
}


sub save_as_png {
    my ($widget, $filename, $left, $top, $width, $height) = @_;
    my $surface = Cairo::ImageSurface->create(argb32 => $width, $height);
    my $cr = Cairo::Context->create($surface);
    $cr->translate(-$left, -$top);
    $widget->draw($cr);
    $surface->write_to_png($filename);
}


sub get_xpath_element {
    my ($doc, $xpath) = @_;

    # Execute the XPath expression
    my $resolver = $doc->create_ns_resolver($doc);
    my $xpath_results = $doc->evaluate(
        $xpath,
        $doc,
        $resolver,
        ORDERED_NODE_SNAPSHOT_TYPE,
    );
    if (! $xpath_results or $xpath_results->get_snapshot_length == 0) {
        print "Can't find $xpath\n";
        return;
    }

    # We always return the first element
    my $element = $xpath_results->snapshot_item(0);
    my $node_type = $element->get_node_type;
    if ($node_type != ELEMENT_NODE and $node_type != DOCUMENT_NODE) {
        print "Can't handle node type $node_type\n";
        return;
    }

    return $element;
}


# Get the offsets of the given element
sub get_offsets {
    my ($element) = @_;

    $element = $element->get('body') if $element->isa('Gtk3::WebKit::DOMDocument');
    my ($width, $height) = ($element->get_offset_width, $element->get_offset_height);
    my ($left, $top) = ($element->get_offset_left, $element->get_offset_top);

    while ($element = $element->get_offset_parent) {
        $left += $element->get_offset_left - $element->get_scroll_left + $element->get_client_left;
        $top += $element->get_offset_top - $element->get_scroll_top + $element->get_client_top;
    }

    return ($left, $top, $width, $height);
}

exit main() unless caller;
Something went wrong with that request. Please try again.