dynamic_image_generator-1.0.x-dev/src/Service/DynamicImageGeneratorService.php
src/Service/DynamicImageGeneratorService.php
<?php
namespace Drupal\dynamic_image_generator\Service;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Template\TwigEnvironment;
use Drupal\Core\Utility\Token;
use Drupal\file\FileInterface;
use Drupal\file\Entity\File;
use Drupal\media\Entity\Media;
use GuzzleHttp\ClientInterface;
use Twig\Error\Error as TwigError;
/**
* Service for generating dynamic images through the third-party API.
*/
class DynamicImageGeneratorService {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The logger service.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The Twig environment service.
*
* @var \Drupal\Core\Template\TwigEnvironment
*/
protected $twig;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The token service.
*
* @var \Drupal\Core\Utility\Token
*/
protected $token;
/**
* The HTTP client.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $httpClient;
/**
* Constructs a DynamicImageGeneratorService object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* The logger factory service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Core\Template\TwigEnvironment $twig
* The Twig environment service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\Core\Utility\Token $token
* The token service.
* @param \GuzzleHttp\ClientInterface $http_client
* The HTTP client.
*/
public function __construct(
EntityTypeManagerInterface $entity_type_manager,
FileSystemInterface $file_system,
LoggerChannelFactoryInterface $logger_factory,
ConfigFactoryInterface $config_factory,
TwigEnvironment $twig,
RendererInterface $renderer,
Token $token,
ClientInterface $http_client
) {
$this->entityTypeManager = $entity_type_manager;
$this->fileSystem = $file_system;
$this->logger = $logger_factory->get('dynamic_image_generator');
$this->configFactory = $config_factory;
$this->twig = $twig;
$this->renderer = $renderer;
$this->token = $token;
$this->httpClient = $http_client;
}
/**
* Generate preview image without saving (for real-time preview).
*/
public function generatePreviewImage($poster_entity_id, array $custom_tokens = []) {
try {
/** @var \Drupal\poster_generator\Entity\PosterEntity $poster_entity */
$poster_entity = $this->entityTypeManager->getStorage('poster_entity')->load($poster_entity_id);
if (!$poster_entity) {
$this->logger->error('Poster entity with ID @id not found for preview.', ['@id' => $poster_entity_id]);
return NULL;
}
$this->logger->info('Generating preview image for poster @poster_id (NO SAVE)', [
'@poster_id' => $poster_entity_id,
]);
$html_template = $poster_entity->getHTML();
$css_template = $poster_entity->getCSS();
// Prepare image tokens
$image_tokens = $this->prepareImageTokens($poster_entity);
// Process tokens with real-time form data
$all_tokens = array_merge($image_tokens, $custom_tokens);
$all_tokens['site_name'] = \Drupal::config('system.site')->get('name');
$all_tokens['date'] = \Drupal::service('date.formatter')->format(time(), 'medium');
// Process Drupal tokens first
if (!empty($custom_tokens)) {
$html_template = $this->processTokensInTemplate($html_template, $custom_tokens);
$css_template = $this->processTokensInTemplate($css_template, $custom_tokens);
}
// Replace image tokens in templates
$html_template = $this->replaceImageTokens($html_template, $image_tokens);
$css_template = $this->replaceImageTokens($css_template, $image_tokens);
// Render templates with Twig context
$html = $this->renderTwigTemplate($html_template, $all_tokens);
$css = $this->renderTwigTemplate($css_template, $all_tokens);
// Sanitize content
$html = $this->sanitizeContent($html);
$css = $this->sanitizeContent($css);
// Call API to generate preview (returns temporary URL)
$api_image_url = $this->callPosterGenerationApi($html, $css);
if ($api_image_url) {
$this->logger->info('Preview image generated successfully: @url', ['@url' => $api_image_url]);
return $api_image_url; // Return direct API URL for preview (no saving)
} else {
$this->logger->error('Failed to generate preview image via API');
}
return NULL;
}
catch (\Exception $e) {
$this->logger->error('Error generating preview image: @error', ['@error' => $e->getMessage()]);
return NULL;
}
}
/**
* Generate poster image and save to dynamic image media first, then reuse file.
*/
public function generatePosterImage($poster_entity_id, array $custom_tokens = [], $entity_id = NULL) {
try {
/** @var \Drupal\poster_generator\Entity\PosterEntity $poster_entity */
$poster_entity = $this->entityTypeManager->getStorage('poster_entity')->load($poster_entity_id);
if (!$poster_entity) {
$this->logger->error('Poster entity with ID @id not found.', ['@id' => $poster_entity_id]);
return NULL;
}
$this->logger->info('Starting poster generation for poster @poster_id, entity @entity_id', [
'@poster_id' => $poster_entity_id,
'@entity_id' => $entity_id ?: 'NULL',
]);
$content_type = $poster_entity->getContentType();
$html_template = $poster_entity->getHTML();
$css_template = $poster_entity->getCSS();
$entity = NULL;
if ($entity_id) {
$entity_type = 'node';
try {
$entity = $this->entityTypeManager->getStorage($entity_type)->load($entity_id);
}
catch (\Exception $e) {
$this->logger->warning('Failed to load entity with ID @id: @error', [
'@id' => $entity_id,
'@error' => $e->getMessage(),
]);
}
if ($entity && $entity->getEntityTypeId() === 'node') {
if ($entity->bundle() !== $content_type) {
$this->logger->warning('Entity with ID @id is of type @actual_type but @expected_type was expected.', [
'@id' => $entity_id,
'@actual_type' => $entity->bundle(),
'@expected_type' => $content_type,
]);
$entity = NULL;
}
}
}
if ($entity) {
$entity_type = $entity->getEntityTypeId();
$token_data = [$entity_type => $entity];
$html_template = $this->processTokens($html_template, $token_data);
$css_template = $this->processTokens($css_template, $token_data);
}
$image_tokens = $this->prepareImageTokens($poster_entity);
$context = array_merge($custom_tokens, $image_tokens);
$context['site_name'] = \Drupal::config('system.site')->get('name');
$context['date'] = \Drupal::service('date.formatter')->format(time(), 'medium');
$html_template = $this->replaceImageTokens($html_template, $image_tokens);
$css_template = $this->replaceImageTokens($css_template, $image_tokens);
$html = $this->renderTwigTemplate($html_template, $context);
$css = $this->renderTwigTemplate($css_template, $context);
$html = $this->sanitizeContent($html);
$css = $this->sanitizeContent($css);
$api_image_url = $this->callPosterGenerationApi($html, $css);
if ($api_image_url) {
$this->logger->info('API generated image at: @api_url', ['@api_url' => $api_image_url]);
// STEP 1: Create dynamic image media first
$dynamic_media_entity = $this->createDynamicImageMedia($poster_entity_id, $entity_id, $api_image_url);
if ($dynamic_media_entity) {
$this->logger->info('Created dynamic image media @media_id', [
'@media_id' => $dynamic_media_entity->id(),
]);
// STEP 2: Reuse the file for target field if entity and target field exist
if ($entity && $poster_entity->getTargetField()) {
$result = $this->reuseFileForTargetField($entity, $poster_entity->getTargetField(), $dynamic_media_entity);
$this->logger->notice('Reused file for target field @field: @result', [
'@field' => $poster_entity->getTargetField(),
'@result' => $result ? 'SUCCESS' : 'FAILED',
]);
}
$public_url = $this->getImageUrlFromMedia($dynamic_media_entity);
if ($public_url) {
$this->logger->info('Successfully generated and saved dynamic image. Public URL: @url', ['@url' => $public_url]);
return $public_url;
} else {
$this->logger->error('Failed to get public URL from dynamic media entity @media_id', [
'@media_id' => $dynamic_media_entity->id(),
]);
}
} else {
$this->logger->error('Failed to create dynamic image media entity');
}
} else {
$this->logger->error('API failed to generate poster image');
}
return NULL;
}
catch (\Exception $e) {
$this->logger->error('Error generating poster: @error', ['@error' => $e->getMessage()]);
return NULL;
}
}
/**
* Process tokens in template with real data for previews.
*/
protected function processTokensInTemplate($template, array $custom_tokens) {
if (empty($template) || empty($custom_tokens)) {
return $template;
}
// Replace Drupal tokens manually for preview
foreach ($custom_tokens as $key => $value) {
if (strpos($key, 'node:') === 0) {
// Direct node token replacement
$template = str_replace('[' . $key . ']', $value, $template);
} else {
// Convert field tokens to node format
$template = str_replace('[node:title]', $custom_tokens['title'] ?? '', $template);
$template = str_replace('[node:body]', $custom_tokens['body'] ?? '', $template);
// Convert custom field tokens
if (strpos($key, 'field_') !== 0 && $key !== 'title' && $key !== 'body') {
$template = str_replace('[node:field_' . $key . ']', $value, $template);
}
}
}
return $template;
}
/**
* Create dynamic image media entity (always save generated images here first).
*/
protected function createDynamicImageMedia($poster_entity_id, $entity_id, $api_image_url) {
try {
$this->logger->info('Creating dynamic image media for poster @poster_id, entity @entity_id', [
'@poster_id' => $poster_entity_id,
'@entity_id' => $entity_id,
]);
$file = $this->downloadAndSaveImageFile($api_image_url, $entity_id);
if (!$file) {
$this->logger->error('Failed to create file for dynamic image');
return NULL;
}
$this->logger->info('Created file @file_id for dynamic image', [
'@file_id' => $file->id(),
]);
// Check if we have the dynamic_image media type
$media_type_storage = $this->entityTypeManager->getStorage('media_type');
$available_types = $media_type_storage->loadMultiple();
$media_bundle = 'dynamic_image';
$image_field_name = 'field_dynamic_image';
// Check if we have the dynamic_image media type
if (isset($available_types['dynamic_image'])) {
$bundle_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('media', 'dynamic_image');
foreach ($bundle_fields as $field_name => $field_definition) {
if ($field_definition->getType() === 'image' && !$field_definition->getFieldStorageDefinition()->isBaseField()) {
$image_field_name = $field_name;
break;
}
}
$this->logger->info('Using dynamic_image media type with field @field for dynamic image', ['@field' => $image_field_name]);
} else {
// Fallback to image media type if dynamic_image doesn't exist
if (isset($available_types['image'])) {
$media_bundle = 'image';
$image_field_name = 'field_media_image';
$bundle_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('media', 'image');
foreach ($bundle_fields as $field_name => $field_definition) {
if ($field_definition->getType() === 'image' && !$field_definition->getFieldStorageDefinition()->isBaseField()) {
$image_field_name = $field_name;
break;
}
}
$this->logger->info('dynamic_image media type not found, using image media type with field @field', ['@field' => $image_field_name]);
} else {
// Use first available media type as last resort
$first_type = reset($available_types);
if ($first_type) {
$media_bundle = $first_type->id();
$bundle_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('media', $media_bundle);
foreach ($bundle_fields as $field_name => $field_definition) {
if ($field_definition->getType() === 'image' && !$field_definition->getFieldStorageDefinition()->isBaseField()) {
$image_field_name = $field_name;
break;
}
}
}
$this->logger->warning('No dynamic_image or image media types found, using @bundle with field @field', [
'@bundle' => $media_bundle,
'@field' => $image_field_name,
]);
}
}
// Create dynamic image media entity with proper metadata
$media_data = [
'bundle' => $media_bundle,
'name' => 'Dynamic Image - ' . ($entity_id ? "Entity $entity_id" : 'Preview') . ' - ' . date('Y-m-d H:i:s'),
$image_field_name => [
'target_id' => $file->id(),
'alt' => 'Generated dynamic image - ' . date('Y-m-d H:i:s'),
],
'uid' => \Drupal::currentUser()->id(),
];
// Add custom fields if they exist for dynamic_image media type
if ($media_bundle === 'dynamic_image') {
$bundle_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('media', 'dynamic_image');
// Add template reference if field exists
if (isset($bundle_fields['field_template_id'])) {
$media_data['field_template_id'] = $poster_entity_id;
}
// Add source entity reference if field exists
if (isset($bundle_fields['field_source_entity']) && $entity_id) {
$media_data['field_source_entity'] = $entity_id;
}
}
$media = Media::create($media_data);
$media->save();
$this->logger->notice('Created dynamic image media entity @media_id (bundle: @bundle) for poster @poster_id and entity @entity_id with file @file_id', [
'@media_id' => $media->id(),
'@bundle' => $media_bundle,
'@poster_id' => $poster_entity_id,
'@entity_id' => $entity_id,
'@file_id' => $file->id(),
]);
return $media;
}
catch (\Exception $e) {
$this->logger->error('Error creating dynamic image media: @error', ['@error' => $e->getMessage()]);
return NULL;
}
}
/**
* Reuse file from dynamic image media for target field.
*/
protected function reuseFileForTargetField(EntityInterface $entity, $field_name, Media $source_media) {
try {
if (!$entity->hasField($field_name)) {
$this->logger->error('Entity @type @id does not have field @field.', [
'@type' => $entity->getEntityTypeId(),
'@id' => $entity->id(),
'@field' => $field_name,
]);
return FALSE;
}
$field_definition = $entity->getFieldDefinition($field_name);
$field_type = $field_definition->getType();
$this->logger->info('Reusing file from dynamic media @media_id for @field_type field @field on entity @entity_id', [
'@media_id' => $source_media->id(),
'@field_type' => $field_type,
'@field' => $field_name,
'@entity_id' => $entity->id(),
]);
if ($field_type === 'image') {
// For image fields, reuse the file directly
$source_image_field = $this->getImageFieldFromMedia($source_media);
if ($source_image_field) {
$file_id = $source_image_field->target_id;
$alt_text = $source_image_field->alt ?: 'Generated dynamic image';
$entity->set($field_name, [
'target_id' => $file_id,
'alt' => $alt_text,
]);
$this->logger->info('Reused file @file_id for image field @field', [
'@file_id' => $file_id,
'@field' => $field_name,
]);
} else {
$this->logger->error('No valid image field found in source media @media_id', [
'@media_id' => $source_media->id(),
]);
return FALSE;
}
}
elseif ($field_type === 'entity_reference' && $field_definition->getSetting('target_type') === 'media') {
// For media reference fields, reference the media entity directly
$entity->set($field_name, $source_media->id());
$this->logger->info('Reused media @media_id for media reference field @field', [
'@media_id' => $source_media->id(),
'@field' => $field_name,
]);
}
else {
$this->logger->error('Unsupported field type @type for field @field.', [
'@type' => $field_type,
'@field' => $field_name,
]);
return FALSE;
}
$entity->save();
$this->logger->notice('Successfully reused dynamic image file for @entity_type/@id field @field.', [
'@entity_type' => $entity->getEntityTypeId(),
'@id' => $entity->id(),
'@field' => $field_name,
]);
return TRUE;
}
catch (\Exception $e) {
$this->logger->error('Error reusing file for target field: @error', ['@error' => $e->getMessage()]);
return FALSE;
}
}
/**
* Get image field from media entity (helper method).
*/
protected function getImageFieldFromMedia(Media $media) {
$media_bundle = $media->bundle();
$possible_fields = [];
if ($media_bundle === 'dynamic_image') {
$possible_fields = ['field_dynamic_image', 'field_media_image', 'field_image'];
} elseif ($media_bundle === 'poster') {
$possible_fields = ['field_poster_image', 'field_media_image', 'field_image'];
} elseif ($media_bundle === 'image') {
$possible_fields = ['field_image', 'field_media_image', 'field_poster_image'];
} else {
$possible_fields = ['field_media_image', 'field_image', 'field_poster_image', 'field_dynamic_image'];
}
foreach ($possible_fields as $field_name) {
if ($media->hasField($field_name) && !$media->get($field_name)->isEmpty()) {
return $media->get($field_name);
}
}
return NULL;
}
/**
* Get image URL from media entity with dynamic field detection.
*/
protected function getImageUrlFromMedia(Media $media) {
try {
$media_bundle = $media->bundle();
$this->logger->info('Getting image URL from media @media_id (bundle: @bundle)', [
'@media_id' => $media->id(),
'@bundle' => $media_bundle,
]);
// Try different possible image field names based on bundle
$possible_fields = [];
if ($media_bundle === 'dynamic_image') {
$possible_fields = ['field_dynamic_image', 'field_media_image', 'field_image'];
} elseif ($media_bundle === 'poster') {
$possible_fields = ['field_poster_image', 'field_media_image', 'field_image'];
} elseif ($media_bundle === 'image') {
$possible_fields = ['field_image', 'field_media_image', 'field_poster_image'];
} else {
// For other bundles, try common field names
$possible_fields = ['field_media_image', 'field_image', 'field_poster_image', 'field_dynamic_image'];
}
$image_field = NULL;
$used_field = NULL;
foreach ($possible_fields as $field_name) {
if ($media->hasField($field_name) && !$media->get($field_name)->isEmpty()) {
$image_field = $media->get($field_name);
$used_field = $field_name;
break;
}
}
if (!$image_field) {
// Log all available fields for debugging
$available_fields = [];
foreach ($media->getFieldDefinitions() as $field_name => $field_definition) {
if ($field_definition->getType() === 'image') {
$available_fields[] = $field_name;
}
}
$this->logger->error('No image field found in media @media_id (bundle: @bundle). Available image fields: @fields', [
'@media_id' => $media->id(),
'@bundle' => $media_bundle,
'@fields' => implode(', ', $available_fields),
]);
return NULL;
}
$this->logger->info('Using image field @field from media @media_id', [
'@field' => $used_field,
'@media_id' => $media->id(),
]);
$file = $image_field->entity;
if ($file) {
$file_url = $file->createFileUrl();
if ($file_url && strpos($file_url, 'http') !== 0) {
$request = \Drupal::request();
$base_url = $request->getSchemeAndHttpHost();
$file_url = $base_url . $file_url;
}
$this->logger->info('Generated public URL: @url for file @file_id', [
'@url' => $file_url,
'@file_id' => $file->id(),
]);
return $file_url;
}
return NULL;
}
catch (\Exception $e) {
$this->logger->error('Error getting image URL from media: @error', ['@error' => $e->getMessage()]);
return NULL;
}
}
/**
* Save media to entity field with dynamic field detection.
*/
protected function saveMediaToEntity(EntityInterface $entity, $field_name, Media $media) {
try {
if (!$entity->hasField($field_name)) {
$this->logger->error('Entity @type @id does not have field @field.', [
'@type' => $entity->getEntityTypeId(),
'@id' => $entity->id(),
'@field' => $field_name,
]);
return FALSE;
}
$field_definition = $entity->getFieldDefinition($field_name);
$field_type = $field_definition->getType();
$this->logger->info('Saving media @media_id to @field_type field @field on entity @entity_id', [
'@media_id' => $media->id(),
'@field_type' => $field_type,
'@field' => $field_name,
'@entity_id' => $entity->id(),
]);
if ($field_type === 'image') {
// For image fields, reference the file directly from the media
$media_bundle = $media->bundle();
$possible_fields = [];
if ($media_bundle === 'dynamic_image') {
$possible_fields = ['field_dynamic_image', 'field_media_image', 'field_image'];
} elseif ($media_bundle === 'poster') {
$possible_fields = ['field_poster_image', 'field_media_image', 'field_image'];
} elseif ($media_bundle === 'image') {
$possible_fields = ['field_image', 'field_media_image', 'field_poster_image'];
} else {
$possible_fields = ['field_media_image', 'field_image', 'field_poster_image', 'field_dynamic_image'];
}
$image_field = NULL;
$used_media_field = NULL;
foreach ($possible_fields as $media_field_name) {
if ($media->hasField($media_field_name) && !$media->get($media_field_name)->isEmpty()) {
$image_field = $media->get($media_field_name);
$used_media_field = $media_field_name;
break;
}
}
if (!$image_field) {
$this->logger->error('No valid image field found in media @media_id for bundle @bundle', [
'@media_id' => $media->id(),
'@bundle' => $media_bundle,
]);
return FALSE;
}
$file_id = $image_field->target_id;
$alt_text = $image_field->alt ?: 'Generated dynamic image';
$entity->set($field_name, [
'target_id' => $file_id,
'alt' => $alt_text,
]);
$this->logger->info('Saved media file @file_id to image field @field on entity @entity_id', [
'@file_id' => $file_id,
'@field' => $field_name,
'@entity_id' => $entity->id(),
]);
}
elseif ($field_type === 'entity_reference' && $field_definition->getSetting('target_type') === 'media') {
// For media reference fields, reference the media entity directly
$entity->set($field_name, $media->id());
$this->logger->info('Saved media @media_id to media reference field @field on entity @entity_id', [
'@media_id' => $media->id(),
'@field' => $field_name,
'@entity_id' => $entity->id(),
]);
}
else {
$this->logger->error('Unsupported field type @type for field @field.', [
'@type' => $field_type,
'@field' => $field_name,
]);
return FALSE;
}
$entity->save();
$this->logger->notice('Successfully saved media @media_id to @entity_type/@id field @field.', [
'@media_id' => $media->id(),
'@entity_type' => $entity->getEntityTypeId(),
'@id' => $entity->id(),
'@field' => $field_name,
]);
return TRUE;
}
catch (\Exception $e) {
$this->logger->error('Error saving media to entity field: @error', ['@error' => $e->getMessage()]);
return FALSE;
}
}
/**
* Calls the API to generate a poster image.
*/
protected function callPosterGenerationApi($html, $css, $options = []) {
try {
// Get configuration settings
$config = $this->configFactory->get('dynamic_image_generator.settings');
$api_provider = $config->get('api_provider') ?: 'htmlcsstoimage';
// If using inbuilt generator, call that service instead
if ($api_provider === 'inbuilt') {
if (\Drupal::hasService('image_creating_engine.generator')) {
$generator = \Drupal::service('image_creating_engine.generator');
$result = $generator->generateImage($html, $css, $options);
// Return the result directly - no need to download anything
return $result;
} else {
$this->logger->error('Inbuilt image generator service not available. Make sure the image_creating_engine module is enabled.');
return NULL;
}
}
// For external API, validate credentials and make API call
$api_user_id = $config->get('api_user_id');
$api_key = $config->get('api_key');
$api_endpoint = $config->get('api_endpoint') ?: 'https://hcti.io/v1/image';
if (empty($api_user_id) || empty($api_key)) {
$this->logger->error('API credentials not configured. Please set API User ID and API Key in the module settings.');
return NULL;
}
// Default options
$width = $options['width'] ?? $config->get('default_width') ?? 1200;
$height = $options['height'] ?? $config->get('default_height') ?? 630;
$data = [
'html' => $html,
'css' => $css,
'width' => $width,
'height' => $height,
];
$format = $config->get('image_format');
if ($format && $format !== 'png') {
$data['format'] = $format;
if (in_array($format, ['jpg', 'jpeg', 'webp']) && $config->get('quality')) {
$data['quality'] = (int) $config->get('quality');
}
}
if ($config->get('debug_mode')) {
$this->logger->info('Sending API request to @endpoint with data: @data', [
'@endpoint' => $api_endpoint,
'@data' => json_encode($data, JSON_PRETTY_PRINT),
]);
}
$attempt = 0;
$max_attempts = ($config->get('retry_attempts') ?: 2) + 1;
while ($attempt < $max_attempts) {
$attempt++;
try {
$start_time = microtime(TRUE);
$response = $this->httpClient->request('POST', $api_endpoint, [
'form_params' => $data,
'auth' => [$api_user_id, $api_key],
'timeout' => $config->get('request_timeout') ?: 60,
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
'User-Agent' => 'Drupal-DynamicImageGenerator/1.0',
],
]);
$response_time = round((microtime(TRUE) - $start_time) * 1000, 2);
if ($response->getStatusCode() === 200) {
$result = json_decode($response->getBody(), TRUE);
if ($config->get('debug_mode')) {
$this->logger->info('API response received in @time ms: @response', [
'@time' => $response_time,
'@response' => json_encode($result, JSON_PRETTY_PRINT),
]);
}
if (isset($result['url'])) {
$this->logger->info('Successfully generated image in @time ms: @url', [
'@time' => $response_time,
'@url' => $result['url'],
]);
return $result['url'];
} else {
$this->logger->error('API did not return image URL. Response: @response', [
'@response' => substr($response->getBody(), 0, 500),
]);
return NULL;
}
}
}
catch (\Exception $e) {
$this->logger->error('API call failed on attempt @attempt/@max: @error', [
'@attempt' => $attempt,
'@max' => $max_attempts,
'@error' => $e->getMessage(),
]);
if ($attempt < $max_attempts) {
sleep(1);
}
}
}
$this->logger->error('All @max API call attempts failed', [
'@max' => $max_attempts,
]);
return NULL;
}
catch (\Exception $e) {
$this->logger->error('Error calling HTML/CSS to Image API: @error', ['@error' => $e->getMessage()]);
return NULL;
}
}
/**
* Downloads and saves an image from a URL.
*
* @param string|array $image_url_or_data
* The URL of the image to download or array with image data.
* @param int|null $entity_id
* Optional entity ID for naming.
*
* @return \Drupal\file\FileInterface|null
* The created file entity or NULL on failure.
*/
protected function downloadAndSaveImageFile($image_url_or_data, $entity_id = NULL) {
try {
// Check if this is already a processed file from inbuilt generator
if (is_array($image_url_or_data) && isset($image_url_or_data['file']) && $image_url_or_data['file'] instanceof \Drupal\file\FileInterface) {
$this->logger->info('Using pre-created file entity from inbuilt generator');
return $image_url_or_data['file'];
}
// Skip downloading if not a URL string
if (!is_string($image_url_or_data)) {
$this->logger->error('Invalid image URL format: ' . gettype($image_url_or_data));
return NULL;
}
$image_url = $image_url_or_data;
// Create the directory
$directory = 'public://dynamic_image_generator/generated';
$this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
// Try to get the file contents
$file_data = @file_get_contents($image_url);
if ($file_data === FALSE) {
$error = error_get_last();
$this->logger->error('Failed to download image from URL @url: @error', [
'@url' => $image_url,
'@error' => isset($error['message']) ? $error['message'] : 'Unknown error',
]);
return NULL;
}
// Create a meaningful filename
$entity_suffix = $entity_id ? "_entity_{$entity_id}" : '';
$file_name = 'dynamic_image' . $entity_suffix . '_' . md5($image_url . time()) . '.png';
$uri = $directory . '/' . $file_name;
// Save the file data to disk first
if (!file_put_contents($uri, $file_data)) {
$this->logger->error('Failed to write file data to @uri', ['@uri' => $uri]);
return NULL;
}
// Create a managed file entity
$file = File::create([
'uri' => $uri,
'filename' => $file_name,
'status' => 1, // FILE_STATUS_PERMANENT = 1
'uid' => \Drupal::currentUser()->id(),
]);
$file->save();
$this->logger->info('Successfully downloaded and saved image to @uri', ['@uri' => $uri]);
return $file;
}
catch (\Exception $e) {
$this->logger->error('Failed to download and save image file: @error', ['@error' => $e->getMessage()]);
return NULL;
}
}
/**
* Downloads an image from a URL and creates a file entity.
* Only used for external API results, not for inbuilt generator.
*/
protected function downloadImageFile($image_url) {
try {
// IMPORTANT: Check if this is already a processed file from inbuilt generator
if (is_array($image_url) && isset($image_url['file']) && $image_url['file'] instanceof \Drupal\file\FileInterface) {
$this->logger->info('Using pre-created file entity from inbuilt generator');
return $image_url['file'];
}
// Skip downloading if not a URL string
if (!is_string($image_url)) {
$this->logger->error('Invalid image URL format: ' . gettype($image_url));
return NULL;
}
// Regular download process for URL strings
$directory = 'public://dynamic_image_generator/generated';
$this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
$file_data = file_get_contents($image_url);
if ($file_data === FALSE) {
$this->logger->error('Failed to download image from URL @url', ['@url' => $image_url]);
return NULL;
}
$file_name = 'dynamic_image_' . md5($image_url . time()) . '.png';
$uri = $directory . '/' . $file_name;
$file = file_save_data($file_data, $uri, FileSystemInterface::EXISTS_REPLACE);
if (!$file) {
$this->logger->error('Failed to save downloaded image data to @uri', ['@uri' => $uri]);
return NULL;
}
return $file;
}
catch (\Exception $e) {
$this->logger->error('Failed to download image file: @error', ['@error' => $e->getMessage()]);
return NULL;
}
}
/**
* Creates a media entity from a generated image.
*/
protected function createMediaEntity($image_data, $template_id, $entity_id = NULL) {
try {
$template = $this->entityTypeManager->getStorage('poster_entity')->load($template_id);
if (!$template) {
$this->logger->error('Failed to load template with ID @id', ['@id' => $template_id]);
return NULL;
}
// Handle different formats of $image_data based on the source
$file = NULL;
// Check if we received data from the inbuilt generator (array with file entity)
if (is_array($image_data) && isset($image_data['file']) && $image_data['file'] instanceof \Drupal\file\FileInterface) {
// For inbuilt generator - use the file entity directly, no download needed
$file = $image_data['file'];
$this->logger->info('Using pre-created file entity from inbuilt generator');
}
// If we got a URL string from an external API, download it
elseif (is_string($image_data)) {
// For external API - download the file from URL
$file = $this->downloadImageFile($image_data);
if (!$file) {
$this->logger->error('Failed to download image from URL @url', ['@url' => $image_data]);
return NULL;
}
}
else {
$this->logger->error('Invalid image data format received: ' . gettype($image_data));
return NULL;
}
// Create media entity with the file
$media_bundle = 'dynamic_image';
$image_field_name = 'field_dynamic_image';
// Check if we have the dynamic_image media type
$media_type_storage = $this->entityTypeManager->getStorage('media_type');
$available_types = $media_type_storage->loadMultiple();
if (isset($available_types['dynamic_image'])) {
$bundle_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('media', 'dynamic_image');
foreach ($bundle_fields as $field_name => $field_definition) {
if ($field_definition->getType() === 'image' && !$field_definition->getFieldStorageDefinition()->isBaseField()) {
$image_field_name = $field_name;
break;
}
}
$this->logger->info('Using dynamic_image media type with field @field for dynamic image', ['@field' => $image_field_name]);
} else {
// Fallback to image media type if dynamic_image doesn't exist
if (isset($available_types['image'])) {
$media_bundle = 'image';
$image_field_name = 'field_media_image';
$bundle_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('media', 'image');
foreach ($bundle_fields as $field_name => $field_definition) {
if ($field_definition->getType() === 'image' && !$field_definition->getFieldStorageDefinition()->isBaseField()) {
$image_field_name = $field_name;
break;
}
}
$this->logger->info('dynamic_image media type not found, using image media type with field @field', ['@field' => $image_field_name]);
} else {
// Use first available media type as last resort
$first_type = reset($available_types);
if ($first_type) {
$media_bundle = $first_type->id();
$bundle_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('media', $media_bundle);
foreach ($bundle_fields as $field_name => $field_definition) {
if ($field_definition->getType() === 'image' && !$field_definition->getFieldStorageDefinition()->isBaseField()) {
$image_field_name = $field_name;
break;
}
}
}
$this->logger->warning('No dynamic_image or image media types found, using @bundle with field @field', [
'@bundle' => $media_bundle,
'@field' => $image_field_name,
]);
}
}
// Create dynamic image media entity with proper metadata
$media_data = [
'bundle' => $media_bundle,
'name' => 'Dynamic Image - ' . ($entity_id ? "Entity $entity_id" : 'Preview') . ' - ' . date('Y-m-d H:i:s'),
$image_field_name => [
'target_id' => $file->id(),
'alt' => 'Generated dynamic image - ' . date('Y-m-d H:i:s'),
],
'uid' => \Drupal::currentUser()->id(),
];
// Add custom fields if they exist for dynamic_image media type
if ($media_bundle === 'dynamic_image') {
$bundle_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('media', 'dynamic_image');
// Add template reference if field exists
if (isset($bundle_fields['field_template_id'])) {
$media_data['field_template_id'] = $poster_entity_id;
}
// Add source entity reference if field exists
if (isset($bundle_fields['field_source_entity']) && $entity_id) {
$media_data['field_source_entity'] = $entity_id;
}
}
$media = Media::create($media_data);
$media->save();
$this->logger->notice('Created dynamic image media entity @media_id (bundle: @bundle) for poster @poster_id and entity @entity_id with file @file_id', [
'@media_id' => $media->id(),
'@bundle' => $media_bundle,
'@poster_id' => $poster_entity_id,
'@entity_id' => $entity_id,
'@file_id' => $file->id(),
]);
return $media;
}
catch (\Exception $e) {
$this->logger->error('Failed to create dynamic image media entity: @error', ['@error' => $e->getMessage()]);
return NULL;
}
}
/**
* Process Drupal tokens in the given text.
*/
protected function processTokens($text, array $data = []) {
if (empty($text) || empty($data)) {
return $text;
}
return $this->token->replace($text, $data, [
'clear' => TRUE,
'sanitize' => FALSE,
]);
}
/**
* Prepare image tokens from a poster entity's images.
*/
protected function prepareImageTokens($poster_entity) {
$image_tokens = [];
$images = $poster_entity->getBackgroundImages();
foreach ($images as $index => $file) {
if ($file instanceof FileInterface) {
$token_name = 'image_' . ($index + 1);
$image_tokens[$token_name] = $file->createFileUrl();
}
}
if (!empty($images)) {
$random_index = array_rand($images);
$random_file = $images[$random_index];
if ($random_file instanceof FileInterface) {
$image_tokens['image_random'] = $random_file->createFileUrl();
}
}
return $image_tokens;
}
/**
* Replace image tokens in a template.
*/
protected function replaceImageTokens($template, array $image_tokens) {
foreach ($image_tokens as $token => $url) {
$template = str_replace('[' . $token . ']', $url, $template);
}
return $template;
}
/**
* Render a string as a Twig template with the given context.
*/
protected function renderTwigTemplate($template_string, array $context = []) {
try {
$template_hash = md5($template_string . microtime());
$template_name = "poster_template_{$template_hash}";
$template_string = "{% autoescape false %}{$template_string}{% endautoescape %}";
$template = $this->twig->createTemplate($template_string, $template_name);
return $template->render($context);
}
catch (TwigError $e) {
$this->logger->error('Twig template error: @error', ['@error' => $e->getMessage()]);
return $template_string;
}
}
/**
* Sanitize content to make sure it's properly formatted.
*/
protected function sanitizeContent($content) {
$content = trim($content);
$content = str_replace(["\r\n", "\r"], "\n", $content);
return $content;
}
}