File Coverage

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