package TiledImage;
=head1 NAME
TiledImage.pm - Perl module to provide a GD-like interface for rendering large images then breaking them into tiles.
=head1 SYNOPSIS
# create a new image
my $im = new TiledImage('-width'=>100,'-height'=>100);
$im->verbose(2);
# allocate some colors
my $white = $im->colorAllocate(255,255,255);
my $black = $im->colorAllocate(0,0,0);
my $red = $im->colorAllocate(255,0,0);
my $blue = $im->colorAllocate(0,0,255);
# make the background transparent and interlaced
$im->transparent($white);
$im->interlaced('true');
# Put a black frame around the picture
$im->rectangle(0,0,99,99,$black);
# Draw a blue oval
$im->arc(50,50,95,75,0,360,$blue);
# draw a polygon
my $poly = GD::Polygon->new;
$poly->addPt(15,15);
$poly->addPt(85,15);
$poly->addPt(50,85);
$im->filledPolygon ($poly, $red);
# draw strings
$im->string(GD::gdLargeFont, 10, 10, "hi world", $blue);
# create a dummy brush image & call setBrush() & copy() to test image storage
my $dummyBrush = new GD::Image (20,20);
my $white2 = $dummyBrush->colorAllocate(255,255,255);
my $black2 = $dummyBrush->colorAllocate(0,0,0);
my $red2 = $dummyBrush->colorAllocate(255,0,0);
my $blue2 = $dummyBrush->colorAllocate(0,0,255);
$dummyBrush->transparent($white2);
$dummyBrush->interlaced('true');
$dummyBrush->filledRectangle(4,4,16,16,$black2);
$dummyBrush->arc(10,10,8,8,0,360,$blue2);
$im->setBrush ($dummyBrush);
$im->line (40, 30, 90, 80, GD::gdBrushed); # libgd bug: lines are clipped
$im->copy ($dummyBrush, 75, 40, 0, 0, 20, 20);
# render and save four tiles
my ($tileWidth, $tileHeight) = (50, 50);
for ($x = 0; $x < $im->width; $x += $tileWidth) {
for ($y = 0; $y < $im->height; $y += $tileHeight) {
my $tile = $im->renderTile ($x, $y, $tileWidth, $tileHeight);
my $file = "TILE.$x.$y.png";
open TILE, ">$file" or die "Couldn't write $file: $!";
print TILE $tile->png;
close TILE or die "Couldn't close $file: $!";
warn "Wrote tile to $file";
}
}
=head1 METHODS
=cut
use GD::Image;
use Carp;
use TiledImage::MemoryPrimStorage;
# "use TiledImage::DBPrimStorage" commented out because it is imported lazily via an "eval" statement below
# use TiledImage::DBPrimStorage;
# Global table of TiledImage's for cleanup
my %tiledImageCleanup;
# Private methods.
# Code to do (X,Y)-translation for various different intercepted subroutines.
# subroutine to generate a closure that translates an argument list
sub makeGDPrimitiveArglistTranslator {
my @xyIndexList = @_;
return sub {
my ($self, $xstart, $ystart, @arglist) = @_;
foreach my $xyIndex (@xyIndexList) {
$arglist[$$xyIndex[0]] -= $xstart;
$arglist[$$xyIndex[1]] -= $ystart;
}
return @arglist;
}
}
# polygon translator
sub GDPolygonTranslate {
my ($self, $xstart, $ystart, $poly, @arglist) = @_;
my $translatedPoly = new GD::Polygon;
foreach my $xy (@{$poly->{'points'}}) {
$translatedPoly->addPt ($$xy[0] - $xstart, $$xy[1] - $ystart);
}
return ($translatedPoly, @arglist);
}
# translators
my $copyTranslate = makeGDPrimitiveArglistTranslator ([1,2]);
my $stringTranslate = makeGDPrimitiveArglistTranslator ([1,2]);
my $stringFTTranslate = makeGDPrimitiveArglistTranslator ([4,5]);
my $xyTranslate = makeGDPrimitiveArglistTranslator ([0,1]);
my $xyxyTranslate = makeGDPrimitiveArglistTranslator ([0,1], [2,3]);
# Code to get the (xmin,ymin,xmax,ymax) bounding-box for intercepted subroutines.
# bounding box getters
sub GDPixelBounds { my ($self, $x, $y) = @_; return ($x, $y, $x+1, $y+1) }
sub GDLineBounds { my ($self, $x1, $y1, $x2, $y2, $col) = @_; return (min($x1,$x2), min($y1,$y2), max($x1,$x2), max($y1,$y2)); }
sub GDPolygonBounds { my ($self, $poly) = @_; return $poly->bounds() }
sub GDEllipseBounds { my ($self, $x, $y, $w, $h) = @_; return ($x-$w, $y-$h, $x+$w, $y+$h) } # is this twice as big as it should be?
sub GDCopyBounds { my ($self, $im, $destx, $desty, $srcx, $srcy, $w, $h) = @_; return ($destx, $desty, $destx+$w-1, $desty+$h-1) }
sub GDStringBounds { my ($self, $font, $x, $y, $text) = @_; return ($x, $y, $x + length($text)*$font->width, $y + $font->height) }
sub GDStringUpBounds { my ($self, $font, $x, $y, $text) = @_; return ($x, $y, $x + $font->width, $y + length($text)*$font->height) }
sub GDStringFTBounds { my ($self, @args) = @_; my @bb = $self->im->stringFT (@args); return @bb ? @bb[0,1,4,5] : () }
# min & max of a list
sub min {
my ($x, @y) = @_;
foreach my $y (@y) {
$x = $y if $y < $x;
}
return $x;
}
sub max {
my ($x, @y) = @_;
foreach my $y (@y) {
$x = $y if $y > $x;
}
return $x;
}
# The %intercept hash: a function intercept table.
# Each value is a reference to a hash of references to interception subroutines.
# Possible interception subroutines:
# 'translator'
# 'boundsGetter'
# 'imageStorer'
# 'imageRetriever'
my %intercept =
('setPixel' => {'translator' => $xyTranslate, 'boundsGetter' => \&GDPixelBounds},
'line' => {'translator' => $xyxyTranslate, 'boundsGetter' => \&GDLineBounds},
'dashedLine' => {'translator' => $xyxyTranslate, 'boundsGetter' => \&GDLineBounds},
'rectangle' => {'translator' => $xyxyTranslate, 'boundsGetter' => \&GDLineBounds},
'filledRectangle' => {'translator' => $xyxyTranslate, 'boundsGetter' => \&GDLineBounds},
'polygon' => {'translator' => \&GDPolygonTranslate, 'boundsGetter' => \&GDPolygonBounds}, # [AVU 12/5/05] added line for bugfix
'openPolygon' => {'translator' => \&GDPolygonTranslate, 'boundsGetter' => \&GDPolygonBounds},
'unclosedPolygon' => {'translator' => \&GDPolygonTranslate, 'boundsGetter' => \&GDPolygonBounds},
'filledPolygon' => {'translator' => \&GDPolygonTranslate, 'boundsGetter' => \&GDPolygonBounds},
'fillPoly' => {'translator' => \&GDPolygonTranslate, 'boundsGetter' => \&GDPolygonBounds},
'ellipse' => {'translator' => $xyTranslate, 'boundsGetter' => \&GDEllipseBounds},
'filledEllipse' => {'translator' => $xyTranslate, 'boundsGetter' => \&GDEllipseBounds},
'arc' => {'translator' => $xyTranslate, 'boundsGetter' => \&GDEllipseBounds},
'filledArc' => {'translator' => $xyTranslate, 'boundsGetter' => \&GDEllipseBounds},
'copy' => {'translator' => $copyTranslate, 'boundsGetter' => \&GDCopyBounds},
'copyMerge' => {'translator' => $copyTranslate, 'boundsGetter' => \&GDCopyBounds},
'copyMergeGray' => {'translator' => $copyTranslate, 'boundsGetter' => \&GDCopyBounds},
'copyResized' => {'translator' => $copyTranslate, 'boundsGetter' => \&GDCopyBounds},
'copyResampled' => {'translator' => $copyTranslate, 'boundsGetter' => \&GDCopyBounds},
'copyRotated' => {'translator' => $copyTranslate, 'boundsGetter' => \&GDCopyBounds},
'string' => {'translator' => $stringTranslate, 'boundsGetter' => \&GDStringBounds},
'stringUp' => {'translator' => $stringTranslate, 'boundsGetter' => \&GDStringUpBounds},
'char' => {'translator' => $stringTranslate, 'boundsGetter' => \&GDStringBounds},
'charUp' => {'translator' => $stringTranslate, 'boundsGetter' => \&GDStringUpBounds},
'stringFT' => {'translator' => $stringFTTranslate, 'boundsGetter' => \&GDStringFTBounds},
'stringFTcircle' => {'translator' => $stringFTTranslate, 'boundsGetter' => \&GDStringFTBounds},
);
@globalPrimNames = qw(colorAllocate rgb setBrush setThickness);
# List of unimplemented functions:-- these will throw an error if called
# (all others are silently passed to a dummy GD object)
my %unimplemented = map (($_=>1),
qw (copyRotate90 copyRotate180 copyRotate270
copyFlipHorizontal copyFlipVertical copyTranspose
copyReverseTranspose rotate180
flipHorizontal flipVertical
fill fillToBorder));
sub getBounds {
my ($self) = @_;
return ($self->width, $self->height);
}
foreach my $sub (keys %intercept) {
no strict "refs";
*$sub = sub {
my ($self, @args) = @_;
# check for intercept: if so, get bounding box & store any images
my @bb = $self->getBoundingBox ($sub, @args);
# update global bounding box
if (@bb) {
$self->xmin ($bb[0]) if !defined ($self->xmin) || $bb[0] < $self->xmin;
$self->ymin ($bb[1]) if !defined ($self->ymin) || $bb[1] < $self->ymin;
$self->xmax ($bb[2]) if !defined ($self->xmax) || $bb[2] >= $self->xmax;
$self->ymax ($bb[3]) if !defined ($self->ymax) || $bb[3] >= $self->ymax;
}
# record primitive
$self->primstorage->GDRecordPrimitive ($sub, \@args, @bb);
# log primitive
warn "Recorded $sub (@args) with ", (@bb>0 ? "bounding box (@bb)" : "no bounding box"), "\n" if $self->verbose == 2;
}
}
foreach my $sub (@globalPrimNames) {
no strict "refs";
*$sub = sub {
my ($self, @args) = @_;
# record primitive
$self->primstorage->GDRecordPrimitive ($sub, \@args);
# log primitive
warn "Recorded global primitive $sub (@args)\n" if $self->verbose == 2;
# delegate
$self->im->$sub (@args);
}
}
foreach my $sub (keys %unimplemented) {
no strict "refs";
*$sub = sub {
croak "Subroutine $sub unimplemented";
}
}
foreach my $field (qw(im width height xmin xmax ymin ymax verbose persistent primstorage)) {
*$field = sub {
my $self = shift;
$self->{$field} = shift if @_;
return $self->{$field};
}
}
# Subroutine interceptions.
# Each of the following can take a ($subroutine, @argument_list) array,
# representing a call to a GD::Image object, $im, of the form $im->$subroutine (@argument_list).
# $self->intercepts ($subroutine)
# returns true if this TiledImage object intercepts the named subroutine
# (i.e. it has an entry in the %intercept hash).
sub intercepts {
my ($self, $sub) = @_;
return exists $intercept{$sub};
}
# $self->translate ($xOrigin, $yOrigin, $subroutine, @argument_list)
# "translates" all (X,Y)-coordinates in the argument list of the named subroutine,
# offsetting them relative to the specified (X,Y) origin.
# Control is dispatched to a "translator" via the %intercept hash.
sub translate {
my ($self, $xstart, $ystart, $sub, @args) = @_;
my $translator = $intercept{$sub}->{'translator'};
return defined($translator) ? &$translator ($self, $xstart, $ystart, @args) : @args;
}
# $self->getBoundingBox ($subroutine, @argument_list)
# returns the (xMin,yMin,xMax,yMax) bounding box for the named subroutine
# with the given argument list.
# Control is dispatched to a "bounding-box getter" via the %intercept hash.
sub getBoundingBox {
my ($self, $sub, @args) = @_;
my $boundsGetter = $intercept{$sub}->{'boundsGetter'};
return defined($boundsGetter) ? &$boundsGetter ($self, @args) : ();
}
# Special-case interceptions of specific GD::Image methods
# intercept clone
sub clone {
my ($self) = @_;
my $clone = {%$self};
bless $clone, ref ($self);
$clone->im ($self->im->clone);
return $clone;
}
# hackily intercept getPixel
sub getPixel {
my ($self, $x, $y) = @_;
my $im = $self->renderTile ($x, $y, 1, 1);
return $im->getPixel (0, 0);
}
# apparently some glyphs call this subroutine to see if a drawing method has
# been implemented in the version of BioPerl at hand (a backward compatability
# check), so we must intercept immediately instead of storing in database
sub can {
my ($self, $method_name) = @_;
#warn "CHECKING FOR $method_name IN can()...\n"; #D!!!
return $self->intercepts($method_name);
}
# AUTOLOAD method: catches all methods by default
sub AUTOLOAD {
my ($self, @args) = @_;
# get subroutine name
my $sub = our $AUTOLOAD;
$sub =~ s/.*:://;
# check for DESTROY
return if $sub eq "DESTROY";
warn "unhandled sub $sub";
# record primitive
# we don't need to worry about the bounding box here because
# all of the primitives with bounding boxes are handled above.
$self->primstorage->GDRecordPrimitive ($sub, \@args);
# delegate
$self->im->$sub (@args);
}
# This needs to be called manually to cleanup and disconnect from database after done with the object;
# otherwise, database connections remain open and clog database until instantiating script exits
#
sub finish {
my $self = shift;
$self->cleanup;
# there was stuff here, but now it is gone... call 'cleanup' directly? !!!
}
# Destructor - TEMPORARILY (?) DISABLED due to DBI connectivity problems, destruction is now the responsibility of the caller
#sub DESTROY {
# my ($self) = @_;
# warn "TiledImage.pm IS CLEANING UP AND DISCONNECTING FROM DATABASE in destructor...\n" if $self->verbose;
# $self->cleanup;
# $self->gdtile->disconnect if $self->gdtile; # just in case we didn't close the database connection using finish()
#}
# Public methods.
=head2 new
my $tiledImage = new TiledImage (%args);
Creates a new TiledImage object.
%args is a key-value hash with the following keys:
=over 2
=item B<-width>: image width in pixels
=item B<-height>: image height in pixels
=item B<-tile_width_hint>: for optimal performance, set this equal to the tile width
=item B<-verbose>: print lots of debugging information
=item B: flag indicating whether to use filesystem links to repeat identical tiles. True by default; set to zero to disable this feature
=item B<-primdb>: use a database to cache GD primitives, rather than storing them in memory (see TiledImage/gdtile.sql for SQL commands to create the database)
=item B<-tiledimage_name>: unique identifier for this TiledImage. mandatory if B<-primdb> is used
=item B<-persistent>: when used with B<-primdb>, do not delete primitives from database after rendering tiles
=back
=cut
# Constructor
sub new {
my ($class, %args) = @_;
my %allowed_args = map {$_ => 1} qw (-primdb -tiledimage_name -width -height -persistent -verbose -tile_width_hint);
my @required_args;
if (exists $args{'-primdb'}) {
push @required_args, '-tiledimage_name';
} else {
$allowed_args{'-tiledimage_name'} = 1;
}
foreach my $arg (keys %args) {
unless ($allowed_args{$arg}) {
croak ("You specified an invalid arg ($arg) to TiledImage constructor (you passed in: ",
join (' ', map { $_ . '=>' . $args{$_} } sort keys %args), ")");
}
}
foreach my $arg (@required_args) {
unless (defined $args{$arg}) {
croak ("You did not specify a required arg ($arg) to TiledImage constructor (you passed in: ",
join (' ', map { $_ . '=>' . $args{$_} } sort keys %args), ")");
}
}
my ($persistent, $verbose) = (1, 0); # defaults
$verbose = $args{'-verbose'} if exists $args{'-verbose'} ;
$persistent = $args{'-persistent'} if exists $args{'-persistent'};
my $primstorage;
if ($args{'-primdb'}) {
eval "use TiledImage::DBPrimStorage"; # import DBPrimStorage here, rather than at top of file, so TiledImage.pm will still work even if DBI.pm is unavailable
$primstorage = DBPrimStorage->new(
-primdb => $args{'-primdb'},
-tiledimage_name => $args{'-tiledimage_name'},
-width => $args{'-width'} || '',
-height => $args{'-height'} || '',
-verbose => $verbose);
} else {
$primstorage = MemoryPrimStorage->new(
-width => $args{-width}, -height => $args{-height},
-tile_width_hint => $args{'-tile_width_hint'} || 1000,
-verbose => $verbose);
}
# create dummy GD image
my $im = GD::Image->new (1, 1);
# create the proxy object
my $self = { 'im' => $im,
'xmin' => undef,
'xmax' => undef,
'ymin' => undef,
'ymax' => undef,
'width' => $primstorage->{width},
'height' => $primstorage->{height},
'verbose' => $verbose,
'persistent' => $persistent,
'primstorage' => $primstorage,
};
# bless it, and add to global table
bless $self, $class;
$tiledImageCleanup{$self} = 1;
# return
return $self;
}
=head2 renderTile
my $gdImage = $tiledImage->renderTile ($xmin, $ymin, $width, $height);
Returns the specified area as a GD::Image object.
=cut
# renderTile:--
# method to render a tile of given dimensions.
sub renderTile {
my ($self, $xmin, $ymin, $width, $height) = @_;
my ($xmax, $ymax) = ($xmin + $width - 1, $ymin + $height - 1);
# print message
warn "\nRendering tile ($xmin,$ymin)+($width,$height)\n" if $self->verbose == 2;
# create GD image
my $im = GD::Image->new ($width, $height);
my @prims = ($self->primstorage->GDGetGlobalPrimitives,
$self->primstorage->GDGetBoundedPrimitives($xmin, $ymin,
$xmax, $ymax));
# sort by command_order
@prims = sort { $a->[0] <=> $b->[0] } @prims;
my $prev_command = -1;
foreach my $primitive (@prims) {
my ($command_order, $sub, @args) = @{$primitive};
# GDGetBoundedPrimitives might in some cases
# return more than one copy of the same
# primitive; here we ignore repeated
# primitives.
next if $command_order == $prev_command;
$prev_command = $command_order;
if ($self->intercepts ($sub)) {
@args = $self->translate ($xmin, $ymin, $sub, @args);
}
warn "Replaying $sub (@args)\n" if $self->verbose == 2;
$im->$sub (@args);
}
$self->primstorage->perTileCleanup();
# return
return $im;
}
=head2 cleanup
$tiledImage->cleanup();
Call this after rendering all tiles, to allow the TiledImage object to perform cleanup operations (e.g. removing primitives from the database).
=cut
sub cleanup {
my $self = shift;
# use explicit hashrefs instead of AUTOLOAD'ed accessors,
# so that this method can be called by the signal handlers
if ($self->{'persistent'} == 0) {
warn "Deleting primitives from database\n";
$self->primstorage->GDDeletePrimitives;
}
$self->primstorage->cleanup();
# drop from cleanup list
delete $tiledImageCleanup{$self} if exists $tiledImageCleanup{$self};
}
=head2 Intercepted GD::Image methods
The following methods of B methods have analogous implementations in TiledImage:
=over 2
=item setPixel
=item line
=item dashedLine
=item rectangle
=item filledRectangle
=item polygon
=item openPolygon
=item unclosedPolygon
=item filledPolygon
=item fillPoly
=item ellipse
=item filledEllipse
=item arc
=item filledArc
=item copy
=item copyMerge
=item copyMergeGray
=item copyResized
=item copyResampled
=item copyRotated
=item string
=item stringUp
=item char
=item charUp
=item stringFT
=item stringFTcircle
=item colorAllocate
=item rgb
=item setBrush
=item setThickness
=back
=head2 Unimplemented GD::Image methods
The following GD::Image methods are B implemented by TiledImage:
=over
=item copyRotate90
=item copyRotate180
=item copyRotate270
=item copyFlipHorizontal
=item copyFlipVertical
=item copyTranspose
=item copyReverseTranspose
=item rotate180
=item flipHorizontal
=item flipVertical
=item fill
=item fillToBorder
=back
=cut
# THERE IS CLEARLY A PROBLEM WITH THESE SIGNAL HANDLERS, SO I'M TAKING THEM
# OUT AND PLACING THE HANDLER IN 'generate_tiles.pl' - it will be the
# responsibility of the instantiating script to clean up and disconnect! [AVU 2/4/06] !!!
# global_cleanup
# method to call cleanup on all existing TiledImage's
#sub global_cleanup {
# warn "in global_cleanup";
# my @tiledImage = keys %tiledImageCleanup;
# foreach my $tiledImage (@tiledImage) {
# cleanup ($tiledImage);
# }
#}
# signal handlers
#my $oldSigInt = $SIG{'INT'};
#$SIG{'INT'} = sub {
# warn "caught SIG{INT}"; #D!!!
# foreach my $tiledImage (keys %tiledImageCleanup) { # if program was interrupted, we should clean up database
# $tiledImage->{'persistent'} = 0; # entries made so far, no matter what the user specified
# }
# global_cleanup();
# &$oldSigInt() if defined $oldSigInt;
#};
#my $oldSigKill = $SIG{'KILL'};
#$SIG{'KILL'} = sub {
# warn "caught SIG{KILL}"; #D!!!
# foreach my $tiledImage (keys %tiledImageCleanup) { # if program was interrupted, we should clean up database
# $tiledImage->{'persistent'} = 0; # entries made so far, no matter what the user specified
# }
# global_cleanup();
# &$oldSigKill() if defined $oldSigKill;
#};
=head1 AUTHORS
Andrew Uzilov Eandrew.uzilov@gmail.comE
Mitchell Skinner Emitch_skinner@berkeley.eduE
Ian Holmes Eihh@berkeley.eduE
Copyright (c) 2007-2010 The Evolutionary Software Foundation
This package and its accompanying libraries are free software; you can
redistribute it and/or modify it under the terms of the LGPL (either
version 2.1, or at your option, any later version) or the Artistic
License 2.0. Refer to LICENSE for the full license text.
=cut
# End of package
1;