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   439 use strict;
  1         1  
  1         22  
4 1     1   3 use warnings;
  1         1  
  1         25  
5              
6             BEGIN {
7 1     1   13 $FuseBead::From::PNG::VERSION = '0.02';
8             }
9              
10 1     1   183 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             return \%info;
439             }
440              
441             =pod
442              
443             =head1 NAME
444              
445             FuseBead::From::PNG - Convert PNGs into plans to build a two dimensional fuse bead replica.
446              
447             =head1 SYNOPSIS
448              
449             use FuseBead::From::PNG;
450              
451             my $object = FuseBead::From::PNG;
452              
453             $object->bead_tally();
454              
455             =head1 DESCRIPTION
456              
457             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.
458              
459             The RGB values where obtained from Bead List with RGB Values (https://docs.google.com/spreadsheets/d/1f988o68HDvk335xXllJD16vxLBuRcmm3vg6U9lVaYpA/edit#gid=0).
460             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/).
461              
462             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)
463              
464             $hash->{'filename'} = $args{'filename'};
465              
466             $hash->{'unit_size'} = $args{'unit_size'} || 1;
467              
468             # White list default
469             $hash->{'whitelist'} = ($args{'whitelist'} && ref($args{'whitelist'}) eq 'ARRAY' && scalar(@{$args{'whitelist'}}) > 0) ? $args{'whitelist'} : undef;
470              
471             # Black list default
472             $hash->{'blacklist'} = ($args{'blacklist'} && ref($args{'blacklist'}) eq 'ARRAY' && scalar(@{$args{'blacklist'}}) > 0) ? $args{'blacklist'} : undef;
473              
474             =head1 USAGE
475              
476             =head2 new
477              
478             Usage : ->new()
479             Purpose : Returns FuseBead::From::PNG object
480              
481             Returns : FuseBead::From::PNG object
482             Argument :
483             filename - Optional. The file name of the PNG to process. Optional but if not provided, can't process the png.
484             e.g. filename => '/location/of/the.png'
485              
486             unit_size - Optional. The size of pixels squared to determine a single unit of a bead. Defaults to 1.
487             e.g. unit_size => 2 # pixelated colors are 2x2 in size
488              
489             whitelist - Optional. Array ref of colors, dimensions or color and dimensions that are allowed in the final plan output.
490             e.g. whitelist => [ 'BLACK', 'WHITE', '1x1x1', '1x2x1', '1x4x1', 'BLACK_1x6x1' ]
491              
492             blacklist - Optional. Array ref of colors, dimensions or color and dimensions that are not allowed in the final plan output.
493             e.g. blacklist => [ 'RED', '1x10x1', '1x12x1', '1x16x1', 'BLUE_1x8x1' ]
494              
495             Throws :
496              
497             Comment :
498             See Also :
499              
500             =head2 bead_dimensions
501              
502             Usage : ->bead_dimensions()
503             Purpose : returns a hashref with bead dimension information in millimeters (metric) or inches (imperial)
504              
505             Returns : hashref with bead dimension information, millimeters is default
506             Argument : $type - if set to imperial then dimension information is returned in inches
507             Throws :
508              
509             Comment :
510             See Also :
511              
512             =head2 bead_colors
513              
514             Usage : ->bead_colors()
515             Purpose : returns bead color constants consolidated as a hash.
516              
517             Returns : hashref with color constants keyed by the official color name in key form.
518             Argument :
519             Throws :
520              
521             Comment :
522             See Also :
523              
524             =head2 beads
525              
526             Usage : ->beads()
527             Purpose : Returns a list of all possible bead beads
528              
529             Returns : Hash ref with L objects keyed by their identifier
530             Argument :
531             Throws :
532              
533             Comment :
534             See Also :
535              
536             =head2 png
537              
538             Usage : ->png()
539             Purpose : Returns Image::PNG::Libpng object.
540              
541             Returns : Returns Image::PNG::Libpng object. See L for more details.
542             Argument :
543             Throws :
544              
545             Comment :
546             See Also :
547              
548             =head2 png_info
549              
550             Usage : ->png_info()
551             Purpose : Returns png IHDR info from the Image::PNG::Libpng object
552              
553             Returns : A hash of values containing information abou the png such as width and height. See get_IHDR in L for more details.
554             Argument : filename => the PNG to load and part
555             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
556             Throws :
557              
558             Comment :
559             See Also :
560              
561             =head2 bead_row_length
562              
563             Usage : ->bead_row_length()
564             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.
565              
566             Returns : The length of a row of beads (image width / unit size)
567             Argument :
568             Throws :
569              
570             Comment :
571             See Also :
572              
573             =head2 bead_col_height
574              
575             Usage : ->bead_col_height()
576             Purpose : Return the height in beads.
577              
578             Returns : The height of a col of beads (image height / unit size)
579             Argument :
580             Throws :
581              
582             Comment :
583             See Also :
584              
585             =head2 process
586              
587             Usage : ->process()
588             Purpose : Convert a provided PNG into a list of bead blocks that will allow building of a two dimensional bead replica.
589              
590             Returns : Hashref containing information about particular bead beads found to be needed based on the provided PNG.
591             Also included is the build order for those beads.
592             Argument : view => 'a view' - optionally format the return data. options include: JSON and HTML
593             Throws :
594              
595             Comment :
596             See Also :
597              
598             =head2 mirror
599              
600             Usage : ->mirror()
601             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.
602              
603             Returns : Either 1 or 0
604             Argument : a true or false value that will set whether the plans are mirrored to the image or not
605             Throws :
606              
607             Comment :
608             See Also :
609              
610             =head2 whitelist
611              
612             Usage : ->whitelist()
613             Purpose : return any whitelist settings stored in this object
614              
615             Returns : an arrayref of whitelisted colors and/or blocks, or undef
616             Argument :
617             Throws :
618              
619             Comment :
620             See Also :
621              
622             =head2 has_whitelist
623              
624             Usage : ->has_whitelist(), ->has_whitelist($filter)
625             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
626              
627             Returns : 1 or 0
628             Argument : $filter - optional scalar containing the filter to restrict test to
629             Throws :
630              
631             Comment :
632             See Also :
633              
634             =head2 is_whitelisted
635              
636             Usage : ->is_whitelisted($value), ->is_whitelisted($value, $filter)
637             Purpose : return a true if the value is whitelisted, otherwise false is returned
638              
639             Returns : 1 or 0
640             Argument : $value - the value to test, $filter - optional scalar containing the filter to restrict test to
641             Throws :
642              
643             Comment :
644             See Also :
645              
646             =head2 blacklist
647              
648             Usage : ->blacklist
649             Purpose : return any blacklist settings stored in this object
650              
651             Returns : an arrayref of blacklisted colors and/or blocks, or undef
652             Argument :
653             Throws :
654              
655             Comment :
656             See Also :
657              
658             =head2 has_blacklist
659              
660             Usage : ->has_blacklist(), ->has_whitelist($filter)
661             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
662              
663             Returns : 1 or 0
664             Argument : $filter - optional scalar containing the filter to restrict test to
665             Throws :
666              
667             Comment :
668             See Also :
669              
670             =head2 is_blacklisted
671              
672             Usage : ->is_blacklisted($value), ->is_whitelisted($value, $filter)
673             Purpose : return a true if the value is blacklisted, otherwise false is returned
674              
675             Returns : 1 or 0
676             Argument : $value - the value to test, $filter - optional scalar containing the filter to restrict test to
677             Throws :
678              
679             Comment :
680             See Also :
681              
682             =head2 _png_blocks_of_color
683              
684             Usage : ->_png_blocks_of_color()
685             Purpose : Convert a provided PNG into a list of rgb values based on [row][color]. Size of blocks are determined by 'unit_size'
686              
687             Returns : A list of hashes contain r, g and b values. e.g. ( { r => #, g => #, b => # }, { ... }, ... )
688             Argument :
689             Throws :
690              
691             Comment :
692             See Also :
693              
694             =head2 _color_score
695              
696             Usage : ->_color_score()
697             Purpose : returns a score indicating the likeness of one color to another. The lower the number the closer the colors are to each other.
698              
699             Returns : returns a positive integer score
700             Argument : $c1 - array ref with rgb color values in that order
701             $c2 - array ref with rgb color values in that order
702             Throws :
703              
704             Comment :
705             See Also :
706              
707             =head2 _find_bead_color
708              
709             Usage : ->_find_bead_color
710             Purpose : given an rgb params, finds the optimal bead color
711              
712             Returns : A bead color common name key that can then reference bead color information using L
713             Argument : $r - the red value of a color
714             $g - the green value of a color
715             $b - the blue value of a color
716             Throws :
717              
718             Comment : this subroutine is memoized as the name _find_bead_color_fast
719             See Also :
720              
721             =head2 _approximate_bead_colors
722              
723             Usage : ->_approximate_bead_colors()
724             Purpose : Generate a list of bead colors based on a list of blocks ( array of hashes containing rgb values )
725              
726             Returns : A list of bead color common name keys that can then reference bead color information using L
727             Argument :
728             Throws :
729              
730             Comment :
731             See Also :
732              
733             =head2 _generate_bead_list
734              
735             Usage : ->_approximate_bead_colors()
736             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)
737              
738             Returns : A list of bead color common name keys that can then reference bead color information using L
739             Argument :
740             Throws :
741              
742             Comment :
743             See Also :
744              
745             =head2 _bead_list
746              
747             Usage : ->_bead_list()
748             Purpose : Generate a list of bead colors based on a list of blocks ( array of hashes containing rgb values ) for knob up orientation
749              
750             Returns : A list of bead color common name keys that can then reference bead color information using L
751             Argument :
752             Throws :
753              
754             Comment :
755             See Also :
756              
757             =head2 _list_filters
758              
759             Usage : ->_list_filters()
760             Purpose : return whitelist/blacklist filters
761              
762             Returns : an hashref of filters
763             Argument : an optional filter restriction to limit set of filters returned to just one
764             Throws :
765              
766             Comment :
767             See Also :
768              
769             =head1 BUGS
770              
771             =head1 SUPPORT
772              
773             =head1 AUTHOR
774              
775             Travis Chase
776             CPAN ID: GAUDEON
777             gaudeon@cpan.org
778             https://github.com/gaudeon/FuseBead-From-Png
779              
780             =head1 COPYRIGHT
781              
782             This program is free software licensed under the...
783              
784             The MIT License
785              
786             The full text of the license can be found in the
787             LICENSE file included with this module.
788              
789             =head1 SEE ALSO
790              
791             perl(1).
792              
793             =cut
794              
795             1;