blib/lib/CSS/SpriteMaker.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 CSS::SpriteMaker; | ||||||
2 | |||||||
3 | 1 | 1 | 13334 | use strict; | |||
1 | 2 | ||||||
1 | 22 | ||||||
4 | 1 | 1 | 3 | use warnings; | |||
1 | 1 | ||||||
1 | 18 | ||||||
5 | |||||||
6 | 1 | 1 | 3 | use File::Find; | |||
1 | 3 | ||||||
1 | 44 | ||||||
7 | 1 | 1 | 172 | use Image::Magick; | |||
0 | |||||||
0 | |||||||
8 | use List::Util qw(max); | ||||||
9 | |||||||
10 | use Module::Pluggable | ||||||
11 | search_path => ['CSS::SpriteMaker::Layout'], | ||||||
12 | except => qr/CSS::SpriteMaker::Layout::Utils::.*/, | ||||||
13 | require => 1, | ||||||
14 | inner => 0; | ||||||
15 | |||||||
16 | use POSIX qw(ceil); | ||||||
17 | |||||||
18 | |||||||
19 | =head1 NAME | ||||||
20 | |||||||
21 | CSS::SpriteMaker - Combine several images into a single CSS sprite | ||||||
22 | |||||||
23 | =head1 VERSION | ||||||
24 | |||||||
25 | Version 1.01 | ||||||
26 | |||||||
27 | =cut | ||||||
28 | |||||||
29 | our $VERSION = '1.01'; | ||||||
30 | |||||||
31 | |||||||
32 | =head1 SYNOPSIS | ||||||
33 | |||||||
34 | use CSS::SpriteMaker; | ||||||
35 | |||||||
36 | my $SpriteMaker = CSS::SpriteMaker->new( | ||||||
37 | verbose => 1, # optional | ||||||
38 | |||||||
39 | # | ||||||
40 | # Options that impact the lifecycle of css class name generation | ||||||
41 | # | ||||||
42 | # if provided will replace the default logic for creating css classnames | ||||||
43 | # out of image filenames. This filename-to-classname is the FIRST step | ||||||
44 | # of css classnames creation. It's safe to return invalid css characters | ||||||
45 | # in this subroutine. They will be cleaned up internally. | ||||||
46 | # | ||||||
47 | rc_filename_to_classname => sub { my $filename = shift; ... } # optional | ||||||
48 | |||||||
49 | # ... cleaning stage happens (all non css safe characters are removed) | ||||||
50 | |||||||
51 | # This adds a prefix to all the css class names. This is called after | ||||||
52 | # the cleaning stage internally. Don't mess with invalid CSS characters! | ||||||
53 | # | ||||||
54 | css_class_prefix => 'myicon-', | ||||||
55 | |||||||
56 | # This is the last step. Change here whatever part of the final css | ||||||
57 | # class name. | ||||||
58 | # | ||||||
59 | rc_override_classname => sub { my $css_class = shift; ... } # optional | ||||||
60 | ); | ||||||
61 | |||||||
62 | $SpriteMaker->make_sprite( | ||||||
63 | source_images => ['/path/to/imagedir', '/images/img1.png', '/img2.png']; | ||||||
64 | target_file => '/tmp/test/mysprite.png', | ||||||
65 | layout_name => 'Packed', # optional | ||||||
66 | remove_source_padding => 1, # optional | ||||||
67 | enable_colormap => 1, # optional | ||||||
68 | add_extra_padding => 31, # optional +31px padding around all images | ||||||
69 | format => 'png8', # optional | ||||||
70 | ); | ||||||
71 | |||||||
72 | $SpriteMaker->print_css(); | ||||||
73 | |||||||
74 | $SpriteMaker->print_html(); | ||||||
75 | |||||||
76 | OR | ||||||
77 | |||||||
78 | my $SpriteMaker = CSS::SpriteMaker->new(); | ||||||
79 | |||||||
80 | $SpriteMaker->make_sprite( | ||||||
81 | source_dir => '/tmp/test/images', | ||||||
82 | target_file => '/tmp/test/mysprite.png', | ||||||
83 | ); | ||||||
84 | |||||||
85 | $SpriteMaker->print_css(); | ||||||
86 | |||||||
87 | $SpriteMaker->print_html(); | ||||||
88 | |||||||
89 | OR | ||||||
90 | |||||||
91 | my $SpriteMaker = CSS::SpriteMaker->new(); | ||||||
92 | |||||||
93 | $SpriteMaker->compose_sprite( | ||||||
94 | parts => [ | ||||||
95 | { source_dir => 'sample_icons', | ||||||
96 | layout_name => 'Packed', | ||||||
97 | add_extra_padding => 32 # just add extra padding in one part | ||||||
98 | }, | ||||||
99 | { source_dir => 'more_icons', | ||||||
100 | layout => { | ||||||
101 | name => 'FixedDimension', | ||||||
102 | options => { | ||||||
103 | 'dimension' => 'horizontal', | ||||||
104 | 'n' => 4, | ||||||
105 | } | ||||||
106 | } | ||||||
107 | }, | ||||||
108 | ], | ||||||
109 | # the composing layout | ||||||
110 | layout => { | ||||||
111 | name => 'FixedDimension', | ||||||
112 | options => { | ||||||
113 | n => 2, | ||||||
114 | } | ||||||
115 | }, | ||||||
116 | target_file => 'composite.png', | ||||||
117 | ); | ||||||
118 | |||||||
119 | $SpriteMaker->print_css(); | ||||||
120 | |||||||
121 | $SpriteMaker->print_html(); | ||||||
122 | |||||||
123 | ALTERNATIVELY | ||||||
124 | |||||||
125 | you can generate a fake CSS only containing the original images... | ||||||
126 | |||||||
127 | my $SpriteMakerOnlyCss = CSS::SpriteMaker->new(); | ||||||
128 | |||||||
129 | $SpriteMakerOnlyCss->print_fake_css( | ||||||
130 | filename => 'some/fake_style.css', | ||||||
131 | source_dir => 'sample_icons' | ||||||
132 | ); | ||||||
133 | |||||||
134 | |||||||
135 | =head1 DESCRIPTION | ||||||
136 | |||||||
137 | A CSS Sprite is an image obtained by arranging many smaller images on a 2D | ||||||
138 | canvas, according to a certain layout. | ||||||
139 | |||||||
140 | Transferring one larger image is generally faster than transferring multiple | ||||||
141 | images separately as it greatly reduces the number of HTTP requests (and | ||||||
142 | overhead) necessary to render the original images on the browser. | ||||||
143 | |||||||
144 | =head1 PUBLIC METHODS | ||||||
145 | |||||||
146 | =head2 new | ||||||
147 | |||||||
148 | Create and configure a new CSS::SpriteMaker object. | ||||||
149 | |||||||
150 | The object can be initialised as follows: | ||||||
151 | |||||||
152 | my $SpriteMaker = CSS::SpriteMaker->new( | ||||||
153 | rc_filename_to_classname => sub { my $filename = shift; ... }, # optional | ||||||
154 | css_class_prefix => 'myicon-', # optional | ||||||
155 | rc_override_classname => sub { my $css_class = shift; ... } # optional | ||||||
156 | source_dir => '/tmp/test/images', # optional | ||||||
157 | target_file => '/tmp/test/mysprite.png' # optional | ||||||
158 | remove_source_padding => 1, # optional | ||||||
159 | add_extra_padding => 1, # optional | ||||||
160 | verbose => 1, # optional | ||||||
161 | enable_colormap => 1, # optional | ||||||
162 | ); | ||||||
163 | |||||||
164 | Default values are set to: | ||||||
165 | |||||||
166 | =over 4 | ||||||
167 | |||||||
168 | =item remove_source_padding : false, | ||||||
169 | |||||||
170 | =item verbose : false, | ||||||
171 | |||||||
172 | =item enable_colormap : false, | ||||||
173 | |||||||
174 | =item format : png, | ||||||
175 | |||||||
176 | =item css_class_prefix : '' | ||||||
177 | |||||||
178 | =back | ||||||
179 | |||||||
180 | The parameter rc_filename_to_classname is a code reference to a function that | ||||||
181 | allow to customize the way class names are generated. This function should take | ||||||
182 | one parameter as input and return a class name | ||||||
183 | |||||||
184 | =cut | ||||||
185 | |||||||
186 | sub new { | ||||||
187 | my $class = shift; | ||||||
188 | my %opts = @_; | ||||||
189 | |||||||
190 | # defaults | ||||||
191 | $opts{remove_source_padding} //= 0; | ||||||
192 | $opts{add_extra_padding} //= 0; | ||||||
193 | $opts{verbose} //= 0; | ||||||
194 | $opts{format} //= 'png'; | ||||||
195 | $opts{layout_name} //= 'Packed'; | ||||||
196 | $opts{css_class_prefix} //= ''; | ||||||
197 | $opts{enable_colormap} //= 0; | ||||||
198 | |||||||
199 | my $self = { | ||||||
200 | css_class_prefix => $opts{css_class_prefix}, | ||||||
201 | source_images => $opts{source_images}, | ||||||
202 | source_dir => $opts{source_dir}, | ||||||
203 | target_file => $opts{target_file}, | ||||||
204 | is_verbose => $opts{verbose}, | ||||||
205 | format => $opts{format}, | ||||||
206 | remove_source_padding => $opts{remove_source_padding}, | ||||||
207 | enable_colormap => $opts{enable_colormap}, | ||||||
208 | add_extra_padding => $opts{add_extra_padding}, | ||||||
209 | output_css_file => $opts{output_css_file}, | ||||||
210 | output_html_file => $opts{output_html_file}, | ||||||
211 | |||||||
212 | # layout_name is used as default | ||||||
213 | layout => { | ||||||
214 | name => $opts{layout_name}, | ||||||
215 | # no options by default | ||||||
216 | options => {} | ||||||
217 | }, | ||||||
218 | rc_filename_to_classname => $opts{rc_filename_to_classname}, | ||||||
219 | rc_override_classname => $opts{rc_override_classname}, | ||||||
220 | |||||||
221 | # the maximum color value | ||||||
222 | color_max => 2 ** Image::Magick->QuantumDepth - 1, | ||||||
223 | }; | ||||||
224 | |||||||
225 | return bless $self, $class; | ||||||
226 | } | ||||||
227 | |||||||
228 | =head2 compose_sprite | ||||||
229 | |||||||
230 | Compose many sprite layouts into one sprite. This is done by applying | ||||||
231 | individual layout separately, then merging the final result together using a | ||||||
232 | glue layout. | ||||||
233 | |||||||
234 | my $is_error = $SpriteMaker->compose_sprite ( | ||||||
235 | parts => [ | ||||||
236 | { source_images => ['some/file.png', 'path/to/some_directory'], | ||||||
237 | layout_name => 'Packed', | ||||||
238 | }, | ||||||
239 | { source_images => ['path/to/some_directory'], | ||||||
240 | layout => { | ||||||
241 | name => 'DirectoryBased', | ||||||
242 | } | ||||||
243 | include_in_css => 0, # optional | ||||||
244 | remove_source_padding => 1, # optional (defaults to 0) | ||||||
245 | enable_colormap => 1, # optional (defaults to 0) | ||||||
246 | add_extra_padding => 40, # optional, px (defaults to 0px) | ||||||
247 | }, | ||||||
248 | ], | ||||||
249 | # arrange the previous two layout using a glue layout | ||||||
250 | layout => { | ||||||
251 | name => 'FixedDimension', | ||||||
252 | dimension => 'horizontal', | ||||||
253 | n => 2 | ||||||
254 | } | ||||||
255 | target_file => 'sample_sprite.png', | ||||||
256 | format => 'png8', # optional, default is png | ||||||
257 | ); | ||||||
258 | |||||||
259 | Note the optional include_in_css option, which allows to exclude a group of | ||||||
260 | images from the CSS (still including them in the resulting image). | ||||||
261 | |||||||
262 | =cut | ||||||
263 | |||||||
264 | sub compose_sprite { | ||||||
265 | my $self = shift; | ||||||
266 | my %options = @_; | ||||||
267 | |||||||
268 | if (exists $options{layout}) { | ||||||
269 | return $self->_compose_sprite_with_glue(%options); | ||||||
270 | } | ||||||
271 | else { | ||||||
272 | return $self->_compose_sprite_without_glue(%options); | ||||||
273 | } | ||||||
274 | } | ||||||
275 | |||||||
276 | =head2 make_sprite | ||||||
277 | |||||||
278 | Creates the sprite file out of the specifed image files or directories, and | ||||||
279 | according to the given layout name. | ||||||
280 | |||||||
281 | my $is_error = $SpriteMaker->make_sprite( | ||||||
282 | source_images => ['some/file.png', path/to/some_directory], | ||||||
283 | target_file => 'sample_sprite.png', | ||||||
284 | layout_name => 'Packed', | ||||||
285 | |||||||
286 | # all imagemagick supported formats | ||||||
287 | format => 'png8', # optional, default is png | ||||||
288 | ); | ||||||
289 | |||||||
290 | returns true if an error occurred during the procedure. | ||||||
291 | |||||||
292 | Available layouts are: | ||||||
293 | |||||||
294 | =over 4 | ||||||
295 | |||||||
296 | =item * Packed: try to pack together the images as much as possible to reduce the | ||||||
297 | image size. | ||||||
298 | |||||||
299 | =item * DirectoryBased: put images under the same directory on the same horizontal | ||||||
300 | row. Order alphabetically within each row. | ||||||
301 | |||||||
302 | =item * FixedDimension: arrange a maximum of B |
||||||
303 | column). | ||||||
304 | |||||||
305 | =back | ||||||
306 | |||||||
307 | =cut | ||||||
308 | |||||||
309 | sub make_sprite { | ||||||
310 | my $self = shift; | ||||||
311 | my %options = @_; | ||||||
312 | |||||||
313 | my $rh_sources_info = $self->_ensure_sources_info(%options); | ||||||
314 | my $Layout = $self->_ensure_layout(%options, | ||||||
315 | rh_sources_info => $rh_sources_info | ||||||
316 | ); | ||||||
317 | |||||||
318 | return $self->_write_image(%options, | ||||||
319 | Layout => $Layout, | ||||||
320 | rh_sources_info => $rh_sources_info | ||||||
321 | ); | ||||||
322 | } | ||||||
323 | |||||||
324 | =head2 print_css | ||||||
325 | |||||||
326 | Creates and prints the css stylesheet for the sprite that was previously | ||||||
327 | produced. | ||||||
328 | |||||||
329 | You can specify the filename or the filehandle where the output CSS should be | ||||||
330 | written: | ||||||
331 | |||||||
332 | $SpriteMaker->print_css( | ||||||
333 | filehandle => $fh, | ||||||
334 | ); | ||||||
335 | |||||||
336 | OR | ||||||
337 | |||||||
338 | $SpriteMaker->print_css( | ||||||
339 | filename => 'relative/path/to/style.css', | ||||||
340 | ); | ||||||
341 | |||||||
342 | Optionally you can provide the name of the image file that should be included in | ||||||
343 | the CSS file instead of the default one: | ||||||
344 | |||||||
345 | # within the style.css file, override the default path to the sprite image | ||||||
346 | # with "custom/path/to/sprite.png". | ||||||
347 | # | ||||||
348 | $SpriteMaker->print_css( | ||||||
349 | filename => 'relative/path/to/style.css', | ||||||
350 | sprite_filename => 'custom/path/to/sprite.png', # optional | ||||||
351 | ); | ||||||
352 | |||||||
353 | |||||||
354 | NOTE: make_sprite() must be called before this method is called. | ||||||
355 | |||||||
356 | =cut | ||||||
357 | |||||||
358 | sub print_css { | ||||||
359 | my $self = shift; | ||||||
360 | my %options = @_; | ||||||
361 | |||||||
362 | my $rh_sources_info = $self->_ensure_sources_info(%options); | ||||||
363 | my $Layout = $self->_ensure_layout(%options, | ||||||
364 | rh_sources_info => $rh_sources_info | ||||||
365 | ); | ||||||
366 | |||||||
367 | my $fh = $self->_ensure_filehandle_write(%options); | ||||||
368 | |||||||
369 | $self->_verbose(" * writing css file"); | ||||||
370 | |||||||
371 | my $target_image_filename; | ||||||
372 | if (exists $options{sprite_filename} && $options{sprite_filename}) { | ||||||
373 | $target_image_filename = $options{sprite_filename}; | ||||||
374 | } | ||||||
375 | |||||||
376 | my $stylesheet = $self->_get_stylesheet_string({ | ||||||
377 | target_image_filename => $target_image_filename, | ||||||
378 | use_full_images => 0 | ||||||
379 | }, | ||||||
380 | %options | ||||||
381 | ); | ||||||
382 | |||||||
383 | print $fh $stylesheet; | ||||||
384 | |||||||
385 | return 0; | ||||||
386 | } | ||||||
387 | |||||||
388 | =head2 print_fake_css | ||||||
389 | |||||||
390 | Fake a css spritesheet by generating a stylesheet containing just the original | ||||||
391 | images (not the ones coming from the sprite!) | ||||||
392 | |||||||
393 | $SpriteMaker->print_fake_css( | ||||||
394 | filename => 'relative/path/to/style.css', | ||||||
395 | fix_image_path => { | ||||||
396 | find: '/some/absolute/path', # a Perl regexp | ||||||
397 | replace: 'some/relative/path' | ||||||
398 | } | ||||||
399 | ); | ||||||
400 | |||||||
401 | NOTE: unlike print_css you don't need to call this method after make_sprite. | ||||||
402 | |||||||
403 | =cut | ||||||
404 | |||||||
405 | sub print_fake_css { | ||||||
406 | my $self = shift; | ||||||
407 | my %options = @_; | ||||||
408 | |||||||
409 | my $rh_sources_info = $self->_ensure_sources_info(%options); | ||||||
410 | |||||||
411 | my $fh = $self->_ensure_filehandle_write(%options); | ||||||
412 | |||||||
413 | $self->_verbose(" * writing fake css file"); | ||||||
414 | |||||||
415 | if (exists $options{sprite_filename}) { | ||||||
416 | die "the sprite_filename option is incompatible with fake_css. In this mode the original images are used in the spritesheet"; | ||||||
417 | } | ||||||
418 | |||||||
419 | my $stylesheet = $self->_get_stylesheet_string({ | ||||||
420 | use_full_images => 1 | ||||||
421 | }, | ||||||
422 | %options | ||||||
423 | ); | ||||||
424 | |||||||
425 | print $fh $stylesheet; | ||||||
426 | |||||||
427 | return 0; | ||||||
428 | } | ||||||
429 | |||||||
430 | =head2 print_html | ||||||
431 | |||||||
432 | Creates and prints an html sample page containing informations about each sprite produced. | ||||||
433 | |||||||
434 | $SpriteMaker->print_html( | ||||||
435 | filehandle => $fh, | ||||||
436 | ); | ||||||
437 | |||||||
438 | OR | ||||||
439 | |||||||
440 | $SpriteMaker->print_html( | ||||||
441 | filename => 'relative/path/to/index.html', | ||||||
442 | ); | ||||||
443 | |||||||
444 | NOTE: make_sprite() must be called before this method is called. | ||||||
445 | |||||||
446 | =cut | ||||||
447 | sub print_html { | ||||||
448 | my $self = shift; | ||||||
449 | my %options = @_; | ||||||
450 | |||||||
451 | my $rh_sources_info = $self->_ensure_sources_info(%options); | ||||||
452 | my $Layout = $self->_ensure_layout(%options, | ||||||
453 | rh_sources_info => $rh_sources_info | ||||||
454 | ); | ||||||
455 | my $fh = $self->_ensure_filehandle_write(%options); | ||||||
456 | |||||||
457 | $self->_verbose(" * writing html sample page"); | ||||||
458 | |||||||
459 | my $stylesheet = $self->_get_stylesheet_string({}, %options); | ||||||
460 | |||||||
461 | print $fh 'CSS::SpriteMaker Image Information'; |
||||||
496 | |||||||
497 | # html | ||||||
498 | for my $id (sort { $a <=> $b } keys %$rh_sources_info) { | ||||||
499 | my $rh_source_info = $rh_sources_info->{$id}; | ||||||
500 | my $css_class = $self->_generate_css_class_name($rh_source_info->{name}); | ||||||
501 | $self->_verbose( | ||||||
502 | sprintf("%s -> %s", $rh_source_info->{name}, $css_class) | ||||||
503 | ); | ||||||
504 | |||||||
505 | $css_class =~ s/[.]//; | ||||||
506 | |||||||
507 | my $is_included = $rh_source_info->{include_in_css}; | ||||||
508 | my $width = $rh_source_info->{original_width}; | ||||||
509 | my $height = $rh_source_info->{original_height}; | ||||||
510 | |||||||
511 | my $onclick = < | ||||||
512 | if (typeof current !== 'undefined' && current !== this) { | ||||||
513 | current.style.width = current.w; | ||||||
514 | current.style.height = current.h; | ||||||
515 | current.style.position = ''; | ||||||
516 | delete current.w; | ||||||
517 | delete current.h; | ||||||
518 | } | ||||||
519 | if (typeof this.h === 'undefined') { | ||||||
520 | this.h = this.style.height; | ||||||
521 | this.w = this.style.width; | ||||||
522 | this.style.width = ''; | ||||||
523 | this.style.height = ''; | ||||||
524 | this.style.position = 'fixed'; | ||||||
525 | current = this; | ||||||
526 | } | ||||||
527 | else { | ||||||
528 | this.style.width = this.w; | ||||||
529 | this.style.height = this.h; | ||||||
530 | this.style.position = ''; | ||||||
531 | delete this.w; | ||||||
532 | delete this.h; | ||||||
533 | current = undefined; | ||||||
534 | } | ||||||
535 | EONCLICK | ||||||
536 | |||||||
537 | |||||||
538 | print $fh sprintf( | ||||||
539 | ' ', |
||||||
540 | $is_included ? ' included' : ' not-included', | ||||||
541 | $onclick, | ||||||
542 | $width, $height | ||||||
543 | ); | ||||||
544 | |||||||
545 | |||||||
546 | if ($is_included) { | ||||||
547 | print $fh " "; | ||||||
548 | } | ||||||
549 | else { | ||||||
550 | print $fh " "; | ||||||
551 | } | ||||||
552 | print $fh " "; |
||||||
553 | for my $key (sort keys %$rh_source_info) { | ||||||
554 | next if $key eq "colors"; | ||||||
555 | print $fh "" . $key . ": " . ($rh_source_info->{$key} // 'none') . " "; |
||||||
556 | } | ||||||
557 | |||||||
558 | print $fh 'Colors'; |
||||||
559 | print $fh "total: " . $rh_source_info->{colors}{total} . ' '; |
||||||
560 | |||||||
561 | if ($self->{enable_colormap}) { | ||||||
562 | for my $colors (sort keys %{$rh_source_info->{colors}{map}}) { | ||||||
563 | my ($r, $g, $b, $a) = split /,/, $colors; | ||||||
564 | my $rrgb = $r * 255 / $self->{color_max}; | ||||||
565 | my $grgb = $g * 255 / $self->{color_max}; | ||||||
566 | my $brgb = $b * 255 / $self->{color_max}; | ||||||
567 | my $argb = 255 - ($a * 255 / $self->{color_max}); | ||||||
568 | print $fh '"; | ||||||
569 | } | ||||||
570 | } | ||||||
571 | |||||||
572 | print $fh " "; | ||||||
573 | print $fh ''; | ||||||
574 | } | ||||||
575 | |||||||
576 | print $fh ""; | ||||||
577 | |||||||
578 | return 0; | ||||||
579 | } | ||||||
580 | |||||||
581 | =head2 get_css_info_structure | ||||||
582 | |||||||
583 | Returns an arrayref of hashrefs like: | ||||||
584 | |||||||
585 | [ | ||||||
586 | { | ||||||
587 | full_path => 'relative/path/to/file.png', | ||||||
588 | css_class => 'file', | ||||||
589 | width => 16, # pixels | ||||||
590 | height => 16, | ||||||
591 | x => 173, # offset within the layout | ||||||
592 | y => 234, | ||||||
593 | }, | ||||||
594 | ...more | ||||||
595 | ] | ||||||
596 | |||||||
597 | This structure can be used to build your own html or css stylesheet for | ||||||
598 | example. | ||||||
599 | |||||||
600 | NOTE: the x y offsets within the layout, will be always positive numbers. | ||||||
601 | |||||||
602 | =cut | ||||||
603 | |||||||
604 | sub get_css_info_structure { | ||||||
605 | my $self = shift; | ||||||
606 | my %options = @_; | ||||||
607 | |||||||
608 | my $rh_sources_info = $self->_ensure_sources_info(%options); | ||||||
609 | my $Layout = $self->_ensure_layout(%options, | ||||||
610 | rh_sources_info => $rh_sources_info | ||||||
611 | ); | ||||||
612 | |||||||
613 | my $rh_id_to_class = $self->_generate_css_class_names($rh_sources_info); | ||||||
614 | |||||||
615 | my @css_info; | ||||||
616 | |||||||
617 | for my $id (sort { $a <=> $b } keys %$rh_sources_info) { | ||||||
618 | my $rh_source_info = $rh_sources_info->{$id}; | ||||||
619 | my $css_class = $rh_id_to_class->{$id}; | ||||||
620 | |||||||
621 | my ($x, $y) = $Layout->get_item_coord($id); | ||||||
622 | |||||||
623 | push @css_info, { | ||||||
624 | full_path => $rh_source_info->{pathname}, | ||||||
625 | x => $x + $rh_source_info->{add_extra_padding}, | ||||||
626 | y => $y + $rh_source_info->{add_extra_padding}, | ||||||
627 | css_class => $css_class, | ||||||
628 | width => $rh_source_info->{original_width}, | ||||||
629 | height => $rh_source_info->{original_height}, | ||||||
630 | }; | ||||||
631 | } | ||||||
632 | |||||||
633 | return \@css_info; | ||||||
634 | } | ||||||
635 | |||||||
636 | =head1 PRIVATE METHODS | ||||||
637 | |||||||
638 | =head2 _generate_css_class_names | ||||||
639 | |||||||
640 | Returns a mapping id -> class_name out of the current information structure. | ||||||
641 | |||||||
642 | It guarantees unique class_name for each id. | ||||||
643 | |||||||
644 | =cut | ||||||
645 | |||||||
646 | sub _generate_css_class_names { | ||||||
647 | my $self = shift; | ||||||
648 | my $rh_source_info = shift;; | ||||||
649 | |||||||
650 | my %existing_classnames_lookup; | ||||||
651 | my %id_to_class_mapping; | ||||||
652 | |||||||
653 | PROCESS_SOURCEINFO: | ||||||
654 | for my $id (sort { $a <=> $b } keys %$rh_source_info) { | ||||||
655 | |||||||
656 | next PROCESS_SOURCEINFO if !$rh_source_info->{$id}{include_in_css}; | ||||||
657 | |||||||
658 | my $css_class = $self->_generate_css_class_name( | ||||||
659 | $rh_source_info->{$id}{name} | ||||||
660 | ); | ||||||
661 | |||||||
662 | # keep modifying the css_class name until it doesn't exist in the hash | ||||||
663 | my $i = 0; | ||||||
664 | while (exists $existing_classnames_lookup{$css_class}) { | ||||||
665 | # ... we want to add an incremental suffix like "-2" | ||||||
666 | if (!$i) { | ||||||
667 | # the first time, we want to add the prefix only, but leave the class name intact | ||||||
668 | if ($css_class =~ m/-\Z/) { | ||||||
669 | # class already ends with a dash | ||||||
670 | $css_class .= '1'; | ||||||
671 | } | ||||||
672 | else { | ||||||
673 | $css_class .= '-1'; | ||||||
674 | } | ||||||
675 | } | ||||||
676 | elsif ($css_class =~ m/-(\d+)\Z/) { # that's our dash added before! | ||||||
677 | my $current_number = $1; | ||||||
678 | $current_number++; | ||||||
679 | $css_class =~ s/-\d+\Z/-$current_number/; | ||||||
680 | } | ||||||
681 | $i++; | ||||||
682 | } | ||||||
683 | |||||||
684 | $existing_classnames_lookup{$css_class} = 1; | ||||||
685 | $id_to_class_mapping{$id} = $css_class; | ||||||
686 | } | ||||||
687 | |||||||
688 | return \%id_to_class_mapping; | ||||||
689 | } | ||||||
690 | |||||||
691 | |||||||
692 | =head2 _image_locations_to_source_info | ||||||
693 | |||||||
694 | Identify informations from the location of each input image, and assign | ||||||
695 | numerical ids to each input image. | ||||||
696 | |||||||
697 | We use a global image identifier for composite layouts. Each identified image | ||||||
698 | must have a unique id in the scope of the same CSS::SpriteMaker instance! | ||||||
699 | |||||||
700 | =cut | ||||||
701 | |||||||
702 | sub _image_locations_to_source_info { | ||||||
703 | my $self = shift; | ||||||
704 | my $ra_locations = shift; | ||||||
705 | my $remove_source_padding = shift; | ||||||
706 | my $add_extra_padding = shift; | ||||||
707 | my $include_in_css = shift // 1; | ||||||
708 | my $enable_colormap = shift; | ||||||
709 | |||||||
710 | my %source_info; | ||||||
711 | |||||||
712 | # collect properties of each input image. | ||||||
713 | IMAGE: | ||||||
714 | for my $rh_location (@$ra_locations) { | ||||||
715 | |||||||
716 | my $id = $self->_get_image_id; | ||||||
717 | |||||||
718 | my %properties = %{$self->_get_image_properties( | ||||||
719 | $rh_location->{pathname}, | ||||||
720 | $remove_source_padding, | ||||||
721 | $add_extra_padding, | ||||||
722 | $enable_colormap | ||||||
723 | )}; | ||||||
724 | |||||||
725 | # add whether to include this item in the css or not | ||||||
726 | $properties{include_in_css} = $include_in_css; | ||||||
727 | |||||||
728 | # this is really for write_image, it should add padding as necessary | ||||||
729 | $properties{add_extra_padding} = $add_extra_padding; | ||||||
730 | |||||||
731 | # skip invalid images | ||||||
732 | next IMAGE if !%properties; | ||||||
733 | |||||||
734 | for my $key (keys %$rh_location) { | ||||||
735 | $source_info{$id}{$key} = $rh_location->{$key}; | ||||||
736 | } | ||||||
737 | for my $key (keys %properties) { | ||||||
738 | $source_info{$id}{$key} = $properties{$key}; | ||||||
739 | } | ||||||
740 | } | ||||||
741 | |||||||
742 | return \%source_info; | ||||||
743 | } | ||||||
744 | |||||||
745 | =head2 _get_image_id | ||||||
746 | |||||||
747 | Returns a global numeric identifier. | ||||||
748 | |||||||
749 | =cut | ||||||
750 | |||||||
751 | sub _get_image_id { | ||||||
752 | my $self = shift; | ||||||
753 | $self->{_unique_id} //= 0; | ||||||
754 | return $self->{_unique_id}++; | ||||||
755 | } | ||||||
756 | |||||||
757 | =head2 _locate_image_files | ||||||
758 | |||||||
759 | Finds the location of image files within the given directory. Returns an | ||||||
760 | arrayref of hashrefs containing information about the names and pathnames of | ||||||
761 | each image file. | ||||||
762 | |||||||
763 | The returned arrayref looks like: | ||||||
764 | |||||||
765 | [ # pathnames of the first image to follow | ||||||
766 | { | ||||||
767 | name => 'image.png', | ||||||
768 | pathname => '/complete/path/to/image.png', | ||||||
769 | parentdir => '/complete/path/to', | ||||||
770 | }, | ||||||
771 | ... | ||||||
772 | ] | ||||||
773 | |||||||
774 | Dies if the given directory is empty or doesn't exist. | ||||||
775 | |||||||
776 | =cut | ||||||
777 | |||||||
778 | sub _locate_image_files { | ||||||
779 | my $self = shift; | ||||||
780 | my $source_directory = shift; | ||||||
781 | |||||||
782 | if (!defined $source_directory) { | ||||||
783 | die "you have called _locate_image_files but \$source_directory was undefined"; | ||||||
784 | } | ||||||
785 | |||||||
786 | $self->_verbose(" * gathering files and directories of source images"); | ||||||
787 | |||||||
788 | my @locations; | ||||||
789 | find(sub { | ||||||
790 | my $filename = $_; | ||||||
791 | my $fullpath = $File::Find::name; | ||||||
792 | my $parentdir = $File::Find::dir; | ||||||
793 | |||||||
794 | return if $filename eq '.'; | ||||||
795 | |||||||
796 | if (-f $filename) { | ||||||
797 | push @locations, { | ||||||
798 | # only the name of the file | ||||||
799 | name => $filename, | ||||||
800 | |||||||
801 | # the full relative pathname | ||||||
802 | pathname => $fullpath, | ||||||
803 | |||||||
804 | # the full relative path to the parent directory | ||||||
805 | parentdir => $parentdir | ||||||
806 | }; | ||||||
807 | } | ||||||
808 | |||||||
809 | }, $source_directory); | ||||||
810 | |||||||
811 | return \@locations; | ||||||
812 | } | ||||||
813 | |||||||
814 | =head2 _get_stylesheet_string | ||||||
815 | |||||||
816 | Returns the stylesheet in a string. | ||||||
817 | |||||||
818 | =cut | ||||||
819 | |||||||
820 | sub _get_stylesheet_string { | ||||||
821 | my $self = shift; | ||||||
822 | my $rh_opts = shift // {}; | ||||||
823 | my %options = @_; | ||||||
824 | |||||||
825 | # defaults | ||||||
826 | my $target_image_filename = $self->{_cache_target_image_file}; | ||||||
827 | if (exists $rh_opts->{target_image_filename} && defined $rh_opts->{target_image_filename}) { | ||||||
828 | $target_image_filename = $rh_opts->{target_image_filename}; | ||||||
829 | } | ||||||
830 | |||||||
831 | my $use_full_images = 0; | ||||||
832 | if (exists $rh_opts->{use_full_images} && defined $rh_opts->{use_full_images}) { | ||||||
833 | $use_full_images = $rh_opts->{use_full_images}; | ||||||
834 | } | ||||||
835 | |||||||
836 | my $rah_cssinfo = $self->get_css_info_structure(%options); | ||||||
837 | |||||||
838 | my @classes = map { "." . $_->{css_class} } | ||||||
839 | grep { defined $_->{css_class} } | ||||||
840 | @$rah_cssinfo; | ||||||
841 | |||||||
842 | my @stylesheet; | ||||||
843 | |||||||
844 | if ($use_full_images) { | ||||||
845 | my ($f, $r); | ||||||
846 | my $is_path_to_be_fixed = 0; | ||||||
847 | if (exists $options{fix_image_path} && | ||||||
848 | exists $options{fix_image_path}{find} && | ||||||
849 | exists $options{fix_image_path}{replace}) { | ||||||
850 | |||||||
851 | $is_path_to_be_fixed = 1; | ||||||
852 | $f = qr/$options{fix_image_path}{find}/; | ||||||
853 | $r = $options{fix_image_path}{replace}; | ||||||
854 | } | ||||||
855 | |||||||
856 | ## | ||||||
857 | ## use full images instead of the ones from the sprite | ||||||
858 | ## | ||||||
859 | for my $rh_info (@$rah_cssinfo) { | ||||||
860 | |||||||
861 | # fix the path (maybe) | ||||||
862 | my $path = $rh_info->{full_path}; | ||||||
863 | if ($is_path_to_be_fixed) { | ||||||
864 | $path =~ s/$f/$r/; | ||||||
865 | } | ||||||
866 | |||||||
867 | if (defined $rh_info->{css_class}) { | ||||||
868 | push @stylesheet, sprintf( | ||||||
869 | ".%s { background-image: url('%s'); width: %spx; height: %spx; }", | ||||||
870 | $rh_info->{css_class}, | ||||||
871 | $path, | ||||||
872 | $rh_info->{width}, | ||||||
873 | $rh_info->{height}, | ||||||
874 | ); | ||||||
875 | } | ||||||
876 | } | ||||||
877 | } | ||||||
878 | else { | ||||||
879 | # write header | ||||||
880 | # header associates the sprite image to each class | ||||||
881 | push @stylesheet, sprintf( | ||||||
882 | "%s { background-image: url('%s'); background-repeat: no-repeat; }", | ||||||
883 | join(",", @classes), | ||||||
884 | $target_image_filename | ||||||
885 | ); | ||||||
886 | |||||||
887 | for my $rh_info (@$rah_cssinfo) { | ||||||
888 | if (defined $rh_info->{css_class}) { | ||||||
889 | push @stylesheet, sprintf( | ||||||
890 | ".%s { background-position: %spx %spx; width: %spx; height: %spx; }", | ||||||
891 | $rh_info->{css_class}, | ||||||
892 | -1 * $rh_info->{x}, | ||||||
893 | -1 * $rh_info->{y}, | ||||||
894 | $rh_info->{width}, | ||||||
895 | $rh_info->{height}, | ||||||
896 | ); | ||||||
897 | } | ||||||
898 | } | ||||||
899 | } | ||||||
900 | |||||||
901 | return join "\n", @stylesheet; | ||||||
902 | } | ||||||
903 | |||||||
904 | |||||||
905 | =head2 _generate_css_class_name | ||||||
906 | |||||||
907 | This method generates the name of the CSS class for a certain image file. Takes | ||||||
908 | the image filename as input and produces a css class name (excluding the | ||||||
909 | prepended "."). | ||||||
910 | |||||||
911 | =cut | ||||||
912 | |||||||
913 | sub _generate_css_class_name { | ||||||
914 | my $self = shift; | ||||||
915 | my $filename = shift; | ||||||
916 | |||||||
917 | my $rc_filename_to_classname = $self->{rc_filename_to_classname}; | ||||||
918 | my $rc_override_classname = $self->{rc_override_classname}; | ||||||
919 | |||||||
920 | if (defined $rc_filename_to_classname) { | ||||||
921 | my $classname = $rc_filename_to_classname->($filename); | ||||||
922 | if (!$classname) { | ||||||
923 | warn "custom sub to generate class names out of file names returned empty class for file name $filename"; | ||||||
924 | } | ||||||
925 | if ($classname =~ m/^[.]/) { | ||||||
926 | warn sprintf('your custom sub should not include \'.\' at the beginning of the class name. (%s was generated from %s)', | ||||||
927 | $classname, | ||||||
928 | $filename | ||||||
929 | ); | ||||||
930 | } | ||||||
931 | |||||||
932 | if (defined $rc_override_classname) { | ||||||
933 | $classname = $rc_override_classname->($classname); | ||||||
934 | } | ||||||
935 | |||||||
936 | return $classname; | ||||||
937 | } | ||||||
938 | |||||||
939 | # prepare (lowercase) | ||||||
940 | my $css_class = lc($filename); | ||||||
941 | |||||||
942 | # remove image extensions if any | ||||||
943 | $css_class =~ s/[.](tif|tiff|gif|jpeg|jpg|jif|jfif|jp2|jpx|j2k|j2c|fpx|pcd|png|pdf)\Z//; | ||||||
944 | |||||||
945 | # remove @ [] + | ||||||
946 | $css_class =~ s/[+@\]\[]//g; | ||||||
947 | |||||||
948 | # turn certain characters into dashes | ||||||
949 | $css_class =~ s/[\s_.]/-/g; | ||||||
950 | |||||||
951 | # remove dashes if they appear at the end | ||||||
952 | $css_class =~ s/-\Z//g; | ||||||
953 | |||||||
954 | # remove initial dashes if any | ||||||
955 | $css_class =~ s/\A-+//g; | ||||||
956 | |||||||
957 | # add prefix if it was requested | ||||||
958 | if (defined $self->{css_class_prefix}) { | ||||||
959 | $css_class = $self->{css_class_prefix} . $css_class; | ||||||
960 | } | ||||||
961 | |||||||
962 | # allow change (e.g., add prefix) | ||||||
963 | if (defined $rc_override_classname) { | ||||||
964 | $css_class = $rc_override_classname->($css_class); | ||||||
965 | } | ||||||
966 | |||||||
967 | return $css_class; | ||||||
968 | } | ||||||
969 | |||||||
970 | |||||||
971 | =head2 _ensure_filehandle_write | ||||||
972 | |||||||
973 | Inspects the input %options hash and returns a filehandle according to the | ||||||
974 | parameters passed in there. | ||||||
975 | |||||||
976 | The filehandle is where something (css stylesheet for example) is going to be | ||||||
977 | printed. | ||||||
978 | |||||||
979 | =cut | ||||||
980 | |||||||
981 | sub _ensure_filehandle_write { | ||||||
982 | my $self = shift; | ||||||
983 | my %options = @_; | ||||||
984 | |||||||
985 | return $options{filehandle} if defined $options{filehandle}; | ||||||
986 | |||||||
987 | if (defined $options{filename}) { | ||||||
988 | open my ($fh), '>', $options{filename}; | ||||||
989 | return $fh; | ||||||
990 | } | ||||||
991 | |||||||
992 | return \*STDOUT; | ||||||
993 | } | ||||||
994 | |||||||
995 | =head2 _ensure_sources_info | ||||||
996 | |||||||
997 | Makes sure the user of this module has provided a valid input parameter for | ||||||
998 | sources_info and return the sources_info structure accordingly. Dies in case | ||||||
999 | something goes wrong with the user input. | ||||||
1000 | |||||||
1001 | Parameters that allow us to obtain a $rh_sources_info structure are: | ||||||
1002 | |||||||
1003 | - source_images: an arrayref of files or directories, directories will be | ||||||
1004 | visited recursively and any image file in it becomes the input. | ||||||
1005 | |||||||
1006 | If none of the above parameters have been found in input options, the cache is | ||||||
1007 | checked before giving up - i.e., the user has previously provided the layout | ||||||
1008 | parameter, and was able to generate a sprite. | ||||||
1009 | |||||||
1010 | =cut | ||||||
1011 | |||||||
1012 | sub _ensure_sources_info { | ||||||
1013 | my $self = shift; | ||||||
1014 | my %options = @_; | ||||||
1015 | |||||||
1016 | ## | ||||||
1017 | ## Shall we remove source padding? | ||||||
1018 | ## - first check if an option is provided | ||||||
1019 | ## - otherwise default to the option in $self | ||||||
1020 | my $remove_source_padding = $self->{remove_source_padding}; | ||||||
1021 | my $add_extra_padding = $self->{add_extra_padding}; | ||||||
1022 | my $enable_colormap = $self->{enable_colormap}; | ||||||
1023 | if (exists $options{remove_source_padding} | ||||||
1024 | && defined $options{remove_source_padding}) { | ||||||
1025 | |||||||
1026 | $remove_source_padding = $options{remove_source_padding}; | ||||||
1027 | } | ||||||
1028 | if (exists $options{add_extra_padding} | ||||||
1029 | && defined $options{add_extra_padding}) { | ||||||
1030 | |||||||
1031 | $add_extra_padding = $options{add_extra_padding}; | ||||||
1032 | } | ||||||
1033 | if (exists $options{enable_colormap} | ||||||
1034 | && defined $options{enable_colormap}) { | ||||||
1035 | |||||||
1036 | $enable_colormap = $options{enable_colormap}; | ||||||
1037 | } | ||||||
1038 | |||||||
1039 | my $rh_source_info; | ||||||
1040 | |||||||
1041 | return $options{source_info} if exists $options{source_info}; | ||||||
1042 | |||||||
1043 | my @source_images; | ||||||
1044 | |||||||
1045 | if (exists $options{source_dir} && defined $options{source_dir}) { | ||||||
1046 | push @source_images, $options{source_dir}; | ||||||
1047 | } | ||||||
1048 | |||||||
1049 | if (exists $options{source_images} && defined $options{source_images}) { | ||||||
1050 | die 'source_images parameter must be an ARRAY REF' if ref $options{source_images} ne 'ARRAY'; | ||||||
1051 | push @source_images, @{$options{source_images}}; | ||||||
1052 | } | ||||||
1053 | |||||||
1054 | if (@source_images) { | ||||||
1055 | # locate each file within each directory and then identify all... | ||||||
1056 | my @locations; | ||||||
1057 | for my $file_or_dir (@source_images) { | ||||||
1058 | my $ra_locations = $self->_locate_image_files($file_or_dir); | ||||||
1059 | push @locations, @$ra_locations; | ||||||
1060 | } | ||||||
1061 | |||||||
1062 | my $include_in_css = exists $options{include_in_css} | ||||||
1063 | ? $options{include_in_css} | ||||||
1064 | : 1; | ||||||
1065 | |||||||
1066 | $rh_source_info = $self->_image_locations_to_source_info( | ||||||
1067 | \@locations, | ||||||
1068 | $remove_source_padding, | ||||||
1069 | $add_extra_padding, | ||||||
1070 | $include_in_css, | ||||||
1071 | $enable_colormap | ||||||
1072 | ); | ||||||
1073 | } | ||||||
1074 | |||||||
1075 | if (!$rh_source_info) { | ||||||
1076 | if (exists $self->{_cache_rh_source_info} | ||||||
1077 | && defined $self->{_cache_rh_source_info}) { | ||||||
1078 | |||||||
1079 | $rh_source_info = $self->{_cache_rh_source_info}; | ||||||
1080 | } | ||||||
1081 | else { | ||||||
1082 | die "Unable to create the source_info_structure!"; | ||||||
1083 | } | ||||||
1084 | } | ||||||
1085 | |||||||
1086 | return $rh_source_info; | ||||||
1087 | } | ||||||
1088 | |||||||
1089 | |||||||
1090 | |||||||
1091 | =head2 _ensure_layout | ||||||
1092 | |||||||
1093 | Makes sure the user of this module has provided valid layout options and | ||||||
1094 | returns a $Layout object accordingly. Dies in case something goes wrong with | ||||||
1095 | the user input. A Layout actually runs over the specified items on instantiation. | ||||||
1096 | |||||||
1097 | Parameters in %options (see code) that allow us to obtain a $Layout object are: | ||||||
1098 | |||||||
1099 | - layout: a CSS::SpriteMaker::Layout object already; | ||||||
1100 | - layout: can also be a hashref like | ||||||
1101 | |||||||
1102 | { | ||||||
1103 | name => 'LayoutName', | ||||||
1104 | options => { | ||||||
1105 | 'Layout-Specific option' => 'value', | ||||||
1106 | ... | ||||||
1107 | } | ||||||
1108 | } | ||||||
1109 | |||||||
1110 | - layout_name: the name of a CSS::SpriteMaker::Layout object. | ||||||
1111 | |||||||
1112 | If none of the above parameters have been found in input options, the cache is | ||||||
1113 | checked before giving up - i.e., the user has previously provided the layout | ||||||
1114 | parameter... | ||||||
1115 | |||||||
1116 | =cut | ||||||
1117 | |||||||
1118 | sub _ensure_layout { | ||||||
1119 | my $self = shift; | ||||||
1120 | my %options = @_; | ||||||
1121 | |||||||
1122 | die 'rh_sources_info parameter is required' if !exists $options{rh_sources_info}; | ||||||
1123 | |||||||
1124 | # Get the layout from the layout parameter in case it is a $Layout object | ||||||
1125 | my $Layout; | ||||||
1126 | if (exists $options{layout} && $options{layout} && ref $options{layout} ne 'HASH') { | ||||||
1127 | if (exists $options{layout}{_layout_ran}) { | ||||||
1128 | $Layout = $options{layout}; | ||||||
1129 | } | ||||||
1130 | else { | ||||||
1131 | warn 'a Layout object was specified but strangely was not executed on the specified items. NOTE: if a layout is instantiated it\'s always ran over the items!'; | ||||||
1132 | } | ||||||
1133 | } | ||||||
1134 | |||||||
1135 | if (defined $Layout) { | ||||||
1136 | if (exists $options{layout_name} && defined $options{layout_name}) { | ||||||
1137 | warn 'the parameter layout_name was ignored as the layout parameter was specified. These two parameters are mutually exclusive.'; | ||||||
1138 | } | ||||||
1139 | } | ||||||
1140 | else { | ||||||
1141 | ## | ||||||
1142 | ## We were unable to get the layout object directly, so we need to | ||||||
1143 | ## create the layout from a name if possible... | ||||||
1144 | ## | ||||||
1145 | |||||||
1146 | $self->_verbose(" * creating layout"); | ||||||
1147 | |||||||
1148 | # the layout name can be specified in the options as layout_name | ||||||
1149 | my $layout_name = ''; | ||||||
1150 | my $layout_options; | ||||||
1151 | if (exists $options{layout_name}) { | ||||||
1152 | $layout_name = $options{layout_name}; | ||||||
1153 | # if this is the case this layout must support no options | ||||||
1154 | $layout_options = {}; | ||||||
1155 | } | ||||||
1156 | |||||||
1157 | # maybe a layout => { name => 'something' was specified } | ||||||
1158 | if (exists $options{layout} && exists $options{layout}{name}) { | ||||||
1159 | $layout_name = $options{layout}{name}; | ||||||
1160 | $layout_options = $options{layout}{options} // {}; | ||||||
1161 | } | ||||||
1162 | |||||||
1163 | LOAD_LAYOUT_PLUGIN: | ||||||
1164 | for my $plugin ($self->plugins()) { | ||||||
1165 | my ($plugin_name) = reverse split "::", $plugin; | ||||||
1166 | if ($plugin eq $layout_name || $plugin_name eq $layout_name) { | ||||||
1167 | $self->_verbose(" * using layout $plugin"); | ||||||
1168 | $Layout = $plugin->new($options{rh_sources_info}, $layout_options); | ||||||
1169 | last LOAD_LAYOUT_PLUGIN; | ||||||
1170 | } | ||||||
1171 | } | ||||||
1172 | |||||||
1173 | if (!defined $Layout && $layout_name ne '') { | ||||||
1174 | die sprintf( | ||||||
1175 | "The layout you've specified (%s) couldn't be found. Valid layouts are:\n%s", | ||||||
1176 | $layout_name, | ||||||
1177 | join "\n", $self->plugins() | ||||||
1178 | ); | ||||||
1179 | } | ||||||
1180 | } | ||||||
1181 | |||||||
1182 | # | ||||||
1183 | # Still no layout, check the cache! | ||||||
1184 | # | ||||||
1185 | if (!defined $Layout) { | ||||||
1186 | # try checking in the cache before giving up... | ||||||
1187 | if (exists $self->{_cache_layout} | ||||||
1188 | && defined $self->{_cache_layout}) { | ||||||
1189 | |||||||
1190 | $Layout = $self->{_cache_layout}; | ||||||
1191 | } | ||||||
1192 | } | ||||||
1193 | |||||||
1194 | # | ||||||
1195 | # Still nothing, then use default layout | ||||||
1196 | # | ||||||
1197 | if (!defined $Layout) { | ||||||
1198 | my $layout_name = $self->{layout}{name}; | ||||||
1199 | my $layout_options = {}; | ||||||
1200 | LOAD_DEFAULT_LAYOUT_PLUGIN: | ||||||
1201 | for my $plugin ($self->plugins()) { | ||||||
1202 | my ($plugin_name) = reverse split "::", $plugin; | ||||||
1203 | if ($plugin eq $layout_name || $plugin_name eq $layout_name) { | ||||||
1204 | $self->_verbose(" * using DEFAULT layout $plugin"); | ||||||
1205 | $Layout = $plugin->new($options{rh_sources_info}, $layout_options); | ||||||
1206 | last LOAD_DEFAULT_LAYOUT_PLUGIN; | ||||||
1207 | } | ||||||
1208 | } | ||||||
1209 | } | ||||||
1210 | |||||||
1211 | return $Layout; | ||||||
1212 | } | ||||||
1213 | |||||||
1214 | sub _write_image { | ||||||
1215 | my $self = shift; | ||||||
1216 | my %options = @_; | ||||||
1217 | |||||||
1218 | my $target_file = $options{target_file} // $self->{target_file}; | ||||||
1219 | my $output_format = $options{format} // $self->{format}; | ||||||
1220 | my $Layout = $options{Layout} // 0; | ||||||
1221 | my $rh_sources_info = $options{rh_sources_info} // 0; | ||||||
1222 | |||||||
1223 | if (!$target_file) { | ||||||
1224 | die "the ``target_file'' parameter is required for this subroutine or you must instantiate Css::SpriteMaker with the target_file parameter"; | ||||||
1225 | } | ||||||
1226 | |||||||
1227 | if (!$rh_sources_info) { | ||||||
1228 | die "The 'rh_sources_info' parameter must be passed to _write_image"; | ||||||
1229 | } | ||||||
1230 | |||||||
1231 | if (!$Layout) { | ||||||
1232 | die "The 'layout' parameter needs to be specified for _write_image, and must be a CSS::SpriteMaker::Layout object"; | ||||||
1233 | } | ||||||
1234 | |||||||
1235 | $self->_verbose(" * writing sprite image"); | ||||||
1236 | |||||||
1237 | $self->_verbose(sprintf("Target image size: %s, %s", | ||||||
1238 | $Layout->width(), | ||||||
1239 | $Layout->height()) | ||||||
1240 | ); | ||||||
1241 | |||||||
1242 | my $Target = Image::Magick->new(); | ||||||
1243 | |||||||
1244 | $Target->Set(size => sprintf("%sx%s", | ||||||
1245 | $Layout->width(), | ||||||
1246 | $Layout->height() | ||||||
1247 | )); | ||||||
1248 | |||||||
1249 | # prepare the target image | ||||||
1250 | if (my $err = $Target->ReadImage('xc:white')) { | ||||||
1251 | warn $err; | ||||||
1252 | } | ||||||
1253 | $Target->Set(type => 'TruecolorMatte'); | ||||||
1254 | |||||||
1255 | # make it transparent | ||||||
1256 | $self->_verbose(" - clearing canvas"); | ||||||
1257 | $Target->Draw( | ||||||
1258 | fill => 'transparent', | ||||||
1259 | primitive => 'rectangle', | ||||||
1260 | points => sprintf("0,0 %s,%s", $Layout->width(), $Layout->height()) | ||||||
1261 | ); | ||||||
1262 | $Target->Transparent('color' => 'white'); | ||||||
1263 | |||||||
1264 | # place each image according to the layout | ||||||
1265 | ITEM_ID: | ||||||
1266 | for my $source_id ($Layout->get_item_ids) { | ||||||
1267 | my $rh_source_info = $rh_sources_info->{$source_id}; | ||||||
1268 | my ($layout_x, $layout_y) = $Layout->get_item_coord($source_id); | ||||||
1269 | |||||||
1270 | $self->_verbose(sprintf(" - placing %s (format: %s size: %sx%s position: [%s,%s])", | ||||||
1271 | $rh_source_info->{pathname}, | ||||||
1272 | $rh_source_info->{format}, | ||||||
1273 | $rh_source_info->{width}, | ||||||
1274 | $rh_source_info->{height}, | ||||||
1275 | $layout_y, | ||||||
1276 | $layout_x | ||||||
1277 | )); | ||||||
1278 | my $I = Image::Magick->new(); | ||||||
1279 | my $err = $I->Read($rh_source_info->{pathname}); | ||||||
1280 | if ($err) { | ||||||
1281 | warn $err; | ||||||
1282 | next ITEM_ID; | ||||||
1283 | } | ||||||
1284 | |||||||
1285 | my $padding = $rh_source_info->{add_extra_padding}; | ||||||
1286 | |||||||
1287 | my $destx = $layout_x + $padding; | ||||||
1288 | my $desty = $layout_y + $padding; | ||||||
1289 | $Target->Composite(image=>$I,compose=>'xor',geometry=>"+$destx+$desty"); | ||||||
1290 | } | ||||||
1291 | |||||||
1292 | # write target image | ||||||
1293 | my $err = $Target->Write("$output_format:".$target_file); | ||||||
1294 | if ($err) { | ||||||
1295 | warn "unable to obtain $target_file for writing it as $output_format. Perhaps you have specified an invalid format. Check http://www.imagemagick.org/script/formats.php for a list of supported formats. Error: $err"; | ||||||
1296 | |||||||
1297 | $self->_verbose("Wrote $target_file"); | ||||||
1298 | |||||||
1299 | return 1; | ||||||
1300 | } | ||||||
1301 | |||||||
1302 | # cache the layout and the rh_info structure so that it hasn't to be passed | ||||||
1303 | # as a parameter next times. | ||||||
1304 | $self->{_cache_layout} = $Layout; | ||||||
1305 | |||||||
1306 | # cache the target image file, as making the stylesheet can't be done | ||||||
1307 | # without this information. | ||||||
1308 | $self->{_cache_target_image_file} = $target_file; | ||||||
1309 | |||||||
1310 | # cache sources info | ||||||
1311 | $self->{_cache_rh_source_info} = $rh_sources_info; | ||||||
1312 | |||||||
1313 | return 0; | ||||||
1314 | |||||||
1315 | } | ||||||
1316 | |||||||
1317 | =head2 _get_image_properties | ||||||
1318 | |||||||
1319 | Return an hashref of information about the image at the given pathname. | ||||||
1320 | |||||||
1321 | =cut | ||||||
1322 | |||||||
1323 | sub _get_image_properties { | ||||||
1324 | my $self = shift; | ||||||
1325 | my $image_path = shift; | ||||||
1326 | my $remove_source_padding = shift; | ||||||
1327 | my $add_extra_padding = shift; | ||||||
1328 | my $enable_colormap = shift; | ||||||
1329 | |||||||
1330 | my $Image = Image::Magick->new(); | ||||||
1331 | |||||||
1332 | my $err = $Image->Read($image_path); | ||||||
1333 | if ($err) { | ||||||
1334 | warn $err; | ||||||
1335 | return {}; | ||||||
1336 | } | ||||||
1337 | |||||||
1338 | my $rh_info = {}; | ||||||
1339 | $rh_info->{first_pixel_x} = 0, | ||||||
1340 | $rh_info->{first_pixel_y} = 0, | ||||||
1341 | $rh_info->{width} = $Image->Get('columns'); | ||||||
1342 | $rh_info->{height} = $Image->Get('rows'); | ||||||
1343 | $rh_info->{comment} = $Image->Get('comment'); | ||||||
1344 | $rh_info->{colors}{total} = $Image->Get('colors'); | ||||||
1345 | $rh_info->{format} = $Image->Get('magick'); | ||||||
1346 | |||||||
1347 | if ($remove_source_padding) { | ||||||
1348 | # | ||||||
1349 | # Find borders for this image. | ||||||
1350 | # | ||||||
1351 | # (RE-)SET: | ||||||
1352 | # - first_pixel(x/y) as the true point the image starts | ||||||
1353 | # - width/height as the true dimensions of the image | ||||||
1354 | # | ||||||
1355 | my $w = $rh_info->{width}; | ||||||
1356 | my $h = $rh_info->{height}; | ||||||
1357 | |||||||
1358 | # seek for left/right borders | ||||||
1359 | my $first_left = 0; | ||||||
1360 | my $first_right = $w-1; | ||||||
1361 | my $left_found = 0; | ||||||
1362 | my $right_found = 0; | ||||||
1363 | |||||||
1364 | BORDER_HORIZONTAL: | ||||||
1365 | for my $x (0 .. ceil(($w-1)/2)) { | ||||||
1366 | my $xr = $w-$x-1; | ||||||
1367 | for my $y (0..$h-1) { | ||||||
1368 | my $al = $Image->Get(sprintf('pixel[%s,%s]', $x, $y)); | ||||||
1369 | my $ar = $Image->Get(sprintf('pixel[%s,%s]', $xr, $y)); | ||||||
1370 | |||||||
1371 | # remove rgb info and only get alpha value | ||||||
1372 | $al =~ s/^.+,//; | ||||||
1373 | $ar =~ s/^.+,//; | ||||||
1374 | |||||||
1375 | if ($al != $self->{color_max} && !$left_found) { | ||||||
1376 | $first_left = $x; | ||||||
1377 | $left_found = 1; | ||||||
1378 | } | ||||||
1379 | if ($ar != $self->{color_max} && !$right_found) { | ||||||
1380 | $first_right = $xr; | ||||||
1381 | $right_found = 1; | ||||||
1382 | } | ||||||
1383 | last BORDER_HORIZONTAL if $left_found && $right_found; | ||||||
1384 | } | ||||||
1385 | } | ||||||
1386 | $rh_info->{first_pixel_x} = $first_left; | ||||||
1387 | $rh_info->{width} = $first_right - $first_left + 1; | ||||||
1388 | |||||||
1389 | # seek for top/bottom borders | ||||||
1390 | my $first_top = 0; | ||||||
1391 | my $first_bottom = $h-1; | ||||||
1392 | my $top_found = 0; | ||||||
1393 | my $bottom_found = 0; | ||||||
1394 | |||||||
1395 | BORDER_VERTICAL: | ||||||
1396 | for my $y (0 .. ceil(($h-1)/2)) { | ||||||
1397 | my $yb = $h-$y-1; | ||||||
1398 | for my $x (0 .. $w-1) { | ||||||
1399 | my $at = $Image->Get(sprintf('pixel[%s,%s]', $x, $y)); | ||||||
1400 | my $ab = $Image->Get(sprintf('pixel[%s,%s]', $x, $yb)); | ||||||
1401 | |||||||
1402 | # remove rgb info and only get alpha value | ||||||
1403 | $at =~ s/^.+,//; | ||||||
1404 | $ab =~ s/^.+,//; | ||||||
1405 | |||||||
1406 | if ($at != $self->{color_max} && !$top_found) { | ||||||
1407 | $first_top = $y; | ||||||
1408 | $top_found = 1; | ||||||
1409 | } | ||||||
1410 | if ($ab != $self->{color_max} && !$bottom_found) { | ||||||
1411 | $first_bottom = $yb; | ||||||
1412 | $bottom_found = 1; | ||||||
1413 | } | ||||||
1414 | last BORDER_VERTICAL if $top_found && $bottom_found; | ||||||
1415 | } | ||||||
1416 | } | ||||||
1417 | $rh_info->{first_pixel_y} = $first_top; | ||||||
1418 | $rh_info->{height} = $first_bottom - $first_top + 1; | ||||||
1419 | } | ||||||
1420 | |||||||
1421 | if ($enable_colormap) { | ||||||
1422 | $self->_generate_colormap_for_image_properties($Image, $rh_info); | ||||||
1423 | } | ||||||
1424 | |||||||
1425 | # save the original width as it may change later | ||||||
1426 | $rh_info->{original_width} = $rh_info->{width}; | ||||||
1427 | $rh_info->{original_height} = $rh_info->{height}; | ||||||
1428 | |||||||
1429 | if ($add_extra_padding) { | ||||||
1430 | # fix the width of the image if a padding was added, as if the image | ||||||
1431 | # was actually wider | ||||||
1432 | $rh_info->{width} += 2 * $add_extra_padding; | ||||||
1433 | $rh_info->{height} += 2 * $add_extra_padding; | ||||||
1434 | } | ||||||
1435 | |||||||
1436 | return $rh_info; | ||||||
1437 | } | ||||||
1438 | |||||||
1439 | =head2 _compose_sprite_with_glue | ||||||
1440 | |||||||
1441 | Compose a layout though a glue layout: first each image set is layouted, then | ||||||
1442 | it is composed using the specified glue layout. | ||||||
1443 | |||||||
1444 | =cut | ||||||
1445 | |||||||
1446 | sub _compose_sprite_with_glue { | ||||||
1447 | my $self = shift; | ||||||
1448 | my %options = @_; | ||||||
1449 | |||||||
1450 | my @parts = @{$options{parts}}; | ||||||
1451 | |||||||
1452 | my $i = 0; | ||||||
1453 | |||||||
1454 | # compose the following rh_source_info of Layout objects | ||||||
1455 | my $rh_layout_source_info = {}; | ||||||
1456 | |||||||
1457 | # also join each rh_sources_info_from the parts... | ||||||
1458 | my %global_sources_info; | ||||||
1459 | |||||||
1460 | # keep all the layouts | ||||||
1461 | my @layouts; | ||||||
1462 | |||||||
1463 | # layout each part | ||||||
1464 | for my $rh_part (@parts) { | ||||||
1465 | |||||||
1466 | my $rh_sources_info = $self->_ensure_sources_info(%$rh_part); | ||||||
1467 | for my $key (sort { $a <=> $b } keys %$rh_sources_info) { | ||||||
1468 | $global_sources_info{$key} = $rh_sources_info->{$key}; | ||||||
1469 | } | ||||||
1470 | |||||||
1471 | my $Layout = $self->_ensure_layout(%$rh_part, | ||||||
1472 | rh_sources_info => $rh_sources_info | ||||||
1473 | ); | ||||||
1474 | |||||||
1475 | # we now do as if we were having images, but actually we have layouts | ||||||
1476 | # to do this we re-build a typical rh_sources_info. | ||||||
1477 | $rh_layout_source_info->{$i++} = { | ||||||
1478 | name => sprintf("%sLayout%s", $options{layout_name} // $options{layout}{name}, $i), | ||||||
1479 | pathname => "/fake/path_$i", | ||||||
1480 | parentdir => "/fake", | ||||||
1481 | width => $Layout->width, | ||||||
1482 | height => $Layout->height, | ||||||
1483 | first_pixel_x => 0, | ||||||
1484 | first_pixel_y => 0, | ||||||
1485 | }; | ||||||
1486 | |||||||
1487 | # save this layout | ||||||
1488 | push @layouts, $Layout; | ||||||
1489 | } | ||||||
1490 | |||||||
1491 | # now that we have the $rh_source_info **about layouts**, we layout the | ||||||
1492 | # layouts... | ||||||
1493 | my $LayoutOfLayouts = $self->_ensure_layout( | ||||||
1494 | layout => $options{layout}, | ||||||
1495 | rh_sources_info => $rh_layout_source_info, | ||||||
1496 | ); | ||||||
1497 | |||||||
1498 | # we need to adjust the position of each element of the layout according to | ||||||
1499 | # the positions of the elements in $LayoutOfLayouts | ||||||
1500 | my $FinalLayout; | ||||||
1501 | for my $layout_id ($LayoutOfLayouts->get_item_ids()) { | ||||||
1502 | my $Layout = $layouts[$layout_id]; | ||||||
1503 | my ($dx, $dy) = $LayoutOfLayouts->get_item_coord($layout_id); | ||||||
1504 | $Layout->move_items($dx, $dy); | ||||||
1505 | if (!$FinalLayout) { | ||||||
1506 | $FinalLayout = $Layout; | ||||||
1507 | } | ||||||
1508 | else { | ||||||
1509 | # merge $FinalLayout <- $Layout | ||||||
1510 | $FinalLayout->merge_with($Layout); | ||||||
1511 | } | ||||||
1512 | } | ||||||
1513 | |||||||
1514 | # fix width and height | ||||||
1515 | $FinalLayout->{width} = $LayoutOfLayouts->width(); | ||||||
1516 | $FinalLayout->{height} = $LayoutOfLayouts->height(); | ||||||
1517 | |||||||
1518 | # now simply draw the FinalLayout | ||||||
1519 | return $self->_write_image(%options, | ||||||
1520 | Layout => $FinalLayout, | ||||||
1521 | rh_sources_info => \%global_sources_info, | ||||||
1522 | ); | ||||||
1523 | } | ||||||
1524 | |||||||
1525 | =head2 _compose_sprite_without_glue | ||||||
1526 | |||||||
1527 | Compose a layout without glue layout: the previously lay-outed image set | ||||||
1528 | becomes part of the next image set. | ||||||
1529 | |||||||
1530 | =cut | ||||||
1531 | |||||||
1532 | sub _compose_sprite_without_glue { | ||||||
1533 | my $self = shift; | ||||||
1534 | my %options = @_; | ||||||
1535 | |||||||
1536 | my %global_sources_info; | ||||||
1537 | |||||||
1538 | my @parts = @{$options{parts}}; | ||||||
1539 | |||||||
1540 | my $LayoutOfLayouts; | ||||||
1541 | |||||||
1542 | my $i = 0; | ||||||
1543 | |||||||
1544 | for my $rh_part (@parts) { | ||||||
1545 | $i++; | ||||||
1546 | |||||||
1547 | # gather information about images in the current part | ||||||
1548 | my $rh_sources_info = $self->_ensure_sources_info(%$rh_part); | ||||||
1549 | |||||||
1550 | # keep composing the global sources_info structure | ||||||
1551 | # as we find new images... we will need this later | ||||||
1552 | # when we actually write the image. | ||||||
1553 | for my $key (sort { $a <=> $b } keys %$rh_sources_info) { | ||||||
1554 | $global_sources_info{$key} = $rh_sources_info->{$key}; | ||||||
1555 | } | ||||||
1556 | |||||||
1557 | if (!defined $LayoutOfLayouts) { | ||||||
1558 | # we keep the first layout | ||||||
1559 | $LayoutOfLayouts = $self->_ensure_layout(%$rh_part, | ||||||
1560 | rh_sources_info => $rh_sources_info | ||||||
1561 | ); | ||||||
1562 | } | ||||||
1563 | else { | ||||||
1564 | # tweak the $rh_sources_info to include a new | ||||||
1565 | # fake image (the previously created layout) | ||||||
1566 | my $fake_img_id = $self->_get_image_id(); | ||||||
1567 | $rh_sources_info->{$fake_img_id} = { | ||||||
1568 | name => 'FakeImage' . $i, | ||||||
1569 | pathname => "/fake/path_$i", | ||||||
1570 | parentdir => "/fake", | ||||||
1571 | width => $LayoutOfLayouts->width, | ||||||
1572 | height => $LayoutOfLayouts->height, | ||||||
1573 | first_pixel_x => 0, | ||||||
1574 | first_pixel_y => 0, | ||||||
1575 | }; | ||||||
1576 | |||||||
1577 | # we merge down this layout with the first | ||||||
1578 | # one, but first we must fix it, as it may | ||||||
1579 | # have been moved during this second | ||||||
1580 | # iteration. | ||||||
1581 | my $Layout = $self->_ensure_layout(%$rh_part, | ||||||
1582 | rh_sources_info => $rh_sources_info | ||||||
1583 | ); | ||||||
1584 | |||||||
1585 | # where was LayoutOfLayout positioned? | ||||||
1586 | my ($lol_x, $lol_y) = $Layout->get_item_coord($fake_img_id); | ||||||
1587 | |||||||
1588 | # fix previous layout | ||||||
1589 | $LayoutOfLayouts->move_items($lol_x, $lol_y); | ||||||
1590 | |||||||
1591 | # now remove it from $Layout and merge down! | ||||||
1592 | $Layout->delete_item($fake_img_id); | ||||||
1593 | $LayoutOfLayouts->merge_with($Layout); | ||||||
1594 | |||||||
1595 | # fix the width that doesn't get updated with | ||||||
1596 | # the new layout... | ||||||
1597 | $LayoutOfLayouts->{width} = $Layout->width(); | ||||||
1598 | $LayoutOfLayouts->{height} = $Layout->height(); | ||||||
1599 | } | ||||||
1600 | } | ||||||
1601 | |||||||
1602 | # draw it all! | ||||||
1603 | return $self->_write_image(%options, | ||||||
1604 | Layout => $LayoutOfLayouts, | ||||||
1605 | rh_sources_info => \%global_sources_info | ||||||
1606 | ); | ||||||
1607 | } | ||||||
1608 | |||||||
1609 | |||||||
1610 | =head2 _generate_color_histogram | ||||||
1611 | |||||||
1612 | Generate color histogram out of the information structure of all the images. | ||||||
1613 | |||||||
1614 | =cut | ||||||
1615 | |||||||
1616 | sub _generate_color_histogram { | ||||||
1617 | my $self = shift; | ||||||
1618 | my $rh_source_info = shift; | ||||||
1619 | |||||||
1620 | if (!$self->{enable_colormap}) { | ||||||
1621 | die "cannot generate color histogram with enable_colormap option disabled"; | ||||||
1622 | } | ||||||
1623 | |||||||
1624 | my %histogram; | ||||||
1625 | for my $id (sort { $a <=> $b } keys %$rh_source_info) { | ||||||
1626 | for my $color (sort keys %{ $rh_source_info->{$id}{colors}{map} }) { | ||||||
1627 | my $rah_colors_info = $rh_source_info->{$id}{colors}{map}{$color}; | ||||||
1628 | |||||||
1629 | $histogram{$color} = scalar @$rah_colors_info; | ||||||
1630 | } | ||||||
1631 | } | ||||||
1632 | |||||||
1633 | return \%histogram; | ||||||
1634 | } | ||||||
1635 | |||||||
1636 | =head2 _verbose | ||||||
1637 | |||||||
1638 | Print verbose output only if the verbose option was passed as input. | ||||||
1639 | |||||||
1640 | =cut | ||||||
1641 | |||||||
1642 | sub _verbose { | ||||||
1643 | my $self = shift; | ||||||
1644 | my $msg = shift; | ||||||
1645 | |||||||
1646 | if ($self->{is_verbose}) { | ||||||
1647 | print "${msg}\n"; | ||||||
1648 | } | ||||||
1649 | } | ||||||
1650 | |||||||
1651 | =head2 _generate_colormap_for_image_properties | ||||||
1652 | |||||||
1653 | Load the color map into the image properties hashref. This method takes 85% of | ||||||
1654 | the execution time when the sprite is generated with enable_colormap = 1. | ||||||
1655 | |||||||
1656 | =cut | ||||||
1657 | |||||||
1658 | |||||||
1659 | sub _generate_colormap_for_image_properties { | ||||||
1660 | my($self, $Image, $rh_info) = @_; | ||||||
1661 | return 1 if ref $rh_info->{colors}{map}; | ||||||
1662 | # Store information about the color of each pixel | ||||||
1663 | $rh_info->{colors}{map} = {}; | ||||||
1664 | my $x = 0; | ||||||
1665 | for my $fake_x ($rh_info->{first_pixel_x} .. $rh_info->{width}) { | ||||||
1666 | |||||||
1667 | my $y = 0; | ||||||
1668 | for my $fake_y ($rh_info->{first_pixel_y} .. $rh_info->{height}) { | ||||||
1669 | |||||||
1670 | my $color = $Image->Get( | ||||||
1671 | sprintf('pixel[%s,%s]', $fake_x, $fake_y), | ||||||
1672 | ); | ||||||
1673 | |||||||
1674 | push @{$rh_info->{colors}{map}{$color}}, { | ||||||
1675 | x => $x, | ||||||
1676 | y => $y, | ||||||
1677 | }; | ||||||
1678 | |||||||
1679 | $y++; | ||||||
1680 | } | ||||||
1681 | } | ||||||
1682 | return 1; | ||||||
1683 | } | ||||||
1684 | |||||||
1685 | =head1 LICENSE AND COPYRIGHT | ||||||
1686 | |||||||
1687 | Copyright 2013 Savio Dimatteo. | ||||||
1688 | |||||||
1689 | This program is free software; you can redistribute it and/or modify it | ||||||
1690 | under the terms of either: the GNU General Public License as published | ||||||
1691 | by the Free Software Foundation; or the Artistic License. | ||||||
1692 | |||||||
1693 | See http://dev.perl.org/licenses/ for more information. | ||||||
1694 | |||||||
1695 | |||||||
1696 | =cut | ||||||
1697 | |||||||
1698 | 1; # End of CSS::SpriteMaker |