File Coverage

blib/lib/Image/Tileset.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Image::Tileset;
2              
3 1     1   34564 use strict;
  1         2  
  1         41  
4 1     1   5 use warnings;
  1         2  
  1         28  
5 1     1   506 use Image::Magick;
  0            
  0            
6             use XML::Simple;
7             use Data::Dumper;
8              
9             our $VERSION = '0.01';
10              
11             =head1 NAME
12              
13             Image::Tileset - A tileset loader.
14              
15             =head1 SYNOPSIS
16              
17             use Image::Tileset;
18              
19             my $ts = new Image::Tileset (
20             image => "my-tileset.png",
21             xml => "my-tileset.xml",
22             );
23              
24             open (OUT, ">grass.png");
25             binmode OUT;
26             print OUT $ts->tile("grass");
27             close (OUT);
28              
29             =head1 DESCRIPTION
30              
31             Image::Tileset is a simple tileset image loader. The preferred usage is to have
32             an XML description file alongside the tileset image that describes how the
33             tiles are to be sliced up.
34              
35             The module supports "simple" tilesets (where all tiles have a uniform width and
36             height, though they don't need to begin at the top left corner of the image)
37             as well as "fixed" tilesets (where you need to specify the exact pixel coords
38             of every tile).
39              
40             It also supports the management of animations for your tiles (but not the means
41             to display them; this is left up to your front-end code. There is a demo that
42             uses Perl/Tk to give you an idea how to do this).
43              
44             =head1 SPECIFICATION FILE
45              
46             Tileset images are paired with a "specification file," which describes how the
47             image is to be sliced up into the different tiles.
48              
49             The spec file is usually an XML document, and it's read with L.
50             If you wish, you can also send the spec data as a Perl data structure, skipping
51             the XML part.
52              
53             An example XML file is as follows, and shows all the capabilities of the
54             spec file markup:
55              
56            
57            
58            
63            
64            
72            
73            
74            
75              
76            
80            
81            
82            
83            
84              
85            
89            
90            
94            
95            
96              
97            
103            
104             water-1
105             water-2
106             water-3
107             water-2
108            
109            
110              
111             Your application can also provide spec data as a Perl structure instead of as
112             XML. Here is an example of the above XML as a Perl structure:
113              
114             $ts->spec( [
115             {
116             type => 'tiles',
117             size => '32x32',
118             x => 0,
119             y => 0,
120             tile => [
121             { x => 0, y => 0, id => 'grass' },
122             { x => 1, y => 0, id => 'sand' },
123             { x => 2, y => 0, id => 'dirt' },
124             { x => 0, y => 1, id => 'water-1' },
125             { x => 1, y => 1, id => 'water-2' },
126             { x => 2, y => 1, id => 'water-3' },
127             },
128             },
129             {
130             type => 'fixed',
131             tile => [
132             { x1 => 96, y1 => 0, x2 => 128, y2 => 96, id => 'avatar' },
133             ],
134             },
135             {
136             type => 'animation',
137             id => 'water',
138             speed => 200,
139             tile => [ 'water-1', 'water-2', 'water-3', 'water-2' ],
140             },
141             ]);
142              
143             See the examples in the C folder for more information.
144              
145             =head1 METHODS
146              
147             =head2 new (hash options)
148              
149             Create a new C object. Options include:
150              
151             bool debug: Debug mode (prints stuff to the terminal on STDERR)
152             string xml: Path to an XML spec file that describes the image.
153             hash spec: Spec data in Perl data structure form (skip XML file).
154             string image: Path to the image file.
155              
156             If you provide C, the XML will be parsed and refined immediately. If you
157             provide C, it will be refined immediately. If you provide C, the
158             image will be loaded immediately.
159              
160             =cut
161              
162             sub new {
163             my $class = shift;
164              
165             my $self = {
166             debug => 0, # Debug mode
167             xml => '', # XML file
168             spec => [], # Spec data (XML data in Perl form)
169             image => '', # Image file
170             magick => undef, # Image::Magick object
171             error => '', # Last error state
172             tiles => {}, # Tile positions in tileset
173             animations => {}, # Animation information
174             @_,
175             };
176             bless ($self,$class);
177              
178             $self->{magick} = Image::Magick->new;
179              
180             # If given an image, load it.
181             if (length $self->{image}) {
182             $self->image ($self->{image});
183             $self->{image} = '';
184             }
185              
186             # If given an XML file, load it.
187             if (length $self->{xml}) {
188             $self->xml ($self->{xml});
189             $self->{xml} = '';
190             }
191              
192             # If given a spec, load it.
193             if (ref($self->{spec}) eq "ARRAY" && scalar @{$self->{spec}} > 0) {
194             $self->refine ($self->{spec});
195             $self->{spec} = [];
196             }
197              
198             return $self;
199             }
200              
201             sub debug {
202             my ($self,$line) = @_;
203             return unless $self->{debug};
204             print STDERR "$line\n";
205             }
206              
207             =head2 void error ()
208              
209             Print the last error message given. Example:
210              
211             $tileset->image("myfile.png") or die $tileset->error;
212              
213             =cut
214              
215             sub error {
216             my ($self,$error) = @_;
217             if (defined $error) {
218             $self->{error} = $error;
219             }
220             return $self->{error};
221             }
222              
223             =head2 bool image (string filepath)
224              
225             Load an image file with C. The object is just kept in memory for
226             when you actually want to get a tile from it.
227              
228             Returns 1 on success, undef on error.
229              
230             =cut
231              
232             sub image {
233             my ($self,$image) = @_;
234             $self->debug("Attempting to load image file from $image");
235              
236             # Exists?
237             if (!-e $image) {
238             $self->error("Can't load image file $image: file not found!");
239             return undef;
240             }
241              
242             # Load it with Image::Magick.
243             my $x = $self->{magick}->Read($image);
244             if ($x) {
245             warn $x;
246             return undef;
247             }
248              
249             return 1;
250             }
251              
252             =head2 bool data (bin data)
253              
254             If your program already has the image's binary data in memory, it can send it
255             directly to this function. It will create an C object based off
256             the binary data directly, instead of needing to read a file from disk.
257              
258             Returns 1 on success, undef on error.
259              
260             =cut
261              
262             sub data {
263             my ($self,$data) = @_;
264              
265             # Load it with Image::Magick.
266             my $x = $self->{magick}->BlobToImage($data);
267             if ($x) {
268             warn $x;
269             return undef;
270             }
271              
272             return 1;
273             }
274              
275             =head2 void clear ()
276              
277             Clear the internal C object, unloading the tileset.
278              
279             =cut
280              
281             sub clear {
282             my $self = shift;
283              
284             undef $self->{magick};
285             $self->{magick} = new Image::Magick();
286             }
287              
288             =head2 bool xml (string xmldata | string specfile)
289              
290             Load a specification file from XML. Pass either XML data or the path to a
291             file name.
292              
293             If the data sent to this command begins with a left chevron, E, or contains
294             newlines, it is assumed to be XML data; otherwise the filesystem is queried.
295              
296             Returns 1 on success, undef on error.
297              
298             =cut
299              
300             sub xml {
301             my ($self,$file) = @_;
302              
303             # Load it with XML::Simple.
304             my $o_xs = new XML::Simple (
305             RootName => 'tileset',
306             ForceArray => 1,
307             KeyAttr => 'id',
308             );
309              
310             my $xs = {};
311             if ($file =~ /^\s*
312             $self->debug("Attempting to load XML data $file!");
313             $xs = $o_xs->XMLin($file);
314             }
315             elsif (-f $file) {
316             $self->debug("Attempting to load XML from file $file!");
317             $xs = $o_xs->XMLin($file);
318             }
319             else {
320             $self->error("Couldn't load XML data: file not found!");
321             return undef;
322             }
323              
324             # Does it look good?
325             if (!exists $xs->{layout}) {
326             $self->error("No layout information was found in XML spec file!");
327             return undef;
328             }
329              
330             # Refine it. We want pixel coords of every named tile.
331             $self->refine($xs->{layout}) or return undef;
332              
333             return 1;
334             }
335              
336             =head2 bool refine (array spec)
337              
338             Refines the specification data. The spec describes how the image is cut up;
339             C goes through that and stores the exact pixel coordinates of every
340             tile named in the spec, for quick extraction when the tile is wanted.
341              
342             This method is called automatically when an XML spec file is parsed. If you
343             pass in a C during the call to C, this method will be called
344             automatically for your spec. If you want to load a spec directly after you've
345             created the object, you can call C directly with your new spec.
346              
347             =cut
348              
349             sub refine {
350             my ($self,$spec) = @_;
351              
352             # It must be an array.
353             if (ref($spec) ne "ARRAY") {
354             $self->error("Spec file must be an array of layouts!");
355             return undef;
356             }
357              
358             # Clear the currently loaded data.
359             delete $self->{tiles};
360             delete $self->{animations};
361             $self->{tiles} = {};
362             $self->{animations} = {};
363              
364             # Go through the layouts.
365             $self->debug("Refining the specification...");
366             foreach my $layout (@{$spec}) {
367             my $type = lc($layout->{type});
368              
369             # Supported layout types:
370             # tiles
371             # fixed
372             # animation
373             if ($type eq "tiles") {
374             # How big are the tiles?
375             if ($layout->{size} !~ /^\d+x\d+$/) {
376             $self->error("Syntax error in spec: 'tiles' layout but no valid tile 'size' set!");
377             return undef;
378             }
379             my ($width,$height) = split(/x/, $layout->{size}, 2);
380             $self->debug("Looking for 'tiles' layout; tile dimensions are $width x $height");
381              
382             # Offset coords.
383             my $x = $layout->{x} || 0;
384             my $y = $layout->{y} || 0;
385              
386             # Collect the tiles.
387             foreach my $id (keys %{$layout->{tile}}) {
388             # Tile coordinates.
389             my $tileX = $layout->{tile}->{$id}->{x};
390             my $tileY = $layout->{tile}->{$id}->{y};
391              
392             # Pixel coordinates.
393             my $x1 = $x + ($width * $tileX);
394             my $x2 = $x1 + $width;
395             my $y1 = $y + ($height * $tileY);
396             my $y2 = $y1 + $height;
397             $self->debug("Found tile '$id' at pixel coords $x1,$y1,$x2,$y2");
398              
399             # Store it.
400             $self->{tiles}->{$id} = [ $x1, $y1, $x2, $y2 ];
401             }
402             }
403             elsif ($type eq "fixed") {
404             # Fixed is easy, we already have all the coords we need.
405             $self->debug("Looking for 'fixed' tiles");
406             foreach my $id (keys %{$layout->{tile}}) {
407             # Pixel coordinates.
408             my $x1 = $layout->{tile}->{$id}->{x1};
409             my $y1 = $layout->{tile}->{$id}->{y1};
410             my $x2 = $layout->{tile}->{$id}->{x2};
411             my $y2 = $layout->{tile}->{$id}->{y2};
412             $self->debug("Found tile '$id' at pixel coords $x1,$y1,$x2,$y2");
413              
414             # Store it.
415             $self->{tiles}->{$id} = [ $x1, $y1, $x2, $y2 ];
416             }
417             }
418             elsif ($type eq "animation") {
419             # Animations just have a list of tiles involved and some meta info.
420             my $id = $layout->{id}; # Name of the animation sprite
421             my $speed = $layout->{speed} || 500; # Speed of animation, in milliseconds
422             $self->{animations}->{$id} = {
423             speed => $speed,
424             tiles => $layout->{tile},
425             };
426             }
427             else {
428             warn "Unknown layout type '$type'!";
429             }
430             }
431             }
432              
433             =head2 data tiles ()
434              
435             Return the tile coordinate information. In array context, returns a list of the
436             tile ID's. In scalar context, returns a hash reference in the following format:
437              
438             {
439             'tile-id' => [
440             x1, y1,
441             x2, y2
442             ],
443             ...
444             };
445              
446             =cut
447              
448             sub tiles {
449             my ($self) = @_;
450             return wantarray ? sort keys %{$self->{tiles}} : $self->{tiles};
451             }
452              
453             =head2 data animations ()
454              
455             Return the animation information. In array context, returns a list of the
456             animation ID's. In scalar context, returns a hash reference in the following
457             format:
458              
459             {
460             'animation-id' => {
461             speed => '...',
462             tiles => [
463             'tile-id',
464             ...
465             ],
466             },
467             };
468              
469             =cut
470              
471             sub animations {
472             my ($self) = @_;
473             return wantarray ? sort keys %{$self->{animations}} : $self->{animations};
474             }
475              
476             =head2 bin tile (string id[, hash options])
477              
478             Get the binary data of one of the tiles, named C, from the original
479             tileset.
480              
481             You can optionally pass in a hash of named options. The following options are
482             supported:
483              
484             int scale: Scale the tile before returning its data. This is a number to
485             scale it by, for example '2' returns it at 200% its original size,
486             while '0.5' returns it at 50% its original size.
487             str size: Resize the tile to this exact size before returning it, for
488             example '64x64'.
489             bool magick: If true, returns the Image::Magick object instead of the binary
490             data. If you want to make additional modifications to the image
491             (i.e. edit its colors, apply special effects), use the 'magick'
492             option and then apply the effects yourself.
493              
494             The options C and C are mutually exclusive.
495              
496             Examples:
497              
498             # The tiles are 32x32, but lets scale it 2X so we get back a 64x64 tile
499             my $tile = $ts->tile("grass", scale => 2);
500              
501             # Get it at 1/2 its original size, or 16x16
502             my $tile = $ts->tile("grass", scale => 0.5);
503              
504             # Get it at 24x24 pixels
505             my $tile = $ts->tile("grass", size => "24x24");
506              
507             Returns undef on error.
508              
509             =cut
510              
511             sub tile {
512             my ($self,$id,%opts) = @_;
513              
514             # Tile exists?
515             if (!exists $self->{tiles}->{$id}) {
516             $self->error("No tile named '$id' in tileset!");
517             return undef;
518             }
519              
520             # Slice the image.
521             my $slice = $self->slice ($id);
522              
523             # Are they transforming the image?
524             if (exists $opts{scale} || exists $opts{size}) {
525             # Get the tile's size.
526             my $width = $self->{tiles}->{$id}->[2] - $self->{tiles}->{$id}->[0];
527             my $height = $self->{tiles}->{$id}->[3] - $self->{tiles}->{$id}->[1];
528              
529             if (exists $opts{scale}) {
530             if ($opts{scale} !~ /^[0-9\.]+$/) {
531             $self->error("Invalid scale factor: $opts{scale}");
532             return undef;
533             }
534              
535             $width = int($width * $opts{scale});
536             $height = int($height * $opts{scale});
537             }
538             elsif (exists $opts{size}) {
539             if ($opts{size} !~ /^\d+x\d+$/) {
540             $self->error("Invalid scale size: $opts{size}");
541             return undef;
542             }
543              
544             ($width,$height) = split(/x/, $opts{size}, 2);
545             }
546              
547             # Scale it.
548             $self->debug("Resizing tile down to $width x $height");
549             $slice->Scale (width => $width, height => $height);
550             }
551              
552             # Do they want the magick object?
553             if (exists $opts{magick} && $opts{magick}) {
554             return $slice;
555             }
556              
557             my $png = $slice->ImageToBlob();
558             return $png;
559             }
560              
561             =head2 data animation (string id)
562              
563             Get the animation information about a specific animation ID.
564              
565             Returns data in the format:
566              
567             {
568             speed => '...',
569             tiles => [ ... ],
570             };
571              
572             Returns undef on error.
573              
574             =cut
575              
576             sub animation {
577             my ($self,$id) = @_;
578              
579             # Animation exists?
580             if (!exists $self->{animations}->{$id}) {
581             $self->error("No animation named '$id' in tileset!");
582             return undef;
583             }
584              
585             return $self->{animations}->{$id};
586             }
587              
588             =head2 ImageMagick slice (string id)
589              
590             Returns an C object that contains the sliced tile from the
591             original tileset. This is mostly for internal use only.
592              
593             =cut
594              
595             sub slice {
596             my ($self,$id) = @_;
597              
598             # Tile exists?
599             if (!exists $self->{tiles}->{$id}) {
600             $self->error("No tile named '$id' in tileset!");
601             return undef;
602             }
603              
604             # Get the dimensions of the tile.
605             my $width = $self->{tiles}->{$id}->[2] - $self->{tiles}->{$id}->[0]; # x2 - x1
606             my $height = $self->{tiles}->{$id}->[3] - $self->{tiles}->{$id}->[1]; # y2 - y1
607             if ($width < 1 || $height < 1) {
608             $self->error("Tile '$id' has impossible dimensions: $width x $height");
609             return undef;
610             }
611              
612             my $dims = $width . 'x' . $height;
613              
614             # Make a new ImageMagick object.
615             my $slice = $self->{magick}->Clone();
616              
617             # Crop it.
618             my $x = $self->{tiles}->{$id}->[0];
619             my $y = $self->{tiles}->{$id}->[1];
620             my $crop = $dims . "+$x+$y";
621             $self->debug("Cropping image clone to $crop for tile $id");
622             $slice->Crop($crop);
623              
624             return $slice;
625             }
626              
627             =head1 SEE ALSO
628              
629             L, which powers this module's graphics handling.
630              
631             L, which powers this module's XML parsing.
632              
633             =head1 CHANGES
634              
635             0.01 Fri Jan 15 2010
636             - Initial release.
637              
638             =head1 COPYRIGHT
639              
640             The tileset graphics included for demonstration purposes are from RPG Maker
641             2003 and are copyright (C) Enterbrain.
642              
643             Code written by Noah Petherbridge, http://www.kirsle.net/
644              
645             This library is free software; you can redistribute it and/or modify it under
646             the same terms as Perl itself, either Perl version 5.10.0 or, at your option,
647             any later version of Perl 5 you may have available.
648              
649             =cut
650              
651             1;