Problem/Motivation
RotateImageEffect::transformDimensions()
only transforms image dimensions for rotation angles that are a multiple of 90°.
However, algorithms exist that can help calculating the dimensions for any angle.
Link where this is explained:
- http://stackoverflow.com/questions/622140/calculate-bounding-box-coordin...
There is a similar issue in the imagemagick toolkit, as this toolkit cannot retrieve dimensions of intermediate processing results (as can be done with GD): #1551262: Rotate should alter dimensions
This can be seen as a follow-up for #1129642: Populate HTML image tags with dimension attributes (like D6 imagefield) without re-introducing I/O
Proposed resolution
Implement an utility class that returns algorithmically the same results that can be expected by executing PHP GD imagerotate() function in PHP versions >5.5
Remaining tasks
- review patch
- commit
User interface changes
None
API changes
None
Data model changes
None
Beta phase evaluation
Issue category | Bug, because responsive images cannot output images in a srcset in combination with sizes, if the width is unknown. See #2420751: ImageEffectBase::transformDimensions() should have a sane default implementation, comments #9 and #13:
|
---|---|
Issue priority | Normal, because affects one piece of functionality and does not impact the overall functionality of the software. |
Disruption | Not disruptive. |
Comment | File | Size | Author |
---|---|---|---|
#52 | 1551686-52.patch | 162.66 KB | mondrake |
Comments
Comment #1
fietserwinPlease find attached a patch that implements image_rotate_dimensions() for all angles including tests.
Note that the current code happens to have an error for angles like 90.5 as modulo (%) operates on integer values (thus values are first rounded, then the remainder is calculated).
Comment #3
fietserwinRemoved the offending test as it tested the old "incorrect" situation: no dimensions returned for angles that are not a multiple of 90 degrees.
Comment #3.0
fietserwinUpdated issue summary.
Comment #4
fietserwinComment #5
fietserwinComment #6
cweagansUpdating tags per http://drupal.org/node/1517250
Comment #7
claudiu.cristeaGreat! Can you rework the patch?
Comment #8
fietserwinReworked.
Comment #10
fietserwinOK, there are tests to check the image dimensions by checking the width and height attributes on generated img tags. I prefer to test the algorithm directly, thus more like a unit test than a functional test. Therefore the added test stays in the ImageEffectsTest class.
Corrected the dimensions test with a rotation over a non-multiple of 90 degrees angle and added a negative angle in the new unit test.
Comment #10.0
fietserwinUpdated issue summary.
Comment #11
fietserwinRerolled and locally tested on both PHP5.4 and 5.5. the bug in PHP5.5 (https://bugs.php.net/bug.php?id=65148) is worse then I thought in #2215369: Various bugs with PHP 5.5 imagerotate(), including when incorrect color indices are passed in, for the tested angles and dimensions, I found that:
This raises the question if, with the given bug, these calculations should be used to derive image dimensions or not? As these calculations are used to generate width and height attributes for an {img} tag, the images may become a bit overstretched on the web page. OTOH, this overstretching may also be seen as partially undoing the error in GD and thereby preventing design mess-ups.
Some more remarks:
- The calculations have been punt in ImageUtility and not in the rotate image effect, even if this will be the only usage in core. I have done this as to allow an IM toolkit in contrib access to this algorithm as well, and IM will need it to be able to return correct values for its getWidth() and getHeight() methods.
- I think that the test file should be moved to core\tests\Drupal\Tests\Component\Utility, what do others think?
Comment #13
fietserwinOops, I only locally tested the added tests, not if existing tests would now fail.
As multiples of 90 degrees versus non-multiples are no longer separate cases, I decided to remove the failing test. IMO this is the correct decision, as it did test the fact that we had a bug (or missing feature if you prefer so) in the image dimensions callback of the rotate effect. Correct return values for this dimensions callback are now much more extensively tested with the added unit test.
Comment #14
fietserwinA new patch. I dove into the actual GD code to find out why and how PHP5.4 and 5.5 differ. This lead to 2 ways of computing the resulting dimensions and the one actually called depends on the PHP version. If one actually matches the way that IM computes dimensions we can make that selection logic depend on the toolkit in use as well.
What does this patch do:
Please review and decide whether to move the Image Utility test file to core\tests\Drupal\Tests\Component\Utility.
Comment #15
mondrakeWell, thanks a lot @fietserwin for the extensive research on this issue.
IMHO, I think we should have 'our truth' for what we expect to be the dimensions resulting from a rotation. It's a tradeoff: is it preferable to go after each single toolkit / version to match their algorithms and adapt code and tests every time a change occurs, or rather have 'our truth' and always adapt the image to this truth which also means consistent image dimensions across toolkits/versions?
I vote for the latter: this would mean to:
Some code review:
this method is never used
I's suggest to alias it to ImageUtility to avoid confusion with the other Image class
Instead of removing this, if we follow my general suggestion, we might keep this with its own expected width and height attributes on the img tag
$this->logger->notice(....
I am dubious about using GD functions to determine the expected output of Image::rotateDimensions
In fact we would make the expected results dependent on the GD version - or am I missing something?
Leaving in NR status as I believe more eyes are needed here.
Comment #16
jhedstromNeeds a reroll, and the feedback in #15 should be addressed.
Comment #17
ankitgarg CreditAttribution: ankitgarg commentedRerolled
Comment #19
adci_contributor CreditAttribution: adci_contributor commentedFixed the exceptions in #17.
Comment #20
attiks CreditAttribution: attiks commentedPatch is looking good, some small code style issues.
don't use camelCase for local variables.
don't use camelCase for local variables.
Comment #23
mondrakeA recap and a new proposal (patch).
Recap:
RotateImageEffect::transformDimension
method to be able to calculate the expected dimensions resulting from a rotate operation for any angle, and at the image effect level we need such results to be implementation agnostic, i.e. independent from toolkit or from version. We do not take interpolation algorithms in the equation at the moment, so our only input variables are image dimensions and degrees of rotation.RotateImageEffect::transformDimension
would return.Proposal:
Rectangle
component class that calculates in abstraction from any implementation the bounding width and height of a rotated image. For the good and for the bad, it uses the PHP 5.4 GD algorithm – taken from @fietserwin excellent patch in #14. PHP 5.4 because its current results are in fact embedded in many tests.RotateImageEffect::transformDimensions()
is altered so that it now also delivers dimensions for non 90 degree multiples.ImageDimensionsTest
is altered so that it now checks that rotation for non 90 degree multiples ends up in the height and width attributes of theimg
tag.ImageToolkit\Operation\gd\Rotate
is altered so that if the dimensions of the image after rotation do not coincide with the expected ones from theRectangle
class calculation, the image is also resized to expected dimensions.ToolkitGdTest
has been removed as now we no longer need to care about it.No interdiff as in fact it would not make much sense.
Thoughts?
Comment #24
fietserwinWithout reviewing the actual code, but reviewing the proposal:
The math is actually not that difficult. But the math works with real numbers while computer image rotation works with pixels. This may lead to differences in the resulting canvas size depending on rotation point (rotate around corner or rotate around the center point) and rounding (go for safe and always round up (or actually "away from 0") or accept that a small part of a corner pixel may fall outside the result canvas).
Another reason that the resulting canvases can differ is when there is an plain error in the bounding box calculation, like in PHP5.5+ GD. This bug does not result in distorted images but in images where the rotated original is partly cropped or where there is to much background.
The rotation algorithm itself will not influence resulting canvas size and will in principle result in the same image size, though, of course, different algorithms will have different sharpness, anti-aliasing and pixel interpolation.
Thus resizing the image to comply with the calculated bounding box is wrong, we should do a canvas resize!
Let's see how that would work out:
What do you think?
Comment #25
mondrake#24: thanks @fietserwin for review.
To be honest I did not give too much thought about 'what' to do if the actual rotated image dimensions differ from the expected ones. I was just thinking that differences would be minimal, and somehow consistent between widht and height, so that a resize would not distort the final result.
But your points are valid, so setting to NW to do that.
Two questions pop up to me now:
a) what if width is smaller and height is bigger than expected (or viceversa)?
b) #24 describes a situation in GD toolkit where we have the possibility to compare rotation results to expected, taking the resource's width and width after the rotation. But what if, like in ImageMagick, we can only determine the expected dimensions and have to force the image to be consistent? Would resize be OK in that case?
More in general - I think we need an update to the issue summary, and probably we need also screenshots to compare PHP 5.4 and PHP 5.5 results based on this patch.
Comment #26
fietserwinIt is not so much the distortion that I am afraid of, but I have seen images resized by 1 pixel (by the browser or by a qualitatively bad resize algorithm) that lost all sharpness there was and rotated images are already not among the sharpest. So IMO any (additional) resize operation on the image itself is bad. That was another reason to propose canvas resize and not image resize (and it's faster as well, but performance is not an issue for me in the case of derivative creation).
I don't see a problem with your point a), we jsut create a canvas with a solid given background color and place the rotated image on that canvas, automatically cropping or adding background.
Other options we have:
This is difficult indeed. I think I (still) prefer the latest option. Anyway, whatever we choose, I think we will have to wait for 8.1 (API addition) or 8.0.1.
Comment #27
mondrakePHP requirements have been raised to 5.5, so it does not make sense to keep an algorithm that calculates rotated dimensions based on 5.4.
This patch now has an algorithm that mimicks the calculation of dimensions done in PHP 5.5+ (after some hair pulling to understand the imprecisions issue as pointed out in #14, thanks again @fietserwin for posting that patch!). Again the logic of that algorithm may be questionable, but I believe we need to be pragmatic and just adopt the one logic that will be most commonly used.
Here I have dropped the changes to the GD rotate operations discussed between #23 and #26 - AFAICS all the different interpolation methods used in GD 5.5 for image rotation rely on the same algorithm to determine the dimensions of the canvas before starting to copy the pixels at destination. We may want to explore in a spin-off issue if the potential use of the imagesetinterpolation() function in contrib could affect dimensions, but that seems quite an edge case.
Note: do not be scared by the size of the patch - most of it is test data for PHPUnit... :)
Comment #29
mondrakeForgot to change another instance of rotate test :(
Comment #33
mondrake#29 is green
Comment #34
mondrakeFound some flaws in the algorithm matching PHP GD calculations. Working on a patch.
Comment #35
mondrakeI could not sort out how to deal with +/- 1 pixel differences in some cases, due to imprecision differences between the use of C floats internally in GD, and of C doubles by PHP.Here I am taking a different approach - an utility class called by RotateImageEffect actually creates a new GD image via the GD toolkit, rotates it, and returns the dimensions after rotation. It's much simpler, but has a small performance impact and makes the calculation dependent on the installed PHP version.Cannot do any better ATM, if we find an algorithm that can match in PHP the same results that GD provides we can go back to it. The PHP unit test data sample is taken from PHP 5.5 GD (I added more samples for float and negative angles); that one can be used to test any alternative algorithm.EDIT: do not consider this.
Comment #37
mondrakeFixed the Rectangle class to cope with negative and float rotation angles. Added more test data in the PHPUnit test class to deal with negative, float and multiple of 30 degrees rotations. The latter one are the trickiest to deal with as it's where imprecision correction kicks in, really. There are 2000 test samples just for these.
Comment #41
mondrake#37 is green.
Comment #42
claudiu.cristeaThe 2 paragraphs need to be separated with a line OR merged into one paragraph?
Is this utility class needed only for PHP <5.5? If so, we can drop it because Drupal 8 requires >=5.5. Maybe I misunderstood the phrase.
@return array[]
?Looks good to me otherwise.
Comment #43
mondrake#42 - fixed all three points.
@claudiu.cristea - thanks for review. We need this utility class regardless of the PHP version. Its purpose is to calculate rotated image dimensions in abstract, without calling GD. This way,
RotateImageEffect::transformDimensions
can calculate expected dimensions after rotation on an algorithmic basis without calling GD primitives.ImageMagick contrib also needs this (there's a copy of it in the contrib module), because since ImageMagick does not operate on an image in memory but produces a sequence of command line arguments for the convert binary, we need to be able to calculate dimensions without having the possibility to access an image resource. See #1551262: Rotate should alter dimensions.
Comment #44
mondrakeUpdated IS.
Comment #45
claudiu.cristea@mondrake, yes, that I understood. But if I remember well there's something related to precision in PHP 5.5 GD. So, based on the former comment, I thought, on first read, that the class is a 5.5 fix only. Now it makes more sense. Thank you.
+1 for the test :)
Comment #48
mondrakebot failure. back to RTBC per #45
Comment #49
mondrakeComment #50
mondrakeAdded beta eval.
Comment #51
fietserwinWhat if any of these is negative? Silent failures later on? Can't we throw an (invalid argument) exception?
The GD rotate operation ensures that imagerotate() is never called with a negative angle (because of the errors that GD has). So I would like to propose to remove all checks for negative angles as well as all tests with negative angles. Also rephrase the class doc a bit so this is documented.
This also means you can get rid of $degrees and use $angle everywhere $degrees is used now.
If $angle indeed is a float, this can never be true: use == instead of ===.
use ==; remove negative angle; personally I prefer less brackets when not needed and the in simple cases like here.
Whole section can be removed: only for negative angles, will never be used
Missing @param lines in documentation.
Comment #52
mondrake#51 all done, thanks @fietserwin for review.
#51.2 - I missed that gd_rotate normalises angle to 0-360. This means that to have results for Rectangle that match gd_rotate, we should have test data that uses the GD toolkit instead of imagerotate() directly. This greatly simplifies the Rectangle class algorithm as most of the precision issues are related to negative angles multiple of 30° that are passed directly to imagerotate(). Still, our Rectangle utility class needs to return results for negative angles, so I kept test data for negative angles in the PHPUnit test.
Comment #53
mondrakeComment #54
fietserwinLooks good, includes even tests for the exception now thrown by the constructor.
Note: I'm not sure about the D7 backport tag, but that's not for now.
Comment #55
mondrakeThere's an additional use case for this in #2583041: GD toolkit & operations should catch \Throwable to fail gracefully in case of errors.
Comment #57
mondrakeApparently, bot failure in #56
Comment #61
mondrakebots're dizzy today
Comment #62
xjmQuite extensive test coverage in this patch!
Since this adds to the API and additional functionality, targeting against 8.1.x per https://www.drupal.org/core/d8-allowed-changes#minor. I'm also not entirely clear on how this is a bug (even after reading #2420751-9: ImageEffectBase::transformDimensions() should have a sane default implementation comments 9 and 12, but leaving that as-is for now).
Thanks everyone!
Comment #63
xjmAlso tagging for an issue summary update to help explain in more detail why this is a bug.
Comment #64
mondrakeHidden old files.
Comment #65
mondrake#63: in fact the most sensible description of the bug I can find is in #2420751-13: ImageEffectBase::transformDimensions() should have a sane default implementation:
Updated the IS accordingly. Whether this still qualifies as a bug I do not know, but it's definitely a feature needed by ImageMagick (see #2620786: Remove the Rectangle class and #2568619: Plan for ImageMagick 8) and potentially in core too (#2583041: GD toolkit & operations should catch \Throwable to fail gracefully in case of errors).
Comment #66
alexpottGiven that this is a behaviour change I think putting this only in 8.1.x makes sense. I've ummed and ahhed about whether a change record is required - given that the outcome of this change is that rotated images always have dimensions if the angle is not random and imagemagick does not need the class I'm not sure that it is required.
I think it is a shame we don't have Drupal\Component\Image but making that now does not make a lot of sense since Color and Image already exist in Utility...
I'm not sure this can be ported to Drupal 7 since image dimension will change (to be consistent with PHP 5.5 GD regardless of which version) - this does not affect Drupal 8 because it only supports PHP5.5 and up.
Committed 1a6baa4 and pushed to 8.1.x. Thanks!
Comment #68
mondrake@alexpott re #66:
I have created a draft CR https://www.drupal.org/node/2645822, please review and publish if it's OK.
It takes into account #2420789: Make RotateImageEffect's random rotation deterministic, so ::transformDimensions() can return the actual dimensions, which will remove the last case of missing width/height attributes for random rotation angles.
Theoretically porting to D7 is still possible, since in the patch in #23 the Rectangle class can provide dimension calculation for PHP <= 5.4 while the one that was committed is for PHP 5.5 and above. So an implementation in D7 could check the PHP version in use and then use one or the other version to make the calculation. Not sure it's worth the effort, though.