File Coverage

blib/lib/Convert/Color.pm
Criterion Covered Total %
statement 94 117 80.3
branch 22 42 52.3
condition 2 11 18.1
subroutine 33 35 94.2
pod 7 20 35.0
total 158 225 70.2


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2009-2024 -- leonerd@leonerd.org.uk
5              
6             package Convert::Color 0.18;
7              
8 18     18   240316 use v5.14;
  18         86  
9 18     18   105 use warnings;
  18         37  
  18         962  
10              
11 18     18   126 use Carp;
  18         32  
  18         1383  
12              
13 18     18   10077 use List::UtilsBy qw( min_by );
  18         40561  
  18         1617  
14              
15 18     18   9436 use meta 0.008;
  18         20607  
  18         876  
16 18     18   123 no warnings 'meta::experimental';
  18         34  
  18         1126  
17              
18             # Maximum number of entries in a ->closest_to cache
19 18     18   151 use constant MAX_CACHE_SIZE => 1000;
  18         37  
  18         1575  
20              
21 18         145 use Module::Pluggable require => 0,
22 18     18   9520 search_path => [ 'Convert::Color' ];
  18         226904  
23             my @plugins = Convert::Color->plugins;
24              
25             =head1 NAME
26              
27             C - color space conversions and named lookups
28              
29             =head1 SYNOPSIS
30              
31             use Convert::Color;
32              
33             my $color = Convert::Color->new( 'hsv:76,0.43,0.89' );
34              
35             my ( $red, $green, $blue ) = $color->rgb;
36              
37             # GTK uses 16-bit values
38             my $gtk_col = Gtk2::Gdk::Color->new( $color->as_rgb16->rgb16 );
39              
40             # HTML uses #rrggbb in hex
41             my $html = '';
42              
43             =head1 DESCRIPTION
44              
45             This module provides conversions between commonly used ways to express colors.
46             It provides conversions between color spaces such as RGB and HSV, and it
47             provides ways to look up colors by a name.
48              
49             This class provides a base for subclasses which represent particular color
50             values in particular spaces. The base class provides methods to represent the
51             color in a few convenient forms, though subclasses may provide more specific
52             details for the space in question.
53              
54             For more detail, read the documentation on these classes; namely:
55              
56             =over 4
57              
58             =item *
59              
60             L - red/green/blue as floats between 0 and 1
61              
62             =item *
63              
64             L - red/green/blue as 8-bit integers
65              
66             =item *
67              
68             L - red/green/blue as 16-bit integers
69              
70             =item *
71              
72             L - hue/saturation/value
73              
74             =item *
75              
76             L - hue/saturation/lightness
77              
78             =item *
79              
80             L - cyan/magenta/yellow
81              
82             =item *
83              
84             L - cyan/magenta/yellow/key (blackness)
85              
86             =back
87              
88             The following classes are subclasses of one of the above, which provide a way
89             to access predefined colors by names:
90              
91             =over 4
92              
93             =item *
94              
95             L - named lookup for the basic VGA colors
96              
97             =item *
98              
99             L - named lookup of colors from X11's F
100              
101             =back
102              
103             =cut
104              
105             =head1 CONSTRUCTOR
106              
107             =cut
108              
109             my $_space2class_cache_initialised;
110             my %_space2class_cache; # {$space} = $class
111             my %_class2space_cache; # {$class} = $space
112              
113             # doc'ed later for readability...
114             sub register_color_space
115             {
116 105     105 1 354 my $class = shift;
117 105         366 my ( $space ) = @_;
118              
119 105 50       492 exists $_space2class_cache{$space} and croak "Color space $space is already defined";
120 105 50       337 exists $_class2space_cache{$class} and croak "Class $class already declared a color space";
121              
122 105         309 $_space2class_cache{$space} = $class;
123 105         230 $_class2space_cache{$class} = $space;
124              
125 105         3779 my $metapkg = meta::get_this_package;
126             $metapkg->add_named_sub( "as_${space}" => sub {
127 83     83 0 449 shift->convert_to( $space )
        83 0    
        83 0    
        83 0    
        83 0    
        83 0    
        83 0    
        83 1    
        92 1    
        84      
128 105         1664 } );
129             }
130              
131             sub _space2class
132             {
133 129     129   258 my ( $space ) = @_;
134              
135 129 100       400 unless( $_space2class_cache_initialised ) {
136 10         24 $_space2class_cache_initialised++;
137             # Initialise the space name to class cache
138 10         30 foreach my $class ( @plugins ) {
139 100         619 ( my $file = "$class.pm" ) =~ s{::}{/}g;
140 100 50       45044 require $file or next;
141              
142 100 50       857 $class->can( 'COLOR_SPACE' ) or next;
143 0 0       0 my $thisspace = $class->COLOR_SPACE or next;
144              
145 0         0 warnings::warn( deprecated => "Discovered $class by deprecated COLOR_SPACE method" );
146              
147 0         0 $class->register_color_space( $thisspace );
148             }
149             }
150              
151 129         585 return $_space2class_cache{$space};
152             }
153              
154             =head2 new
155              
156             $color = Convert::Color->new( STRING );
157              
158             Return a new value to represent the color specified by the string. This string
159             should be prefixed by the name of the color space to which it applies. For
160             example
161              
162             rgb:RED,GREEN,BLUE
163             rgb8:RRGGBB
164             rgb16:RRRRGGGGBBBB
165             hsv:HUE,SAT,VAL
166             hsl:HUE,SAT,LUM
167             cmy:CYAN,MAGENTA,YELLOW
168             cmyk:CYAN,MAGENTA,YELLOW,KEY
169              
170             vga:NAME
171             vga:INDEX
172              
173             x11:NAME
174              
175             For more detail, see the constructor of the color space subclass in question.
176              
177             =cut
178              
179             sub new
180             {
181 8     8 1 249175 shift;
182 8         20 my ( $str ) = @_;
183              
184 8 50       64 $str =~ m/^(\w+):(.*)$/ or croak "Unable to parse color name $str";
185 8         29 ( my $space, $str ) = ( $1, $2 );
186              
187 8 50       20 my $class = _space2class( $space ) or croak "Unrecognised color space name '$space'";
188              
189 8         51 return $class->new( $str );
190             }
191              
192             =head1 METHODS
193              
194             =cut
195              
196             =head2 rgb
197              
198             ( $red, $green, $blue ) = $color->rgb;
199              
200             Returns the individual red, green and blue color components of the color
201             value. For RGB values, this is done directly. For values in other spaces, this
202             is done by first converting them to an RGB value using their C
203             method.
204              
205             =cut
206              
207             sub rgb
208             {
209 0     0 1 0 my $self = shift;
210 0         0 croak "Abstract method - should be overloaded by ".ref($self);
211             }
212              
213             =head1 COLOR SPACE CONVERSIONS
214              
215             Cross-conversion between color spaces is provided by the C
216             method, assisted by helper methods in the two color space classes involved.
217              
218             When converting C<$color> from color space SRC to color space DEST, the
219             following operations are attemped, in this order. SRC and DEST refer to the
220             names of the color spaces, e.g. C.
221              
222             =over 4
223              
224             =item 1.
225              
226             If SRC and DEST are equal, return C<$color> as it stands.
227              
228             =item 2.
229              
230             If the SRC space's class provides a C method, use it.
231              
232             =item 3.
233              
234             If the DEST space's class provides a C constructor, call it and
235             pass C<$color>.
236              
237             =item 4.
238              
239             If the DEST space's class provides a C constructor, convert C<$color>
240             to red/green/blue components then call it.
241              
242             =item 5.
243              
244             If none of these operations worked, then throw an exception.
245              
246             =back
247              
248             These functions may be called in the following ways:
249              
250             $other = $color->convert_to_DEST();
251             $other = Dest::Class->new_from_SRC( $color );
252             $other = Dest::Class->new_rgb( $color->rgb );
253              
254             =cut
255              
256             =head2 convert_to
257              
258             $other = $color->convert_to( $space );
259              
260             Attempt to convert the color into its representation in the given space. See
261             above for the various ways this may be achieved.
262              
263             If the relevant subclass has already been loaded (either explicitly, or
264             implicitly by either the C or C methods), then a specific
265             conversion method will be installed in the class.
266              
267             $other = $color->as_$space;
268              
269             Methods of this form are currently Ced if they do not yet exist, but
270             this feature should not be relied upon - see below.
271              
272             =cut
273              
274             sub convert_to
275             {
276 121     121 1 346 my $self = shift;
277 121         325 my ( $to_space ) = @_;
278              
279 121 50       293 my $to_class = _space2class( $to_space ) or croak "Unrecognised color space name '$to_space'";
280              
281 121         326 my $from_space = $_class2space_cache{ref $self};
282              
283 121 100       312 if( $from_space eq $to_space ) {
284             # Identity conversion
285 42         166 return $self;
286             }
287              
288 79         116 my $code;
289 79 100       298 if( $code = $self->can( "convert_to_$to_space" ) ) {
    100          
    50          
290 30         75 return $code->( $self );
291             }
292             elsif( $code = $to_class->can( "new_from_$from_space" ) ) {
293 3         13 return $code->( $to_class, $self );
294             }
295             elsif( $code = $to_class->can( "new_rgb" ) ) {
296             # TODO: check that $self->rgb is overloaded
297 46         163 return $code->( $to_class, $self->rgb );
298             }
299             else {
300 0         0 croak "Cannot convert from space '$from_space' to space '$to_space'";
301             }
302             }
303              
304             # Fallback implementations in case subclasses don't provide anything better
305              
306             sub convert_to_rgb
307             {
308 28     28 0 49 my $self = shift;
309 28         195 require Convert::Color::RGB;
310 28         106 return Convert::Color::RGB->new( $self->rgb );
311             }
312              
313             =head1 AUTOLOADED CONVERSION METHODS
314              
315             This class provides C and C behaviour which automatically
316             constructs conversion methods. The following method calls are identical:
317              
318             $color->convert_to('rgb');
319             $color->as_rgb;
320              
321             The generated method will be stored in the package, so that future calls will
322             not have the AUTOLOAD overhead.
323              
324             This feature is deprecated and should not be relied upon, due to the delicate
325             nature of C.
326              
327             =cut
328              
329             # Since this is AUTOLOADed, we can dynamically provide new methods for classes
330             # discovered at runtime.
331              
332             sub can
333             {
334 274     274 0 492 my $self = shift;
335 274         494 my ( $method ) = @_;
336              
337 274 50       753 if( $method =~ m/^as_(.*)$/ ) {
338 0         0 my $to_space = $1;
339 0 0       0 _space2class( $to_space ) or return undef;
340              
341             return sub {
342 0     0   0 my $self = shift;
343 0         0 return $self->convert_to( $to_space );
344 0         0 };
345             }
346              
347 274         2181 return $self->SUPER::can( $method );
348             }
349              
350             sub AUTOLOAD
351             {
352 197     197   14474 my ( $method ) = our $AUTOLOAD =~ m/::([^:]+)$/;
353              
354 197 50       3561 return if $method eq "DESTROY";
355              
356 0 0 0     0 if( ref $_[0] and my $code = $_[0]->can( $method ) ) {
357             # It's possible that the lazy loading by ->can has just created this method
358 0         0 warnings::warn( deprecated => "Relying on AUTOLOAD to provide $method" );
359 0 0       0 unless( defined &{$method} ) {
  0         0  
360 0         0 my $metapkg = meta::get_this_package;
361 0         0 $metapkg->add_named_sub( $method => $code );
362             }
363 0         0 goto &$code;
364             }
365              
366 0   0     0 my $class = ref $_[0] || $_[0];
367 0         0 croak qq(Cannot locate object method "$method" via package "$class");
368             }
369              
370             =head1 OTHER METHODS
371              
372             As well as the above, it is likely the subclass will provide accessors to
373             directly obtain the components of its representation in the specific space.
374             For more detail, see the documentation for the specific subclass in question.
375              
376             =cut
377              
378             =head1 SUBCLASS METHODS
379              
380             This base class is intended to be subclassed to provide more color spaces.
381              
382             =cut
383              
384             =head2 register_color_space
385              
386             $class->register_color_space( $space );
387              
388             A subclass should call this method to register itself as a named color space.
389              
390             =cut
391              
392             =head2 register_palette
393              
394             $class->register_palette( %args );
395              
396             A subclass that provides a fixed set of color values should call this method,
397             to set up automatic conversions that look for the closest match within the
398             set. This conversion process is controlled by the C<%args>:
399              
400             =over 8
401              
402             =item enumerate => STRING or CODE
403              
404             A method name or anonymous CODE reference which will be used to generate the
405             list of color values.
406              
407             =item enumerate_once => STRING or CODE
408              
409             As per C, but will be called only once and the results cached.
410              
411             =back
412              
413             This method creates a new class method on the calling package, called
414             C.
415              
416             =head2 closest_to
417              
418             $color = $pkg->closest_to( $orig, $space );
419              
420             Returns the color in the space closest to the given value. The distance is
421             measured in the named space; defaulting to C if this is not provided.
422              
423             In the case of a tie, where two or more colors have the same distance from the
424             target, the first one will be chosen.
425              
426             =cut
427              
428             sub register_palette
429             {
430 21     21 1 46 my $pkg = shift;
431 21         74 my %args = @_;
432              
433 21         37 my $enumerate;
434              
435 21 100       104 if( $args{enumerate} ) {
    50          
436 11         31 $enumerate = $args{enumerate};
437             }
438             elsif( my $enumerate_once = $args{enumerate_once} ) {
439 10         46 my @colors;
440             $enumerate = sub {
441 1     1   3 my $class = shift;
442 1 50       10 @colors = $class->$enumerate_once unless @colors;
443 1         46 return @colors;
444             }
445 10         51 }
446             else {
447 0         0 croak "Require 'enumerate' or 'enumerate_once'";
448             }
449              
450 21         399 my $metapkg = meta::package->get( $pkg );
451              
452 21         49 my %cache;
453             $metapkg->add_named_sub( closest_to => sub {
454 1     1 0 2 my $class = shift;
        1 0    
        1      
455 1         2 my ( $orig, $space ) = @_;
456              
457 1   50     5 $space ||= "rgb";
458              
459             # Prevent the cache getting -too- big
460 1         3 delete $cache{ each %cache } while keys %cache > MAX_CACHE_SIZE;
461              
462 1         21 $orig = $orig->convert_to( $space );
463 1         3 my $dst = "dst_${space}_cheap";
464              
465 1         6 my $key = join ",", $space, $orig->$space;
466              
467             return $cache{$key} //=
468 1   33 1   12 min_by { $orig->$dst( $_->convert_to( $space ) ) } $class->$enumerate;
  8         63  
469 21         213 } );
470              
471 21         78 foreach my $space (qw( rgb hsv hsl )) {
472             $metapkg->add_named_sub( "new_from_${space}" => sub {
473 1     2 0 3 my $class = shift;
          0    
474 1         2 my ( $rgb ) = @_;
475 1         5 return $pkg->closest_to( $rgb, $space );
476 63         463 } );
477             }
478              
479             $metapkg->add_named_sub( new_rgb => sub {
480 0     2     my $class = shift;
481 0           return $class->closest_to( Convert::Color::RGB->new( @_ ), "rgb" );
482 21         157 } );
483             }
484              
485             =head1 AUTHOR
486              
487             Paul Evans
488              
489             =cut
490              
491             0x55AA;