View source  
  <?php
namespace Drupal\textimage;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Image\ImageFactory;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\image\ImageEffectManager;
use Drupal\image\ImageStyleInterface;
use Drupal\file\Entity\File;
use Drupal\file\FileInterface;
use Drupal\image\Entity\ImageStyle;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Psr\Log\LoggerInterface;
class Textimage implements TextimageInterface {
  use StringTranslationTrait;
  
  protected $factory;
  
  protected $lock;
  
  protected $imageFactory;
  
  protected $configFactory;
  
  protected $cache;
  
  protected $logger;
  
  protected $fileSystem;
  
  protected $imageEffectManager;
  
  protected $id = NULL;
  
  protected $processed = FALSE;
  
  protected $built = FALSE;
  
  protected $imageData = [];
  
  protected $uri = NULL;
  
  protected $width = NULL;
  
  protected $height = NULL;
  
  protected $style = NULL;
  
  protected $effects = [];
  
  protected $text = [];
  
  protected $extension = NULL;
  
  protected $gifTransparentColor = '#FFFFFF';
  
  protected $caching = TRUE;
  
  protected $sourceImageFile = NULL;
  
  protected $tokenData = [];
  
  protected $bubbleableMetadata = NULL;
  
  public function __construct(TextimageFactory $textimage_factory, LockBackendInterface $lock_service, ImageFactory $image_factory, ConfigFactoryInterface $config_factory, LoggerInterface $logger, CacheBackendInterface $cache_service, FileSystemInterface $file_system, ImageEffectManager $image_effect_manager) {
    $this->factory = $textimage_factory;
    $this->lock = $lock_service;
    $this->imageFactory = $image_factory;
    $this->configFactory = $config_factory;
    $this->logger = $logger;
    $this->cache = $cache_service;
    $this->fileSystem = $file_system;
    $this->imageEffectManager = $image_effect_manager;
  }
  
  public static function create(ContainerInterface $container) {
    return new static($container
      ->get('textimage.factory'), $container
      ->get('lock'), $container
      ->get('image.factory'), $container
      ->get('config.factory'), $container
      ->get('textimage.logger'), $container
      ->get('cache.textimage'), $container
      ->get('file_system'), $container
      ->get('plugin.manager.image.effect'));
  }
  
  protected function set($property, $value) {
    if (!property_exists($this, $property)) {
      throw new TextimageException("Attempted to set non existing property '{$property}'");
    }
    if (!$this->processed) {
      $this->{$property} = $value;
    }
    else {
      throw new TextimageException("Attempted to set property '{$property}' when image was processed already");
    }
    return $this;
  }
  
  public function setStyle(ImageStyleInterface $image_style) {
    if ($this->style) {
      throw new TextimageException("Image style already set");
    }
    $this
      ->set('style', $image_style);
    $effects = @$this->style
      ->getEffects()
      ->getConfiguration();
    $this
      ->setEffects($effects);
    return $this;
  }
  
  public function setEffects(array $effects) {
    if ($this->effects) {
      throw new TextimageException("Image effects already set");
    }
    return $this
      ->set('effects', $effects);
  }
  
  public function setTargetExtension($extension) {
    if ($this->extension) {
      throw new TextimageException("Extension already set");
    }
    $extension = strtolower($extension);
    if (!in_array($extension, $this->imageFactory
      ->getSupportedExtensions())) {
      $this->logger
        ->error("Unsupported image file extension (%extension) requested.", [
        '%extension' => $extension,
      ]);
      throw new TextimageException("Attempted to set an unsupported file image extension ({$extension})");
    }
    return $this
      ->set('extension', $extension);
  }
  
  public function setGifTransparentColor($color) {
    return $this
      ->set('gifTransparentColor', $color);
  }
  
  public function setSourceImageFile(FileInterface $source_image_file, $width = NULL, $height = NULL) {
    if ($source_image_file) {
      $this
        ->set('sourceImageFile', $source_image_file);
    }
    if ($width && $height) {
      $this
        ->set('width', $width);
      $this
        ->set('height', $height);
    }
    return $this;
  }
  
  public function setTokenData(array $token_data) {
    if ($this->tokenData) {
      throw new TextimageException("Token data already set");
    }
    return $this
      ->set('tokenData', $token_data);
  }
  
  public function setTemporary($is_temp) {
    if ($this->uri) {
      throw new TextimageException("URI already set");
    }
    $this
      ->set('caching', !$is_temp);
    return $this;
  }
  
  public function setTargetUri($uri) {
    if ($this->uri) {
      throw new TextimageException("URI already set");
    }
    if ($uri) {
      if (!file_valid_uri($uri)) {
        throw new TextimageException("Invalid target URI '{$uri}' specified");
      }
      $dir_name = $this->fileSystem
        ->dirname($uri);
      $base_name = $this->fileSystem
        ->basename($uri);
      $valid_uri = $this
        ->createFilename($base_name, $dir_name);
      if ($uri != $valid_uri) {
        throw new TextimageException("Invalid target URI '{$uri}' specified");
      }
      $this
        ->setTargetExtension(pathinfo($uri, PATHINFO_EXTENSION));
      $this
        ->set('uri', $uri);
      $this
        ->set('caching', FALSE);
    }
    return $this;
  }
  
  public function setBubbleableMetadata(BubbleableMetadata $bubbleable_metadata = NULL) {
    if ($this->bubbleableMetadata) {
      throw new TextimageException("Bubbleable metadata already set");
    }
    $bubbleable_metadata = $bubbleable_metadata ?: new BubbleableMetadata();
    return $this
      ->set('bubbleableMetadata', $bubbleable_metadata);
  }
  
  public function id() {
    return $this->processed ? $this->id : NULL;
  }
  
  public function getText() {
    return $this->processed ? array_values($this->text) : [];
  }
  
  public function getUri() {
    return $this->processed ? $this->uri : NULL;
  }
  
  public function getUrl() {
    return $this->processed ? Url::fromUri(file_create_url($this
      ->getUri())) : NULL;
  }
  
  public function getHeight() {
    return $this->processed ? $this->height : NULL;
  }
  
  public function getWidth() {
    return $this->processed ? $this->width : NULL;
  }
  
  public function getBubbleableMetadata() {
    return $this->processed ? $this->bubbleableMetadata : NULL;
  }
  
  public function load($id) {
    
    if ($this->processed) {
      return $this;
    }
    
    $this->id = $id;
    if ($cached_data = $this
      ->getCachedData()) {
      $this
        ->set('imageData', $cached_data['imageData']);
      $this
        ->set('uri', $cached_data['uri']);
      $this
        ->set('width', $cached_data['width']);
      $this
        ->set('height', $cached_data['height']);
      $this
        ->set('effects', $cached_data['effects']);
      $this
        ->set('text', $cached_data['imageData']['text']);
      $this
        ->set('extension', $cached_data['imageData']['extension']);
      if ($cached_data['imageData']['sourceImageFileId']) {
        $this
          ->set('sourceImageFile', File::load($cached_data['imageData']['sourceImageFileId']));
      }
      $this
        ->set('gifTransparentColor', $cached_data['imageData']['gifTransparentColor']);
      $this
        ->set('caching', TRUE);
      $this
        ->set('bubbleableMetadata', $cached_data['bubbleableMetadata']);
      $this->processed = TRUE;
    }
    else {
      throw new TextimageException("Missing Textimage cache entry {$this->id}");
    }
    return $this;
  }
  
  public function process($text) {
    
    if ($this->processed) {
      throw new TextimageException("Attempted to re-process an already processed Textimage");
    }
    
    if (empty($this->effects)) {
      $this->logger
        ->error('Textimage had no image effects to process.');
      return $this;
    }
    
    if ($this->style) {
      $this->bubbleableMetadata = $this->bubbleableMetadata
        ->addCacheableDependency($this->style);
    }
    if ($this->sourceImageFile) {
      $this->bubbleableMetadata = $this->bubbleableMetadata
        ->addCacheableDependency($this->sourceImageFile);
    }
    
    if (!$text) {
      $text = [];
    }
    if (!is_array($text)) {
      $text = [
        $text,
      ];
    }
    
    $default_text = [];
    foreach ($this->effects as $uuid => $effect_configuration) {
      if ($effect_configuration['id'] == 'image_effects_text_overlay') {
        $uuid = isset($effect_configuration['uuid']) ? $effect_configuration['uuid'] : $uuid;
        $default_text[$uuid] = $effect_configuration['data']['text_string'];
      }
    }
    
    $processed_text = [];
    $this->tokenData['file'] = isset($this->tokenData['file']) ? $this->tokenData['file'] : $this->sourceImageFile;
    foreach ($default_text as $uuid => $default_text_item) {
      $text_item = array_shift($text);
      $effect_instance = $this->imageEffectManager
        ->createInstance($this->effects[$uuid]['id']);
      $effect_instance
        ->setConfiguration($this->effects[$uuid]);
      if ($text_item) {
        
        $text_item = $text_item == '[textimage:default]' ? $default_text_item : $text_item;
        $processed_text[$uuid] = $this->factory
          ->processTextString($text_item, NULL, $this->tokenData, $this->bubbleableMetadata);
      }
      else {
        $processed_text[$uuid] = $this->factory
          ->processTextString($default_text_item, NULL, $this->tokenData, $this->bubbleableMetadata);
      }
      
      $processed_text[$uuid] = $effect_instance
        ->getAlteredText($processed_text[$uuid]);
    }
    $this->text = $processed_text;
    
    $runtime_effects = $this->effects;
    foreach ($this->text as $uuid => $text_item) {
      $runtime_effects[$uuid]['data']['text_string'] = $text_item;
    }
    $runtime_style = $this
      ->buildStyleFromEffects($runtime_effects);
    if ($this->sourceImageFile) {
      if ($this->width && $this->height) {
        $dimensions = [
          'width' => $this->width,
          'height' => $this->height,
        ];
      }
      else {
        
        $source_image = $this->imageFactory
          ->get($this->sourceImageFile
          ->getFileUri());
        $dimensions = [
          'width' => $source_image
            ->getWidth(),
          'height' => $source_image
            ->getHeight(),
        ];
      }
      $uri = $this->sourceImageFile
        ->getFileUri();
    }
    else {
      $dimensions = [
        'width' => 1,
        'height' => 1,
      ];
      $uri = NULL;
    }
    $runtime_style
      ->transformDimensions($dimensions, $uri);
    $this
      ->set('width', $dimensions['width']);
    $this
      ->set('height', $dimensions['height']);
    
    if (!$this->extension) {
      if ($this->sourceImageFile) {
        $extension = pathinfo($this->sourceImageFile
          ->getFileUri(), PATHINFO_EXTENSION);
      }
      else {
        $extension = $this->configFactory
          ->get('textimage.settings')
          ->get('default_extension');
      }
      $this
        ->setTargetExtension($runtime_style
        ->getDerivativeExtension($extension));
    }
    
    $this->imageData = [
      'text' => $this->text,
      'extension' => $this->extension,
      'sourceImageFileId' => $this->sourceImageFile ? $this->sourceImageFile
        ->id() : NULL,
      'sourceImageFileUri' => $this->sourceImageFile ? $this->sourceImageFile
        ->getFileUri() : NULL,
      'gifTransparentColor' => $this->gifTransparentColor,
    ];
    
    foreach ($this->effects as $uuid => &$effect_configuration) {
      if ($effect_configuration['id'] == 'image_effects_text_overlay') {
        unset($effect_configuration['data']['text_string']);
      }
    }
    
    $hash_input = [
      'effects_outline' => $this->effects,
      'image_data' => $this->imageData,
    ];
    $this->id = hash('sha256', serialize($hash_input));
    
    if ($this->caching && ($cached_data = $this
      ->getCachedData())) {
      $this
        ->set('uri', $cached_data['uri']);
      $this->processed = TRUE;
      $this->logger
        ->debug('Cached Textimage, @uri', [
        '@uri' => $this
          ->getUri(),
      ]);
      if (is_file($this
        ->getUri())) {
        $this->built = TRUE;
      }
      return $this;
    }
    else {
      
      if (!$this->uri) {
        $this
          ->buildUri();
      }
      $this->processed = TRUE;
      if ($this->caching) {
        $this
          ->setCached();
      }
    }
    return $this;
  }
  
  public function buildImage() {
    
    if (!$this->processed) {
      throw new TextimageException("Attempted to build Textimage before processing data");
    }
    
    if ($this->built) {
      return $this;
    }
    
    if ($this->caching && is_file($this
      ->getUri())) {
      $this->logger
        ->debug('Stored Textimage, @uri', [
        '@uri' => $this
          ->getUri(),
      ]);
      return $this;
    }
    
    $source = isset($this->sourceImageFile) ? $this->sourceImageFile
      ->getFileUri() : NULL;
    $image = $this->imageFactory
      ->get($source);
    if (!$source) {
      $image
        ->createNew(1, 1, $this->extension, $this->gifTransparentColor);
    }
    
    $this->factory
      ->setState();
    $this->factory
      ->setState('building_module', 'textimage');
    
    $lock_name = 'textimage_process:' . Crypt::hashBase64($this
      ->getUri());
    if (!($lock_acquired = $this->lock
      ->acquire($lock_name))) {
      return file_exists($this
        ->getUri()) ? TRUE : FALSE;
    }
    
    $runtime_effects = $this->effects;
    foreach ($this->text as $uuid => $text_item) {
      $runtime_effects[$uuid]['data']['text_string'] = $text_item;
    }
    $runtime_style = $this
      ->buildStyleFromEffects($runtime_effects);
    
    if ($this->sourceImageFile) {
      $runtime_extension = pathinfo($this->sourceImageFile
        ->getFileUri(), PATHINFO_EXTENSION);
    }
    else {
      $runtime_extension = $this->extension;
    }
    $runtime_extension = $runtime_style
      ->getDerivativeExtension($runtime_extension);
    if ($runtime_extension != $this->extension) {
      
      $max_weight = NULL;
      foreach ($runtime_style
        ->getEffects()
        ->getConfiguration() as $effect_configuration) {
        if (!$max_weight || $effect_configuration['weight'] > $max_weight) {
          $max_weight = $effect_configuration['weight'];
        }
      }
      
      $convert = [
        'id' => 'image_convert',
        'weight' => ++$max_weight,
        'data' => [
          'extension' => $this->extension,
        ],
      ];
      $runtime_style
        ->addImageEffect($convert);
    }
    
    if (!($this->processed = $this
      ->createDerivativeFromImage($runtime_style, $image, $this
      ->getUri()))) {
      if (isset($this->style)) {
        throw new TextimageException("Textimage failed to build an image for image style '{$this->style->id()}'");
      }
      else {
        throw new TextimageException("Textimage failed to build an image");
      }
    }
    $this->logger
      ->debug('Built Textimage, @uri', [
      '@uri' => $this
        ->getUri(),
    ]);
    
    if (!empty($lock_acquired)) {
      $this->lock
        ->release($lock_name);
    }
    
    $this->factory
      ->setState();
    $this->built = TRUE;
    return $this;
  }
  
  protected function buildStyleFromEffects(array $effects) {
    $style = ImageStyle::create([]);
    foreach ($effects as $effect) {
      $effect_instance = $this->imageEffectManager
        ->createInstance($effect['id']);
      $default_config = $effect_instance
        ->defaultConfiguration();
      $effect['data'] = NestedArray::mergeDeep($default_config, $effect['data']);
      $style
        ->addImageEffect($effect);
    }
    $style
      ->getEffects()
      ->sort();
    return $style;
  }
  
  protected function createFilename($basename, $directory) {
    
    $basename = preg_replace('/[\\x00-\\x1F]/u', '_', $basename);
    if (substr(PHP_OS, 0, 3) == 'WIN') {
      
      $basename = str_replace([
        ':',
        '*',
        '?',
        '"',
        '<',
        '>',
        '|',
      ], '_', $basename);
    }
    
    if (substr($directory, -1) == '/') {
      $separator = '';
    }
    else {
      $separator = '/';
    }
    return $directory . $separator . $basename;
  }
  
  protected function createDerivativeFromImage($style, $image, $derivative_uri) {
    
    $directory = $this->fileSystem
      ->dirname($derivative_uri);
    
    if (!file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
      $this->logger
        ->error('Failed to create Textimage directory: %directory', [
        '%directory' => $directory,
      ]);
      return FALSE;
    }
    if (!$image
      ->isValid()) {
      if ($image
        ->getSource()) {
        $this->logger
          ->error("Invalid image at '%image'.", [
          '%image' => $image
            ->getSource(),
        ]);
      }
      else {
        $this->logger
          ->error("Invalid source image.");
      }
      return FALSE;
    }
    foreach ($style
      ->getEffects() as $effect) {
      $effect
        ->applyEffect($image);
    }
    if (!$image
      ->save($derivative_uri)) {
      if (file_exists($derivative_uri)) {
        $this->logger
          ->error('Cached image file %destination already exists. There may be an issue with your rewrite configuration.', [
          '%destination' => $derivative_uri,
        ]);
      }
      return FALSE;
    }
    return TRUE;
  }
  
  
  protected function buildUri() {
    
    if ($this->caching) {
      $base_name = $this->id . '.' . $this->extension;
      if ($this->style) {
        $scheme = $this->style
          ->getThirdPartySetting('textimage', 'uri_scheme', $this->configFactory
          ->get('system.file')
          ->get('default_scheme'));
        $this
          ->set('uri', $this->factory
          ->getStoreUri('/cache/styles/', $scheme) . $this->style
          ->id() . '/' . substr($base_name, 0, 1) . '/' . substr($base_name, 0, 2) . '/' . $base_name);
      }
      else {
        $this
          ->set('uri', $this->factory
          ->getStoreUri('/cache/api/') . substr($base_name, 0, 1) . '/' . substr($base_name, 0, 2) . '/' . $base_name);
      }
    }
    else {
      $base_name = hash('sha256', session_id() . microtime()) . '.' . $this->extension;
      $this
        ->set('uri', $this->factory
        ->getStoreUri('/temp/') . $base_name);
    }
    return $this;
  }
  
  protected function getCachedData() {
    if ($cached = $this->cache
      ->get('tiid:' . $this->id)) {
      return $cached->data;
    }
    return FALSE;
  }
  
  protected function setCached() {
    $data = [
      'imageData' => $this->imageData,
      'uri' => $this
        ->getUri(),
      'width' => $this
        ->getWidth(),
      'height' => $this
        ->getHeight(),
      'effects' => $this->effects,
      'bubbleableMetadata' => $this
        ->getBubbleableMetadata(),
    ];
    $this->cache
      ->set('tiid:' . $this->id, $data, Cache::PERMANENT, $this
      ->getBubbleableMetadata()
      ->getCacheTags());
    return $this;
  }
}