view_mode_crop-1.0.x-dev/src/CropImageHelper.php
src/CropImageHelper.php
<?php
namespace Drupal\view_mode_crop;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\EntityReferenceFieldItemList;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Image\ImageInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManager;
use Drupal\file\FileInterface;
use Drupal\image\Entity\ImageStyle;
use Drupal\media\MediaInterface;
use Drupal\view_mode_crop\Plugin\Field\FieldType\ViewModeCropEntityReferenceItem;
use Drupal\view_mode_crop\Plugin\Field\FieldType\ViewModeCropImageItem;
use Drupal\view_mode_crop\StreamWrapper\CropPrivateStreamWrapper;
use Drupal\view_mode_crop\StreamWrapper\CropPublicStreamWrapper;
/**
* Helper functions for cropping images.
*/
class CropImageHelper {
/**
* Generate a derivated (cropped) image.
*
* @param \Drupal\view_mode_crop\Plugin\Field\FieldType\ViewModeCropImageItem $imageItem
* THe field item.
* @param string $viewMode
* The view mode.
* @param string $derivative_uri
* The derivate uri to save to.
* @param array $crop_data
* The crop data.
*
* @return bool
* Succeeded or not.
*/
public static function createDerivate(ViewModeCropImageItem $imageItem, string $viewMode, string $derivative_uri, array $crop_data) {
/**
* @var \Drupal\Core\File\FileSystemInterface $file_system
*/
$file_system = \Drupal::service('file_system');
/**
* @var \Drupal\Core\Image\ImageFactory $image_factory
*/
$image_factory = \Drupal::service('image.factory');
if (!isset($imageItem->entity) || !$imageItem->entity instanceof FileInterface) {
return FALSE;
}
$original_uri = $imageItem->entity->_initialUri ?? $imageItem->entity->getFileUri();
if (!$original_uri || !file_exists($original_uri)) {
return FALSE;
}
// If the source file doesn't exist, return FALSE without creating folders.
$image = $image_factory->get($original_uri);
if (!$image->isValid()) {
return FALSE;
}
$is_cropped = self::cropImage($image, $viewMode, $crop_data);
// Get the folder for the final location of this image.
$directory = $file_system->dirname($derivative_uri);
// Build the destination folder tree if it doesn't already exist.
if (!$file_system->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) {
\Drupal::logger('view_mode_crop')
->error('Failed to create style directory: %directory', ['%directory' => $directory]);
return FALSE;
}
if (!$is_cropped) {
// Not cropped.
$file_system->delete($derivative_uri);
}
else {
if (!$image->save($derivative_uri)) {
if (file_exists($derivative_uri)) {
\Drupal::logger('view_mode_crop')
->error('Cached image file %destination already exists. There may be an issue with your rewrite configuration.', ['%destination' => $derivative_uri]);
}
return FALSE;
}
}
$module_handler = \Drupal::moduleHandler();
if ($module_handler->moduleExists('image')) {
image_path_flush($derivative_uri);
if ($module_handler->moduleExists('webp')) {
// If the webp module exists, we should also clear the webp generated
// files.
/**
* @var \Drupal\webp\Webp $webp
*/
$webp = \Drupal::service('webp.webp');
$webp_derivative_uri = $webp->getWebpSrcset($derivative_uri);
if (file_exists($webp_derivative_uri)) {
$file_system->delete($webp_derivative_uri);
clearstatcache(TRUE, $webp_derivative_uri);
}
$styles = ImageStyle::loadMultiple();
foreach ($styles as $style) {
$webp_derivative_uri = $webp->getWebpSrcset($style->buildUri($derivative_uri));
if (file_exists($webp_derivative_uri)) {
$file_system->delete($webp_derivative_uri);
clearstatcache(TRUE, $webp_derivative_uri);
}
}
}
}
clearstatcache(TRUE, $derivative_uri);
return TRUE;
}
/**
* Crop the given image.
*
* @param \Drupal\Core\Image\ImageInterface $image
* The image.
* @param string $view_mode
* The view mode.
* @param array $crop_data
* The crop data.
*
* @return bool
* Returns true when the image was cropped.
*/
public static function cropImage(ImageInterface $image, string $view_mode, array $crop_data) {
if (!isset($crop_data[$view_mode])) {
// View mode is not set. Fall back to default.
$view_mode = 'default';
}
if (isset($crop_data[$view_mode]) && $crop_data[$view_mode]->w >= 0 && $crop_data[$view_mode]->h >= 0) {
if (
$crop_data[$view_mode]->x == 0 &&
$crop_data[$view_mode]->y == 0 &&
$crop_data[$view_mode]->w == $image->getWidth() &&
$crop_data[$view_mode]->h == $image->getHeight()
) {
// No cropping.
return FALSE;
}
$image->crop(
$crop_data[$view_mode]->x,
$crop_data[$view_mode]->y,
$crop_data[$view_mode]->w,
$crop_data[$view_mode]->h
);
return TRUE;
}
return FALSE;
}
/**
* Get the uri to the cropped image.
*
* @param \Drupal\view_mode_crop\Plugin\Field\FieldType\ViewModeCropImageItem $imageItem
* The field item.
* @param int $delta
* The field delta.
* @param string $viewMode
* The view mode.
*
* @return string
* The uri.
*/
public static function getUri(ViewModeCropImageItem $imageItem, int $delta, string $viewMode): string {
$image_item_entity = $imageItem->getEntity();
if ($image_item_entity instanceof MediaInterface) {
$referring_item = $image_item_entity->_referringItem;
if (
$referring_item instanceof ViewModeCropEntityReferenceItem
) {
// Node.
$parent_entity = $referring_item->getEntity();
$field_name = $referring_item->getFieldDefinition()
// Node field.
->getName();
$crop_data = $referring_item->getViewModeCropData();
// Get the delta from the parent entity of the referencing field.
foreach ($referring_item->getParent() as $parent_entity_field_item_delta => $parent_entity_field_item) {
if ($parent_entity_field_item->getPropertyPath() === $referring_item->getPropertyPath()) {
$delta = $parent_entity_field_item_delta;
break;
}
}
}
else {
throw new \RuntimeException('Cannot handle ' . get_class($referring_item));
}
}
else {
$parent_entity = $imageItem->getEntity();
$field_name = $imageItem->getFieldDefinition()
->getName();
$crop_data = $imageItem->getViewModeCropData();
}
/**
* @var \Drupal\file\FileInterface $file
*/
$file = $imageItem->entity;
if (isset($file->_initialUri)) {
$file_uri = $file->_initialUri;
}
else {
$file_uri = $file->getFileUri();
}
$target = StreamWrapperManager::getTarget($file_uri);
$scheme = StreamWrapperManager::getScheme($file_uri);
if (stripos($scheme, 'crop') === 0) {
// Already in crop scheme.
return $file_uri;
}
$uri = 'crop-' . $scheme . '://' . $parent_entity->getEntityTypeId() . '/' . $parent_entity->id() . '/' . $field_name . '/' . $delta . '/' . $viewMode . '/' . $target;
if (!file_exists($uri)) {
static $generating = [];
if (!isset($generating[$uri])) {
$generating[$uri] = TRUE;
CropImageHelper::createDerivate($imageItem, $viewMode, $uri, $crop_data);
unset($generating[$uri]);
}
}
return $uri;
}
/**
* Parse the given path or uri.
*
* This returns an array containing all view mode crop properties contained
* in the path or uri.
*
* @param string $path_or_uri
* The path or uri.
*
* @return array|null
* The parsed path or uri.
*/
public static function parsePathOrUri(string $path_or_uri): ?array {
/**
* @var \Drupal\Core\StreamWrapper\PublicStream $public_stream_wrapper
*/
$public_stream_wrapper = \Drupal::service('stream_wrapper_manager')
->getViaScheme('public');
$public_directory_path = $public_stream_wrapper->getDirectoryPath();
$scheme = NULL;
if (strpos($path_or_uri, '/' . $public_directory_path . '/crop/') === 0 || strpos($path_or_uri, 'crop-public://') === 0) {
$path_prefix = '/' . $public_directory_path . '/crop/';
$path_or_uri = str_replace($path_prefix, '', '/' . StreamWrapperManager::getTarget($path_or_uri));
$scheme = 'public';
}
elseif (strpos($path_or_uri, '/system/files/crop/') === 0 || strpos($path_or_uri, 'crop-private://') === 0) {
$path_prefix = '/system/files/crop/';
$path_or_uri = StreamWrapperManager::getTarget($path_or_uri);
$scheme = 'private';
}
else {
$path_prefix = '';
}
if ($scheme === NULL) {
$rest = $path_or_uri;
$scheme = 'public';
}
else {
// Strip out path prefix.
$rest = preg_replace('|^' . preg_quote($path_prefix, '|') . '|', '', $path_or_uri);
}
// Get the image style, scheme and path.
if (substr_count($rest, '/') >= 5) {
[
$entity_type_id,
$entity_id,
$field_name,
$delta,
$view_mode,
$file,
] = explode('/', $rest, 6);
return [
'path' => $path_or_uri,
'path_prefix' => $path_prefix,
'scheme' => $scheme,
'entity_type_id' => $entity_type_id,
'entity_id' => $entity_id,
'field_name' => $field_name,
'delta' => $delta,
'view_mode' => $view_mode,
'file' => $file,
];
}
return NULL;
}
/**
* Get the crop data.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity.
* @param string $field_name
* The field name.
* @param \Drupal\view_mode_crop\ViewModeCropData[] $view_mode_crop_data
* The crop data.
*
* @return \Drupal\view_mode_crop\ViewModeCropData[]
* The crop data
*/
public static function getCropData(ContentEntityInterface $entity, string $field_name, array $view_mode_crop_data) {
/**
* @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
*/
$entity_display_repository = \Drupal::service('entity_display.repository');
$all_view_modes = $entity_display_repository->getViewModeOptionsByBundle($entity->getEntityTypeId(), $entity->bundle());
$field = $entity->get($field_name);
$entity_reference_entity_type_id = NULL;
$entity_reference_entity_type_bundles = [];
if ($field instanceof EntityReferenceFieldItemList) {
$definition = $field->getFieldDefinition();
if (($handler = $definition->getSetting('handler')) === 'default:media') {
// This references a media entity, to determine if cropping is available
// we should examine the used media view mode.
$entity_reference_entity_type_id = str_replace('default:', '', $handler);
$entity_reference_entity_type_bundles = array_keys($definition->getSetting('handler_settings')['target_bundles']);
}
}
$crop_data = [];
/**
* @var \Drupal\Core\Field\FormatterPluginManager $field_formatter_plugin_manager
*/
$field_formatter_plugin_manager = \Drupal::service('plugin.manager.field.formatter');
foreach ($all_view_modes as $view_mode_id => $view_mode_label) {
$view_mode_display = $entity_display_repository->getViewDisplay($entity->getEntityTypeId(), $entity->bundle(), $view_mode_id);
$hidden_fields = $view_mode_display->get('hidden');
if (isset($hidden_fields[$field_name])) {
// Field is not visible in this display mode.
continue;
}
$field_name_component = $view_mode_display->getComponent($field_name);
if ($field_name_component['type'] === 'entity_reference_entity_view' && !empty($entity_reference_entity_type_id)) {
// Check the referenced view_mode if cropping is available.
$view_mode_crop_enabled = FALSE;
foreach ($entity_reference_entity_type_bundles as $entity_reference_entity_type_bundle) {
$referenced_entity_view_mode_display = $entity_display_repository->getViewDisplay($entity_reference_entity_type_id, $entity_reference_entity_type_bundle, $field_name_component['settings']['view_mode']);
foreach ($referenced_entity_view_mode_display->getComponents() as $referenced_entity_view_mode_display_component) {
$field_formatter_plugin_definition = $field_formatter_plugin_manager->getDefinition($referenced_entity_view_mode_display_component['type'], FALSE);
if (!$field_formatter_plugin_definition) {
continue;
}
if (isset($field_formatter_plugin_definition['view_mode_crop_enabled']) && $field_formatter_plugin_definition['view_mode_crop_enabled']) {
$view_mode_crop_enabled = TRUE;
}
}
}
if (!$view_mode_crop_enabled) {
continue;
}
}
else {
$field_formatter_plugin_definition = $field_formatter_plugin_manager->getDefinition($field_name_component['type'], FALSE);
if (!$field_formatter_plugin_definition) {
continue;
}
if (!isset($field_formatter_plugin_definition['view_mode_crop_enabled']) || !$field_formatter_plugin_definition['view_mode_crop_enabled']) {
continue;
}
}
if (isset($view_mode_crop_data[$view_mode_id])) {
$crop_data[$view_mode_id] = $view_mode_crop_data[$view_mode_id];
}
if (!isset($crop_data[$view_mode_id])) {
$crop_data[$view_mode_id] = new ViewModeCropData($view_mode_id, $view_mode_label);
}
$crop_data[$view_mode_id]->label = $view_mode_label;
}
return $crop_data;
}
/**
* Delete cropped images for the given entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*/
public static function deleteCroppedImagesForEntity(EntityInterface $entity): void {
if (!$entity instanceof ContentEntityInterface) {
return;
}
/**
* @var \Drupal\Core\File\FileSystemInterface $file_system
*/
$file_system = \Drupal::service('file_system');
$public_path = CropPublicStreamWrapper::basePath() . '/' . $entity->getEntityTypeId() . '/' . $entity->id();
$private_path = CropPrivateStreamWrapper::basePath() . '/' . $entity->getEntityTypeId() . '/' . $entity->id();
if (is_dir($public_path)) {
$file_system->deleteRecursive($public_path);
}
if (is_dir($private_path)) {
$file_system->deleteRecursive($private_path);
}
$cache_tags_to_invalidate = [];
foreach ($entity->getFields() as $field_item_list) {
foreach ($field_item_list as $item) {
if ($item instanceof ViewModeCropImageItem) {
$cache_tags_to_invalidate = array_merge($cache_tags_to_invalidate, $item->entity->getCacheTags());
}
if ($item instanceof ViewModeCropEntityReferenceItem) {
$settings = $item->getFieldDefinition()->getSettings();
if ($settings['target_type'] === 'media' && $item->entity instanceof MediaInterface) {
$cache_tags_to_invalidate = array_merge($cache_tags_to_invalidate, $item->entity->getCacheTags());
}
}
}
}
Cache::invalidateTags($cache_tags_to_invalidate);
}
}
