File Coverage

blib/lib/Graphics/DZI.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Graphics::DZI;
2              
3 2     2   202963 use strict;
  2         5  
  2         85  
4 2     2   14 use warnings;
  2         4  
  2         193  
5 2     2   2506 use POSIX;
  2         28730  
  2         27  
6              
7 2     2   8221 use Moose;
  0            
  0            
8              
9             our $log;
10             use Log::Log4perl;
11             BEGIN {
12             $log = Log::Log4perl->get_logger ();
13             }
14              
15             =head1 NAME
16              
17             Graphics::DZI - DeepZoom Image Pyramid Generation
18              
19             =head1 SYNOPSIS
20              
21             use Graphics::DZI;
22             my $dzi = Graphics::DZI (image => $image,
23             overlap => $overlap,
24             tilesize => $tilesize,
25             format => $format,
26             );
27              
28             write_file ('/var/www/xxx.xml', $dzi->descriptor);
29             $dzi->iterate ();
30             # !!! this does only display the tiles on the screen
31             # !!! see Graphics::DZI::Files for a subclass which
32             # !!! actually writes to files
33              
34             =head1 DESCRIPTION
35              
36             This base package generates tiles from a given image in such a way that they follow the DeepZoom
37             image pyramid scheme. Consequently this image becomes zoomable with tools like Seadragon.
38              
39             http://en.wikipedia.org/wiki/Deep_Zoom
40              
41             As this is a base class, you may want to look either at the I<deepzoom> script which operators on
42             the command line, or at one of the subclasses.
43              
44             =head1 INTERFACE
45              
46             =head2 Constructor
47              
48             The constructor accepts the following fields:
49              
50             =over
51              
52             =item C<image>
53              
54             The L<Image::Magick> object which is used as canvas.
55              
56             (since 0.05)
57              
58             The image can also be a whole stack (L<Image::Magick> allows you to do that). In that case the
59             bottom image is regarded as the one with the I<highest> degree of detail, and that is tiled first
60             (at the higher resolutions). Images up the stack are then taken in turn, until only the top-level
61             image remains. See C<pop> if you want to influence this policy.
62              
63             =item C<scale> (integer, default: 1)
64              
65             Specifies how much the image is stretched in the process.
66              
67             =item C<overlap> (integer, default: 4)
68              
69             Specifies how much the individual tiles overlap.
70              
71             =item C<tilesize> (integer, default: 128)
72              
73             Specifies the quadratic size of each tile.
74              
75             =item C<overlays> (list reference, default: [])
76              
77             An array of L<Graphics::DZI::Overlay> objects which describe how further images are supposed to be
78             composed onto the canvas image.
79              
80             =back
81              
82             =cut
83              
84             has 'image' => (isa => 'Image::Magick', is => 'rw', required => 1);
85             has 'scale' => (isa => 'Int', is => 'ro', default => 1);
86             has 'overlap' => (isa => 'Int', is => 'ro', default => 4);
87             has 'tilesize' => (isa => 'Int', is => 'ro', default => 256);
88             has 'format' => (isa => 'Str' , is => 'ro', default => 'png');
89             has 'overlays' => (isa => 'ArrayRef', is => 'rw', default => sub { [] });
90              
91             =head2 Methods
92              
93             =over
94              
95             =item B<crop>
96              
97             I<$tile> = I<$dzi>->crop (I<$scale>, I<$x>, I<$y>, I<$dx>, I<$dy>)
98              
99             Given the dimensions of a tile and a current (not the original)
100             stretch factor this method will return a tile object.
101              
102             =cut
103              
104             sub crop {
105             my $self = shift;
106             my $scale = shift;
107             my ($tx, $ty, $tdx, $tdy) = @_;
108              
109             my $tile = $self->{image}->[-1]->clone; # always take the "last" (lowest) image
110             if ($scale != 1) { # if our image is not quite the total space
111             # warn "new canvas tile scaled $scale";
112             my ($htx, $hty, $htdx, $htdy) = map { int ($_ / $scale) }
113             ($tx, $ty, $tdx, $tdy); # rescale this tile to the image dims we have
114             $log->debug ("rescale $tx, $ty --> $htx, $hty");
115             $tile->Crop (geometry => "${htdx}x${htdy}+${htx}+${hty}"); # cut that smaller one out
116             $tile->Resize ("${tdx}x${tdy}"); # and make it bigger
117             } else { # otherwise we are happy with what we have, dimension-wise
118             # warn "new canvas tile unscaled";
119             $tile->Crop (geometry => "${tdx}x${tdy}+${tx}+${ty}"); # cut one out
120             }
121             $log->debug ("tiled ${tdx}x${tdy}+${tx}+${ty}");
122             # $tile->Display();
123             return $tile;
124             }
125              
126             =item B<dimensions>
127              
128             (I<$W>, I<$H>) = I<$dzi>->dimensions ('total')
129              
130             (I<$W>, I<$H>) = I<$dzi>->dimensions ('canvas')
131              
132             This method computes how large (in pixels) the overall image will be. If C<canvas> is passed in,
133             then any overlays are ignored. Otherwise their size (with their squeeze factors) are used to blow up
134             the canvas, so that the overlays fit onto the canvas.
135              
136             =cut
137              
138             sub dimensions {
139             my $self = shift;
140             my $what = shift || 'total';
141              
142             my ($W, $H);
143             if ($what eq 'total') {
144             use List::Util qw(max);
145             my $max_squeeze = max map { $_->squeeze } @{ $self->overlays };
146             $self->{scale} = defined $max_squeeze ? $max_squeeze : 1;
147             ($W, $H) = map { $_ * $self->{scale} } $self->image->GetAttributes ('width', 'height');
148             } else {
149             ($W, $H) = $self->image->GetAttributes ('width', 'height');
150             }
151             use POSIX;
152             my $level = POSIX::ceil (log ($W > $H ? $W : $H) / log (2));
153             $log->debug (" dimensions: $W, $H --> levels: $level");
154             return ($W, $H, $level);
155             }
156              
157             =item B<iterate>
158              
159             I<$dzi>->iterate
160              
161             This method will generate all necessary tiles, invoking the I<manifest> method. You may want to
162             override that one, if you do not want the tiles to be simply displayed on screen :-) Any options
163             you add as parameters will be passed on to I<manifest>.
164              
165             B<NOTE>: During the process the image will be modified!
166              
167             =cut
168              
169             sub iterate {
170             my $self = shift;
171              
172             my $overlap_tilesize = $self->{tilesize} + 2 * $self->{overlap};
173             my $border_tilesize = $self->{tilesize} + $self->{overlap};
174              
175             my ($CWIDTH, $CHEIGHT, $CANVAS_LEVEL) = $self->dimensions ('canvas');
176             my ($WIDTH, $HEIGHT, $MAXLEVEL) = $self->dimensions ('total');
177              
178             my ($width, $height) = ($WIDTH, $HEIGHT);
179             my $scale = $self->{scale};
180             foreach my $level (reverse (0..$MAXLEVEL)) {
181              
182             my ($x, $col) = (0, 0);
183             while ($x < $width) {
184             my ($y, $row) = (0, 0);
185             my $tile_dx = $x == 0 ? $border_tilesize : $overlap_tilesize;
186             while ($y < $height) {
187              
188             my $tile_dy = $y == 0 ? $border_tilesize : $overlap_tilesize;
189              
190             my @tiles = grep { defined $_ } # only where there was some intersection
191             map {
192             $_->crop ($x, $y, $tile_dx, $tile_dy); # and for each overlay crop it onto a tile
193             } @{ $self->overlays }; # look at all overlays
194              
195             if (@tiles) { # if there is at least one overlay tile
196             my $tile = $self->crop ($scale, $x, $y, $tile_dx, $tile_dy); # do a crop in the canvas and try to get a tile
197             map {
198             $tile->Composite (image => $_, x => 0, 'y' => 0, compose => 'Over')
199             } @tiles;
200             $self->manifest ($tile, $level, $row, $col); # we flush it
201              
202             } elsif ($level <= $CANVAS_LEVEL) { # only if we are in the same granularity of the canvas
203             my $tile = $self->crop ($scale, $x, $y, $tile_dx, $tile_dy); # do a crop there and try to get a tile
204             #warn "tile "; $tile->Display();
205             $self->manifest ($tile, $level, $row, $col); # we flush it
206             }
207              
208             $y += ($tile_dy - 2 * $self->{overlap}); # progress y forward
209             $row++; # also the row count
210             }
211             $x += ($tile_dx - 2 * $self->{overlap}); # progress x forward
212             $col++; # the col count
213             }
214              
215             #-- resizing canvas
216             ($width, $height) = map { POSIX::ceil ($_ / 2) } ($width, $height);
217             if (@{ $self->overlays }) { # do we have overlays from which the scale came?
218             $scale /= 2; # the overall magnification is to be reduced
219             foreach my $o (@{ $self->overlays }) { # also resize all overlays
220             $o->halfsize;
221             }
222             } else {
223             # keep scale == 1
224             $self->{image}->Resize (width => $width, height => $height); # resize the canvas for next iteration
225             }
226             $self->pop; # for multi-level images
227             }
228             }
229              
230             =pod
231              
232             =item B<pop>
233              
234             (since 0.05)
235              
236             This method is only interesting to you if your canvas images is a whole stack, not just a single
237             image. In that case, it will remove the first of the stack (a shift) to make the next in the line
238             visible to the further tiling process. As the tiling starts with the highest resolution, your image
239             stack should be organized that the one with the most details is on the bottom (highest index, pushed
240             last).
241              
242             This method will do a C<pop> B<at every> half-sizing step and obviously only that long as there is
243             something to shift. If you are not happy with this default policy, you will have to subclass.
244              
245             =cut
246              
247             sub pop {
248             my $self = shift;
249             pop @{ $self->image } if scalar @{ $self->image } > 1; # if we have a stack of images, remove that with the most details (i.e. first)
250             }
251              
252             =item B<manifest>
253              
254             I<$dzi>->manifest (I<$tile>, I<$level>, I<$row>, I<$col>)
255              
256             This method will get one tile as parameter and will simply display the tile on the screen.
257             Subclasses which want to persist the tiles, can use the additional parameters (level, row and
258             column) to create file names.
259              
260             =cut
261              
262             sub manifest {
263             my $self = shift;
264             my $tile = shift;
265             $tile->Display();
266             }
267              
268             =item B<descriptor>
269              
270             I<$string> = I<$dzi>->descriptor
271              
272             This method returns the DZI XML descriptor as string.
273              
274             =cut
275              
276             sub descriptor {
277             my $self = shift;
278             my $overlap = $self->{overlap};
279             my $tilesize = $self->{tilesize};
280             my $format = $self->{format};
281             my ($width, $height) = $self->dimensions ('total');
282             return qq{<?xml version='1.0' encoding='UTF-8'?>
283             <Image TileSize='$tilesize'
284             Overlap='$overlap'
285             Format='$format'
286             xmlns='http://schemas.microsoft.com/deepzoom/2008'>
287             <Size Width='$width' Height='$height'/>
288             </Image>
289             };
290              
291              
292             }
293              
294             =back
295              
296             =head1 TODOs
297              
298             See the TODOs file in the distribution.
299              
300             =head1 AUTHOR
301              
302             Robert Barta, C<< <drrho at cpan.org> >>
303              
304             =head1 COPYRIGHT & LICENSE
305              
306             Copyright 2010 Robert Barta, all rights reserved.
307              
308             This program is free software; you can redistribute it and/or modify it under the same terms as Perl
309             itself.
310              
311             =cut
312              
313             our $VERSION = '0.05';
314              
315             "against all odds";