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