<?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))); } }