<?php namespace App\Classes; use Illuminate\Support\Arr; use Illuminate\Support\Collection; class Font { private const DEBUG = FALSE; protected const MSG_WIDTH = 79; private string $text = ''; private int $width = 0; private int $height = 0; public function __get($key) { switch ($key) { case 'height': return $this->height; case 'width': return $this->width; default: throw new \Exception(sprintf('Unknown key %s',$key)); } } /** * Message text, goes after header, and if a logo, to the right of it * * @param string $text */ public function addText(string $text) { $this->text = $text; $this->dimensions(); } /** * Characters used in the font * * @return Collection */ private function chars(): Collection { static $chars = []; if ($this->text && empty($chars[$this->text])) { // Trim any leading/trailing spaces $text = trim(strtolower($this->text)); $chars[$this->text] = collect(); // Work out the characters we need foreach (array_unique(str_split($text)) as $c) { if (! $x=Arr::get(static::FONT,$c)) continue; $chars[$this->text]->put($c,$x); } } return $chars[$this->text] ?: collect(); } /** * Full width of the rendered text * * @return void */ private function dimensions(): void { $chars = $this->chars(); $escape = FALSE; foreach (str_split(strtolower($this->text)) as $c) { if ($c == "\x1b") { $escape = TRUE; continue; } elseif ($escape) { $escape = FALSE; continue; } $this->width += ($x=Arr::get($chars->get($c),0)) ? count($x) : 1; if ($x) $this->height = (($y=count($chars->get($c))) > $this->height) ? $y : $this->height; } // If the last character is a space, we'll reduce the width $space = TRUE; foreach ($chars->get($c) as $x) if (array_pop($x) != 32) { $space = FALSE; break; } if ($space) $this->width--; } public function render_line(int $line): string { $chars = $this->chars(); $result = ''; $escape = FALSE; $ansi = FALSE; foreach (str_split(strtolower($this->text)) as $c) { if (ord($c) == 0x1b) { $escape = TRUE; } elseif ($escape && $c) { $result .= ANSI::ansi_color(ord($c)); $escape = FALSE; $ansi = TRUE; } elseif (($c == ' ') || (! $font_chars=$chars->get($c))) { $result .= $c; } else { foreach (Arr::get($font_chars,$line) as $char) $result .= chr($char); } } return $result.($ansi ? ANSI::ansi_color(0x0e) : ''); } /** * This function will format text to static::MSG_WIDTH, as well as adding the logo. * It is up to the text to be spaced appropriately to wrap around the icon. * * @param string $text * @param ANSI|null $logo * @param bool $right * @param int $step * @return string */ public static function format_msg(string $text,ANSI $logo=NULL,int $step=1,bool $right=FALSE): string { $result = ''; $result_height = 0; $current_pos = 0; while ($current_pos < strlen($text)) { $result_line = ''; // Line being created $lc = 0; // Line length count (without ANSI) $buffer = $step ? $logo->width->skip(intdiv($result_height,$step)*$step)->take($step)->max() : 1; // Add our logo if ($result_height <= $logo->height-1) { $line = ANSI::bin_to_ansi([$logo->line($result_height)],FALSE); $lc = $logo->line_width($logo->line_raw($result_height),FALSE); $result_line = str_repeat(' ',ANSI::LOGO_OFFSET_WIDTH) .$line .str_repeat(' ',ANSI::LOGO_BUFFER_WIDTH+($right ? 0 : $buffer-$lc)); } // Look for a return $return_pos = strpos($text,"\r",$current_pos); // We have a return if ($return_pos !== FALSE) { $subtext = substr($text,$current_pos,$return_pos-$current_pos); // If the reset of the string will fit on the current line } elseif (strlen($text)-$current_pos < static::MSG_WIDTH-$lc) { $subtext = substr($text,$current_pos); // Get the next lines worth of chars } else { $subtext = substr($text,$current_pos,static::MSG_WIDTH-$buffer); // Include the text up to the last space if (substr($text,$current_pos+strlen($subtext),1) !== ' ') $subtext = substr($text,$current_pos,strrpos($subtext,' ')); } $result .= $result_line. str_repeat(' ',($right ? static::MSG_WIDTH-$lc-strlen($subtext)+((! $lc) ? (ANSI::LOGO_OFFSET_WIDTH+ANSI::LOGO_BUFFER_WIDTH) : 0) : 0)) .$subtext."\r\n"; $current_pos += strlen($subtext)+1; $result_height++; } // In case our text is shorter than the logo for (;$result_height<$logo->height;$result_height++) { $result .= str_repeat(' ',ANSI::LOGO_OFFSET_WIDTH) .ANSI::bin_to_ansi([$logo->line($result_height)],FALSE) .str_repeat(' ',ANSI::LOGO_BUFFER_WIDTH) ."\r\n"; } return $result; } /** * The height of this font (based on the 1st char) * * @return int */ public static function height(): int { return count(Arr::get(static::FONT,'a')); } /** * Convert text into a graphical font * * @param string $text * @param Collection $width * @param int $height * @param int $step * @return string */ public static function fontText(string $text,Collection $width,int $height,int $step): string { return self::text_to_font($text,$width,$height,$step); } /** * Convert text to this font * This function will pad the text to fit around the icon, so that the icon+font fils to self::MSG_WIDTH * * @param string $text * @param Collection $icon_width Width to make the font * @param int $icon_height Minimal width for this height, then full width (self::MSG_WIDTH) * @param int $step The grouping of lines (normally font height) around the icon * @return string */ protected static function text_to_font(string $text,Collection $icon_width,int $icon_height,int $step): string { $chars = collect(); // Characters needed for this $text $font_height = 0; // Max height of text using font // Trim any leading/trailing spaces $text = trim(strtolower($text)); // Work out the characters we need foreach (array_unique(str_split($text)) as $c) { if (($c == ' ') || (! $x=Arr::get(static::FONT,$c))) { continue; } $chars->put($c,$x); $font_height = (($y=count($x)) > $font_height) ? $y : $font_height; } if (self::DEBUG) dump(['uniquechars'=>$chars->count(),'font_height'=>$font_height]); if (self::DEBUG) dump(['drawing'=>$text,'textlen'=>strlen($text),'logo_width'=>$icon_width,'logo_height'=>$icon_height]); $result = ''; // Our result $current_pos = 0; // Our current position through $text $result_height = 0; // Our current line height $line_pos = 0; // Our current character position for this line of the font while ($current_pos < strlen($text)) { if (self::DEBUG) dump(sprintf('current position %d of %d',$current_pos,strlen($text))); for ($line=0;$line<$font_height;$line++) { if ($line == 0) { $line_icon_width = $icon_width ->skip(intdiv($result_height,$step)*$step) ->take($step) ->max(); if ($line_icon_width) $line_icon_width += ANSI::LOGO_OFFSET_WIDTH+ANSI::LOGO_BUFFER_WIDTH; $line_width = self::MSG_WIDTH-$line_icon_width; // Width we are working towards, initially $icon_width until height then its self::MSG_WIDTH } $line_result = ''; // Our current line of font if (self::DEBUG) dump(sprintf('- current line %d of %d',$line+1,$font_height)); // If we are mid way through rendering a font, and have already finished with the height offset, we'll need to fill with blanks if (($line_width !== self::MSG_WIDTH) && ($result_height > $icon_height-1)) $line_result .= str_repeat(' ',$line_icon_width); $line_pos = $current_pos; $next_space_pos = $current_pos; $next_next_space_width = 0; // What our width will be after the next next space $next_next_space_pos = 0; // The position of the next space after the next one $next_next_space_chars = 0; // The number of chars between the next space and the next next space $current_line_width = 0; // Our current width of the line while ($current_line_width < $line_width) { if (self::DEBUG) dump(sprintf(' - current width %d of %d, and we are working on char %d',$current_line_width,$line_width,$line_pos)); $find_space_pos = $line_pos; // Find our next char if (self::DEBUG) dump(sprintf(' - find our next space from %d after %d',$find_space_pos,$next_space_pos)); $next_space_chars = 0; if ($next_space_pos <= $line_pos) { if (! $next_next_space_pos) { while (($find_space_pos < strlen($text)) && (($c=substr($text,$find_space_pos++,1)) !== ' ')) { $x = count(Arr::get($chars->get($c),$line)); if (self::DEBUG) dump(sprintf(' + char is [%s] (%x) and will take %d chars',$c,ord($c),$x)); $next_space_chars += $x; } $next_space_pos = $find_space_pos; $next_next_space_pos = $find_space_pos; $next_next_space_width = $current_line_width+$next_space_chars; } else { $next_space_pos = $next_next_space_pos; $next_space_chars = $next_next_space_chars; } // Find our next next space, which we'll use to decide whether we need to include a space when we find one $next_next_space_chars = 0; while (($next_next_space_pos < strlen($text)) && (($c=substr($text,$next_next_space_pos++,1)) !== ' ')) { $next_next_space_chars += count(Arr::get($chars->get($c),$line,[])); if (self::DEBUG) dump(sprintf(' + char is [%s] (%x) and will take us to %d',$c,ord($c),$next_next_space_chars)); } $next_next_space_width = $current_line_width+$next_space_chars+$next_next_space_chars; }; if (self::DEBUG) dump(sprintf(' - our next space is: [%s] (%x) at %d in %d chars, taking %d chars (taking our width to %d)',$c,ord($c),$find_space_pos,$find_space_pos-$line_pos,$next_space_chars,$current_line_width+$next_space_chars)); // We are only spaces, so we can return to the next line if ($current_line_width+$next_space_chars > $line_width) { if (self::DEBUG) dump(' = next char should go onto new line'); // Only go to a new line if we already have chars if ($current_line_width) break; } $c = substr($text,$line_pos,1); if (($c == ' ') || (! $font_chars=$chars->get($c))) { // Ignore this space if we are at the beginning of the line if ($current_line_width && ($next_next_space_width < $line_width)) { $line_result .= $c; $current_line_width++; } } else { if (self::DEBUG) dump(sprintf('adding char [%s] which is [%s]',$c,join('|',Arr::get($font_chars,$line)))); foreach ($x=Arr::get($font_chars,$line) as $char) $line_result .= chr($char); $current_line_width += count($x); } $line_pos++; if (self::DEBUG) dump(sprintf(' = line width [%d of %d] and we are on char [%d] our space is [%d]',$current_line_width,$line_width,$line_pos,$find_space_pos)); if ($line_pos == strlen($text)) { if (self::DEBUG) dump(sprintf(' = we are finished, as we are on char %d on line %d',$line_pos,$line)); break; } } $result_height++; $result .= $line_result."\r"; } $current_pos = $line_pos; if (self::DEBUG) dump(sprintf('= new line starting with char [%d] - our width is [%d] and we are on line [%d]',$current_pos,$line_width,$result_height)); } if (self::DEBUG) dd(['result'=>$result]); return $result; } }