5f37ebeed469a4689c1c217e3a8a9a1898f1669e
[yaffs-website] / web / modules / contrib / image_widget_crop / src / ImageWidgetCropManager.php
1 <?php
2
3 namespace Drupal\image_widget_crop;
4
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;
14
15 /**
16  * ImageWidgetCropManager calculation class.
17  */
18 class ImageWidgetCropManager {
19
20   /**
21    * The entity type manager service.
22    *
23    * @var \Drupal\Core\Entity\EntityTypeManagerInterface;
24    */
25   protected $entityTypeManager;
26
27   /**
28    * The crop storage.
29    *
30    * @var \Drupal\crop\CropStorage.
31    */
32   protected $cropStorage;
33
34   /**
35    * The crop storage.
36    *
37    * @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface.
38    */
39   protected $cropTypeStorage;
40
41   /**
42    * The image style storage.
43    *
44    * @var \Drupal\image\ImageStyleStorageInterface
45    */
46   protected $imageStyleStorage;
47
48   /**
49    * The File storage.
50    *
51    * @var \Drupal\Core\Config\Entity\ConfigEntityStorage.
52    */
53   protected $fileStorage;
54
55   /**
56    * Constructs a ImageWidgetCropManager.
57    *
58    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
59    *   Entity type manager service.
60    */
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');
67   }
68
69   /**
70    * Create new crop entity with user properties.
71    *
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.
79    */
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());
85
86     $this->saveCrop($crop_properties, $field_value, $image_styles, $crop_type, FALSE);
87   }
88
89   /**
90    * Update old crop with new properties choose in UI.
91    *
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.
99    */
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);
103
104     // Get all imagesStyle used this crop_type.
105     $image_styles = $this->getImageStylesByCrop($crop_type->id());
106
107     if (!empty($image_styles)) {
108       $crops = $this->loadImageStyleByCrop($image_styles, $crop_type, $field_value['file-uri']);
109     }
110
111     // If any crop exist add new crop.
112     if (empty($crops)) {
113       $this->saveCrop($crop_properties, $field_value, $image_styles, $crop_type);
114       return;
115     }
116
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;
121
122       if (!$this->cropHasChanged($crop_properties, array_merge($crop->position(), $crop->size()))) {
123         return;
124       }
125
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()]));
128     }
129   }
130
131   /**
132    * Save the crop when this crop not exist.
133    *
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).
144    */
145   public function saveCrop(array $crop_properties, $field_value, array $image_styles, CropType $crop_type, $notify = TRUE) {
146     $values = [
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'],
155     ];
156
157     // Save crop with previous values.
158     /** @var \Drupal\crop\CropInterface $crop */
159     $crop = $this->cropStorage->create($values);
160     $crop->save();
161
162     if ($notify) {
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()]));
164     }
165   }
166
167   /**
168    * Delete the crop when user delete it.
169    *
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.
176    */
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(),
181       'uri' => $file_uri,
182     ]);
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(),
188     ]));
189   }
190
191   /**
192    * Get center of crop selection.
193    *
194    * @param int[] $axis
195    *   Coordinates of x-axis & y-axis.
196    * @param array $crop_selection
197    *   Coordinates of crop selection (width & height).
198    *
199    * @return array<string,double>
200    *   Coordinates (x-axis & y-axis) of crop selection zone.
201    */
202   public function getAxisCoordinates(array $axis, array $crop_selection) {
203     return [
204       'x' => (int) round($axis['x'] + ($crop_selection['width'] / 2)),
205       'y' => (int) round($axis['y'] + ($crop_selection['height'] / 2)),
206     ];
207   }
208
209   /**
210    * Get the size and position of the crop.
211    *
212    * @param array $field_values
213    *   The original values of image.
214    * @param array $properties
215    *   The original height of image.
216    *
217    * @return null|array
218    *   The data dimensions (width & height) into this ImageStyle.
219    */
220   public function getCropOriginalDimension(array $field_values, array $properties) {
221     $crop_coordinates = [];
222
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'],
228       ]), 'error');
229       return NULL;
230     }
231
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']]
236     );
237
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'];
243
244     return $crop_coordinates;
245   }
246
247   /**
248    * Get one effect instead of ImageStyle.
249    *
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.
254    *
255    * @return mixed|null
256    *   The effect data in current ImageStyle.
257    */
258   public function getEffectData(ImageStyle $image_style, $data_type) {
259     $data = NULL;
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];
265       }
266     }
267
268     return $data;
269   }
270
271   /**
272    * Get the imageStyle using this crop_type.
273    *
274    * @param string $crop_type_name
275    *   The id of the current crop_type entity.
276    *
277    * @return array
278    *   All imageStyle used by this crop_type.
279    */
280   public function getImageStylesByCrop($crop_type_name) {
281     $styles = [];
282     $image_styles = $this->imageStyleStorage->loadMultiple();
283
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;
289       }
290     }
291
292     return $styles;
293   }
294
295   /**
296    * Apply different operation on ImageStyles.
297    *
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.
304    */
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);
311
312         // Create a derivative of the original image with a good uri.
313         $image_style->createDerivative($file_uri, $destination_uri);
314       }
315       // Flush the cache of this ImageStyle.
316       $image_style->flush($file_uri);
317     }
318   }
319
320   /**
321    * Update existent crop entity properties.
322    *
323    * @param \Drupal\crop\Entity\Crop $crop
324    *   The crop object loaded.
325    * @param array $crop_properties
326    *   The machine name of ImageStyle.
327    */
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);
333     }
334
335     $crop->save();
336   }
337
338   /**
339    * Load all crop using the ImageStyles.
340    *
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.
347    *
348    * @return array
349    *   All crop used this ImageStyle.
350    */
351   public function loadImageStyleByCrop(array $image_styles, CropType $crop_type, $file_uri) {
352     $crops = [];
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());
357       if (!empty($crop)) {
358         $crops[$image_style->id()] = $crop;
359       }
360     }
361
362     return $crops;
363   }
364
365   /**
366    * Compare crop zone properties when user saved one crop.
367    *
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.
373    *
374    * @return bool
375    *   Return true if properties is not identical.
376    */
377   public function cropHasChanged(array $crop_properties, array $old_crop) {
378     if (!empty(array_diff_assoc($crop_properties, $old_crop))) {
379       return TRUE;
380     }
381     return FALSE;
382   }
383
384   /**
385    * Verify if ImageStyle is correctly configured.
386    *
387    * @param array $styles
388    *   The list of available ImageStyle.
389    *
390    * @return array<integer>
391    *   The list of styles filtred.
392    */
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;
402       }
403     }
404
405     return $available_styles;
406   }
407
408   /**
409    * Verify if the crop is used by a ImageStyle.
410    *
411    * @param array $crop_list
412    *   The list of existent Crop Type.
413    *
414    * @return array<integer>
415    *   The list of Crop Type filtred.
416    */
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;
423       }
424     }
425
426     return $available_crop;
427   }
428
429   /**
430    * Get All sizes properties of the crops for an file.
431    *
432    * @param \Drupal\crop\Entity\Crop $crop
433    *   All crops attached to this file based on URI.
434    *
435    * @return array<array>
436    *   Get all crop zone properties (x, y, height, width),
437    */
438   public static function getCropProperties(Crop $crop) {
439     $anchor = $crop->anchor();
440     $size = $crop->size();
441     return [
442       'x' => $anchor['x'],
443       'y' => $anchor['y'],
444       'height' => $size['height'],
445       'width' => $size['width'],
446     ];
447   }
448
449   /**
450    * Fetch all fields FileField and use "image_crop" element on an entity.
451    *
452    * @param \Drupal\Core\Entity\EntityInterface $entity
453    *   The entity object.
454    */
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);
476
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');
480                     return;
481                   }
482
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);
487                     }
488                   }
489                   else {
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']);
497                     }
498                     elseif (isset($properties) && (!empty($properties['width']) && !empty($properties['height']))) {
499                       $this->updateCrop($properties, $crop_element, $crop_type);
500                     }
501                   }
502                 }
503               }
504             }
505           }
506         }
507       }
508     }
509   }
510
511   /**
512    * Fetch all form elements using image_crop element.
513    *
514    * @param \Drupal\Core\Form\FormStateInterface $form_state
515    *   The current state of the form.
516    */
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);
527
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');
531           return;
532         }
533
534         if (is_array($properties) && isset($properties)) {
535           $crop_exists = Crop::cropExists($entity->getFileUri(), $crop_type_name);
536           if (!$crop_exists) {
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);
539             }
540           }
541           else {
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());
549             }
550             elseif (isset($properties) && (!empty($properties['width']) && !empty($properties['height']))) {
551               $this->updateCrop($properties, [
552                 'file-uri' => $entity->getFileUri(),
553                 'file-id' => $entity->id(),
554               ], $crop_type);
555             }
556           }
557         }
558       }
559     }
560   }
561
562 }