Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/wand/color.py : 28%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1""":mod:`wand.color` --- Colors
2~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4.. versionadded:: 0.1.2
6"""
7import ctypes
8import numbers
10from .api import library
11from .cdefs.structures import MagickPixelPacket, PixelInfo
12from .compat import binary, text
13from .resource import Resource
14from .version import MAGICK_VERSION_NUMBER, MAGICK_HDRI, QUANTUM_DEPTH
16__all__ = 'Color', 'scale_quantum_to_int8'
19class Color(Resource):
20 """Color value.
22 Unlike any other objects in Wand, its resource management can be
23 implicit when it used outside of :keyword:`with` block. In these case,
24 its resource are allocated for every operation which requires a resource
25 and destroyed immediately. Of course it is inefficient when the
26 operations are much, so to avoid it, you should use color objects
27 inside of :keyword:`with` block explicitly e.g.::
29 red_count = 0
30 with Color('#f00') as red:
31 with Image(filename='image.png') as img:
32 for row in img:
33 for col in row:
34 if col == red:
35 red_count += 1
37 :param string: a color name string e.g. ``'rgb(255, 255, 255)'``,
38 ``'#fff'``, ``'white'``. see `ImageMagick Color Names`_
39 doc also
40 :type string: :class:`basestring`
42 .. versionchanged:: 0.3.0
43 :class:`Color` objects become hashable.
45 .. versionchanged:: 0.5.1
46 Color channel properties can now be set.
48 .. versionchanged:: 0.5.1
49 Added :attr:`cyan`, :attr:`magenta`, :attr:`yellow`, & :attr:`black`
50 properties for CMYK :class:`Color` instances.
52 .. versionchanged:: 0.5.1
53 Method :meth:`Color.from_hsl()` can create a RGB color from ``hue``,
54 ``saturation``, & ``lightness`` values.
56 .. seealso::
58 `ImageMagick Color Names`_
59 The color can then be given as a color name (there is a limited
60 but large set of these; see below) or it can be given as a set
61 of numbers (in decimal or hexadecimal), each corresponding to
62 a channel in an RGB or RGBA color model. HSL, HSLA, HSB, HSBA,
63 CMYK, or CMYKA color models may also be specified. These topics
64 are briefly described in the sections below.
66 .. _ImageMagick Color Names: http://www.imagemagick.org/script/color.php
68 .. describe:: == (other)
70 Equality operator.
72 :param other: a color another one
73 :type color: :class:`Color`
74 :returns: ``True`` only if two images equal.
75 :rtype: :class:`bool`
77 """
79 #: (:class:`bool`) Whether the color has changed or not.
80 dirty = None
82 c_is_resource = library.IsPixelWand
83 c_destroy_resource = library.DestroyPixelWand
84 c_get_exception = library.PixelGetException
85 c_clear_exception = library.PixelClearException
87 __slots__ = 'raw', 'c_resource', 'allocated'
89 def __init__(self, string=None, raw=None):
90 if (string is None and raw is None or
91 string is not None and raw is not None):
92 raise TypeError('expected one argument')
94 # MagickPixelPacket has been deprecated, use PixelInfo
95 self.use_pixel = MAGICK_VERSION_NUMBER >= 0x700
96 self.dirty = False
97 self.allocated = 0
98 if raw is None:
99 if self.use_pixel: # pragma: no cover
100 self.raw = ctypes.create_string_buffer(
101 ctypes.sizeof(PixelInfo)
102 )
103 else:
104 self.raw = ctypes.create_string_buffer(
105 ctypes.sizeof(MagickPixelPacket)
106 )
107 with self:
108 # Create color from string.
109 ok = library.PixelSetColor(self.resource, binary(string))
110 if not ok:
111 # Could not understand color-input. Try sending
112 # ImageMagick's exception.
113 self.raise_exception()
114 # That might be only a warning. Try a more generic message.
115 msg = 'Unrecognized color string "{0}"'.format(string)
116 raise ValueError(msg)
117 # Copy color value to structure buffer for future read.
118 library.PixelGetMagickColor(self.resource, self.raw)
119 else:
120 self.raw = raw
122 def __getinitargs__(self):
123 return self.string, None
125 def __enter__(self):
126 if self.allocated < 1:
127 with self.allocate():
128 # Initialize resource.
129 self.resource = library.NewPixelWand()
130 # Restore color value from structure buffer.
131 if self.use_pixel: # pragma: no cover
132 library.PixelSetPixelColor(self.resource, self.raw)
133 else:
134 library.PixelSetMagickColor(self.resource, self.raw)
135 self.allocated = 1
136 else:
137 self.allocated += 1
138 return Resource.__enter__(self)
140 def __exit__(self, type, value, traceback):
141 self.allocated -= 1
142 if self.dirty:
143 library.PixelGetMagickColor(self.resource, self.raw)
144 self.dirty = False
145 if self.allocated < 1:
146 Resource.__exit__(self, type, value, traceback)
148 def __eq__(self, other):
149 if not isinstance(other, Color):
150 return False
151 with self as this:
152 with other:
153 return self.c_equals(this.resource, other.resource)
155 def __ne__(self, other):
156 return not (self == other)
158 def __hash__(self):
159 if self.alpha:
160 return hash(self.normalized_string)
161 return hash(None)
163 def __str__(self):
164 return self.string
166 def __repr__(self):
167 c = type(self)
168 return '{0}.{1}({2!r})'.format(c.__module__, c.__name__, self.string)
170 def _assert_double(self, subject):
171 """Ensure the given ``subject`` is a float type, and value between
172 0.0 & 1.0.
174 :param subject: value to assert as a valid double.
175 :type subject: :class:`numbers.Real`
176 :raises ValueError: if the subject is not between 0.0 and 1.0
177 :raises TypeError: if the subject is not a float-point number.
179 ..versionadded:: 0.5.1
180 """
181 if not isinstance(subject, numbers.Real):
182 raise TypeError('Expecting a float-point real number, not ' +
183 repr(subject))
184 if subject < 0.0 or subject > 1.0:
185 raise ValueError('Expecting a real number between 0.0 & 1.0, not' +
186 repr(subject))
188 def _assert_int8(self, subject):
189 """Ensure the given ``subject`` is a integer type, and value between
190 0 & 255.
192 :param subject: value to assert as a valid number.
193 :type subject: :class:`numbers.Integral`
194 :raises ValueError: if the subject is not between 0 and 255
195 :raises TypeError: if the subject is not a Integral number.
197 ..versionadded:: 0.5.1
198 """
199 if not isinstance(subject, numbers.Integral):
200 raise TypeError('Expecting an integer number, not ' +
201 repr(subject))
202 if subject < 0 or subject > 255:
203 raise ValueError('Expecting a real number between 0 & 255, not' +
204 repr(subject))
206 def _assert_quantum(self, subject):
207 """Ensure the given ``subject`` is a number, and value between
208 0.0 & QuantumRange.
210 The QuantumRange is the max value based on the QuantumDepth of the
211 ImageMagick library (i.e. Q16).
213 :param subject: value to assert as a valid double.
214 :type subject: :class:`numbers.Number`
215 :raises ValueError: if the subject is not between 0 and QuantumRange
216 :raises TypeError: if the subject is not a number.
218 ..versionadded:: 0.5.1
219 """
220 quantum_range = {
221 8: 255.0,
222 16: 65535.0,
223 32: 4294967295.0,
224 64: 18446744073709551615.0
225 }
226 if not isinstance(subject, numbers.Number):
227 raise TypeError('Expecting a number, not ' + repr(subject))
228 if subject < 0.0 or subject > quantum_range[QUANTUM_DEPTH]:
229 message = 'Expecting a number between 0 & {0}, not {1}'
230 raise ValueError(message.format(quantum_range[QUANTUM_DEPTH],
231 repr(subject)))
233 def _repr_html_(self):
234 html = """
235 <span style="background-color:#{red:02X}{green:02X}{blue:02X};
236 display:inline-block;
237 line-height:1em;
238 width:1em;"> </span>
239 <strong>#{red:02X}{green:02X}{blue:02X}</strong>
240 """
241 return html.format(red=self.red_int8,
242 green=self.green_int8,
243 blue=self.blue_int8)
245 @staticmethod
246 def c_equals(a, b):
247 """Raw level version of equality test function for two pixels.
249 :param a: a pointer to PixelWand to compare
250 :type a: :class:`ctypes.c_void_p`
251 :param b: a pointer to PixelWand to compare
252 :type b: :class:`ctypes.c_void_p`
253 :returns: ``True`` only if two pixels equal
254 :rtype: :class:`bool`
256 .. note::
258 It's only for internal use. Don't use it directly.
259 Use ``==`` operator of :class:`Color` instead.
261 """
262 alpha = library.PixelGetAlpha
263 return bool(library.IsPixelWandSimilar(a, b, 0) and
264 alpha(a) == alpha(b))
266 @classmethod
267 def from_hsl(cls, hue=0.0, saturation=0.0, lightness=0.0):
268 """Creates a RGB color from HSL values. The ``hue``, ``saturation``,
269 and ``lightness`` must be normalized between 0.0 & 1.0.
271 .. code::
273 h=0.75 # 270 Degrees
274 s=1.0 # 100 Percent
275 l=0.5 # 50 Percent
276 with Color.from_hsl(hue=h, saturation=s, lightness=l) as color:
277 print(color) #=> srgb(128,0,255)
279 :param hue: a normalized double between 0.0 & 1.0.
280 :type hue: :class:`numbers.Real`
281 :param saturation: a normalized double between 0.0 & 1.0.
282 :type saturation: :class:`numbers.Real`
283 :param lightness: a normalized double between 0.0 & 1.0.
284 :type lightness: :class:`numbers.Real`
285 :rtype: :class:`Color`
287 .. versionadded:: 0.5.1
288 """
289 color = cls('WHITE')
290 color._assert_double(hue)
291 color._assert_double(saturation)
292 color._assert_double(lightness)
293 color.dirty = True
294 with color:
295 library.PixelSetHSL(color.resource, hue, saturation, lightness)
296 return color
298 @classmethod
299 def from_pixelwand(cls, pixelwand):
300 assert pixelwand
301 if MAGICK_VERSION_NUMBER < 0x700:
302 pixel_structure = MagickPixelPacket
303 else: # pragma: no cover
304 pixel_structure = PixelInfo
305 size = ctypes.sizeof(pixel_structure)
306 raw_buffer = ctypes.create_string_buffer(size)
307 library.PixelGetMagickColor(pixelwand, raw_buffer)
308 return cls(raw=raw_buffer)
310 @property
311 def alpha(self):
312 """(:class:`numbers.Real`) Alpha value, from 0.0 to 1.0."""
313 with self:
314 return library.PixelGetAlpha(self.resource)
316 @alpha.setter
317 def alpha(self, value):
318 self._assert_double(value)
319 self.dirty = True
320 with self:
321 library.PixelSetAlpha(self.resource, value)
323 @property
324 def alpha_int8(self):
325 """(:class:`numbers.Integral`) Alpha value as 8bit integer which is
326 a common style. From 0 to 255.
328 .. versionadded:: 0.3.0
330 """
331 return scale_quantum_to_int8(self.alpha_quantum)
333 @alpha_int8.setter
334 def alpha_int8(self, value):
335 self._assert_int8(value)
336 self.alpha = float(value) / 255.0
338 @property
339 def alpha_quantum(self):
340 """(:class:`numbers.Integral`) Alpha value.
341 Scale depends on :const:`~wand.version.QUANTUM_DEPTH`.
343 .. versionadded:: 0.3.0
345 """
346 with self:
347 return library.PixelGetAlphaQuantum(self.resource)
349 @alpha_quantum.setter
350 def alpha_quantum(self, value):
351 self._assert_quantum(value)
352 self.dirty = True
353 with self:
354 library.PixelSetAlphaQuantum(self.resource, value)
356 @property
357 def black(self):
358 """(:class:`numbers.Real`) Black, or ``'K'``, color channel in CMYK
359 colorspace. Unused by RGB colorspace.
361 .. versionadded:: 0.5.1
362 """
363 with self:
364 return library.PixelGetBlack(self.resource)
366 @black.setter
367 def black(self, value):
368 self._assert_double(value)
369 self.dirty = True
370 with self:
371 library.PixelSetBlack(self.resource, value)
373 @property
374 def black_int8(self):
375 """(:class:`numbers.Integral`) Black value as 8bit integer which is
376 a common style. From 0 to 255.
378 .. versionadded:: 0.5.1
379 """
380 return scale_quantum_to_int8(self.black_quantum)
382 @black_int8.setter
383 def black_int8(self, value):
384 self._assert_int8(value)
385 self.black = float(value) / 255.0
387 @property
388 def black_quantum(self):
389 """(:class:`numbers.Integral`) Black.
390 Scale depends on :const:`~wand.version.QUANTUM_DEPTH`.
392 .. versionadded:: 0.5.1
393 """
394 with self:
395 return library.PixelGetBlackQuantum(self.resource)
397 @black_quantum.setter
398 def black_quantum(self, value):
399 self._assert_quantum(value)
400 self.dirty = True
401 with self:
402 library.PixelSetBlackQuantum(self.resource, value)
404 @property
405 def blue(self):
406 """(:class:`numbers.Real`) Blue, from 0.0 to 1.0."""
407 with self:
408 return library.PixelGetBlue(self.resource)
410 @blue.setter
411 def blue(self, value):
412 self._assert_double(value)
413 self.dirty = True
414 with self:
415 library.PixelSetBlue(self.resource, value)
417 @property
418 def blue_int8(self):
419 """(:class:`numbers.Integral`) Blue as 8bit integer which is
420 a common style. From 0 to 255.
422 .. versionadded:: 0.3.0
424 """
425 return scale_quantum_to_int8(self.blue_quantum)
427 @blue_int8.setter
428 def blue_int8(self, value):
429 self._assert_int8(value)
430 self.blue = float(value) / 255.0
432 @property
433 def blue_quantum(self):
434 """(:class:`numbers.Integral`) Blue.
435 Scale depends on :const:`~wand.version.QUANTUM_DEPTH`.
437 .. versionadded:: 0.3.0
439 """
440 with self:
441 return library.PixelGetBlueQuantum(self.resource)
443 @blue_quantum.setter
444 def blue_quantum(self, value):
445 self._assert_quantum(value)
446 self.dirty = True
447 with self:
448 library.PixelSetBlueQuantum(self.resource, value)
450 @property
451 def cyan(self):
452 """(:class:`numbers.Real`) Cyan color channel in CMYK
453 colorspace. Unused by RGB colorspace.
455 .. versionadded:: 0.5.1
456 """
457 with self:
458 return library.PixelGetCyan(self.resource)
460 @cyan.setter
461 def cyan(self, value):
462 self._assert_double(value)
463 self.dirty = True
464 with self:
465 library.PixelSetCyan(self.resource, value)
467 @property
468 def cyan_int8(self):
469 """(:class:`numbers.Integral`) Cyan value as 8bit integer which is
470 a common style. From 0 to 255.
472 .. versionadded:: 0.5.1
473 """
474 return scale_quantum_to_int8(self.cyan_quantum)
476 @cyan_int8.setter
477 def cyan_int8(self, value):
478 self._assert_int8(value)
479 self.cyan = float(value) / 255.0
481 @property
482 def cyan_quantum(self):
483 """(:class:`numbers.Integral`) Cyan.
484 Scale depends on :const:`~wand.version.QUANTUM_DEPTH`.
486 .. versionadded:: 0.5.1
487 """
488 with self:
489 return library.PixelGetCyanQuantum(self.resource)
491 @cyan_quantum.setter
492 def cyan_quantum(self, value):
493 self._assert_quantum(value)
494 self.dirty = True
495 with self:
496 library.PixelSetCyanQuantum(self.resource, value)
498 @property
499 def fuzz(self):
500 with self:
501 return library.PixelGetFuzz(self.resource)
503 @fuzz.setter
504 def fuzz(self, value):
505 if not isinstance(value, numbers.Real):
506 raise TypeError('Expecting a float-point real number, not ' +
507 repr(value))
508 self.dirty = True
509 with self:
510 library.PixelSetFuzz(self.resource, value)
512 @property
513 def green(self):
514 """(:class:`numbers.Real`) Green, from 0.0 to 1.0."""
515 with self:
516 return library.PixelGetGreen(self.resource)
518 @green.setter
519 def green(self, value):
520 self._assert_double(value)
521 self.dirty = True
522 with self:
523 library.PixelSetGreen(self.resource, value)
525 @property
526 def green_int8(self):
527 """(:class:`numbers.Integral`) Green as 8bit integer which is
528 a common style. From 0 to 255.
530 .. versionadded:: 0.3.0
532 """
533 return scale_quantum_to_int8(self.green_quantum)
535 @green_int8.setter
536 def green_int8(self, value):
537 self._assert_int8(value)
538 self.green = float(value) / 255.0
540 @property
541 def green_quantum(self):
542 """(:class:`numbers.Integral`) Green.
543 Scale depends on :const:`~wand.version.QUANTUM_DEPTH`.
545 .. versionadded:: 0.3.0
547 """
548 with self:
549 return library.PixelGetGreenQuantum(self.resource)
551 @green_quantum.setter
552 def green_quantum(self, value):
553 self._assert_quantum(value)
554 self.dirty = True
555 with self:
556 library.PixelSetGreenQuantum(self.resource, value)
558 @property
559 def magenta(self):
560 """(:class:`numbers.Real`) Magenta color channel in CMYK
561 colorspace. Unused by RGB colorspace.
563 .. versionadded:: 0.5.1
564 """
565 with self:
566 return library.PixelGetMagenta(self.resource)
568 @magenta.setter
569 def magenta(self, value):
570 self._assert_double(value)
571 self.dirty = True
572 with self:
573 library.PixelSetMagenta(self.resource, value)
575 @property
576 def magenta_int8(self):
577 """(:class:`numbers.Integral`) Magenta value as 8bit integer which is
578 a common style. From 0 to 255.
580 .. versionadded:: 0.5.1
581 """
582 return scale_quantum_to_int8(self.magenta_quantum)
584 @magenta_int8.setter
585 def magenta_int8(self, value):
586 self._assert_int8(value)
587 self.magenta = float(value) / 255.0
589 @property
590 def magenta_quantum(self):
591 with self:
592 return library.PixelGetMagentaQuantum(self.resource)
594 @magenta_quantum.setter
595 def magenta_quantum(self, value):
596 """(:class:`numbers.Integral`) Magenta.
597 Scale depends on :const:`~wand.version.QUANTUM_DEPTH`.
599 .. versionadded:: 0.5.1
600 """
601 self._assert_quantum(value)
602 self.dirty = True
603 with self:
604 library.PixelSetMagentaQuantum(self.resource, value)
606 @property
607 def normalized_string(self):
608 """(:class:`basestring`) The normalized string representation of
609 the color. The same color is always represented to the same
610 string.
612 .. versionadded:: 0.3.0
614 """
615 with self:
616 string = library.PixelGetColorAsNormalizedString(self.resource)
617 return text(string.value)
619 @property
620 def red(self):
621 """(:class:`numbers.Real`) Red, from 0.0 to 1.0."""
622 with self:
623 return library.PixelGetRed(self.resource)
625 @red.setter
626 def red(self, value):
627 self._assert_double(value)
628 self.dirty = True
629 with self:
630 library.PixelSetRed(self.resource, value)
632 @property
633 def red_int8(self):
634 """(:class:`numbers.Integral`) Red as 8bit integer which is a common
635 style. From 0 to 255.
637 .. versionadded:: 0.3.0
639 """
640 return scale_quantum_to_int8(self.red_quantum)
642 @red_int8.setter
643 def red_int8(self, value):
644 self._assert_int8(value)
645 self.red = float(value) / 255.0
647 @property
648 def red_quantum(self):
649 """(:class:`numbers.Integral`) Red.
650 Scale depends on :const:`~wand.version.QUANTUM_DEPTH`.
652 .. versionadded:: 0.3.0
654 """
655 with self:
656 return library.PixelGetRedQuantum(self.resource)
658 @red_quantum.setter
659 def red_quantum(self, value):
660 self._assert_quantum(value)
661 self.dirty = True
662 with self:
663 library.PixelSetRedQuantum(self.resource, value)
665 @property
666 def string(self):
667 """(:class:`basestring`) The string representation of the color."""
668 with self:
669 color_string = library.PixelGetColorAsString(self.resource)
670 return text(color_string.value)
672 @property
673 def yellow(self):
674 """(:class:`numbers.Real`) Yellow color channel in CMYK
675 colorspace. Unused by RGB colorspace.
677 .. versionadded:: 0.5.1
678 """
679 with self:
680 return library.PixelGetYellow(self.resource)
682 @yellow.setter
683 def yellow(self, value):
684 self._assert_double(value)
685 self.dirty = True
686 with self:
687 library.PixelSetYellow(self.resource, value)
689 @property
690 def yellow_int8(self):
691 """(:class:`numbers.Integral`) Yellow as 8bit integer which is a common
692 style. From 0 to 255.
694 .. versionadded:: 0.5.1
695 """
696 return scale_quantum_to_int8(self.yellow_quantum)
698 @yellow_int8.setter
699 def yellow_int8(self, value):
700 self._assert_int8(value)
701 self.yellow = float(value) / 255.0
703 @property
704 def yellow_quantum(self):
705 """(:class:`numbers.Integral`) Yellow.
706 Scale depends on :const:`~wand.version.QUANTUM_DEPTH`.
708 .. versionadded:: 0.5.1
709 """
710 with self:
711 return library.PixelGetYellowQuantum(self.resource)
713 @yellow_quantum.setter
714 def yellow_quantum(self, value):
715 self._assert_quantum(value)
716 self.dirty = True
717 with self:
718 library.PixelSetYellowQuantum(self.resource, value)
720 def hsl(self):
721 """Calculate the HSL color values from the RGB color.
723 :returns: Tuple containing three normalized doubles, between 0.0 &
724 1.0, representing ``hue``, ``saturation``, and ``lightness``.
725 :rtype: :class:`collections.Sequence`
727 .. versionadded:: 0.5.1
728 """
729 hue = ctypes.c_double(0.0)
730 saturation = ctypes.c_double(0.0)
731 lightness = ctypes.c_double(0.0)
732 with self:
733 library.PixelGetHSL(self.resource,
734 ctypes.byref(hue),
735 ctypes.byref(saturation),
736 ctypes.byref(lightness))
737 return (hue.value, saturation.value, lightness.value)
740def scale_quantum_to_int8(quantum):
741 """Straightforward port of :c:func:`ScaleQuantumToChar()` inline
742 function.
744 :param quantum: quantum value
745 :type quantum: :class:`numbers.Integral`
746 :returns: 8bit integer of the given ``quantum`` value
747 :rtype: :class:`numbers.Integral`
749 .. versionadded:: 0.3.0
750 .. versionchanged:: 0.5.0
751 Added HDRI support
752 """
753 if quantum <= 0:
754 return 0
755 table = {8: 1, 16: 257.0, 32: 16843009.0, 64: 72340172838076673.0}
756 if MAGICK_HDRI: # pragma: no cover
757 if QUANTUM_DEPTH == 8:
758 v = quantum / table[QUANTUM_DEPTH]
759 elif QUANTUM_DEPTH == 16:
760 v = ((int(quantum + 128) - (int(quantum + 128) >> 8)) >> 8)
761 elif QUANTUM_DEPTH == 32:
762 v = ((quantum + 8421504) / table[QUANTUM_DEPTH])
763 elif QUANTUM_DEPTH == 64:
764 v = quantum / table[QUANTUM_DEPTH]
765 else:
766 v = quantum / table[QUANTUM_DEPTH]
767 if v >= 255:
768 return 255
769 return int(v + 0.5)