First Commit
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* 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 iTerm2 Inline Images Protocol (OSC 1337).
|
||||
*
|
||||
* The iTerm2 protocol uses Operating System Command (OSC) sequences:
|
||||
* - Start: ESC ] 1337 ; File= (0x1B 0x5D 0x31 0x33 0x33 0x37 0x3B 0x46 0x69 0x6C 0x65 0x3D)
|
||||
* - End: BEL (0x07) or ESC \ (0x1B 0x5C)
|
||||
*
|
||||
* Format: ESC]1337;File=[arguments]:[base64 data]BEL
|
||||
*
|
||||
* @see https://iterm2.com/documentation-images.html
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class ITerm2Protocol implements ImageProtocolInterface
|
||||
{
|
||||
public const OSC_START = "\x1b]1337;File=";
|
||||
public const BEL = "\x07";
|
||||
public const ST = "\x1b\\";
|
||||
|
||||
public function detectPastedImage(string $data): bool
|
||||
{
|
||||
return str_contains($data, self::OSC_START);
|
||||
}
|
||||
|
||||
public function decode(string $data): array
|
||||
{
|
||||
if (false === $start = strpos($data, self::OSC_START)) {
|
||||
return ['data' => '', 'format' => null];
|
||||
}
|
||||
|
||||
if (false === $end = strpos($data, self::BEL, $start)) {
|
||||
$end = strpos($data, self::ST, $start);
|
||||
}
|
||||
|
||||
if (false === $end) {
|
||||
return ['data' => '', 'format' => null];
|
||||
}
|
||||
|
||||
$content = substr($data, $start + \strlen(self::OSC_START), $end - $start - \strlen(self::OSC_START));
|
||||
|
||||
if (false === $colonPos = strpos($content, ':')) {
|
||||
return ['data' => '', 'format' => null];
|
||||
}
|
||||
|
||||
$payload = substr($content, $colonPos + 1);
|
||||
|
||||
if (false === $decodedData = base64_decode($payload, true)) {
|
||||
return ['data' => '', 'format' => null];
|
||||
}
|
||||
|
||||
$format = $this->detectImageFormat($decodedData);
|
||||
|
||||
return ['data' => $decodedData, 'format' => $format];
|
||||
}
|
||||
|
||||
public function encode(string $imageData, ?int $maxWidth = null): string
|
||||
{
|
||||
$arguments = [
|
||||
'inline=1',
|
||||
];
|
||||
|
||||
if ($maxWidth) {
|
||||
$arguments[] = \sprintf('width=%d', $maxWidth);
|
||||
}
|
||||
|
||||
$arguments[] = 'preserveAspectRatio=1';
|
||||
$argumentString = implode(';', $arguments);
|
||||
$payload = base64_encode($imageData);
|
||||
|
||||
return self::OSC_START.$argumentString.':'.$payload.self::BEL;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'iterm2';
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Contract for terminal image protocol handlers.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface ImageProtocolInterface
|
||||
{
|
||||
public function detectPastedImage(string $data): bool;
|
||||
|
||||
/**
|
||||
* @return array{data: string, format: string|null}
|
||||
*/
|
||||
public function decode(string $data): array;
|
||||
|
||||
public function encode(string $imageData, ?int $maxWidth = null): string;
|
||||
|
||||
public function getName(): string;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* 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<control data>;<payload>ESC\
|
||||
*
|
||||
* @see https://sw.kovidgoyal.net/kitty/graphics-protocol/
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*
|
||||
* @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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user