* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Terminal\Image; /** * Handles the Kitty Graphics Protocol for terminal image paste/display. * * The Kitty Graphics Protocol uses Application Programming Command (APC) sequences: * - Start: ESC _ G (0x1B 0x5F 0x47) * - End: ESC \ (0x1B 0x5C) - also known as ST (String Terminator) * * Format: ESC_G;ESC\ * * @see https://sw.kovidgoyal.net/kitty/graphics-protocol/ * * @author Robin Chalas * * @internal */ final class KittyGraphicsProtocol implements ImageProtocolInterface { public const APC_START = "\x1b_G"; public const ST = "\x1b\\"; public function detectPastedImage(string $data): bool { return str_contains($data, self::APC_START); } public function decode(string $data): array { if (false === $start = strpos($data, self::APC_START)) { return ['data' => '', 'format' => null]; } if (false === $end = strpos($data, self::ST, $start)) { $end = strpos($data, "\x07", $start); } if (false === $end) { return ['data' => '', 'format' => null]; } $content = substr($data, $start + \strlen(self::APC_START), $end - $start - \strlen(self::APC_START)); if (false === $semicolonPos = strpos($content, ';')) { return ['data' => '', 'format' => null]; } $controlData = substr($content, 0, $semicolonPos); $payload = substr($content, $semicolonPos + 1); $decodedData = base64_decode($payload, true); if (false === $decodedData) { return ['data' => '', 'format' => null]; } return ['data' => $decodedData, 'format' => $this->parseFormat($controlData)]; } public function encode(string $imageData, ?int $maxWidth = null): string { if ('png' !== $this->detectImageFormat($imageData)) { return ''; } $controlParts = ['a=T', 'f=100']; if (null !== $maxWidth) { $controlParts[] = \sprintf('c=%d', $maxWidth); } $controlData = implode(',', $controlParts); $payload = base64_encode($imageData); $maxChunkSize = 4096; if (\strlen($payload) <= $maxChunkSize) { return self::APC_START.$controlData.';'.$payload.self::ST; } $chunks = str_split($payload, $maxChunkSize); $result = ''; foreach ($chunks as $i => $chunk) { $isLast = ($i === \count($chunks) - 1); $chunkControl = $i > 0 ? 'm='.($isLast ? '0' : '1') : $controlData.',m='.($isLast ? '0' : '1'); $result .= self::APC_START.$chunkControl.';'.$chunk.self::ST; } return $result; } public function getName(): string { return 'kitty'; } private function parseFormat(string $controlData): ?string { foreach (explode(',', $controlData) as $pair) { $parts = explode('=', $pair, 2); if (2 === \count($parts) && 'f' === $parts[0]) { return match ($parts[1]) { '24' => 'rgb', '32' => 'rgba', '100' => 'png', default => null, }; } } return null; } private function detectImageFormat(string $data): ?string { return match (true) { str_starts_with($data, "\x89PNG\r\n\x1a\n") => 'png', str_starts_with($data, "\xFF\xD8\xFF") => 'jpg', str_starts_with($data, 'GIF87a'), str_starts_with($data, 'GIF89a') => 'gif', str_starts_with($data, 'RIFF') && 'WEBP' === substr($data, 8, 4) => 'webp', default => null, }; } }