clrghouz/app/Classes/ANSI.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)));
}
}