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