dynamic_image_generator-1.0.x-dev/image_creating_engine/src/Service/WkhtmlImageGenerator.php
image_creating_engine/src/Service/WkhtmlImageGenerator.php
<?php
namespace Drupal\image_creating_engine\Service;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\file\Entity\File;
use Drupal\file\FileInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
/**
* Service for generating images using wkhtmltoimage.
*/
class WkhtmlImageGenerator {
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The logger service.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The private tempstore.
*
* @var \Drupal\Core\TempStore\PrivateTempStore
*/
protected $tempStore;
/**
* The file URL generator service.
*
* @var \Drupal\Core\File\FileUrlGeneratorInterface
*/
protected $fileUrlGenerator;
/**
* Constructs a WkhtmlImageGenerator object.
*/
public function __construct(
FileSystemInterface $file_system,
EntityTypeManagerInterface $entity_type_manager,
LoggerChannelFactoryInterface $logger_factory,
ConfigFactoryInterface $config_factory,
RendererInterface $renderer,
PrivateTempStoreFactory $temp_store_factory,
FileUrlGeneratorInterface $file_url_generator
) {
$this->fileSystem = $file_system;
$this->entityTypeManager = $entity_type_manager;
$this->logger = $logger_factory->get('wkhtml_image_generator');
$this->configFactory = $config_factory;
$this->renderer = $renderer;
$this->tempStore = $temp_store_factory->get('wkhtml_image_generator');
$this->fileUrlGenerator = $file_url_generator;
}
/**
* Generate an image from HTML and CSS.
*
* @param string $html
* The HTML content.
* @param string $css
* The CSS content.
* @param array $options
* An array of options for image generation.
*
* @return array|null
* An array containing the file URI and File entity, or NULL on failure.
*/
public function generateImage($html, $css, array $options = []) {
try {
// Check dependencies first
if (!$this->checkDependencies()) {
return NULL;
}
// Set up options for image generation
$width = $options['width'] ?? 1200;
$height = $options['height'] ?? 630;
$format = strtolower($options['format'] ?? 'png');
$quality = $options['quality'] ?? 94;
// Make sure format is valid
if (!in_array($format, ['png', 'jpg', 'jpeg'])) {
$format = 'png';
}
// Set up the output file path
$output_dir = 'public://dynamic_image_generator/generated';
$this->fileSystem->prepareDirectory($output_dir, FileSystemInterface::CREATE_DIRECTORY);
$output_file = $output_dir . '/image_' . uniqid() . '.' . $format;
$output_path = $this->fileSystem->realpath($output_file);
// Generate the image using wkhtmltoimage
$result = $this->generateImageWithWkhtml($html, $css, $output_path, $width, $height, $format, $quality);
if ($result && file_exists($output_path) && filesize($output_path) > 0) {
// Create a file entity for the generated image
$file = File::create([
'uri' => $output_file,
'uid' => \Drupal::currentUser()->id(),
'status' => FileInterface::STATUS_PERMANENT,
]);
$file->save();
$this->logger->info('Successfully generated image at: @uri with dimensions @widthx@height, size: @size bytes', [
'@uri' => $output_file,
'@width' => $width,
'@height' => $height,
'@size' => filesize($output_path),
]);
// Return both the file URI and the File entity
return [
'uri' => $output_file,
'file' => $file,
'url' => $this->fileUrlGenerator->generateAbsoluteString($output_file),
'width' => $width,
'height' => $height,
'format' => $format,
'generator' => 'wkhtmltoimage',
];
} else {
$this->logger->error('Failed to generate image with wkhtmltoimage');
}
return NULL;
}
catch (\Exception $e) {
$this->logger->error('Error generating image: @error', ['@error' => $e->getMessage()]);
return NULL;
}
}
/**
* Check if wkhtmltoimage is available.
*
* @return bool
* TRUE if wkhtmltoimage is available, FALSE otherwise.
*/
protected function checkDependencies() {
$wkhtml_path = $this->findWkhtmlPath();
if (!$wkhtml_path) {
$this->logger->error('wkhtmltoimage is not installed or not in PATH. Install with: sudo apt-get install wkhtmltopdf');
return FALSE;
}
// Test if wkhtmltoimage can run
$test_command = escapeshellarg($wkhtml_path) . ' --version 2>&1';
$output = shell_exec($test_command);
if (!$output || strpos($output, 'wkhtmltoimage') === FALSE) {
$this->logger->error('wkhtmltoimage test failed. Output: @output', ['@output' => $output]);
return FALSE;
}
$this->logger->info('wkhtmltoimage available at: @path, version: @version', [
'@path' => $wkhtml_path,
'@version' => trim($output),
]);
return TRUE;
}
/**
* Find wkhtmltoimage executable path.
*
* @return string|null
* Path to wkhtmltoimage executable or NULL if not found.
*/
public function findWkhtmlPath() {
$possible_paths = [
'/usr/bin/wkhtmltoimage',
'/usr/local/bin/wkhtmltoimage',
'/opt/bin/wkhtmltoimage',
'/bin/wkhtmltoimage',
];
// Check direct paths first
foreach ($possible_paths as $path) {
if (file_exists($path) && is_executable($path)) {
return $path;
}
}
// Try which command
$which_result = trim(shell_exec('which wkhtmltoimage 2>/dev/null') ?: '');
if ($which_result && file_exists($which_result) && is_executable($which_result)) {
return $which_result;
}
return NULL;
}
/**
* Generate image using wkhtmltoimage.
*
* @param string $html
* The HTML content.
* @param string $css
* The CSS content.
* @param string $output_path
* Path to save the image.
* @param int $width
* Image width.
* @param int $height
* Image height.
* @param string $format
* Image format.
* @param int $quality
* Image quality (for JPEG).
*
* @return bool
* TRUE if successful, FALSE otherwise.
*/
protected function generateImageWithWkhtml($html, $css, $output_path, $width, $height, $format, $quality) {
try {
$wkhtml_path = $this->findWkhtmlPath();
if (!$wkhtml_path) {
throw new \Exception('wkhtmltoimage not found');
}
// Create temporary HTML file
$temp_html = tempnam(sys_get_temp_dir(), 'wkhtml_') . '.html';
$full_html = $this->buildCompleteHtml($html, $css);
file_put_contents($temp_html, $full_html);
// Build wkhtmltoimage command
$command_parts = [
escapeshellarg($wkhtml_path),
'--width', (string)$width,
'--height', (string)$height,
'--format', ($format === 'jpg' ? 'jpg' : 'png'),
];
// Add quality for JPEG
if ($format === 'jpg' || $format === 'jpeg') {
$command_parts[] = '--quality';
$command_parts[] = (string)$quality;
}
// Add other useful options
$command_parts = array_merge($command_parts, [
'--enable-local-file-access',
'--disable-smart-width',
'--no-stop-slow-scripts',
'--no-pdf-compression',
'--disable-javascript', // Disable JS for security and speed
escapeshellarg($temp_html),
escapeshellarg($output_path),
]);
$command = implode(' ', $command_parts) . ' 2>&1';
$this->logger->info('Executing wkhtmltoimage command: @command', ['@command' => $command]);
// Execute command
$output = shell_exec($command);
$success = file_exists($output_path) && filesize($output_path) > 0;
if ($success) {
$this->logger->info('wkhtmltoimage generated image successfully. Size: @size bytes', [
'@size' => filesize($output_path),
]);
} else {
$this->logger->error('wkhtmltoimage failed. Output: @output', ['@output' => $output]);
}
// Clean up temporary file
if (file_exists($temp_html)) {
unlink($temp_html);
}
return $success;
} catch (\Exception $e) {
$this->logger->error('Error in wkhtmltoimage generation: @error', ['@error' => $e->getMessage()]);
// Clean up temporary file
if (isset($temp_html) && file_exists($temp_html)) {
unlink($temp_html);
}
return FALSE;
}
}
/**
* Build complete HTML document with CSS.
*
* @param string $html
* The HTML content.
* @param string $css
* The CSS content.
*
* @return string
* Complete HTML document.
*/
protected function buildCompleteHtml($html, $css) {
$full_html = '<!DOCTYPE html>';
$full_html .= '<html lang="en">';
$full_html .= '<head>';
$full_html .= '<meta charset="UTF-8">';
$full_html .= '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
$full_html .= '<style>';
$full_html .= 'body { margin: 0; padding: 20px; font-family: Arial, sans-serif; }';
$full_html .= $css;
$full_html .= '</style>';
$full_html .= '</head>';
$full_html .= '<body>';
$full_html .= $html;
$full_html .= '</body>';
$full_html .= '</html>';
return $full_html;
}
/**
* Test wkhtmltoimage installation.
*
* @return array
* Test results.
*/
public function testInstallation() {
$results = [
'wkhtml_found' => FALSE,
'wkhtml_path' => NULL,
'version' => '',
'can_generate' => FALSE,
'error_messages' => [],
];
try {
// Find wkhtmltoimage
$wkhtml_path = $this->findWkhtmlPath();
$results['wkhtml_path'] = $wkhtml_path;
$results['wkhtml_found'] = !empty($wkhtml_path);
if (!$wkhtml_path) {
$results['error_messages'][] = 'wkhtmltoimage executable not found';
return $results;
}
// Get version
$version_cmd = escapeshellarg($wkhtml_path) . ' --version 2>&1';
$version_output = shell_exec($version_cmd);
$results['version'] = trim($version_output);
// Test image generation
$test_html = '<div style="width:200px;height:100px;background:green;color:white;text-align:center;line-height:100px;font-weight:bold;">WKHTML TEST</div>';
$test_css = 'body { margin: 0; padding: 10px; }';
$temp_output = tempnam(sys_get_temp_dir(), 'wkhtml_test_') . '.png';
$test_result = $this->generateImageWithWkhtml($test_html, $test_css, $temp_output, 220, 120, 'png', 94);
if ($test_result && file_exists($temp_output) && filesize($temp_output) > 0) {
$results['can_generate'] = TRUE;
$results['test_file_size'] = filesize($temp_output);
unlink($temp_output);
} else {
$results['error_messages'][] = 'Test image generation failed';
}
} catch (\Exception $e) {
$results['error_messages'][] = 'Test failed: ' . $e->getMessage();
}
return $results;
}
}
