432 lines
9.4 KiB
PHP
432 lines
9.4 KiB
PHP
<?php
|
|
|
|
namespace App\Classes;
|
|
|
|
use Illuminate\Support\Collection;
|
|
|
|
class ANSI
|
|
{
|
|
/* 8 BIT COLORS
|
|
* Foreground 0-3, Background 4-7
|
|
* Color 0-2, 4-6, High 3, 7
|
|
*/
|
|
private const COLOR_8BIT = 0x1B;
|
|
private const COLOR_HIGH = 1; // Bit 1.
|
|
private const COLOR_BLACK = 0; // F30 B40
|
|
private const COLOR_RED = 1; // F31 B41
|
|
private const COLOR_GREEN = 2; // F32 B42
|
|
private const COLOR_YELLOW = 3; // F33 B43
|
|
private const COLOR_BLUE = 4; // F34 B44
|
|
private const COLOR_MAGENTA = 5; // F35 B45
|
|
private const COLOR_CYAN = 6; // F36 B46
|
|
private const COLOR_WHITE = 7; // F37 B47
|
|
|
|
private const DEFAULT_FORE = 37;
|
|
private const DEFAULT_BACK = 40;
|
|
|
|
/* 256 BIT COLORS */
|
|
/* 0x26 0xAA 0xBB */
|
|
|
|
public const LOGO_BUFFER_WIDTH = 1;
|
|
public const LOGO_BUFFER_HEIGHT = 0; // Not implemented
|
|
public const LOGO_OFFSET_WIDTH = 1;
|
|
public const LOGO_OFFSET_HEIGHT = 0; // Not implemented
|
|
|
|
private Collection $width;
|
|
private Collection $ansi;
|
|
private const BUFREAD = 2048;
|
|
|
|
/* MAGIC METHODS */
|
|
|
|
public function __construct(string $file='')
|
|
{
|
|
$this->width = collect();
|
|
$this->ansi = collect();
|
|
|
|
if ($file) {
|
|
$f = fopen($file,'r');
|
|
while (! feof($f)) {
|
|
$line = stream_get_line($f,self::BUFREAD,"\r");
|
|
|
|
// If the last line is blank, we'll ignore it
|
|
if ((! feof($f)) || $line) {
|
|
$this->width->push(self::line_width($line,FALSE));
|
|
$this->ansi->push(array_map(function($item) { return ord($item); },str_split($line,1)));
|
|
}
|
|
}
|
|
fclose($f);
|
|
}
|
|
|
|
return $this->ansi;
|
|
}
|
|
|
|
public function __get($key)
|
|
{
|
|
switch ($key) {
|
|
case 'width':
|
|
return $this->width;
|
|
|
|
case 'max_width':
|
|
return $this->width->max()+self::LOGO_BUFFER_WIDTH+self::LOGO_OFFSET_WIDTH;
|
|
|
|
case 'height':
|
|
return $this->ansi->count()+self::LOGO_BUFFER_HEIGHT+self::LOGO_OFFSET_HEIGHT;
|
|
}
|
|
}
|
|
|
|
/* STATIC METHODS */
|
|
|
|
/**
|
|
* Convert a binary ANSI file back to its ANSI version
|
|
*
|
|
* @param string $file
|
|
* @return string
|
|
*/
|
|
public static function ansi(string $file)
|
|
{
|
|
return static::bin_to_ansi((new self($file))->ansi->toArray());
|
|
}
|
|
|
|
/**
|
|
* Convert an array of ANSI codes into a binary equivalent
|
|
*
|
|
* @param array $code
|
|
* @return string
|
|
*/
|
|
public static function ansi_code(array $code): string
|
|
{
|
|
if (!$code)
|
|
return '';
|
|
|
|
return "\x1b".chr(self::code($code));
|
|
}
|
|
|
|
/**
|
|
* Render an ANSI binary code into an ANSI string
|
|
*
|
|
* @param int $code
|
|
* @return string
|
|
*/
|
|
public static function ansi_color(int $code): string
|
|
{
|
|
static $current = [];
|
|
|
|
return "\x1b[".self::color($code,$current);
|
|
}
|
|
|
|
/**
|
|
* Convert a binary ANS file to ANSI text
|
|
*
|
|
* @param array $ansi
|
|
* @param bool $return
|
|
* @return string
|
|
*/
|
|
public static function bin_to_ansi(array $ansi,bool $return=TRUE): string
|
|
{
|
|
$output = '';
|
|
$escape = FALSE;
|
|
$current = []; // Default Screen
|
|
|
|
foreach ($ansi as $line) {
|
|
foreach ($line as $char) {
|
|
if ($char === 0x1b) {
|
|
$escape = TRUE;
|
|
continue;
|
|
}
|
|
|
|
if ($escape) {
|
|
if ($x=static::color($char,$current))
|
|
$output .= "\x1b[".$x;
|
|
|
|
$escape = FALSE;
|
|
|
|
} else {
|
|
$output .= chr($char);
|
|
}
|
|
}
|
|
|
|
if ($return)
|
|
$output .= "\r\n";
|
|
}
|
|
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* Convert an ANSI file into a binary form
|
|
*
|
|
* @param string $file
|
|
* @return Collection
|
|
*/
|
|
public static function binary(string $file): Collection
|
|
{
|
|
$f = fopen($file,'r');
|
|
$escape = FALSE;
|
|
$ansi = FALSE;
|
|
$buffer = '';
|
|
$line = '';
|
|
$result = collect();
|
|
|
|
$current = self::reset();
|
|
|
|
while (! feof($f)) {
|
|
$c = fread($f,1);
|
|
|
|
switch (ord($c)) {
|
|
// Ignore \n (0x0a)
|
|
case 0x0a:
|
|
continue 2;
|
|
|
|
// New line \r (0x0d)
|
|
case 0x0d:
|
|
$result->push($line);
|
|
$line = '';
|
|
continue 2;
|
|
|
|
// We got our ESC
|
|
case 0x1b:
|
|
$escape = TRUE;
|
|
continue 2;
|
|
|
|
case ord('['):
|
|
if ($escape) {
|
|
$ansi = TRUE;
|
|
continue 2;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if ($ansi) {
|
|
switch ($c) {
|
|
case ';':
|
|
case 'm':
|
|
if ((int)$buffer === 0) {
|
|
$current = self::reset();
|
|
|
|
} elseif ((int)$buffer === 1) {
|
|
$current['h'] = 1;
|
|
|
|
} elseif (((int)$buffer >= 30) && (int)$buffer <= 37) {
|
|
$current['f'] = (int)$buffer;
|
|
|
|
} elseif (((int)$buffer >= 40) && (int)$buffer <= 47) {
|
|
$current['b'] = (int)$buffer;
|
|
}
|
|
|
|
if ($c === 'm') {
|
|
$ansi = FALSE;
|
|
$escape = FALSE;
|
|
$line .= chr(0x1b).chr(self::code($current));
|
|
}
|
|
|
|
$buffer = '';
|
|
break;
|
|
|
|
default:
|
|
$buffer .= $c;
|
|
}
|
|
|
|
} else {
|
|
// If escape is still set, but we didnt get an ANSI starter, then we need to record the ESC.
|
|
if ($escape) {
|
|
$line .= chr(0x1b);
|
|
$escape = FALSE;
|
|
}
|
|
|
|
$line .= $c;
|
|
}
|
|
}
|
|
|
|
// In case our line didnt end \r and we still have data
|
|
if ($line)
|
|
$result->push($line);
|
|
|
|
fclose($f);
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Convert an array of 8 bit color codes to a binary form
|
|
*
|
|
* @param array $code
|
|
* @return int
|
|
*/
|
|
private static function code(array $code): int
|
|
{
|
|
$result = 0;
|
|
foreach ($code as $item) {
|
|
switch ($item) {
|
|
// Color Reset
|
|
case 0:
|
|
// Low Intensity
|
|
case 2: $result = 0; break;
|
|
|
|
// High Intensity
|
|
case 1: $result |= self::COLOR_HIGH; break;
|
|
|
|
// Foreground
|
|
case 30: $result |= (self::COLOR_BLACK<<1); break;
|
|
case 31: $result |= (self::COLOR_RED<<1); break;
|
|
case 32: $result |= (self::COLOR_GREEN<<1); break;
|
|
case 33: $result |= (self::COLOR_YELLOW<<1); break;
|
|
case 34: $result |= (self::COLOR_BLUE<<1); break;
|
|
case 35: $result |= (self::COLOR_MAGENTA<<1); break;
|
|
case 36: $result |= (self::COLOR_CYAN<<1); break;
|
|
case 37: $result |= (self::COLOR_WHITE<<1); break;
|
|
|
|
// Background
|
|
case 40: $result |= (self::COLOR_BLACK<<5); break;
|
|
case 41: $result |= (self::COLOR_RED<<5); break;
|
|
case 42: $result |= (self::COLOR_GREEN<<5); break;
|
|
case 43: $result |= (self::COLOR_YELLOW<<5); break;
|
|
case 44: $result |= (self::COLOR_BLUE<<5); break;
|
|
case 45: $result |= (self::COLOR_MAGENTA<<5); break;
|
|
case 46: $result |= (self::COLOR_CYAN<<5); break;
|
|
case 47: $result |= (self::COLOR_WHITE<<5); break;
|
|
|
|
default:
|
|
dd('unhandled code:'.$item);
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public static function color_array(int $code): array
|
|
{
|
|
$h = ($code&0x01);
|
|
|
|
switch ($x=(($code>>1)&0x07)) {
|
|
case self::COLOR_BLACK: $f = '30'; break;
|
|
case self::COLOR_RED: $f = '31'; break;
|
|
case self::COLOR_GREEN: $f = '32'; break;
|
|
case self::COLOR_YELLOW: $f = '33'; break;
|
|
case self::COLOR_BLUE: $f = '34'; break;
|
|
case self::COLOR_MAGENTA: $f = '35'; break;
|
|
case self::COLOR_CYAN: $f = '36'; break;
|
|
case self::COLOR_WHITE: $f = '37'; break;
|
|
default:
|
|
dump(['unknown color'=>$x]);
|
|
}
|
|
|
|
switch ($x=(($code>>5)&0x07)) {
|
|
case self::COLOR_BLACK: $b = '40'; break;
|
|
case self::COLOR_RED: $b = '41'; break;
|
|
case self::COLOR_GREEN: $b = '42'; break;
|
|
case self::COLOR_YELLOW: $b = '43'; break;
|
|
case self::COLOR_BLUE: $b = '44'; break;
|
|
case self::COLOR_MAGENTA: $b = '45'; break;
|
|
case self::COLOR_CYAN: $b = '46'; break;
|
|
case self::COLOR_WHITE: $b = '47'; break;
|
|
default:
|
|
dump(['unknown color'=>$x]);
|
|
}
|
|
|
|
return ['h'=>$h,'f'=>$f,'b'=>$b];
|
|
}
|
|
|
|
/**
|
|
* Convert binary code to ANSI escape code
|
|
*
|
|
* @param int $code
|
|
* @param array $current
|
|
* @return string
|
|
*/
|
|
private static function color(int $code,array &$current): string
|
|
{
|
|
if (! $current)
|
|
$current = static::reset();
|
|
|
|
$return = '';
|
|
$color = self::color_array($code);
|
|
$highlight_changed = FALSE;
|
|
|
|
if ($color['h'] !== $current['h']) {
|
|
$return .= $color['h'] ?: ($code != 0x0e ? 2 : 0);
|
|
$current['h'] = $color['h'];
|
|
$highlight_changed = TRUE;
|
|
}
|
|
|
|
if ($color['f'] !== $current['f']) {
|
|
if (! $highlight_changed || $color['h'] || (($color['f'] != self::DEFAULT_FORE) || ($color['b'] != self::DEFAULT_BACK)))
|
|
$return .= (strlen($return) ? ';' : '').$color['f'];
|
|
|
|
$x = $color['f'];
|
|
$current['f'] = $color['f'];
|
|
}
|
|
|
|
if ($color['b'] !== $current['b']) {
|
|
if (! $highlight_changed || $color['h'] || (($x != self::DEFAULT_FORE) || ($color['b'] != self::DEFAULT_BACK)))
|
|
$return .= (strlen($return) ? ';' : '').$color['b'];
|
|
|
|
$current['b'] = $color['b'];
|
|
}
|
|
|
|
return ($return !== '') ? $return.'m' : '';
|
|
}
|
|
|
|
/**
|
|
* Calculate the width of a line
|
|
*
|
|
* @param string $line
|
|
* @param bool $buffer
|
|
* @return int
|
|
*/
|
|
public static function line_width(string $line,bool $buffer=TRUE): int
|
|
{
|
|
return strlen(preg_replace('/\x1b./','',$line))+($buffer ? self::LOGO_OFFSET_WIDTH+self::LOGO_BUFFER_WIDTH : 0);
|
|
}
|
|
|
|
/**
|
|
* The ANSI was reset (normally CSI [ 0m)
|
|
*
|
|
* @return int[]
|
|
*/
|
|
private static function reset(): array
|
|
{
|
|
return [
|
|
'h'=>0,
|
|
'f'=>self::DEFAULT_FORE,
|
|
'b'=>self::DEFAULT_BACK
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Show a line with embedded color codes in ANSI
|
|
*
|
|
* @param string $text
|
|
* @return string
|
|
*/
|
|
public static function text_to_ansi(string $text): string
|
|
{
|
|
$ansi = preg_match('/\x1b./',$text);
|
|
|
|
return self::bin_to_ansi([array_map(function($item) {return ord($item); },str_split($text))],FALSE).((strlen($text) && $ansi) ? "\x1b[0m" : '');
|
|
}
|
|
|
|
/* METHODS */
|
|
|
|
/**
|
|
* Return a specific line
|
|
*
|
|
* @param int $line
|
|
* @return array
|
|
*/
|
|
public function line(int $line): array
|
|
{
|
|
return $this->ansi->get($line);
|
|
}
|
|
|
|
/**
|
|
* Return the binary line
|
|
*
|
|
* @param int $line
|
|
* @return string
|
|
*/
|
|
public function line_raw(int $line): string
|
|
{
|
|
return join('',array_map(function($item) { return chr($item); },$this->line($line)));
|
|
}
|
|
} |