File Coverage

blib/lib/OCBNET/WebSprite.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             ###################################################################################################
2             # Copyright 2013/2014 by Marcel Greter
3             # This file is part of OCBNET-WebSprite (GPL3)
4             ####################################################################################################
5             package OCBNET::WebSprite;
6             ####################################################################################################
7             our $VERSION = '1.0.2';
8             ####################################################################################################
9            
10 4     4   40711 use Carp;
  4         9  
  4         327  
11 4     4   20 use strict;
  4         7  
  4         121  
12 4     4   19 use warnings;
  4         11  
  4         118  
13            
14             ####################################################################################################
15            
16 4     4   2757 use OCBNET::CSS3;
  4         177488  
  4         119  
17 4     4   1209 use OCBNET::Image;
  0            
  0            
18            
19             use OCBNET::WebSprite::Fit;
20             use OCBNET::WebSprite::Edge;
21             use OCBNET::WebSprite::Corner;
22             use OCBNET::WebSprite::Canvas;
23             use OCBNET::WebSprite::Sprite;
24            
25             use OCBNET::CSS3::Styles::Margin;
26             use OCBNET::CSS3::Styles::Padding;
27             use OCBNET::CSS3::Styles::Background;
28             use OCBNET::CSS3::Styles::References;
29             use OCBNET::CSS3::DOM::Comment::Options;
30            
31             use OCBNET::CSS3::Regex::Base qw(unwrapUrl wrapUrl);
32             use OCBNET::CSS3::Regex::Numbers qw(fromPx toPx);
33             use OCBNET::CSS3::Regex::Background qw(fromPosition);
34            
35             # load function from core module
36             use List::MoreUtils qw(uniq);
37            
38             ####################################################################################################
39             # Constructor - not much going on
40             ####################################################################################################
41            
42             sub new
43             {
44            
45             # get arguments
46             my ($pkg) = @_;
47            
48             # create object
49             my $obj = {
50             # init array
51             'sprites' => [],
52             # init hash
53             'spritesets' => {}
54             };
55            
56             # bless into package
57             bless $obj, $pkg;
58            
59             }
60            
61             ####################################################################################################
62             # helper to accept various input sources
63             # will finally return a css stylesheet object
64             ####################################################################################################
65            
66             my $parseCSS = sub
67             {
68             # check first if the data is already in desired format
69             return $_[0] if UNIVERSAL::isa($_[0], "OCBNET::CSS3::Stylesheet");
70             # data was probably a string containing css
71             return OCBNET::CSS3::Stylesheet->new->parse($_[0]) unless ref $_[0];
72             # data was probably a string reference containing css
73             return OCBNET::CSS3::Stylesheet->new->parse(${$_[0]}) if ref $_[0] eq "SCALAR";
74             # otherwise we got some invalid data type
75             Carp::confess "invalid input data";
76             };
77            
78             ####################################################################################################
79             # method to find exactly equal sprite
80             ####################################################################################################
81            
82             sub findSprite
83             {
84            
85             # get arguments
86             my ($self, $config) = @_;
87            
88             # disable specific warning
89             no warnings 'uninitialized';
90            
91             # try all known sprites to find equivalent
92             foreach my $sprite (@{$self->{'sprites'}})
93             {
94            
95             # skip if any of the attributes differ
96             next if $sprite->{'filename'} ne $config->{'filename'};
97             next if $sprite->{'size-x'} ne $config->{'size-x'};
98             next if $sprite->{'size-y'} ne $config->{'size-y'};
99             next if $sprite->{'repeat-x'} ne $config->{'repeat-x'};
100             next if $sprite->{'repeat-y'} ne $config->{'repeat-y'};
101             next if $sprite->{'enclosed-x'} ne $config->{'enclosed-x'};
102             next if $sprite->{'enclosed-y'} ne $config->{'enclosed-y'};
103             next if $sprite->{'position-x'} ne $config->{'position-x'};
104             next if $sprite->{'position-y'} ne $config->{'position-y'};
105            
106             # found a sprite
107             return $sprite;
108            
109             }
110            
111             # nothing found
112             return undef;
113            
114             }
115            
116             ####################################################################################################
117             # method is responsible to write spritesets to the disk
118             # overload this method if you want to implement it different
119             ####################################################################################################
120            
121             sub writer
122             {
123             # get input arguments
124             my ($self, $path, $data, $opt) = @_;
125             # load module optionally
126             require File::Slurp;
127             # store path to opt if it is an array
128             push @{$opt}, $path if ref $opt eq "ARRAY";
129             # write the image to the disk (passed data is a scalar ref)
130             File::Slurp::write_file $path, { binmode => ':raw' }, $data;
131             }
132            
133             ####################################################################################################
134             # method is responsible to read images from the disk
135             # overload this method if you want to implement it different
136             ####################################################################################################
137            
138             sub reader
139             {
140             # get input arguments
141             my ($self, $path) = @_;
142             # load module optionally
143             require File::Slurp;
144             # read the file from the disk
145             File::Slurp::read_file($path, { binmode => ':raw' });
146             }
147            
148             ####################################################################################################
149             # main method to create spritesets
150             ####################################################################################################
151            
152             sub create
153             {
154            
155             # get input arguments
156             my ($self, $data, $opt) = @_;
157            
158             # convert data to stylesheet
159             my $css = &{$parseCSS}($data);
160            
161             # put all blocks in a flat array
162             my @blocks = ($css, $css->blocks);
163            
164             # this will process all and each sub block
165             for (my $i = 0; $i < scalar(@blocks); $i ++)
166             { push @blocks, $blocks[$i]->blocks; }
167            
168             # remove possible duplicates
169             @blocks = uniq @blocks;
170            
171             # process to setup canvas
172             foreach my $block (@blocks)
173             {
174            
175             # check if this comment is meant for us
176             next unless $block->option('sprite-image');
177            
178             # get parsed options collection
179             my $options = $block->options;
180            
181             # check if this comment is meant for us
182             next unless $options->get('sprite-image');
183            
184             # check if the sprite image has an associated id
185             die "sprite image has no id" unless $options->get('css-id');
186            
187             # get the id of this spriteset
188             my $id = $block->option('css-id');
189            
190             # pass debug mode from config to options
191             $options->{'debug'} = $self->{'config'}->{'debug'};
192            
193             # create a new canvas object to hold all sprites
194             my $canvas = OCBNET::WebSprite::Canvas->new(undef, $options);
195            
196             # add canvas to global hash object
197             $self->{'spritesets'}->{$id} = $canvas;
198            
199             # associate canvas with block
200             $block->{'canvas'} = $canvas;
201            
202             # store the id for canvas
203             $canvas->{'id'} = $id;
204            
205             }
206             # EO each block
207            
208             # filter out all unqiue selectors from each css blocks
209             my @selectors = grep { $_->type eq 'selector' } @blocks;
210            
211             # now process each selector and setup references
212             foreach my $selector (@selectors)
213             {
214             # find the block where the sprite-image is declared
215             # if there is no such block, the selector is not a sprite
216             my $block = $selector->find('option', 'sprite-image') || next;
217             # check if selector is not a canvas itself and sprite has css-id
218             if (! $selector->{'canvas'} && (my $id = $block->option('css-id')))
219             {
220             # connect the references spriteset to this selector
221             $selector->{'canvas'} = $self->{'spritesets'}->{$id};
222             }
223             }
224             # EO each selector
225            
226             # now process each selector and setup sprites
227             foreach my $selector (@selectors)
228             {
229            
230             # check if this selector block has a background
231             next unless $selector->style('background-image');
232            
233             # get associated spriteset canvas
234             my $canvas = $selector->{'canvas'} || next;
235            
236             # fill sprite config
237             my $config = {
238             # connect spriteset
239             # needed for reader
240             'spriteset' => $self,
241             # pass debug mode from config
242             # will draw funky color backgrounds
243             'debug' => $self->{'config'}->{'debug'},
244             # get the filename from the url (must be "normalized")
245             'filename' => unwrapUrl($selector->style('background-image')),
246             # the size the sprite is actually shown in (from css styles)
247             'size-x' => fromPx($selector->style('background-size-x')) || undef,
248             'size-y' => fromPx($selector->style('background-size-y')) || undef,
249             # set repeat options to decide where to ditribute
250             'repeat-x' => $selector->style('background-repeat-x') || 0,
251             'repeat-y' => $selector->style('background-repeat-y') || 0,
252             # set enclosed options to decide where to ditribute
253             'enclosed-x' => fromPx($selector->style('width') || 0) || 0,
254             'enclosed-y' => fromPx($selector->style('height') || 0) || 0,
255             # set position/align options to decide where to ditribute
256             'position-x' => fromPosition($selector->style('background-position-x') || 0),
257             'position-y' => fromPosition($selector->style('background-position-y') || 0)
258             };
259            
260             # try to find already loaded sprite
261             my $sprite = $self->findSprite($config);
262            
263             # or create a new sprite and setup most options
264             $sprite = OCBNET::WebSprite::Sprite->new($config) unless $sprite;
265            
266             # add sprite to collection
267             push @{$self->{'sprites'}}, $sprite;
268            
269             # store sprite object on selector
270             $selector->{'sprite'} = $sprite;
271            
272             # and also store the selector on the sprite
273             $sprite->{'selector'} = $selector;
274            
275             # add sprite to canvas
276             $canvas->add($sprite);
277            
278             }
279             # EO each selector
280            
281             # do the work on every spriteset
282             $_->optimize foreach $self->spritesets;
283             $_->distribute foreach $self->spritesets;
284             $_->finalize foreach $self->spritesets;
285            
286             # call write with our file writer
287             my $written = $self->write($opt);
288            
289             # now process each selector and setup sprites
290             foreach my $selector (@selectors)
291             {
292            
293             # new styles
294             my %styles;
295            
296             # selector has a canvas, this means the spriteset
297             # has been declares within this block, so render it
298             # check this directly and not with the object method
299             # this way we will really only check the local block
300             if ($selector->{'canvas'})
301             {
302            
303             # get canvas directly from selector block
304             # this means that the spriteset was defined
305             # inline and not in referenced selector block
306             my $canvas = $selector->{'canvas'};
307            
308             # get the url of the spriteset image
309             my $url = $canvas->{'options'}->get('url');
310            
311             # find block that has sprite-image obtion declared
312             if (my $block = $selector->find('option', 'sprite-image'))
313             {
314             # add background image to the selector if
315             # sprite-image has been declared on this selector
316             # or the sprite-image was declared on the stylesheet
317             if ($block eq $selector || $block->isa('Stylesheet'))
318             {
319             $styles{'background-image'} = wrapUrl($url);
320             $styles{'background-repeat'} = 'no-repeat';
321             }
322             }
323            
324             # remove all background styles from selector
325             $selector->clean(qr/background(?:\-[a-z0-9])*/);
326            
327             };
328             # EO each selector
329            
330             # check if this selector is configured for a sprite
331             if ($selector->{'sprite'})
332             {
333            
334             # get the sprite for selector
335             my $sprite = $selector->{'sprite'};
336            
337             # spriteset canvas of block
338             my $canvas = $selector->{'canvas'};
339            
340             # get the url of the spriteset image
341             my $url = $canvas->{'options'}->get('url');
342            
343             # get the sprite position within set
344             my $offset = $sprite->offset;
345            
346             # get position offset vars
347             my $offset_x = $offset->{'x'};
348             my $offset_y = $offset->{'y'};
349            
350             # assertion that the values are defined
351             die "no sprite x" unless defined $offset_x;
352             die "no sprite y" unless defined $offset_y;
353            
354             # get pre-caluculated position in spriteset
355             my $spriteset_x = $sprite->{'position-x'};
356             my $spriteset_y = $sprite->{'position-y'};
357            
358             # assertion that the values are defined
359             die "no spriteset x" unless defined $spriteset_x;
360             die "no spriteset y" unless defined $spriteset_y;
361            
362             # calculate the axes for background size
363             my $background_w = toPx($canvas->width / $sprite->scaleX);
364             my $background_h = toPx($canvas->height / $sprite->scaleY);
365            
366             # align relative to the top and relative to the left
367             $spriteset_y = toPx($spriteset_y - ($offset_y + $sprite->paddingTop) / $sprite->scaleY) if $sprite->alignTop;
368             $spriteset_x = toPx($spriteset_x - ($offset_x + $sprite->paddingLeft) / $sprite->scaleX) if $sprite->alignLeft;
369            
370             # assertion that the actual background position is always a full integer
371             warn "spriteset_x is not an integer $spriteset_x" unless $spriteset_x =~ m/^(?:\-?[0-9]+px|top|left|right|bottom)$/i;
372             warn "spriteset_y is not an integer $spriteset_y" unless $spriteset_y =~ m/^(?:\-?[0-9]+px|top|left|right|bottom)$/i;
373            
374             # check if sprite was distributed
375             # if it has no parent it means the
376             # sprite has not been included yet
377             unless ($sprite->{'parent'})
378             {
379             # check for debug mode on canvas or sprite
380             if ($canvas->{'debug'} || $sprite->{'debug'})
381             {
382             # make border dark red and background lightly red
383             $styles{'border-color'} = 'rgba(96, 0, 0, 0.875)';
384             $styles{'background-color'} = 'rgba(255, 0, 0, 0.125)';
385             }
386             }
387            
388             # sprite was distributed
389             else
390             {
391            
392             # add shorthand styles for sprite sizing and position
393             $styles{'background-size'} = join(' ', $background_w, $background_h);
394             $styles{'background-position'} = join(' ', $spriteset_x, $spriteset_y);
395            
396             # add repeating if sprite has it configured
397             if ($sprite->isRepeatX && $sprite->isFlexibleX)
398             { $styles{'background-repeat'} = 'repeat-x'; }
399             if ($sprite->isRepeatY && $sprite->isFlexibleY)
400             { $styles{'background-repeat'} = 'repeat-y'; }
401            
402             # remove all background styles from selector
403             $selector->clean(qr/background(?:\-[a-z0-9])*/);
404            
405             }
406            
407             }
408             # EO if has sprite
409            
410             # do we have new styles
411             if (scalar %styles)
412             {
413            
414             # render the selector bodies
415             my $body = $selector->body;
416            
417             # find the first indenting to reuse it
418             my $indent = $body =~ m/^([ ]*)\S/m ? $1 : ' ';
419            
420             # get the traling whitespace on last line
421             my $footer = $body =~ s/([ ]*)$// ? $1 : '';
422            
423             # add some debugger statements into css
424             $selector->{'footer'} .= "\n" . $indent . ";/* \\/ added by WebSprite \\/ */\n";
425            
426             # add these declarations to the footer to be included within block
427             $selector->{'footer'} .= sprintf "%s%s: %s;\n", $indent, $_, $styles{$_} foreach keys %styles;
428            
429             # add some debugger statements into css
430             $selector->{'footer'} .= $indent . "/* /\\ added by WebSprite /\\ */\n";
431            
432             # append traling whitespace again
433             $selector->{'footer'} .= $footer;
434            
435             }
436             # EO if has styles
437            
438             }
439             # EO each selector
440            
441             # css stylesheet
442             return $css;
443            
444             }
445             # EO create
446            
447            
448             # write out all spritesets within stylesheet
449             # ***************************************************************************************
450             sub write
451             {
452            
453             # get passed arguments
454             my ($self, $opt) = @_;
455            
456             # status variable
457             # info about all writes which is
458             # used to optimize files afterwards
459             my %written;
460            
461             # write all registered spritesets
462             foreach my $canvas ($self->spritesets)
463             {
464            
465             # get name of the canvas
466             my $id = $canvas->{'id'};
467            
468             # get the css options for canvas
469             # they are gathered from block comments
470             my $options = $canvas->{'options'};
471            
472             # parse sprite image option and add to options for later use
473             $options->set('url', unwrapUrl($options->get('sprite-image')));
474            
475             # assertion that we have gotten some usefull url to store the image
476             die "no sprite image defined for <$id>" unless $options->get('url');
477            
478             # call layout on canvas
479             $canvas->layout;
480            
481             # draw image and check for success
482             if (my $image = $canvas->draw)
483             {
484             # set the output format
485             $image->Set(magick => 'png');
486             # cal image to binary object
487             my $blob = $image->ImageToBlob();
488             # get the filename to store image
489             my $file = $options->get('url');
490             # call method (can be overriden)
491             $self->writer($file, $blob, $opt);
492             }
493             # couldn't draw the image
494             else
495             {
496             # throw a error message, maybe we could
497             # extend this a bit to be more verbose
498             die "canvas could not be drawn";
499             }
500             # EO if drawn
501            
502             }
503             # EO each spriteset
504            
505             # return status variable
506             return \%written;
507            
508             }
509             # EO sub write
510            
511             ####################################################################################################
512            
513             # return all spritesets in list context
514             sub spritesets { values %{$_[0]->{'spritesets'}} }
515            
516             ####################################################################################################
517             ####################################################################################################
518             1;