Manipulando e Criando Imagens com PHP 7

Tempo de Leitura: 13 Minutos

Olá, Devs. A ideia deste POST é explicar a facilidade que o PHP consegue manipular imagens de maneira dinâmica, neste artigo, vamos explicar como criamos um gerador de GIF ANIMADO, e usamos uma CONTAGEM REGRESSIVA que cria um GIF89 ANIMADO, que você pode usar como uma imagem, ou mesmo inserir em um e-mail e ele vai rodar de maneira dinâmica. Todos os parâmetros podem ser configurados, e é possível alterar o código para rodar o tempo como um relógio, ou qualquer outro texto.

Antes de mais nada, vamos explicar a lógica por traz desse script, primeiro, você passa pela própria chamada do arquivo, todos os parâmetros que vão ser usados, no exemplo, usei alguns que dão conta da maior parte da configuração do sistema, porém, é possível expandir e acrescentar outros dando mais controle sobre como a imagem será gerada, e até mesmo colocar e criar texturas.

O parâmetro mais importante, é a data final, e esse valor que vai gerar todos os possíveis frames, pelo número de segundos da data e hora do servidor até a data e hora futura passada. Os demais parâmetros são o tamanho da fonte em pixels, o tipo de fonte instalado no servidor, a cor do fundo, a cor da fonte, e o ponto X e Y inicial da linha de texto e claro, a posição de cada uma das legendas. Uma possível melhoria, seria permitir a inclusão do texto das legendas, e também uma opção de cor e tamanho da legenda, mais isso fica para um próximo post.

Dividimos o projeto em basicamente 3 arquivos, um deles é uma classe que possui todo o código para gerar e manipular os arquivos GIF89 e esta classe está separada, pois, pode ser incluída em outros projetos onde seja necessário manipular arquivos GIF.

O segundo, é uma extensão da classe, que manipula e extende a classe original para acomodar os valores dos parâmetros, e também implementa os cálculos dos frames e encapsula a chamada dando a saída a um arquivo GIF89 já com todos os cabeçalhos HTTP necessários para o Browser ou Cliente de e-mail entenderem o arquivo como uma imagem e a exibirem como tal.

O terceiro, é nada mais nada menos que um formulário que permite editar os parâmetros de maneira um pouco mais visual, além de gerar a saída de arquivo para pré-visualização e também o código para ser colado em um editor de e-mail ou de página da web.

Pelo menos um quarto arquivo (podem ser mais de um) com fonte TTF (True Type) deve estar presente no diretório para poder ser incluído e servir como a imagem do relógio, conforme a fonte escolhida, é a fonte apresentada na saída, por isso podem existir vários arquivos TTF e o usuário passa o nome da fonte para ser usada.

<?php

/*
:: GIFEncoder Version 2.0
:: Implementa um manipulador de imagens no formato GIF89 (animado)
:: Updated at 2020.11.13 - Adaptado para PHP7.4
*/

/** Encode animated gifs */
class AnimatedGif {
  private $image = '';
  private $buffer = array(); //frames da imagem animada
  private $number_of_loops = 0; //numero de vezes que o GIF repete a animação
  private $DIS = 2;
  private $transparent_colour = -1;
  private $first_frame = true;

/**
 * Encode (cria) a GIF
 * @param array $source_images array de images de origem
 * @param array $image_delays Tempo de delay em segundos
 * @param type $number_of_loops número de loops do GIF
 * @param int $transparent_colour_red
 * @param int $transparent_colour_green
 * @param int $transparent_colour_blue
 */
  public function __construct(array $source_images, array $image_delays, $number_of_loops, $transparent_colour_red = -1, $transparent_colour_green = -1, $transparent_colour_blue = -1) {
    //cria os pixels transparentes conforme a cor de fundo
    $transparent_colour_red = 0;
    $transparent_colour_green = 0;
    $transparent_colour_blue = 0;
    $this->number_of_loops = ($number_of_loops > -1) ? $number_of_loops : 0;
    $this->set_transparent_colour($transparent_colour_red, $transparent_colour_green, $transparent_colour_blue);
    $this->buffer_images($source_images);
    $this->addHeader();
    for ($i = 0; $i < count($this->buffer); $i++) {
      $this->addFrame($i, $image_delays[$i]);
    }
  }

/**
 * SELECIONA A COR DE FNDO QUE SERÁ TRANSPARENTE
 * @param int $red
 * @param int $green
 * @param int $blue
 */
  private function set_transparent_colour($red, $green, $blue)
  {
    $this->transparent_colour = ($red > -1 && $green > -1 && $blue > -1) ?
    ($red | ($green << 8) | ($blue << 16)) : -1;
  }

/**
 * Buffer com as imagens (frames) válidos
 * @param array $source_images (cada frame)
 * @throws Exception faults
 */
  private function buffer_images($source_images)
  {
    for ($i = 0; $i < count($source_images); $i++) {
      $this->buffer[] = $source_images[$i];
      if (substr($this->buffer[$i], 0, 6) != "GIF87a" && substr($this->buffer[$i], 0, 6) != "GIF89a") {
        throw new Exception('Image at position ' . $i . ' is not a gif');
      }
      for ($j = (13 + 3 * (2 << (ord($this->buffer[$i][10]) & 0x07))), $k = true; $k; $j++) {
        switch ($this->buffer[$i][ $j]) {
        case "!":
          if ((substr($this->buffer[$i], ($j + 3), 8)) == "NETSCAPE") {
            throw new Exception('A ORIGEM NAO PODE SER UM GIF ANIMADO, E SIM UM FRAME');
          }
          break;
        case ";":
          $k = false;
          break;
        }
      }
    }
  }

/**
 * Cria o CABEÇALHO GIF89 padrão, que identifica o arquivo GIF
 */
  private function addHeader()
  {
    $cmap = 0;
    $this->image = 'GIF89a';
    if (ord($this->buffer[0][10]) & 0x80) {
      $cmap = 3 * (2 << (ord($this->buffer[0][10]) & 0x07));
      $this->image .= substr($this->buffer[0], 6, 7);
      $this->image .= substr($this->buffer[0], 13, $cmap);
      $this->image .= "!7NETSCAPE2.0" . $this->word($this->number_of_loops) . "
<?php
/*
:: GIFEncoder Version 2.0
:: Implementa um manipulador de imagens no formato GIF89 (animado)
:: Updated at 2020.11.13 - Adaptado para PHP7.4
*/
/** Encode animated gifs */
class AnimatedGif {
private $image = '';
private $buffer = array(); //frames da imagem animada
private $number_of_loops = 0; //numero de vezes que o GIF repete a animação
private $DIS = 2;
private $transparent_colour = -1;
private $first_frame = true;
/**
* Encode (cria) a GIF
* @param array $source_images array de images de origem
* @param array $image_delays Tempo de delay em segundos
* @param type $number_of_loops número de loops do GIF
* @param int $transparent_colour_red
* @param int $transparent_colour_green
* @param int $transparent_colour_blue
*/
public function __construct(array $source_images, array $image_delays, $number_of_loops, $transparent_colour_red = -1, $transparent_colour_green = -1, $transparent_colour_blue = -1) {
//cria os pixels transparentes conforme a cor de fundo
$transparent_colour_red = 0;
$transparent_colour_green = 0;
$transparent_colour_blue = 0;
$this->number_of_loops = ($number_of_loops > -1) ? $number_of_loops : 0;
$this->set_transparent_colour($transparent_colour_red, $transparent_colour_green, $transparent_colour_blue);
$this->buffer_images($source_images);
$this->addHeader();
for ($i = 0; $i < count($this->buffer); $i++) {
$this->addFrame($i, $image_delays[$i]);
}
}
/**
* SELECIONA A COR DE FNDO QUE SERÁ TRANSPARENTE
* @param int $red
* @param int $green
* @param int $blue
*/
private function set_transparent_colour($red, $green, $blue)
{
$this->transparent_colour = ($red > -1 && $green > -1 && $blue > -1) ?
($red | ($green << 8) | ($blue << 16)) : -1;
}
/**
* Buffer com as imagens (frames) válidos
* @param array $source_images (cada frame)
* @throws Exception faults
*/
private function buffer_images($source_images)
{
for ($i = 0; $i < count($source_images); $i++) {
$this->buffer[] = $source_images[$i];
if (substr($this->buffer[$i], 0, 6) != "GIF87a" && substr($this->buffer[$i], 0, 6) != "GIF89a") {
throw new Exception('Image at position ' . $i . ' is not a gif');
}
for ($j = (13 + 3 * (2 << (ord($this->buffer[$i][10]) & 0x07))), $k = true; $k; $j++) {
switch ($this->buffer[$i][ $j]) {
case "!":
if ((substr($this->buffer[$i], ($j + 3), 8)) == "NETSCAPE") {
throw new Exception('A ORIGEM NAO PODE SER UM GIF ANIMADO, E SIM UM FRAME');
}
break;
case ";":
$k = false;
break;
}
}
}
}
/**
* Cria o CABEÇALHO GIF89 padrão, que identifica o arquivo GIF
*/
private function addHeader()
{
$cmap = 0;
$this->image = 'GIF89a';
if (ord($this->buffer[0][10]) & 0x80) {
$cmap = 3 * (2 << (ord($this->buffer[0][10]) & 0x07));
$this->image .= substr($this->buffer[0], 6, 7);
$this->image .= substr($this->buffer[0], 13, $cmap);
$this->image .= "!\377\13NETSCAPE2.0\3\1" . $this->word($this->number_of_loops) . "\0";
}
}
/**
* Adiciona um novo Frame
* @param int $frame frame a ser adicionado
* @param int $delay o delay desse frame (tempo que ele fica na tela)
*/
private function addFrame($frame, $delay)
{
$Locals_str = 13 + 3 * (2 << (ord($this->buffer[$frame][10]) & 0x07));
$Locals_end = strlen($this->buffer[$frame]) - $Locals_str - 1;
$Locals_tmp = substr($this->buffer[$frame], $Locals_str, $Locals_end);
$Global_len = 2 << (ord($this->buffer[0][10]) & 0x07);
$Locals_len = 2 << (ord($this->buffer[$frame][10]) & 0x07);
$Global_rgb = substr($this->buffer[0], 13, 3 * (2 << (ord($this->buffer[0][10]) & 0x07)));
$Locals_rgb = substr($this->buffer[$frame], 13, 3 * (2 << (ord($this->buffer[$frame][10]) & 0x07)));
$Locals_ext = "!\xF9\x04" . chr(($this->DIS << 2) + 0) .
chr(($delay >> 0) & 0xFF) . chr(($delay >> 8) & 0xFF) . "\x0\x0";
if ($this->transparent_colour > -1 && ord($this->buffer[$frame][10]) & 0x80) {
for ($j = 0; $j < (2 << (ord($this->buffer[$frame][10]) & 0x07)); $j++) {
if (
ord($Locals_rgb[3 * $j + 0]) == (($this->transparent_colour >> 16) & 0xFF) &&
ord($Locals_rgb[3 * $j + 1]) == (($this->transparent_colour >> 8) & 0xFF) &&
ord($Locals_rgb[3 * $j + 2]) == (($this->transparent_colour >> 0) & 0xFF)
) {
$Locals_ext = "!\xF9\x04" . chr(($this->DIS << 2) + 1) .
chr(($delay >> 0) & 0xFF) . chr(($delay >> 8) & 0xFF) . chr($j) . "\x0";
break;
}
}
}
switch ($Locals_tmp[0]) {
case "!":
$Locals_img = substr($Locals_tmp, 8, 10);
$Locals_tmp = substr($Locals_tmp, 18, strlen($Locals_tmp) - 18);
break;
case ",":
$Locals_img = substr($Locals_tmp, 0, 10);
$Locals_tmp = substr($Locals_tmp, 10, strlen($Locals_tmp) - 10);
break;
}
if (ord($this->buffer[$frame][10]) & 0x80 && $this->first_frame === false) {
if ($Global_len == $Locals_len) {
if ($this->blockCompare($Global_rgb, $Locals_rgb, $Global_len)) {
$this->image .= ($Locals_ext . $Locals_img . $Locals_tmp);
} else {
$byte = ord($Locals_img[9]);
$byte |= 0x80;
$byte &= 0xF8;
$byte |= (ord($this->buffer[0][10]) & 0x07);
$Locals_img[9] = chr($byte);
$this->image .= ($Locals_ext . $Locals_img . $Locals_rgb . $Locals_tmp);
}
} else {
$byte = ord($Locals_img[9]);
$byte |= 0x80;
$byte &= 0xF8;
$byte |= (ord($this->buffer[$frame][10]) & 0x07);
$Locals_img[9] = chr($byte);
$this->image .= ($Locals_ext . $Locals_img . $Locals_rgb . $Locals_tmp);
}
} else {
$this->image .= ($Locals_ext . $Locals_img . $Locals_tmp);
}
$this->first_frame = false;
}
/**
* Adiciona o FOOTER como fechamento do arquivo, pelo padrão GIF89
*/
private function addFooter()
{
$this->image .= ";";
}
/**
* Compara os Blocos para alterar só a parte da imagem
* @param type $GlobalBlock
* @param type $LocalBlock
* @param type $Len
* @return type
*/
private function blockCompare($GlobalBlock, $LocalBlock, $Len)
{
for ($i = 0; $i < $Len; $i++) {
if (
$GlobalBlock[3 * $i + 0] != $LocalBlock[3 * $i + 0] ||
$GlobalBlock[3 * $i + 1] != $LocalBlock[3 * $i + 1] ||
$GlobalBlock[3 * $i + 2] != $LocalBlock[3 * $i + 2]
) {
return (0);
}
}
return (1);
}
/**
* Sem Emenda
* @param int $int
* @return string com cada caractere
*/
private function word($int)
{
return (chr($int & 0xFF) . chr(($int >> 8) & 0xFF));
}
/**
* Returna a imagem gerada (binário)
* @return type
*/
public function getAnimation()
{
return $this->image;
}
/**
* Retorna a imagem com cabeçalho (display)
* @return type
*/
public function display()
{
$this->addFooter();
header('Content-type:image/jpg');
echo $this->image;
}
}
"; } } /** * Adiciona um novo Frame * @param int $frame frame a ser adicionado * @param int $delay o delay desse frame (tempo que ele fica na tela) */ private function addFrame($frame, $delay) { $Locals_str = 13 + 3 * (2 << (ord($this->buffer[$frame][10]) & 0x07)); $Locals_end = strlen($this->buffer[$frame]) - $Locals_str - 1; $Locals_tmp = substr($this->buffer[$frame], $Locals_str, $Locals_end); $Global_len = 2 << (ord($this->buffer[0][10]) & 0x07); $Locals_len = 2 << (ord($this->buffer[$frame][10]) & 0x07); $Global_rgb = substr($this->buffer[0], 13, 3 * (2 << (ord($this->buffer[0][10]) & 0x07))); $Locals_rgb = substr($this->buffer[$frame], 13, 3 * (2 << (ord($this->buffer[$frame][10]) & 0x07))); $Locals_ext = "!\xF9\x04" . chr(($this->DIS << 2) + 0) . chr(($delay >> 0) & 0xFF) . chr(($delay >> 8) & 0xFF) . "\x0\x0"; if ($this->transparent_colour > -1 && ord($this->buffer[$frame][10]) & 0x80) { for ($j = 0; $j < (2 << (ord($this->buffer[$frame][10]) & 0x07)); $j++) { if ( ord($Locals_rgb[3 * $j + 0]) == (($this->transparent_colour >> 16) & 0xFF) && ord($Locals_rgb[3 * $j + 1]) == (($this->transparent_colour >> 8) & 0xFF) && ord($Locals_rgb[3 * $j + 2]) == (($this->transparent_colour >> 0) & 0xFF) ) { $Locals_ext = "!\xF9\x04" . chr(($this->DIS << 2) + 1) . chr(($delay >> 0) & 0xFF) . chr(($delay >> 8) & 0xFF) . chr($j) . "\x0"; break; } } } switch ($Locals_tmp[0]) { case "!": $Locals_img = substr($Locals_tmp, 8, 10); $Locals_tmp = substr($Locals_tmp, 18, strlen($Locals_tmp) - 18); break; case ",": $Locals_img = substr($Locals_tmp, 0, 10); $Locals_tmp = substr($Locals_tmp, 10, strlen($Locals_tmp) - 10); break; } if (ord($this->buffer[$frame][10]) & 0x80 && $this->first_frame === false) { if ($Global_len == $Locals_len) { if ($this->blockCompare($Global_rgb, $Locals_rgb, $Global_len)) { $this->image .= ($Locals_ext . $Locals_img . $Locals_tmp); } else { $byte = ord($Locals_img[9]); $byte |= 0x80; $byte &= 0xF8; $byte |= (ord($this->buffer[0][10]) & 0x07); $Locals_img[9] = chr($byte); $this->image .= ($Locals_ext . $Locals_img . $Locals_rgb . $Locals_tmp); } } else { $byte = ord($Locals_img[9]); $byte |= 0x80; $byte &= 0xF8; $byte |= (ord($this->buffer[$frame][10]) & 0x07); $Locals_img[9] = chr($byte); $this->image .= ($Locals_ext . $Locals_img . $Locals_rgb . $Locals_tmp); } } else { $this->image .= ($Locals_ext . $Locals_img . $Locals_tmp); } $this->first_frame = false; } /** * Adiciona o FOOTER como fechamento do arquivo, pelo padrão GIF89 */ private function addFooter() { $this->image .= ";"; } /** * Compara os Blocos para alterar só a parte da imagem * @param type $GlobalBlock * @param type $LocalBlock * @param type $Len * @return type */ private function blockCompare($GlobalBlock, $LocalBlock, $Len) { for ($i = 0; $i < $Len; $i++) { if ( $GlobalBlock[3 * $i + 0] != $LocalBlock[3 * $i + 0] || $GlobalBlock[3 * $i + 1] != $LocalBlock[3 * $i + 1] || $GlobalBlock[3 * $i + 2] != $LocalBlock[3 * $i + 2] ) { return (0); } } return (1); } /** * Sem Emenda * @param int $int * @return string com cada caractere */ private function word($int) { return (chr($int & 0xFF) . chr(($int >> 8) & 0xFF)); } /** * Returna a imagem gerada (binário) * @return type */ public function getAnimation() { return $this->image; } /** * Retorna a imagem com cabeçalho (display) * @return type */ public function display() { $this->addFooter(); header('Content-type:image/jpg'); echo $this->image; } }

Este primeiro arquivo, é a class que transforma os frames de imagem, em uma imagem composta no formato GIF89, note que ela já recebe os frames codificados no formato GIF estático, criando somente a animação, com uma sequencia de frames e o HEADER/FOOTER do arquivo.

<?php
include 'GIFEncoder.class.php'; //Inclui a classe para criar a animação

class CountdownTimer
{
  private $base;
  private $box;
  private $width = 0;
  private $height = 0;
  private $xOffset = 0;
  private $yOffset = 0;
  private $delay = 100; // 1 segundo, tempo do frame
  private $frames = array();
  private $delays = array();
  private $date = array();
  private $fontSettings = array();
  private $boundingBox = array();
  private $fontPath = './';
  private $seconds = 30;

  /**
   * hex2rgb
   * Converte a fonte de HEXA para RGB
   */
  private function hex2rgb($hex)
  {
    $hex = str_replace('#', '', $hex);

    if (strlen($hex) == 3) {
      $r = hexdec(substr($hex, 0, 1) . substr($hex, 0, 1));
      $g = hexdec(substr($hex, 1, 1) . substr($hex, 1, 1));
      $b = hexdec(substr($hex, 2, 1) . substr($hex, 2, 1));
    } else {
      $r = hexdec(substr($hex, 0, 2));
      $g = hexdec(substr($hex, 2, 2));
      $b = hexdec(substr($hex, 4, 2));
    }
    $rgb = array($r, $g, $b);
    return $rgb;
  }

  /**
   * Cria o BOX do fundo, usado como a base
   */
  private function createFilledBox($image)
  {
    imagefilledrectangle(
      $image,
      0,
      0,
      $this->width,
      $this->height,
      imagecolorallocate(
        $image,
        $this->boxColor[0],
        $this->boxColor[1],
        $this->boxColor[2]
      )
    );
  }

  /**
   * CountdownTimer Construtor da classe
   */
  public function __construct($settings)
  {
    $this->width = $settings['width'];
    $this->height = $settings['height'];
    $this->boxColor = $settings['boxColor'];
    $this->xOffset = $settings['xOffset'];
    $this->yOffset = $settings['yOffset'];
    $this->boxColor = $this->hex2rgb($settings['boxColor']);
    $this->fontColor = $this->hex2rgb($settings['fontColor']);
    $this->labelOffsets = explode(',', $settings['labelOffsets']);
    $this->date['time'] = $settings['time'];
    $this->date['futureDate'] = new DateTime(date('r', strtotime($settings['time'])));
    $this->date['timeNow'] = time();
    $this->date['now'] = new DateTime(date('r', time()));
    // cria nova imagem/frame
    $this->box = imagecreatetruecolor($this->width, $this->height);
    $this->base = imagecreatetruecolor($this->width, $this->height);

    $this->fontSettings['path'] = './'.'f1' . '.ttf';
    $this->fontSettings['color'] = imagecolorallocate($this->box, $this->fontColor[0], $this->fontColor[1], $this->fontColor[2]);
    $this->fontSettings['size'] = $settings['fontSize'];
    $this->fontSettings['characterWidth'] = imagefontwidth(1);

    // pega a largura,com base na altura (estima)
    $string = "0:";
    $size = $this->fontSettings['size'];
    $angle = 0;
    $fontfile = 'f1.ttf';

    $strlen = strlen($string);
    for ($i = 0; $i < $strlen; $i++) {
      $dimensions = imagettfbbox($size, $angle, $fontfile, $string[$i]);
      $this->fontSettings['characterWidths'][] = array(
        $string[$i] => $dimensions[2]
      );
    }

    $this->images = array(
      'box' => $this->box,
      'base' => $this->base,
    );

    // cria o FRAME VAZIO DE FUNDO
    foreach ($this->images as $image) {
      $this->createFilledBox($image);
    }

    $this->createFrames();
  }

  /**
   * Cria todos os frames para o contador
   */
  public function createFrames()
  {
    $this->boundingBox = imagettfbbox($this->fontSettings['size'], 0, $this->fontSettings['path'], '00:00:00:00');
    $this->characterDimensions = imagettfbbox($this->fontSettings['size'], 0, $this->fontSettings['path'], '0');
    $this->characterWidth = $this->characterDimensions[2];
    $this->characterHeight = abs($this->characterDimensions[1] + $this->characterDimensions[7]);

    $this->base = $this->applyTextToImage($this->base, $this->fontSettings, $this->date);

    // cria o QUADRO a QUADRO
    for ($i = 0; $i <= $this->seconds; $i++) {
      $layer = imagecreatetruecolor($this->width, $this->height);
      $this->createFilledBox($layer);

      $layer = $this->applyTextToImage($layer, $this->fontSettings, $this->date);
    }

    $this->showImage();
  }

  /**
   * Apresenta o TEXTO do FRAME
   */
  private function applyTextToImage($image, $font, $date)
  {
    $interval = date_diff(
      $date['futureDate'],
      $date['now']
    );

    if ($date['futureDate'] < $date['now']) {
      $text = $interval->format('00:00:00:00');
      $this->loops = 1;
    } else {
      $text = $interval->format('0%a:%H:%I:%S');
      $this->loops = 0;
    }

    $labels = array('DIAS', 'HORAS', 'MINUTOS', 'SEGUNDOS'); // PODE SER DINÂMICO PARA PERMITIR INTERNACIONALIZAÇÂO
    foreach ($labels as $key => $label) {
      imagettftext($image, 15, 0, $this->xOffset + ($this->characterWidth * $this->labelOffsets[$key]), 99, $font['color'], $font['path'], $label);
    }

    // Mostra o TEMPO no frame atual
    imagettftext($image, $font['size'], 0, $this->xOffset, $this->yOffset, $font['color'], $font['path'], $text);

    ob_start();
    imagegif($image);
    $this->frames[] = ob_get_contents();
    $this->delays[] = $this->delay;
    ob_end_clean();

    $this->date['now']->modify('+1 second');

    return $image;
  }

  /**
   * showImage
   * Cria a saida de imegem (stream) para os frames
   */
  public function showImage()
  {
    $gif = new AnimatedGif($this->frames, $this->delays, $this->loops);
    $gif->display();
  }
}

/**
 * Chamada de CONSTRUÇÃO com os parametros recebidos pelo GET ou pega os valores DEFAULT
 */
new CountdownTimer(array(
  'time' => isset($_GET['width']) ? $_GET['time'] : date("Y")."12-31",
  'width' => isset($_GET['width']) ? $_GET['width'] : 640,
  'height' => isset($_GET['height']) ? $_GET['height'] : 110,
  'boxColor' => isset($_GET['boxColor']) ? $_GET['boxColor'] : '#000',
  'font' => isset($_GET['font']) ? $_GET['font'] : 'f1',
  'fontColor' => isset($_GET['fontColor']) ? $_GET['fontColor'] : '#fff',
  'fontSize' => isset($_GET['fontSize']) ? $_GET['fontSize'] : 60,
  'xOffset' => isset($_GET['xOffset']) ? $_GET['xOffset'] : 155,
  'yOffset' => isset($_GET['yOffset']) ? $_GET['yOffset'] : 70,
  'labelOffsets' => isset($_GET['labelOffsets']) ? $_GET['labelOffsets'] : "1.4,5,8,11",
));

Neste segundo arquivo, é que manipulamos os frames conforme os parâmetros e criamos a animação. Ele é o coração do funcionamento, criando a imagem e a entregado em stream para o browser.

<?php
// RECEBE OS PARAMETROS DO FORMUlÀRIO ou INICIALIZA COM PADRÕES
if(isset($_GET['time'])){
    $time=$_GET['time'];
} else {
    $time=date('Y-m-d').'T23:59:59';
}
if(isset($_GET['width'])){
    $width=$_GET['width'];
} else {
    $width=500;
}
if(isset($_GET['height'])){
    $height=$_GET['height'];
} else {
    $height=150;
}
if(isset($_GET['boxc'])){
    $boxc=$_GET['boxc'];
} else {
    $boxc="#000000";
}
if(isset($_GET['fontc'])){
    $fontc=$_GET['fontc'];
} else {
    $fontc="#FFFFFF";
}
if(isset($_GET['fonts'])){
    $fonts=$_GET['fonts'];
} else {
    $fonts="60";
}
if(isset($_GET['xoff'])){
    $xoff=$_GET['xoff'];
} else {
    $xoff="1";
}
if(isset($_GET['loff'])){
    $loff=$_GET['loff'];
} else {
    $loff="70";
}
if(isset($_GET['doff'])){
    $doff=$_GET['doff'];
} else {
    $doff="1";
}
if(isset($_GET['hoff'])){
    $hoff=$_GET['hoff'];
} else {
    $hoff="1";
}
if(isset($_GET['moff'])){
    $moff=$_GET['moff'];
} else {
    $moff="1";
}
if(isset($_GET['soff'])){
    $soff=$_GET['soff'];
} else {
    $soff="1";
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=1920, initial-scale=1.0">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
    <title>Countdown - Configuração</title>
</head>
<body>
    <div class="container-fluid">
        <h1>Countdown</h1>
        <h2>Configurar</h2>
            <form action='index.php' method="GET">
                <div class="form-group">
                    <label>Horario Final:</label>
                    <input type="datetime-local" id="date-time" name="time" value="<?php echo $time; ?>" min="<?php date('Y-m-dTh:m:s');?>" max="<?php (date('Y')+1).'-'.date('m-d');?>T23:59:59">
                </div>
                <div class="form-group">
                    <label>Largura:</label>
                    <input type="number" id="width" name="width" value="<?php echo $width; ?>" min="100" max="1920" step="1">
                </div>
                <div class="form-group">
                    <label>Altura:</label>
                    <input type="number" id="height" name="height" value="<?php echo $height; ?>" min="100" max="1080" step="1">
                </div>
                <div class="form-group">
                    <label>Cor do Fundo:</label>
                    <input type="color" id="boxc" name="boxc" value="<?php echo $boxc; ?>">
                </div>
                <div class="form-group">
                    <label>Cor da Fonte:</label>
                    <input type="color" id="fontc" name="fontc" value="<?php echo $fontc; ?>">
                </div>
                <div class="form-group">
                    <label>Tamanho da Fonte:</label>
                    <input type="number" id="fonts" name="fonts" value="<?php echo $fonts; ?>" min='20' max='1000'>
                </div>
                <div class="form-group">
                    <label>OffSet Largura:</label>
                    <input type="number" id="xoff" name="xoff" value="<?php echo $xoff; ?>" min='0' max='1000'>
                </div>
                <div class="form-group">
                    <label>OffSet Altura:</label>
                    <input type="number" id="loff" name="loff" value="<?php echo $loff; ?>" min='0' max='1000'>
                </div>
                <div class="form-group">
                    <label>OffSet Legenda: (dia/hora/minuto/segundo)</label>
                    <input type="number" id="doff" name="doff" value="<?php echo $doff; ?>" min='0' max='10' step=0.1>
                    <input type="number" id="doff" name="hoff" value="<?php echo $hoff; ?>" min='0' max='10' step=0.1>
                    <input type="number" id="doff" name="moff" value="<?php echo $moff; ?>" min='0' max='10' step=0.1>
                    <input type="number" id="doff" name="soff" value="<?php echo $soff; ?>" min='0' max='10' step=0.1>
                </div>

            <button type=submit class='btn btn-primary'>PREVISUALIZAR</button>
            </form>
        <h2>Preview</h2>
            <?php
                $CTDOWN="countdown.php?time=$time".
                "&width=$width".
                "&height=$height".
                "&boxColor=".substr("$boxc", -6).
                "&font=BebasNeue&".
                "fontColor=".substr("$fontc", -6).
                "&fontSize=$fonts".
                "&xOffset=$xoff".
                "&yOffset=$loff".
                "&labelOffsets=$doff,$hoff,$moff,$soff";
            ?>
            <img src='<?php echo($CTDOWN); ?>'></br>
            </br>
            Use o código abaixo para usar este countdow em seus projetos.</br>
            <div class='w-100 border'><code>&lt;img src='<?php echo($CTDOWN); ?>'&gt;</code></div>
    </div>
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
</body>
</html>

Este arquivo é somente um HTML com um formulário que permite editar os parâmetros de forma mais visual com um preview. Porém, note que ele é basicamente um HTML, sem função no funcionamento do gerador, servindo somente para criar os parametros.

Veja em Funcionamento

Neste endereço (https://countdown.brasap.com.br) é possivel ver o sript funcionando, e mesmo importa-lo para seu uso próprio em algum projeto. Se você gostou, não deixe de deixar um comentário, ou acessar a página do projeto no GitHub e seguir, sugerir melhorias ou reportar algum problema. (https://github.com/RomeuTMC/Countdown)

Como Funciona

O arquvo countdown.php (o segundo) possui funções que recebem os parâmetros passados pela URL e criam uma sequencia de frames com pixels transparentes referentes a cor de fundo, e criam uma pilha (array) de frames cada qual com uma sequencia de alterações que aplicadas na ordem, correta dão a sensação da animação, como em um arquivo GIF89 animado.

Note, como a imagem é gerada dinamicamente, só são criados os frames do horário atual do servidor, até o zero do contador (data futura), ou seja, se você salvar a imagem gerada, ela vai ser referente ao instantâneo do momento onde foi gerada, e não vai funcionar como o esperado, a não ser que seja um contador de tempo (daqui 1 dia, daqui a x tempo).

Se você incluir a imagem no corpo de um e-mail, por exemplo, como no carregamento ele cria o anexo puxando-o do servidor, o efeito funciona, porém, você não consegue baixar a imagem de maneira estática, ou seja, a cada pessoa que abre o email uma cópia diferente da imagem é gerada.

Como o formato GIF89 produz imagens que só alteram a parte do frame mesmo para um período de 1 ano, e de uma imagem de tamanho médio (1920×1080) o tamanho do arquivo gerado não passa de 1 a 3 mebabytes, sendo que em uma resolução como 640×200 (tradicional) o arquivo tem alguns Kb de tamanho.

Esse script mostra o poder de manipulação de arquivos e de formatos que a linguagem PHP possui, além de sua facilidade e velocidade de execução para gerar dados dinâmicos e menipular diferentes formatos de arquivos, não só arquivos de texto e de banco de dados.