Problem/Motivation

In the current code, the determination of the effective cropping coordinates are spread between JS and PHP.

We should have a clean architecture that makes us error prone and flexible in presentation, also responsiveness.

Proposed resolution

IMHO, the only thing that is relevant for the server is the original image dimension.
Agree, we introduce a crop preview image that is downscaled already, but that's more a convenience for image delivery with smaller file size.

It's the client that then needs to determine based on the screen geometry at what scale the image is presented (1:1 or for instance 100% on any mobile screen).
Thus, the cropper is initialised at a specific size with cropping data.
At that display time, initialisation can determine a scaling in JS.
When extracting the effective cropping coordinates, the displaying scale can be inverted to determine the paremeters in context of the original image.

I propose to add data attributes to the cropper image output with original width and height to drive the cropper initialisation.
The form values to collect the effective cropping coordinates / width / height are also in original context.

Thus PHP doesn't care about image scaling at all. Just JS.

Most calculations from getThumbnailCalculatedProperties would move to JS. Also getAxisCoordinates would be more a client side determination.

I might be wrong, but i also don't get why we need thumb-w and thump-h at all, at least not as hidden input elements.

At the same time, an aspect-ratio data attribute can be added that is properly precalculated in PHP and doesn't need JS eval.

Additionally, the server should do some health checks to avoid crop parameters exceed the image max dimensions in validation.

Remaining tasks

Discuss if this approach is supported by all, decide.
Then reimplement calculations.

User interface changes

API changes

Data model changes

Comments

miro_dietiker created an issue. See original summary.

slashrsm’s picture

I agree. Anything that has to do with display is something that should be handled by code that is responsible for display. Crop information itself doesn't and shouldn't care about this details.

luksak’s picture

IMO JS should contain as few calculation logic as possible. All of the crop calculation happens on the PHP side already. So why should we have it in JS here?

Do we have that much calculation on the JS side? the only thing we do is rounding the values Cropper outputs:

var cropperOptions = {
    background: false,
    zoomable: false,
    viewMode: 3,
    autoCropArea: 1,
    cropend: function (e) {
      var $this = $(this);
      var $values = $this.siblings(cropperValuesSelector);
      var data = $this.cropper('getData');
      $values.find('.crop-x').val(Math.round(data.x));
      $values.find('.crop-y').val(Math.round(data.y));
      $values.find('.crop-width').val(Math.round(data.width));
      $values.find('.crop-height').val(Math.round(data.height));
      $values.find('.crop-applied').val(1);
      Drupal.imageWidgetCrop.updateCropSummaries($this);
    }
  };

Before the cropend callback is being executed, Cropper calculates the coordinates based on the downscaling of the image style.

Is this solution that bad?

Another option would be to only calculate relative (percentages) of the cropping data in JS and then do the rest of the calculations in PHP. I guess this would be more accurate since we round only once.

miro_dietiker’s picture

As long as you think in image styles and static values, you think this is a good idea.

When you then consider that the effective cropper size (width, height) is a dynamic decision and can be any scale... a UI could offer full screen zoom any everything else, you realise that scale is a pure client decision. And the PHP domain should only operate in origin scale. Everything else adds complexity.
I guess, the current solution didn't yet end up at that problem because you're not yet dynamically supporting to scale cropper to the effective viewport size of mobile cases. Related tickets are open, is it?

sanja_m’s picture

Assigned: Unassigned » sanja_m

Assigning to me.

sanja_m’s picture

I removed thumb-w and thumb-h input elements and eval() from JS and precalculated aspect ratio in PHP, currently I am working on scaling determination in JS during initialisation of the cropper, I am not sure if I understood that part of the issue properly, I will use getImageData() function from the cropper to determine width and height of the preview image that is used to initialize cropper and will send data attribute with original image width and height, and with those data I will calculate the delta, that is image scaling.

woprrr’s picture

I think we need at first to agree. I highly Lukas joined in the sense that we must not calculate dimenssions via JS because the whole system is based on images_style. Let me explain to correctly pass the user select area we need more data that is not available through JS interogeant but in drupal. To reflect the selected area on a ui style picture we need to know the dimmensions thumbnail image to determine the enlargement or reduction rate of the thumbnail image to affect the original image

The data manipulated by JS are primarily intended to pass on a crop area on the thumbnail precise manner.

@sanja_m : The ratio must not be it calculated automatically, this was one of the old functionality of the module and it has been proved that often the user or graphic uses ratios "as non-standard 95:35.

sanja_m’s picture

Status: Active » Needs review
StatusFileSize
new3.89 KB

This is my unfinished patch, what I started to change, now I am not sure should we continue with this issue, and if we should is this the right direction?

miro_dietiker’s picture

woprrr, i have proposed the approach and discussed with team. i updated the proposal and explained all reasons why this is important and simplifies the situation. Janez also agrees.

Please let sanja provide a patch that cleans up the situation with our review.
I'm pretty sure the patch will easily visualise that the complexity is reduced with this approach and that the responsibility between backend and UI is better separated and you will like it when you think about it again.

  1. +++ b/js/imageWidgetCrop.js
    @@ -93,10 +93,14 @@
    -    options.aspectRatio = eval(ratio);
    ...
    +    options.aspectRatio = ratio;
    
    +++ b/src/Plugin/Field/FieldWidget/ImageCropWidget.php
    @@ -211,7 +211,8 @@ class ImageCropWidget extends ImageWidget {
    +            $ratio = explode(':', $ratio);
    

    woprrr, if it's important for you to keep aspect ratio in the original definition domain, we can do this:
    We add two data attributes ratio_x and ratio_y and do the devision in JS.

  2. +++ b/js/imageWidgetCrop.js
    @@ -93,10 +93,14 @@
    +    $values.find('.crop-delta').val(delta.toFixed(2));
    

    Why limit precision to only 2 digits? That's too short.

  3. +++ b/src/Plugin/Field/FieldWidget/ImageCropWidget.php
    @@ -211,7 +211,8 @@ class ImageCropWidget extends ImageWidget {
                 $has_ratio = $crop_type->getAspectRatio();
                 $ratio = !empty($has_ratio) ? $has_ratio : t('NaN');
    ...
    +            $ratio = $ratio[0] / $ratio[1];
    

    For sure, "NaN" is not to be translated. Remove t(). Also in case of NaN, the division is messing up. Let's add a proper if for the case an aspect ratio is defined.

  4. +++ b/src/Plugin/Field/FieldWidget/ImageCropWidget.php
    @@ -229,7 +230,7 @@ class ImageCropWidget extends ImageWidget {
    +              '#attributes' => ['data-ratio' => [$ratio], 'data-name' => [$crop_type_id], 'data-original-width' => [$element['#default_value']['width']], 'data-original-height' => [$element['#default_value']['height']]],
    

    I prefer a multiline notation so we can see the attributes easily.

  5. +++ b/src/Plugin/Field/FieldWidget/ImageCropWidget.php
    @@ -311,8 +312,9 @@ class ImageCropWidget extends ImageWidget {
    +     // 'thumb-w' => ['label' => t('Thumbnail Width'), 'value' => NULL],
    

    Please delete, not comment.

miro_dietiker’s picture

Status: Needs review » Needs work
slashrsm’s picture

Yes, I agree. It totally makes sense to have all calculation logic in one place. All PHP should be interested in is original image and crop information in it's context. This also makes code useful for other usages where presentation might be different. We are already trying to bring IWC to file_entity edit page. True, presentation will be very similar, but anyway. I still think it is a valid point.

sanja_m’s picture

Status: Needs work » Needs review
StatusFileSize
new14.58 KB
new14.5 KB

Fixed all issues from comment #9 except the first one. @woprrr should I add two data attributes ratio_x and ratio_y and move the devision in JS like Miro suggested?
Also removed some unnecessary functions from PHP, because now coordinate calculations are moved to JS.

miro_dietiker’s picture

Yeah this looks much better and i can see that a significant part of complexity is going away in the diff. (I hope you didn't drop too much though.)

This needs more testing and in depth review. Happy if everyone involved can take a look at it!

woprrr’s picture

I test & review it today but seem very GOOD :) @sanja_m thanks you verry much & @miro too for your help :)

slashrsm’s picture

Status: Needs review » Needs work

I found a bug that this patch introduces. I tested on 8.x-1.x I wasn't able to reproduce. Steps:

- Enable image_widget_crop_example module
- Navigate to node/add/crop_simple_example
- Upload image and apply crop
- Save node
- Expected result: cropped image appears on view page
- Actual result: image is not cropped
- Edit node
- Re-apply crop
- Save
- Image is cropped now

And few more other comments:

  1. +++ b/src/Plugin/Field/FieldWidget/ImageCropWidget.php
    @@ -207,11 +206,16 @@ class ImageCropWidget extends ImageWidget {
                 // Add compatibility to PHP 5.3.
    -            $has_ratio = $crop_type->getAspectRatio();
    -            $ratio = !empty($has_ratio) ? $has_ratio : t('NaN');
    

    D8 requires 5.5. Let's remove this comment as it makes no sense.

  2. +++ b/src/Plugin/Field/FieldWidget/ImageCropWidget.php
    @@ -229,7 +233,12 @@ class ImageCropWidget extends ImageWidget {
    -              '#attributes' => ['data-ratio' => [$ratio], 'data-name' => [$crop_type_id]],
    +              '#attributes' => [
    +                  'data-ratio' => [$ratio],
    +                  'data-name' => [$crop_type_id],
    +                  'data-original-width' => [$element['#default_value']['width']],
    +                  'data-original-height' => [$element['#default_value']['height']]
    +              ],
    

    Why do we need to pass attribute values as arrays?

  3. +++ b/src/Plugin/Field/FieldWidget/ImageCropWidget.php
    @@ -426,42 +431,25 @@ class ImageCropWidget extends ImageWidget {
    +    // Get the Crop selection Size (into Uploaded image).
    +    $crop_thumbnail['height'] = round($original_crop['size']['height']);
    +    $crop_thumbnail['width'] = round($original_crop['size']['width']);
    

    Rounding is not needed since we removed division with delta.

    This also makes this function just a wrapper that copies array values to another array. We may not need it any more.

  4. +++ b/src/Plugin/Field/FieldWidget/ImageCropWidget.php
    @@ -426,42 +431,25 @@ class ImageCropWidget extends ImageWidget {
    +    // Get the Top-Left corner for Thumbnail.
    +    $crop_thumbnail['x'] = round($original_crop['anchor']['x']);
    +    $crop_thumbnail['y'] = round($original_crop['anchor']['y']);
    

    .

sasanikolic’s picture

The comments above make sense... I've also tested it and can confirm that the cropping is not saved correctly. Let's debug and fix it.

But the approach is good and nice to have a proper cleanup!

sanja_m’s picture

Status: Needs work » Needs review
StatusFileSize
new14.75 KB
new5.03 KB

Fixed all bugs from comment #15.

woprrr’s picture

Issue tags: +D8Media
StatusFileSize
new14.37 KB

I ve re-create patch with last version of repo, there was a problem to apply the patch on the most recent version of the module. (Line 209)

I am under review thank you for the work.

woprrr’s picture

Status: Needs review » Needs work

Great job !!!
Your patch fix another problem too (the image appears more completely in the thumbnail).
There is just one aspect that is not available is the respect of the ratio, the crop area is always free now since I applied the patch.
But this is good way !

Step to reproduce :

Enable image_widget_crop_example module
- Navigate to node/add/crop_simple_example
- Upload image and apply crop in 16:9 tabs

Your crop area is free and not fix to 16:9 ratio.

@see screenshot

slashrsm’s picture

I was able to reproduce this problem too.

woprrr’s picture

Is eval() deletion.... :( I try to find a way not to do that ...

I think cropper accept only an number value with this option. We sould calculate this 16 : 9 = 1.7777777777777777 and if we take this cropper work fine.

slashrsm’s picture

eval() is evil. We should find a different solution.

woprrr’s picture

Version: » 8.x-1.x-dev
StatusFileSize
new317 bytes
new12.33 KB

Yes this is evil but the solution is under our eye O-O, the ratio is formated in good format like (16/9) and we need to just added () in ratio variable to proced the division W/H = aspect ratio number.

All seems good now for me aspect ratio are applied now we can confirm if is good for you too ?

woprrr’s picture

Status: Needs work » Needs review

The last submitted patch, 17: 2641970-17.patch, failed testing.

The last submitted patch, 18: 2641970-18.patch, failed testing.

slashrsm’s picture

Status: Needs review » Needs work

True. It is formatted correctly when module does it. But what if someone changes that somehow?

I'd feel much better if we'd actually make sure ratio is what we expect it to be. Maybe split string, make sure there are integers on both sides and do division.

+++ b/js/imageWidgetCrop.js
@@ -93,8 +95,7 @@
     options.data = data;
-    // @TODO: eval() is evil.
-    options.aspectRatio = eval(ratio);
+    options.aspectRatio = (ratio);
 
     $element.cropper(cropperOptions);

Another strange thing. Above we assign values of cropperOptions to options. Then we add some stuff to options and pass cropperOptions to the cropper initialization.

Why do we need to create new variable (options) at all?

woprrr’s picture

Status: Needs work » Needs review
StatusFileSize
new2.5 KB
new13.88 KB

Ok i ve seen and add some test to ensure data ratio isNumeric to procced to division.

For me all it's all Ok fonctionaly.

@slashrsm : I think we add some stuff in option when user not have aready clic on vertical tab, when user open details element js trigger we need to get an options with data empty but with aspectRatio set.

sanja_m’s picture

StatusFileSize
new15.92 KB
new2.22 KB

Just removed ImageWidgetCropManager::getThumbnailCalculatedProperties() since we don't need it anymore.

slashrsm’s picture

Status: Needs review » Reviewed & tested by the community

Looks good.

  • woprrr committed f7ef4ab on 8.x-1.x authored by sanja_m
    Issue #2641970 by sanja_m, woprrr, slashrsm, miro_dietiker, Lukas von...
woprrr’s picture

Status: Reviewed & tested by the community » Fixed

Thank a lot all :) @sanja_m Great job !!

miro_dietiker’s picture

Many thanks from my side also. Party time! :-)

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.