3 namespace Drupal\image_widget_crop;
5 use Drupal\Core\Entity\EntityInterface;
6 use Drupal\Core\Entity\EntityTypeManagerInterface;
7 use Drupal\Core\Entity\FieldableEntityInterface;
8 use Drupal\Core\Form\FormStateInterface;
9 use Drupal\crop\Entity\Crop;
10 use Drupal\crop\Entity\CropType;
11 use Drupal\file\Entity\File;
12 use Drupal\file\Plugin\Field\FieldType\FileFieldItemList;
13 use Drupal\image\Entity\ImageStyle;
16 * ImageWidgetCropManager calculation class.
18 class ImageWidgetCropManager {
21 * The entity type manager service.
23 * @var \Drupal\Core\Entity\EntityTypeManagerInterface;
25 protected $entityTypeManager;
30 * @var \Drupal\crop\CropStorage.
32 protected $cropStorage;
37 * @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface.
39 protected $cropTypeStorage;
42 * The image style storage.
44 * @var \Drupal\image\ImageStyleStorageInterface
46 protected $imageStyleStorage;
51 * @var \Drupal\Core\Config\Entity\ConfigEntityStorage.
53 protected $fileStorage;
56 * Constructs a ImageWidgetCropManager.
58 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
59 * Entity type manager service.
61 public function __construct(EntityTypeManagerInterface $entity_type_manager) {
62 $this->entityTypeManager = $entity_type_manager;
63 $this->cropStorage = $this->entityTypeManager->getStorage('crop');
64 $this->cropTypeStorage = $this->entityTypeManager->getStorage('crop_type');
65 $this->imageStyleStorage = $this->entityTypeManager->getStorage('image_style');
66 $this->fileStorage = $this->entityTypeManager->getStorage('file');
70 * Create new crop entity with user properties.
72 * @param array $properties
73 * All properties returned by the crop plugin (js),
74 * and the size of thumbnail image.
75 * @param array|mixed $field_value
76 * An array of values for the contained properties of image_crop widget.
77 * @param CropType $crop_type
78 * The entity CropType.
80 public function applyCrop(array $properties, $field_value, CropType $crop_type) {
81 // Get Original sizes and position of crop zone.
82 $crop_properties = $this->getCropOriginalDimension($field_value, $properties);
83 // Get all imagesStyle used this crop_type.
84 $image_styles = $this->getImageStylesByCrop($crop_type->id());
86 $this->saveCrop($crop_properties, $field_value, $image_styles, $crop_type, FALSE);
90 * Update old crop with new properties choose in UI.
92 * @param array $properties
93 * All properties returned by the crop plugin (js),
94 * and the size of thumbnail image.
95 * @param array|mixed $field_value
96 * An array of values contain properties of image_crop widget.
97 * @param CropType $crop_type
98 * The entity CropType.
100 public function updateCrop(array $properties, $field_value, CropType $crop_type) {
101 // Get Original sizes and position of crop zone.
102 $crop_properties = $this->getCropOriginalDimension($field_value, $properties);
104 // Get all imagesStyle used this crop_type.
105 $image_styles = $this->getImageStylesByCrop($crop_type->id());
107 if (!empty($image_styles)) {
108 $crops = $this->loadImageStyleByCrop($image_styles, $crop_type, $field_value['file-uri']);
111 // If any crop exist add new crop.
113 $this->saveCrop($crop_properties, $field_value, $image_styles, $crop_type);
117 foreach ($crops as $crop_element) {
118 // Get Only first crop entity @see https://www.drupal.org/node/2617818.
119 /** @var \Drupal\crop\Entity\Crop $crop */
120 $crop = $crop_element;
122 if (!$this->cropHasChanged($crop_properties, array_merge($crop->position(), $crop->size()))) {
126 $this->updateCropProperties($crop, $crop_properties);
127 drupal_set_message(t('The crop "@cropType" were successfully updated for image "@filename".', ['@cropType' => $crop_type->label(), '@filename' => $this->fileStorage->load($field_value['file-id'])->getFilename()]));
132 * Save the crop when this crop not exist.
134 * @param double[] $crop_properties
135 * The properties of the crop applied to the original image (dimensions).
136 * @param array|mixed $field_value
137 * An array of values for the contained properties of image_crop widget.
138 * @param array $image_styles
139 * The list of imagesStyle available for this crop.
140 * @param CropType $crop_type
141 * The entity CropType.
142 * @param bool $notify
143 * Show notification after actions (default TRUE).
145 public function saveCrop(array $crop_properties, $field_value, array $image_styles, CropType $crop_type, $notify = TRUE) {
147 'type' => $crop_type->id(),
148 'entity_id' => $field_value['file-id'],
149 'entity_type' => 'file',
150 'uri' => $field_value['file-uri'],
151 'x' => $crop_properties['x'],
152 'y' => $crop_properties['y'],
153 'width' => $crop_properties['width'],
154 'height' => $crop_properties['height'],
157 // Save crop with previous values.
158 /** @var \Drupal\crop\CropInterface $crop */
159 $crop = $this->cropStorage->create($values);
163 drupal_set_message(t('The crop "@cropType" was successfully added for image "@filename".', ['@cropType' => $crop_type->label(), '@filename' => $this->fileStorage->load($field_value['file-id'])->getFilename()]));
168 * Delete the crop when user delete it.
170 * @param string $file_uri
171 * Uri of image uploaded by user.
172 * @param \Drupal\crop\Entity\CropType $crop_type
173 * The CropType object.
174 * @param int $file_id
175 * Id of image uploaded by user.
177 public function deleteCrop($file_uri, CropType $crop_type, $file_id) {
178 $image_styles = $this->getImageStylesByCrop($crop_type->id());
179 $crop = $this->cropStorage->loadByProperties([
180 'type' => $crop_type->id(),
183 $this->cropStorage->delete($crop);
184 $this->imageStylesOperations($image_styles, $file_uri);
185 drupal_set_message(t('The crop "@cropType" was successfully deleted for image "@filename".', [
186 '@cropType' => $crop_type->label(),
187 '@filename' => $this->fileStorage->load($file_id)->getFilename(),
192 * Get center of crop selection.
195 * Coordinates of x-axis & y-axis.
196 * @param array $crop_selection
197 * Coordinates of crop selection (width & height).
199 * @return array<string,double>
200 * Coordinates (x-axis & y-axis) of crop selection zone.
202 public function getAxisCoordinates(array $axis, array $crop_selection) {
204 'x' => (int) round($axis['x'] + ($crop_selection['width'] / 2)),
205 'y' => (int) round($axis['y'] + ($crop_selection['height'] / 2)),
210 * Get the size and position of the crop.
212 * @param array $field_values
213 * The original values of image.
214 * @param array $properties
215 * The original height of image.
218 * The data dimensions (width & height) into this ImageStyle.
220 public function getCropOriginalDimension(array $field_values, array $properties) {
221 $crop_coordinates = [];
223 /** @var \Drupal\Core\Image\Image $image */
224 $image = \Drupal::service('image.factory')->get($field_values['file-uri']);
225 if (!$image->isValid()) {
226 drupal_set_message(t('The file "@file" is not valid, your crop is not applied.', [
227 '@file' => $field_values['file-uri'],
232 // Get Center coordinate of crop zone on original image.
233 $axis_coordinate = $this->getAxisCoordinates(
234 ['x' => $properties['x'], 'y' => $properties['y']],
235 ['width' => $properties['width'], 'height' => $properties['height']]
238 // Calculate coordinates (position & sizes) of crop zone on original image.
239 $crop_coordinates['width'] = $properties['width'];
240 $crop_coordinates['height'] = $properties['height'];
241 $crop_coordinates['x'] = $axis_coordinate['x'];
242 $crop_coordinates['y'] = $axis_coordinate['y'];
244 return $crop_coordinates;
248 * Get one effect instead of ImageStyle.
250 * @param \Drupal\image\Entity\ImageStyle $image_style
251 * The ImageStyle to get data.
252 * @param string $data_type
253 * The type of data needed in current ImageStyle.
256 * The effect data in current ImageStyle.
258 public function getEffectData(ImageStyle $image_style, $data_type) {
260 /* @var \Drupal\image\ImageEffectInterface $effect */
261 foreach ($image_style->getEffects() as $uuid => $effect) {
262 $data_effect = $image_style->getEffect($uuid)->getConfiguration()['data'];
263 if (isset($data_effect[$data_type])) {
264 $data = $data_effect[$data_type];
272 * Get the imageStyle using this crop_type.
274 * @param string $crop_type_name
275 * The id of the current crop_type entity.
278 * All imageStyle used by this crop_type.
280 public function getImageStylesByCrop($crop_type_name) {
282 $image_styles = $this->imageStyleStorage->loadMultiple();
284 /** @var \Drupal\image\Entity\ImageStyle $image_style */
285 foreach ($image_styles as $image_style) {
286 $image_style_data = $this->getEffectData($image_style, 'crop_type');
287 if (!empty($image_style_data) && ($image_style_data == $crop_type_name)) {
288 $styles[] = $image_style;
296 * Apply different operation on ImageStyles.
298 * @param array $image_styles
299 * All ImageStyles used by this cropType.
300 * @param string $file_uri
301 * Uri of image uploaded by user.
302 * @param bool $create_derivative
303 * Boolean to create an derivative of the image uploaded.
305 public function imageStylesOperations(array $image_styles, $file_uri, $create_derivative = FALSE) {
306 /** @var \Drupal\image\Entity\ImageStyle $image_style */
307 foreach ($image_styles as $image_style) {
308 if ($create_derivative) {
309 // Generate the image derivative uri.
310 $destination_uri = $image_style->buildUri($file_uri);
312 // Create a derivative of the original image with a good uri.
313 $image_style->createDerivative($file_uri, $destination_uri);
315 // Flush the cache of this ImageStyle.
316 $image_style->flush($file_uri);
321 * Update existent crop entity properties.
323 * @param \Drupal\crop\Entity\Crop $crop
324 * The crop object loaded.
325 * @param array $crop_properties
326 * The machine name of ImageStyle.
328 public function updateCropProperties(Crop $crop, array $crop_properties) {
329 // Parse all properties if this crop have changed.
330 foreach ($crop_properties as $crop_coordinate => $value) {
331 // Edit the crop properties if he have changed.
332 $crop->set($crop_coordinate, $value, TRUE);
339 * Load all crop using the ImageStyles.
341 * @param array $image_styles
342 * All ImageStyle for this current CROP.
343 * @param CropType $crop_type
344 * The entity CropType.
345 * @param string $file_uri
346 * Uri of uploded file.
349 * All crop used this ImageStyle.
351 public function loadImageStyleByCrop(array $image_styles, CropType $crop_type, $file_uri) {
353 /** @var \Drupal\image\Entity\ImageStyle $image_style */
354 foreach ($image_styles as $image_style) {
355 /** @var \Drupal\crop\Entity\Crop $crop */
356 $crop = Crop::findCrop($file_uri, $crop_type->id());
358 $crops[$image_style->id()] = $crop;
366 * Compare crop zone properties when user saved one crop.
368 * @param array $crop_properties
369 * The crop properties after saved the form.
370 * @param array $old_crop
371 * The crop properties save in this crop entity,
372 * Only if this crop already exist.
375 * Return true if properties is not identical.
377 public function cropHasChanged(array $crop_properties, array $old_crop) {
378 if (!empty(array_diff_assoc($crop_properties, $old_crop))) {
385 * Verify if ImageStyle is correctly configured.
387 * @param array $styles
388 * The list of available ImageStyle.
390 * @return array<integer>
391 * The list of styles filtred.
393 public function getAvailableCropImageStyle(array $styles) {
394 $available_styles = [];
395 foreach ($styles as $style_id => $style_label) {
396 $style_loaded = $this->imageStyleStorage->loadByProperties(['name' => $style_id]);
397 /** @var \Drupal\image\Entity\ImageStyle $image_style */
398 $image_style = $style_loaded[$style_id];
399 $effect_data = $this->getEffectData($image_style, 'width');
400 if (!empty($effect_data)) {
401 $available_styles[$style_id] = $style_label;
405 return $available_styles;
409 * Verify if the crop is used by a ImageStyle.
411 * @param array $crop_list
412 * The list of existent Crop Type.
414 * @return array<integer>
415 * The list of Crop Type filtred.
417 public function getAvailableCropType(array $crop_list) {
418 $available_crop = [];
419 foreach ($crop_list as $crop_machine_name => $crop_label) {
420 $image_styles = $this->getImageStylesByCrop($crop_machine_name);
421 if (!empty($image_styles)) {
422 $available_crop[$crop_machine_name] = $crop_label;
426 return $available_crop;
430 * Get All sizes properties of the crops for an file.
432 * @param \Drupal\crop\Entity\Crop $crop
433 * All crops attached to this file based on URI.
435 * @return array<array>
436 * Get all crop zone properties (x, y, height, width),
438 public static function getCropProperties(Crop $crop) {
439 $anchor = $crop->anchor();
440 $size = $crop->size();
444 'height' => $size['height'],
445 'width' => $size['width'],
450 * Fetch all fields FileField and use "image_crop" element on an entity.
452 * @param \Drupal\Core\Entity\EntityInterface $entity
455 public function buildCropToEntity(EntityInterface $entity) {
456 if (isset($entity) && $entity instanceof FieldableEntityInterface) {
457 // Loop all fields of the saved entity.
458 foreach ($entity->getFields() as $entity_fields) {
459 // If current field is FileField and use imageWidgetCrop.
460 if ($entity_fields instanceof FileFieldItemList) {
461 /* First loop to get each elements independently in the field values.
462 Required if the image field cardinality > 1. */
463 foreach ($entity_fields->getValue() as $crop_elements) {
464 foreach ($crop_elements as $crop_element) {
465 if (is_array($crop_element) && isset($crop_element['crop_wrapper'])) {
466 // Reload image since its URI could have been changed by other modules.
467 /** @var \Drupal\file_entity\Entity\FileEntity $file */
468 $file = $this->fileStorage->load($crop_element['file-id']);
469 $crop_element['file-uri'] = $file->getFileUri();
470 // Parse all value of a crop_wrapper element and get properties
471 // associate with her CropType.
472 foreach ($crop_element['crop_wrapper'] as $crop_type_name => $properties) {
473 $properties = $properties['crop_container']['values'];
474 /** @var \Drupal\crop\Entity\CropType $crop_type */
475 $crop_type = $this->cropTypeStorage->load($crop_type_name);
477 // If the crop type needed is disabled or delete.
478 if (empty($crop_type) && $crop_type instanceof CropType) {
479 drupal_set_message(t("The CropType ('@cropType') is not active or not defined. Please verify configuration of image style or ImageWidgetCrop formatter configuration", ['@cropType' => $crop_type->id()]), 'error');
483 // If this crop is available to create an crop entity.
484 if ($entity->isNew()) {
485 if ($properties['crop_applied'] == '1' && isset($properties) && (!empty($properties['width']) && !empty($properties['height']))) {
486 $this->applyCrop($properties, $crop_element, $crop_type);
490 // Get all imagesStyle used this crop_type.
491 $image_styles = $this->getImageStylesByCrop($crop_type_name);
492 $crops = $this->loadImageStyleByCrop($image_styles, $crop_type, $crop_element['file-uri']);
493 // If the entity already exist & is not deleted by user
494 // update $crop_type_name crop entity.
495 if ($properties['crop_applied'] == '0' && !empty($crops)) {
496 $this->deleteCrop($crop_element['file-uri'], $crop_type, $crop_element['file-id']);
498 elseif (isset($properties) && (!empty($properties['width']) && !empty($properties['height']))) {
499 $this->updateCrop($properties, $crop_element, $crop_type);
512 * Fetch all form elements using image_crop element.
514 * @param \Drupal\Core\Form\FormStateInterface $form_state
515 * The current state of the form.
517 public function buildCropToForm(FormStateInterface $form_state) {
518 /** @var \Drupal\file_entity\Entity\FileEntity $entity */
519 $entity = $form_state->getFormObject()->getEntity();
520 $form_state_values = $form_state->getValues();
521 if (is_array($form_state_values['image_crop']) && isset($form_state_values['image_crop']['crop_wrapper'])) {
522 // Parse all values and get properties associate with the crop type.
523 foreach ($form_state_values['image_crop']['crop_wrapper'] as $crop_type_name => $properties) {
524 $properties = $properties['crop_container']['values'];
525 /** @var \Drupal\crop\Entity\CropType $crop_type */
526 $crop_type = $this->cropTypeStorage->load($crop_type_name);
528 // If the crop type needed is disabled or delete.
529 if (empty($crop_type) && $crop_type instanceof CropType) {
530 drupal_set_message(t("The CropType ('@cropType') is not active or not defined. Please verify configuration of image style or ImageWidgetCrop formatter configuration", ['@cropType' => $crop_type->id()]), 'error');
534 if (is_array($properties) && isset($properties)) {
535 $crop_exists = Crop::cropExists($entity->getFileUri(), $crop_type_name);
537 if ($properties['crop_applied'] == '1' && isset($properties) && (!empty($properties['width']) && !empty($properties['height']))) {
538 $this->applyCrop($properties, $form_state_values['image_crop'], $crop_type);
542 // Get all imagesStyle used this crop_type.
543 $image_styles = $this->getImageStylesByCrop($crop_type_name);
544 $crops = $this->loadImageStyleByCrop($image_styles, $crop_type, $entity->getFileUri());
545 // If the entity already exist & is not deleted by user update
546 // $crop_type_name crop entity.
547 if ($properties['crop_applied'] == '0' && !empty($crops)) {
548 $this->deleteCrop($entity->getFileUri(), $crop_type, $entity->id());
550 elseif (isset($properties) && (!empty($properties['width']) && !empty($properties['height']))) {
551 $this->updateCrop($properties, [
552 'file-uri' => $entity->getFileUri(),
553 'file-id' => $entity->id(),