Color.php 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Console;
  11. use Symfony\Component\Console\Exception\InvalidArgumentException;
  12. /**
  13. * @author Fabien Potencier <fabien@symfony.com>
  14. */
  15. final class Color
  16. {
  17. private const COLORS = [
  18. 'black' => 0,
  19. 'red' => 1,
  20. 'green' => 2,
  21. 'yellow' => 3,
  22. 'blue' => 4,
  23. 'magenta' => 5,
  24. 'cyan' => 6,
  25. 'white' => 7,
  26. 'default' => 9,
  27. ];
  28. private const BRIGHT_COLORS = [
  29. 'gray' => 0,
  30. 'bright-red' => 1,
  31. 'bright-green' => 2,
  32. 'bright-yellow' => 3,
  33. 'bright-blue' => 4,
  34. 'bright-magenta' => 5,
  35. 'bright-cyan' => 6,
  36. 'bright-white' => 7,
  37. ];
  38. private const AVAILABLE_OPTIONS = [
  39. 'bold' => ['set' => 1, 'unset' => 22],
  40. 'underscore' => ['set' => 4, 'unset' => 24],
  41. 'blink' => ['set' => 5, 'unset' => 25],
  42. 'reverse' => ['set' => 7, 'unset' => 27],
  43. 'conceal' => ['set' => 8, 'unset' => 28],
  44. ];
  45. private $foreground;
  46. private $background;
  47. private $options = [];
  48. public function __construct(string $foreground = '', string $background = '', array $options = [])
  49. {
  50. $this->foreground = $this->parseColor($foreground);
  51. $this->background = $this->parseColor($background, true);
  52. foreach ($options as $option) {
  53. if (!isset(self::AVAILABLE_OPTIONS[$option])) {
  54. throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(self::AVAILABLE_OPTIONS))));
  55. }
  56. $this->options[$option] = self::AVAILABLE_OPTIONS[$option];
  57. }
  58. }
  59. public function apply(string $text): string
  60. {
  61. return $this->set().$text.$this->unset();
  62. }
  63. public function set(): string
  64. {
  65. $setCodes = [];
  66. if ('' !== $this->foreground) {
  67. $setCodes[] = $this->foreground;
  68. }
  69. if ('' !== $this->background) {
  70. $setCodes[] = $this->background;
  71. }
  72. foreach ($this->options as $option) {
  73. $setCodes[] = $option['set'];
  74. }
  75. if (0 === \count($setCodes)) {
  76. return '';
  77. }
  78. return sprintf("\033[%sm", implode(';', $setCodes));
  79. }
  80. public function unset(): string
  81. {
  82. $unsetCodes = [];
  83. if ('' !== $this->foreground) {
  84. $unsetCodes[] = 39;
  85. }
  86. if ('' !== $this->background) {
  87. $unsetCodes[] = 49;
  88. }
  89. foreach ($this->options as $option) {
  90. $unsetCodes[] = $option['unset'];
  91. }
  92. if (0 === \count($unsetCodes)) {
  93. return '';
  94. }
  95. return sprintf("\033[%sm", implode(';', $unsetCodes));
  96. }
  97. private function parseColor(string $color, bool $background = false): string
  98. {
  99. if ('' === $color) {
  100. return '';
  101. }
  102. if ('#' === $color[0]) {
  103. $color = substr($color, 1);
  104. if (3 === \strlen($color)) {
  105. $color = $color[0].$color[0].$color[1].$color[1].$color[2].$color[2];
  106. }
  107. if (6 !== \strlen($color)) {
  108. throw new InvalidArgumentException(sprintf('Invalid "%s" color.', $color));
  109. }
  110. return ($background ? '4' : '3').$this->convertHexColorToAnsi(hexdec($color));
  111. }
  112. if (isset(self::COLORS[$color])) {
  113. return ($background ? '4' : '3').self::COLORS[$color];
  114. }
  115. if (isset(self::BRIGHT_COLORS[$color])) {
  116. return ($background ? '10' : '9').self::BRIGHT_COLORS[$color];
  117. }
  118. throw new InvalidArgumentException(sprintf('Invalid "%s" color; expected one of (%s).', $color, implode(', ', array_merge(array_keys(self::COLORS), array_keys(self::BRIGHT_COLORS)))));
  119. }
  120. private function convertHexColorToAnsi(int $color): string
  121. {
  122. $r = ($color >> 16) & 255;
  123. $g = ($color >> 8) & 255;
  124. $b = $color & 255;
  125. // see https://github.com/termstandard/colors/ for more information about true color support
  126. if ('truecolor' !== getenv('COLORTERM')) {
  127. return (string) $this->degradeHexColorToAnsi($r, $g, $b);
  128. }
  129. return sprintf('8;2;%d;%d;%d', $r, $g, $b);
  130. }
  131. private function degradeHexColorToAnsi(int $r, int $g, int $b): int
  132. {
  133. if (0 === round($this->getSaturation($r, $g, $b) / 50)) {
  134. return 0;
  135. }
  136. return (round($b / 255) << 2) | (round($g / 255) << 1) | round($r / 255);
  137. }
  138. private function getSaturation(int $r, int $g, int $b): int
  139. {
  140. $r = $r / 255;
  141. $g = $g / 255;
  142. $b = $b / 255;
  143. $v = max($r, $g, $b);
  144. if (0 === $diff = $v - min($r, $g, $b)) {
  145. return 0;
  146. }
  147. return (int) $diff * 100 / $v;
  148. }
  149. }