php-termbuf/src/Renderer/GdRenderer.php

286 lines
8.7 KiB
PHP

<?php
namespace NoccyLabs\TermBuf\Renderer;
use NoccyLabs\TermBuf\TerminalBuffer;
class GdRenderer
{
const CURSOR_NONE = 0;
const CURSOR_BLOCK = 1;
const CURSOR_SOLID = 2;
const CURSOR_UNDER = 3;
const CURSOR_LEFT = 4;
const CURSOR_IBEAM = 5;
private static $ValidCusors = [ self::CURSOR_NONE, self::CURSOR_BLOCK, self::CURSOR_SOLID, self::CURSOR_UNDER,self::CURSOR_LEFT ];
private $font = __DIR__."/../../firacode.ttf";
private $boldFont = __DIR__."/../../firacode-bold.ttf";
private $gd;
private $palette = [
0x000000,
0xCC0000,
0x00CC00,
0xCCCC00,
0x0000CC,
0xCC00CC,
0x00CCCC,
0xCCCCCC,
];
private $annotations = [];
private $annotationFontSize = 2;
private $keyOverlay = [];
private $cellWidth;
private $cellHeight;
private $cursorStyle;
public function render(TerminalBuffer $buffer)
{
$lines = $buffer->getLines();
$cols = $buffer->getColumns();
$cw = 8;
$ch = 17;
$this->cellWidth = $cw;
$this->cellHeight = $ch;
$this->gd = imagecreatetruecolor($cols * $cw, $lines * $ch);
for ($line = 0; $line < $lines; $line++) {
for ($column = 0; $column < $cols; $column++) {
$this->renderChar($line*$ch, $column*$cw, $cw, $ch, $buffer->bufferGetRaw($line, $column));
}
}
[$cursorLine,$cursorColumn] = $buffer->getCursorPosition();
$this->drawCursor($cursorLine, $cursorColumn);
$this->drawAnnotations();
$this->drawKeyOverlay();
}
private function renderChar(int $y, int $x, int $w, int $h, array $raw)
{
[ $char, $attrs ] = $raw;
$bold = false;
$underline = false;
$color = 0xFFFFFF;
$bgcolor = 0x000000;
//echo "\rattr: " . json_encode($attrs); usleep(100000);
foreach ($attrs as $attr) {
$attr = intval($attr);
if ($attr === 0) {
$color = 0xFFFFFF;
$bgcolor = 0x000000;
$bold = false;
$underline = false;
} elseif ($attr == 1) {
$bold = true;
} elseif ($attr == 4) {
$underline = true;
} elseif ($attr == 22) {
$bold = false;
} elseif ($attr == 24) {
$underline = false;
} elseif ($attr >= 30 && $attr <= 39) {
$color = $this->palette[$attr - 30];
} elseif ($attr >= 40 && $attr <= 49) {
$bgcolor = $this->palette[$attr - 40];
}
}
//$attr = join(";", $attrs)??"0";
//printf("\e[%d;%dH\e[%sm%s", $line + 1, $column + 1, $attr, $char);
imagefilledrectangle($this->gd, $x, $y, $x + $w, $y + $h, $bgcolor);
imagettftext($this->gd, 10, 0, $x, $y + 12, $color, $bold?$this->boldFont:$this->font, $char);
if ($bold) {
//imagettftext($this->gd, 10, 0, $x + 1, $y + 10, $color, $this->font, $char);
}
if ($underline) {
imageline($this->gd, $x, $y + $h - 2, $x + $w, $y + $h - 2, $color);
}
}
public function writePng(string $filename)
{
imagepng($this->gd, $filename);
}
public function setCursorStyle(int $style)
{
if (!in_array($style, self::$ValidCusors)) {
throw new \RuntimeException("Invalid cursor style");
}
$this->cursorStyle = $style;
}
private function drawCursor($line, $column)
{
$cursorX1 = $column * $this->cellWidth;
$cursorY1 = $line * $this->cellHeight;
$cursorX2 = $cursorX1 + $this->cellWidth;
$cursorY2 = $cursorY1 + $this->cellHeight - 2;
switch ($this->cursorStyle) {
case self::CURSOR_NONE:
return;
case self::CURSOR_BLOCK:
imagerectangle($this->gd, $cursorX1, $cursorY1, $cursorX2, $cursorY2, 0xFFFFFF);
break;
case self::CURSOR_SOLID:
imagefilledrectangle($this->gd, $cursorX1, $cursorY1, $cursorX2, $cursorY2, 0xFFFFFF);
break;
case self::CURSOR_LEFT:
case self::CURSOR_UNDER:
case self::CURSOR_IBEAM:
}
}
private function drawAnnotations()
{
$fw = imagefontwidth($this->annotationFontSize);
$fh = imagefontheight($this->annotationFontSize);
$bg = 0xFFEEDD;
$bgs = 0x808080;
$bgf = 0xFF8800;
foreach ($this->annotations as $annotation) {
[$line, $column, $text, $width] = $annotation;
$x = $this->cellWidth * $column;
$y = $this->cellHeight * ($line + 1) + 5;
$lines = explode("\\n", $text);
$lc = count($lines);
$lw = max(array_map("strlen", $lines));
$w = 4 + ($fw * $lw);
$h = 2 + ($fh * $lc);
$aw = floor($this->cellWidth / 2);
for ($n = 0; $n < $aw; $n++) {
imageline($this->gd, $x + $n, $y - $n, $x + (2 * $aw) - $n, $y - $n, $bg);
}
imagerectangle($this->gd, $x+1, $y+1, $x + $w + 1, $y + $h + 1, $bgs);
imagefilledrectangle($this->gd, $x, $y, $x + $w, $y + $h, $bg);
$lp = 0;
foreach ($lines as $line) {
imagestring($this->gd, $this->annotationFontSize, $x + 2, $y + 1 + ($lp++ * $fh), $line, 0x0);
}
if ($width > 0) {
imageline($this->gd, $x - 1, $y - 4, $x - 1, $y - 7, $bgf);
imageline($this->gd, $x + ($width * $this->cellWidth) + 1, $y - 4, $x + ($width * $this->cellWidth) + 1, $y - 7, $bgf);
imageline($this->gd, $x - 1, $y - 4, $x + ($width * $this->cellWidth) + 1, $y - 4, $bgf);
imageline($this->gd, $x - 1, $y - 5, $x + ($width * $this->cellWidth) + 1, $y - 5, $bgf);
}
}
}
public function addAnnotation(string $id, int $line, int $column, string $text)
{
$this->annotations[$id] = [ $line, $column, $text, 0 ];
}
public function updateAnnotation(string $id, array $props)
{
if (!array_key_exists($id, $this->annotations)) {
return;
}
foreach ($props as $k=>$v) {
switch ($k) {
case 'length':
$this->annotations[$id][3] = $v;
break;
}
}
}
public function removeAnnotation(string $id)
{
unset ($this->annotations[$id]);
}
public function clearAnnotations()
{
$this->annotations = [];
}
private function drawKeyOverlay()
{
$x = 0;
$d = 5;
$y = imagesy($this->gd) - 32;
$h = 24;
$w = 32;
$ww = 48;
foreach ($this->keyOverlay as $key) {
$x += $d;
$char = $this->translateKeyToGlyph($key);
if (mb_strlen($char) > 2) {
$this->drawRoundedRectangle($x, $y, $ww, $h, 3, 0xCCCCCC);
imagettftext($this->gd, 8, 0, $x + 5, $y + 16, 0x000000, $this->boldFont, $char);
$x += $ww;
} else {
$this->drawRoundedRectangle($x, $y, $w, $h, 3, 0xCCCCCC);
imagettftext($this->gd, 10, 0, $x + ((mb_strlen($char)==1)?12:8), $y + 16, 0x000000, $this->boldFont, $char);
$x += $w;
}
}
}
public function setKeyOverlay(array $keys)
{
$this->keyOverlay = $keys;
}
private function drawRoundedRectangle(int $left, int $top, int $width, int $height, int $radius, $color)
{
$right = $left + $width;
$bottom = $top + $height;
imagefilledrectangle($this->gd, $left + $radius, $top, $right - $radius, $bottom, $color);
imagefilledrectangle($this->gd, $left, $top + $radius, $right, $bottom - $radius, $color);
imagefilledellipse($this->gd, $left + $radius, $top + $radius, $radius * 2, $radius * 2, $color);
imagefilledellipse($this->gd, $left + $radius, $bottom - $radius, $radius * 2, $radius * 2, $color);
imagefilledellipse($this->gd, $right - $radius, $top + $radius, $radius * 2, $radius * 2, $color);
imagefilledellipse($this->gd, $right - $radius, $bottom - $radius, $radius * 2, $radius * 2, $color);
}
private function translateKeyToGlyph(string $key)
{
switch ($key) {
case "up":
return "";
case "down":
return "";
case "left":
return "";
case "right":
return "";
case "backspace":
case "bs":
return mb_chr(0x232b);
default:
return $key;
}
}
}