Color.php 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  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 AVAILABLE_OPTIONS = [
  29. 'bold' => ['set' => 1, 'unset' => 22],
  30. 'underscore' => ['set' => 4, 'unset' => 24],
  31. 'blink' => ['set' => 5, 'unset' => 25],
  32. 'reverse' => ['set' => 7, 'unset' => 27],
  33. 'conceal' => ['set' => 8, 'unset' => 28],
  34. ];
  35. private $foreground;
  36. private $background;
  37. private $options = [];
  38. public function __construct(string $foreground = '', string $background = '', array $options = [])
  39. {
  40. $this->foreground = $this->parseColor($foreground);
  41. $this->background = $this->parseColor($background);
  42. foreach ($options as $option) {
  43. if (!isset(self::AVAILABLE_OPTIONS[$option])) {
  44. throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(self::AVAILABLE_OPTIONS))));
  45. }
  46. $this->options[$option] = self::AVAILABLE_OPTIONS[$option];
  47. }
  48. }
  49. public function apply(string $text): string
  50. {
  51. return $this->set().$text.$this->unset();
  52. }
  53. public function set(): string
  54. {
  55. $setCodes = [];
  56. if ('' !== $this->foreground) {
  57. $setCodes[] = '3'.$this->foreground;
  58. }
  59. if ('' !== $this->background) {
  60. $setCodes[] = '4'.$this->background;
  61. }
  62. foreach ($this->options as $option) {
  63. $setCodes[] = $option['set'];
  64. }
  65. if (0 === \count($setCodes)) {
  66. return '';
  67. }
  68. return sprintf("\033[%sm", implode(';', $setCodes));
  69. }
  70. public function unset(): string
  71. {
  72. $unsetCodes = [];
  73. if ('' !== $this->foreground) {
  74. $unsetCodes[] = 39;
  75. }
  76. if ('' !== $this->background) {
  77. $unsetCodes[] = 49;
  78. }
  79. foreach ($this->options as $option) {
  80. $unsetCodes[] = $option['unset'];
  81. }
  82. if (0 === \count($unsetCodes)) {
  83. return '';
  84. }
  85. return sprintf("\033[%sm", implode(';', $unsetCodes));
  86. }
  87. private function parseColor(string $color): string
  88. {
  89. if ('' === $color) {
  90. return '';
  91. }
  92. if ('#' === $color[0]) {
  93. $color = substr($color, 1);
  94. if (3 === \strlen($color)) {
  95. $color = $color[0].$color[0].$color[1].$color[1].$color[2].$color[2];
  96. }
  97. if (6 !== \strlen($color)) {
  98. throw new InvalidArgumentException(sprintf('Invalid "%s" color.', $color));
  99. }
  100. return $this->convertHexColorToAnsi(hexdec($color));
  101. }
  102. if (!isset(self::COLORS[$color])) {
  103. throw new InvalidArgumentException(sprintf('Invalid "%s" color; expected one of (%s).', $color, implode(', ', array_keys(self::COLORS))));
  104. }
  105. return (string) self::COLORS[$color];
  106. }
  107. private function convertHexColorToAnsi(int $color): string
  108. {
  109. $r = ($color >> 16) & 255;
  110. $g = ($color >> 8) & 255;
  111. $b = $color & 255;
  112. // see https://github.com/termstandard/colors/ for more information about true color support
  113. if ('truecolor' !== getenv('COLORTERM')) {
  114. return (string) $this->degradeHexColorToAnsi($r, $g, $b);
  115. }
  116. return sprintf('8;2;%d;%d;%d', $r, $g, $b);
  117. }
  118. private function degradeHexColorToAnsi(int $r, int $g, int $b): int
  119. {
  120. if (0 === round($this->getSaturation($r, $g, $b) / 50)) {
  121. return 0;
  122. }
  123. return (round($b / 255) << 2) | (round($g / 255) << 1) | round($r / 255);
  124. }
  125. private function getSaturation(int $r, int $g, int $b): int
  126. {
  127. $r = $r / 255;
  128. $g = $g / 255;
  129. $b = $b / 255;
  130. $v = max($r, $g, $b);
  131. if (0 === $diff = $v - min($r, $g, $b)) {
  132. return 0;
  133. }
  134. return (int) $diff * 100 / $v;
  135. }
  136. }