File Coverage

blib/lib/Lego/From/PNG.pm
Criterion Covered Total %
statement 8 10 80.0
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 12 14 85.7


line stmt bran cond sub pod time code
1             package Lego::From::PNG;
2              
3 1     1   438 use strict;
  1         2  
  1         36  
4 1     1   5 use warnings;
  1         1  
  1         43  
5              
6             BEGIN {
7 1     1   22 $Lego::From::PNG::VERSION = '0.02';
8             }
9              
10 1     1   165 use Image::PNG::Libpng qw(:all);
  0            
  0            
11             use Image::PNG::Const qw(:all);
12              
13             use Lego::From::PNG::Const qw(:all);
14              
15             use Lego::From::PNG::Brick;
16              
17             use Data::Debug;
18              
19             sub new {
20             my $class = shift;
21             my %args = ref $_[0] eq 'HASH' ? %{$_[0]} : @_;
22              
23             my $hash = {};
24              
25             $hash->{'filename'} = $args{'filename'};
26              
27             $hash->{'unit_size'} = $args{'unit_size'} || 1;
28              
29             # Brick depth and height defaults
30             $hash->{'brick_depth'} = 1;
31              
32             $hash->{'brick_height'} = 1;
33              
34             # White list default
35             $hash->{'whitelist'} = ($args{'whitelist'} && ref($args{'whitelist'}) eq 'ARRAY' && scalar(@{$args{'whitelist'}}) > 0) ? $args{'whitelist'} : undef;
36              
37             # Black list default
38             $hash->{'blacklist'} = ($args{'blacklist'} && ref($args{'blacklist'}) eq 'ARRAY' && scalar(@{$args{'blacklist'}}) > 0) ? $args{'blacklist'} : undef;
39              
40             my $self = bless ($hash, ref ($class) || $class);
41              
42             return $self;
43             }
44              
45             sub lego_colors {
46             my $self = shift;
47              
48             return $self->{'lego_colors'} ||= do {
49             my $hash = {};
50              
51             for my $color ( LEGO_COLORS ) {
52             my ($on_key, $cn_key, $hex_key, $r_key, $g_key, $b_key) = (
53             $color . '_OFFICIAL_NAME',
54             $color . '_COMMON_NAME',
55             $color . '_HEX_COLOR',
56             $color . '_RGB_COLOR_RED',
57             $color . '_RGB_COLOR_GREEN',
58             $color . '_RGB_COLOR_BLUE',
59             );
60              
61             no strict 'refs';
62              
63             $hash->{ $color } = {
64             'cid' => $color,
65             'official_name' => Lego::From::PNG::Const->$on_key,
66             'common_name' => Lego::From::PNG::Const->$cn_key,
67             'hex_color' => Lego::From::PNG::Const->$hex_key,
68             'rgb_color' => [
69             Lego::From::PNG::Const->$r_key,
70             Lego::From::PNG::Const->$g_key,
71             Lego::From::PNG::Const->$b_key,
72             ],
73             };
74             }
75              
76             $hash;
77             };
78             }
79              
80             sub lego_bricks {
81             my $self = shift;
82              
83             return $self->{'lego_bricks'} ||= do {
84             my $hash = {};
85              
86             for my $color ( LEGO_COLORS ) {
87             for my $length ( LEGO_BRICK_LENGTHS ) {
88             my $brick = Lego::From::PNG::Brick->new( color => $color, length => $length );
89              
90             $hash->{ $brick->identifier } = $brick;
91             }
92             }
93              
94             $hash;
95             };
96             }
97              
98             sub png {
99             my $self = shift;
100              
101             return $self->{'png'} ||= do {
102             my $png = read_png_file($self->{'filename'}, transforms => PNG_TRANSFORM_STRIP_ALPHA);
103              
104             $png;
105             };
106             };
107              
108             sub png_info {
109             my $self = shift;
110              
111             return $self->{'png_info'} ||= $self->png->get_IHDR;
112             }
113              
114             sub block_row_width {
115             my $self = shift;
116              
117             return $self->{'block_row_width'} ||= $self->png_info->{'width'} / $self->{'unit_size'};
118             }
119              
120             sub process {
121             my $self = shift;
122             my %args = ref $_[0] eq 'HASH' ? %{$_[0]} : @_;
123              
124             my $tally = {
125             bricks => {},
126             plan => [],
127             };
128              
129             if($self->{'filename'}) {
130             my @blocks = $self->_png_blocks_of_color;
131              
132             my @units = $self->_approximate_lego_colors( blocks => \@blocks );
133              
134             my @bricks = $self->_generate_brick_list(units => \@units);
135              
136             $tally->{'plan'} = [ map { $_->flatten } @bricks ];
137              
138             my %list;
139             for my $brick(@bricks) {
140             if(! exists $list{ $brick->identifier }) {
141             $list{ $brick->identifier } = $brick->flatten;
142              
143             delete $list{ $brick->identifier }{'meta'}; # No need for meta in brick list
144              
145             $list{ $brick->identifier }{'quantity'} = 1;
146             }
147             else {
148             $list{ $brick->identifier }{'quantity'}++;
149             }
150             }
151              
152             $tally->{'bricks'} = \%list;
153             }
154              
155             if($args{'view'}) {
156             my $view = $args{'view'};
157             my $module = "Lego::From::PNG::View::$view";
158              
159             $tally = eval {
160             (my $file = $module) =~ s|::|/|g;
161             require $file . '.pm';
162              
163             $module->new($self)->print($tally);
164             };
165              
166             die "Failed to format as a view ($view). $@" if $@;
167             }
168              
169             return $tally;
170             }
171              
172             sub _png_blocks_of_color {
173             my $self = shift;
174             my %args = ref $_[0] eq 'HASH' ? %{$_[0]} : @_;
175              
176             my @blocks;
177              
178             return @blocks unless $self->{'filename'}; # No file, no blocks
179              
180             my $pixel_bytecount = 3;
181              
182             my $y = -1;
183              
184             for my $pixel_row( @{$self->png->get_rows} ) {
185             $y++;
186              
187             next unless ($y % $self->{'unit_size'}) == 0;
188              
189             my $row = $y / $self->{'unit_size'}; # get actual row of blocks we are current on
190              
191             my @values = unpack 'C*', $pixel_row;
192              
193             my $row_width = ( scalar(@values) / $pixel_bytecount ) / $self->{'unit_size'};
194              
195             for(my $col = 0; $col < $row_width; $col++) {
196             my ($r, $g, $b) = (
197             $values[ ($self->{'unit_size'} * $pixel_bytecount * $col) ],
198             $values[ ($self->{'unit_size'} * $pixel_bytecount * $col) + 1 ],
199             $values[ ($self->{'unit_size'} * $pixel_bytecount * $col) + 2 ]
200             );
201              
202             $blocks[ ($row * $row_width) + $col ] = {
203             r => $r,
204             g => $g,
205             b => $b,
206             };
207             }
208             }
209              
210             return @blocks;
211             }
212              
213             sub _find_lego_color {
214             my $self = shift;
215             my $block = shift;
216              
217             my @optimal_color =
218             map { $_->{'cid'} }
219             sort { $a->{'score'} <=> $b->{'score'} }
220             map {
221             +{
222             cid => $_->{'cid'},
223             score => abs( $block->{'r'} - $_->{'rgb_color'}[0] )
224             + abs( $block->{'g'} - $_->{'rgb_color'}[1] )
225             + abs( $block->{'b'} - $_->{'rgb_color'}[2] ),
226             };
227             }
228             values %{ $self->lego_colors };
229              
230             my ($optimal_color) = grep {
231             my $choose_this_color = 1;
232              
233             $choose_this_color = 0 if ! $self->is_whitelisted( $_, 'color' );
234              
235             $choose_this_color = 0 if $self->is_blacklisted( $_, 'color' );
236              
237             $choose_this_color; # return result
238             } @optimal_color; # first color in list that passes whitelist and blacklist should be the optimal color for tested block
239              
240             return $optimal_color;
241             }
242              
243             sub _approximate_lego_colors {
244             my $self = shift;
245             my %args = ref $_[0] eq 'HASH' ? %{ $_[0] } : @_;
246              
247             die 'blocks not valid' unless $args{'blocks'} && ref( $args{'blocks'} ) eq 'ARRAY';
248              
249             my @colors;
250              
251             for my $block(@{ $args{'blocks'} }) {
252             push @colors, $self->_find_lego_color( $block );
253             }
254              
255             return @colors;
256             }
257              
258             sub _generate_brick_list {
259             my $self = shift;
260             my %args = ref $_[0] eq 'HASH' ? %{ $_[0] } : @_;
261              
262             die 'units not valid' unless $args{'units'} && ref( $args{'units'} ) eq 'ARRAY';
263              
264             my $unit_count = scalar(@{ $args{'units'} });
265             my @units = @{ $args{'units'} };
266             my $row_width = $self->block_row_width;
267             my $brick_height = 1; # bricks are only one unit high
268             my @brick_list;
269              
270             for(my $y = 0; $y < ($unit_count / $row_width); $y++) {
271             my @row = splice @units, 0, $row_width;
272              
273             my $push_color = sub {
274             my ($color, $length) = @_;
275              
276             if($color) {
277             push @brick_list, Lego::From::PNG::Brick->new(
278             color => $color,
279             depth => $self->{'brick_depth'},
280             length => $length,
281             height => $self->{'brick_height'},
282             meta => {
283             y => $y,
284             },
285             );
286             }
287             };
288              
289             my $process_color_sample = sub {
290             my ($color, $length) = @_;
291              
292             return if $length <= 0;
293              
294             # Now make sure we find bricks we are allowed to use
295             FIND_BRICKS: {
296             for( 1 .. $length) { # Only need to loop at least the number of times equal to the length of color found
297             my $valid_length = $length;
298             FIND_VALID_LENGTH: {
299             for(;$valid_length > 0;$valid_length--) {
300             my $dim = join('x',$self->{'brick_depth'},$valid_length,$self->{'brick_height'});
301             my $brk = join('_', $color, $dim);
302              
303             next FIND_VALID_LENGTH if $self->is_blacklisted( $dim, 'dimension' ) || $self->is_blacklisted( $brk, 'brick' );
304              
305             last FIND_VALID_LENGTH if $self->is_whitelisted( $dim, 'dimension' ) && $self->is_whitelisted( $brk, 'brick' );
306             }
307             }
308              
309             $push_color->($color, $valid_length);
310             $length -= $valid_length;
311              
312             last FIND_BRICKS if $length <= 0; # No need to push more bricks, we found them all
313             }
314             }
315              
316             die "No valid bricks found for remaining units of color" if $length > 0; # Catch if we have gremlins in our whitelist/blacklist
317             };
318              
319             # Run through rows and process colors
320             my $next_brick_color = '';
321             my $next_brick_length = 0;
322              
323             for my $color(@row) {
324             if( $color ne $next_brick_color ) {
325             $process_color_sample->($next_brick_color, $next_brick_length);
326              
327             $next_brick_color = $color;
328             $next_brick_length = 0;
329             }
330              
331             $next_brick_length++;
332             }
333              
334             $process_color_sample->($next_brick_color, $next_brick_length); # Process last color found
335             }
336              
337             return @brick_list;
338             }
339              
340             sub _list_filters {
341             my $self = shift;
342             my $allowed = $_[0] && ref($_[0]) eq 'ARRAY' ? $_[0]
343             : ($_[0]) ? [ shift ]
344             : []; # optional filter restriction
345              
346             my $filters = {
347             color => qr{^([A-Z_]+)(?:_\d+x\d+x\d+)?$}i,
348             dimension => qr{^(\d+x\d+x\d+)$}i,
349             brick => qr{^([A-Z_]+_\d+x\d+x\d+)$}i,
350             };
351              
352             $filters = +{ map { $_ => $filters->{$_} } @$allowed } if scalar @$allowed;
353              
354             return $filters;
355             }
356              
357             sub whitelist { shift->{'whitelist'} }
358              
359             sub has_whitelist {
360             my $self = shift;
361             my $allowed = shift; # arrayref listing filters we can use
362              
363             my $found = 0;
364             for my $filter(values $self->_list_filters($allowed)) {
365             $found += scalar( grep { /$filter/ } @{ $self->whitelist || [] } );
366             }
367              
368             return $found;
369             }
370              
371             sub is_whitelisted {
372             my $self = shift;
373             my $val = shift;
374             my $allowed = shift; # arrayref listing filters we can use
375              
376             return 1 if ! $self->has_whitelist($allowed); # return true if there is no whitelist
377              
378             for my $entry( @{ $self->whitelist || [] } ) {
379             for my $filter( values %{ $self->_list_filters($allowed) } ) {
380             next unless $entry =~ /$filter/; # if there is at least a letter at the beginning then this entry has a color we can check
381              
382             my $capture = $entry;
383             $capture =~ s/$filter/$1/;
384              
385             return 1 if $val eq $capture;
386             }
387             }
388              
389             return 0; # value is not in whitelist
390             }
391              
392             sub blacklist { shift->{'blacklist'} }
393              
394             sub has_blacklist {
395             my $self = shift;
396             my $allowed = shift; # optional filter restriction
397              
398             my $found = 0;
399              
400             for my $filter(values $self->_list_filters($allowed)) {
401             $found += scalar( grep { /$filter/ } @{ $self->blacklist || [] } );
402             }
403              
404             return $found;
405             }
406              
407             sub is_blacklisted {
408             my $self = shift;
409             my $val = shift;
410             my $allowed = shift; # optional filter restriction
411              
412             return 0 if ! $self->has_blacklist($allowed); # return false if there is no blacklist
413              
414             for my $entry( @{ $self->blacklist || [] } ) {
415             for my $filter( values %{ $self->_list_filters($allowed) } ) {
416             next unless $entry =~ /$filter/; # if there is at least a letter at the beginning then this entry has a color we can check
417              
418             my $capture = $1 || $entry;
419              
420             return 1 if $val eq $capture;
421             }
422             }
423              
424             return 0; # value is not in blacklist
425             }
426              
427             =pod
428              
429             =head1 NAME
430              
431             Lego::From::PNG - Convert PNGs into plans to build a two dimensional lego replica.
432              
433             =head1 SYNOPSIS
434              
435             use Lego::From::PNG;
436              
437             my $object = Lego::From::PNG;
438              
439             $object->brick_tally();
440              
441             =head1 DESCRIPTION
442              
443             Convert a PNG into a block list and plans to build a two dimensional replica of the PNG. The plans are built with brick
444             knobs pointed vertically so the picture will look like a flat surface to the viewer. Meaning the only dimension
445             of the brick being determined is the length. Depth and height are all the same for all bricks.
446              
447             $hash->{'filename'} = $args{'filename'};
448              
449             $hash->{'unit_size'} = $args{'unit_size'} || 1;
450              
451             # Brick depth and height defaults
452             $hash->{'brick_depth'} = 1;
453              
454             $hash->{'brick_height'} = 1;
455              
456             # White list default
457             $hash->{'whitelist'} = ($args{'whitelist'} && ref($args{'whitelist'}) eq 'ARRAY' && scalar(@{$args{'whitelist'}}) > 0) ? $args{'whitelist'} : undef;
458              
459             # Black list default
460             $hash->{'blacklist'} = ($args{'blacklist'} && ref($args{'blacklist'}) eq 'ARRAY' && scalar(@{$args{'blacklist'}}) > 0) ? $args{'blacklist'} : undef;
461              
462             =head1 USAGE
463              
464             =head2 new
465              
466             Usage : ->new()
467             Purpose : Returns Lego::From::PNG object
468              
469             Returns : Lego::From::PNG object
470             Argument :
471             filename - Optional. The file name of the PNG to process. Optional but if not provided, can't process the png.
472             e.g. filename => '/location/of/the.png'
473              
474             unit_size - Optional. The size of pixels squared to determine a single unit of a brick. Defaults to 1.
475             e.g. unit_size => 2 # pixelated colors are 2x2 in size
476              
477             brick_depth - Optional. The depth of all generated bricks. Defaults to 1.
478             e.g. brick_depth => 2 # final depth of all bricks are 2. So 2 x length x height
479              
480             brick_heigtht - Optional. The height of all generated bricks. Defaults to 1.
481             e.g. brick_height => 2 # final height of all bricks are 2. So depth x length x 2
482              
483             whitelist - Optional. Array ref of colors, dimensions or color and dimensions that are allowed in the final plan output.
484             e.g. whitelist => [ 'BLACK', 'WHITE', '1x1x1', '1x2x1', '1x4x1', 'BLACK_1x6x1' ]
485              
486             blacklist - Optional. Array ref of colors, dimensions or color and dimensions that are not allowed in the final plan output.
487             e.g. blacklist => [ 'RED', '1x10x1', '1x12x1', '1x16x1', 'BLUE_1x8x1' ]
488              
489             Throws :
490              
491             Comment :
492             See Also :
493              
494             =head2 lego_colors
495              
496             Usage : ->lego_colors()
497             Purpose : Returns lego color constants consolidated as a hash.
498              
499             Returns : Hash ref with color constants keyed by the official color name in key form.
500             Argument :
501             Throws :
502              
503             Comment :
504             See Also :
505              
506             =head2 lego_bricks
507              
508             Usage : ->lego_bricks()
509             Purpose : Returns a list of all possible lego bricks
510              
511             Returns : Hash ref with L objects keyed by their identifier
512             Argument :
513             Throws :
514              
515             Comment :
516             See Also :
517              
518             =head2 png
519              
520             Usage : ->png()
521             Purpose : Returns Image::PNG::Libpng object.
522              
523             Returns : Returns Image::PNG::Libpng object. See L for more details.
524             Argument :
525             Throws :
526              
527             Comment :
528             See Also :
529              
530             =head2 png_info
531              
532             Usage : ->png_info()
533             Purpose : Returns png IHDR info from the Image::PNG::Libpng object
534              
535             Returns : A hash of values containing information abou the png such as width and height. See get_IHDR in L for more details.
536             Argument : filename => the PNG to load and part
537             unit_size => the pixel width and height of one unit, blocks are generally identified as Nx1 blocks where N is the number of units of the same color
538             Throws :
539              
540             Comment :
541             See Also :
542              
543             =head2 block_row_width
544              
545             Usage : ->block_row_width()
546             Purpose : Return the width of one row of blocks. Since a block list is a single dimension array this is useful to figure out whict row a block is on.
547              
548             Returns : The length of a row of blocks (image width / unit size)
549             Argument :
550             Throws :
551              
552             Comment :
553             See Also :
554              
555             =head2 process
556              
557             Usage : ->process()
558             Purpose : Convert a provided PNG into a list of lego blocks that will allow building of a two dimensional lego replica.
559              
560             Returns : Hashref containing information about particular lego bricks found to be needed based on the provided PNG.
561             Also included is the build order for those bricks.
562             Argument : view => 'a view' - optionally format the return data. options include: JSON and HTML
563             Throws :
564              
565             Comment :
566             See Also :
567              
568             =head2 _png_blocks_of_color
569              
570             Usage : ->_png_blocks_of_color()
571             Purpose : Convert a provided PNG into a list of rgb values based on [row][color]. Size of blocks are determined by 'unit_size'
572              
573             Returns : A list of hashes contain r, g and b values. e.g. ( { r => #, g => #, b => # }, { ... }, ... )
574             Argument :
575             Throws :
576              
577             Comment :
578             See Also :
579              
580             =head2 _find_lego_color
581              
582             Usage : ->_find_lego_color
583             Purpose : given an rgb hash, finds the optimal lego color
584              
585             Returns : A lego color common name key that can then reference lego color information using L
586             Argument :
587             Throws :
588              
589             Comment :
590             See Also :
591              
592             =head2 _approximate_lego_colors
593              
594             Usage : ->_approximate_lego_colors()
595             Purpose : Generate a list of lego colors based on a list of blocks ( array of hashes containing rgb values )
596              
597             Returns : A list of lego color common name keys that can then reference lego color information using L
598             Argument :
599             Throws :
600              
601             Comment :
602             See Also :
603              
604             =head2 _generate_brick_list
605              
606             Usage : ->_approximate_lego_colors()
607             Purpose : Generate a list of lego colors based on a list of blocks ( array of hashes containing rgb values )
608              
609             Returns : A list of lego color common name keys that can then reference lego color information using L
610             Argument :
611             Throws :
612              
613             Comment :
614             See Also :
615              
616             =head2 _list_filters
617              
618             Usage : ->_list_filters()
619             Purpose : return whitelist/blacklist filters
620              
621             Returns : an hashref of filters
622             Argument : an optional filter restriction to limit set of filters returned to just one
623             Throws :
624              
625             Comment :
626             See Also :
627              
628             =head2 whitelist
629              
630             Usage : ->whitelist()
631             Purpose : return any whitelist settings stored in this object
632              
633             Returns : an arrayref of whitelisted colors and/or blocks, or undef
634             Argument :
635             Throws :
636              
637             Comment :
638             See Also :
639              
640             =head2 has_whitelist
641              
642             Usage : ->has_whitelist(), ->has_whitelist($filter)
643             Purpose : return a true value if there is a whitelist with at least one entry in it based on the allowed filters, otherwise a false value is returned
644              
645             Returns : 1 or 0
646             Argument : $filter - optional scalar containing the filter to restrict test to
647             Throws :
648              
649             Comment :
650             See Also :
651              
652             =head2 is_whitelisted
653              
654             Usage : ->is_whitelisted($value), ->is_whitelisted($value, $filter)
655             Purpose : return a true if the value is whitelisted, otherwise false is returned
656              
657             Returns : 1 or 0
658             Argument : $value - the value to test, $filter - optional scalar containing the filter to restrict test to
659             Throws :
660              
661             Comment :
662             See Also :
663              
664             =head2 blacklist
665              
666             Usage : ->blacklist
667             Purpose : return any blacklist settings stored in this object
668              
669             Returns : an arrayref of blacklisted colors and/or blocks, or undef
670             Argument :
671             Throws :
672              
673             Comment :
674             See Also :
675              
676             =head2 has_blacklist
677              
678             Usage : ->has_blacklist(), ->has_whitelist($filter)
679             Purpose : return a true value if there is a blacklist with at least one entry in it based on the allowed filters, otherwise a false value is returned
680              
681             Returns : 1 or 0
682             Argument : $filter - optional scalar containing the filter to restrict test to
683             Throws :
684              
685             Comment :
686             See Also :
687              
688             =head2 is_blacklisted
689              
690             Usage : ->is_blacklisted($value), ->is_whitelisted($value, $filter)
691             Purpose : return a true if the value is blacklisted, otherwise false is returned
692              
693             Returns : 1 or 0
694             Argument : $value - the value to test, $filter - optional scalar containing the filter to restrict test to
695             Throws :
696              
697             Comment :
698             See Also :
699              
700             =head1 BUGS
701              
702             =head1 SUPPORT
703              
704             =head1 AUTHOR
705              
706             Travis Chase
707             CPAN ID: GAUDEON
708             gaudeon@cpan.org
709             https://github.com/gaudeon/Lego-From-Png
710              
711             =head1 COPYRIGHT
712              
713             This program is free software licensed under the...
714              
715             The MIT License
716              
717             The full text of the license can be found in the
718             LICENSE file included with this module.
719              
720             =head1 SEE ALSO
721              
722             perl(1).
723              
724             =cut
725              
726             1;
727