FocusCropDataCalculator::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 5
nc 1
nop 4
1
<?php
2
/**
3
 * This file is part of the IrishDan\ResponsiveImageBundle package.
4
 *
5
 * (c) Daniel Byrne <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE file that was distributed with this source
8
 * code.
9
 */
10
11
namespace IrishDan\ResponsiveImageBundle\ImageProcessing;
12
13
/**
14
 * Class FocusCropDataCalculator
15
 *
16
 * @package IrishDan\ResponsiveImageBundle\ImageProcessing
17
 */
18
class FocusCropDataCalculator
19
{
20
    private $cropCoordinates;
21
    private $focusCoordinates;
22
    private $styleWidth;
23
    private $styleHeight;
24
    private $geometry;
25
26
    /**
27
     * FocusCropDataCalculator constructor.
28
     *
29
     * @param $cropCoordinates
30
     * @param $focusCoordinates
31
     * @param $styleWidth
32
     * @param $styleHeight
33
     */
34
    public function __construct($cropCoordinates, $focusCoordinates, $styleWidth, $styleHeight)
35
    {
36
        $this->cropCoordinates = $cropCoordinates;
37
        $this->focusCoordinates = $focusCoordinates;
38
        $this->styleWidth = $styleWidth;
39
        $this->styleHeight = $styleHeight;
40
    }
41
42
    /**
43
     * @return array
44
     */
45
    public function getFocusCropData()
46
    {
47
        // If there is a focus rectangle,
48
        // We have the image shape (or crop rectangle),
49
        // and we have the the final image style rectangle.
50
        //
51
        // The image style shape should be as large as possible so,
52
        // there are three possibilities:
53
        // 1: The style rectangle fits inside the crop rectangle vertically.
54
        //    The sides of the image will be cropped.
55
        // 2: The style rectangle fits inside the crop rectangle horizontally.
56
        //    The top and bottom of the image will be cropped.
57
        // 3: The style rectangle fits inside the crop rectangle perfectly.
58
        //    no cropping in required
59
        //
60
        // To determine which type of cropping should be used, the aspect-ratio of the image/crop rectangle ($imageAspectRatio)
61
        // and the aspect-ratio of the style ($styleAspectRatio) are compared.
62
        // 1: $imageAspectRatio > $styleAspectRatio
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
63
        // 2: $imageAspectRatio < $styleAspectRatio
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
64
        // 3: $imageAspectRatio === $styleAspectRatio
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
65
66
        list($x1, $y1, $x2, $y2) = $this->cropCoordinates;
67
        $this->geometry = new CoordinateGeometry($x1, $y1, $x2, $y2);
68
69
        $newWidth = $this->geometry->axisLength('x');
70
        $newHeight = $this->geometry->axisLength('y');
71
72
        // Find out what type of style crop we are dealing with.
73
        // @TODO: Checkout the geometry calculation.
74
        // $imageAspectRatio = $this->geometry->getAspectRatio();
0 ignored issues
show
Unused Code Comprehensibility introduced by
54% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
75
        $imageAspectRatio = $newWidth / $newHeight;
76
        $styleAspectRatio = $this->styleWidth / $this->styleHeight;
77
78
        if ($imageAspectRatio > $styleAspectRatio) {
79
            $axis = 'x';
80
        }
81
        else if ($imageAspectRatio < $styleAspectRatio) {
82
            $axis = 'y';
83
        }
84
        else {
85
            return [
86
                'width'  => $newWidth,
87
                'height' => $newHeight,
88
                'x'      => $this->cropCoordinates[0],
89
                'y'      => $this->cropCoordinates[1],
90
            ];
91
        }
92
93
        return $this->calculateAxisCropData($axis, $this->cropCoordinates, $styleAspectRatio, $newWidth, $newHeight);
94
    }
95
96
    /**
97
     * @param string $axis
98
     * @param        $cropCoordinates
99
     * @param        $aspectRatio
100
     * @param        $newWidth
101
     * @param        $newHeight
102
     *
103
     * @return array
104
     */
105
    protected function calculateAxisCropData($axis = 'x', $cropCoordinates, $aspectRatio, $newWidth, $newHeight)
0 ignored issues
show
Unused Code introduced by
The parameter $aspectRatio is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
106
    {
107
        if ($axis !== 'x' && $axis !== 'y') {
108
            throw new \InvalidArgumentException('$axis can only have a value of x or y. ' . $axis . ' given');
109
        }
110
111
        if ($axis == 'x') {
112
            $cropHeight = $newHeight;
113
114
            // How many times the style height goes into the new height
115
            $scaleFactor = $newHeight / $this->styleHeight;
116
            $cropWidth = $this->styleWidth * $scaleFactor;
117
        }
118
        else {
119
            $cropWidth = $newWidth;
120
121
            // How many times the style height goes into the new height
122
            $scaleFactor = $newWidth / $this->styleWidth;
123
            $cropHeight = $this->styleHeight * $scaleFactor;
124
        }
125
        $data['scale_factor'] = $scaleFactor;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$data was never initialized. Although not strictly required by PHP, it is generally a good practice to add $data = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
126
127
        $cropXOffset = ($axis == 'y') ? $cropCoordinates[0] : $this->getFloatingOffset(
128
            'x',
129
            $cropWidth,
130
            $cropCoordinates[0]
131
        );
132
        $cropYOffset = ($axis == 'y') ? $this->getFloatingOffset(
133
            'y',
134
            $cropHeight,
135
            $cropCoordinates[1]
136
        ) : $cropCoordinates[1];
137
138
        return [
139
            'width'  => $cropWidth,
140
            'height' => $cropHeight,
141
            'x'      => $cropXOffset,
142
            'y'      => $cropYOffset,
143
        ];
144
    }
145
146
    /**
147
     * @param string $axis
148
     * @param        $point
149
     * @param        $start
150
     *
151
     * @return mixed
152
     */
153
    protected function getFloatingOffset($axis = 'y', $point, $start)
154
    {
155
        $offset = $this->findFocusOffset($axis, $point);
156
        $offset = $offset + $start;
157
158
        return $offset;
159
    }
160
161
    /**
162
     * @param        $axis
163
     * @param string $type
164
     *
165
     * @return mixed
166
     */
167
    protected function getFocusPointForAxis($axis, $type = 'near')
168
    {
169
        $cropX1 = $this->cropCoordinates[0];
170
        $cropY1 = $this->cropCoordinates[1];
171
        $cropX2 = $this->cropCoordinates[2];
172
        $cropY2 = $this->cropCoordinates[3];
173
174
        $focusX1 = $this->focusCoordinates[0];
175
        $focusY1 = $this->focusCoordinates[1];
176
        $focusX2 = $this->focusCoordinates[2];
177
        $focusY2 = $this->focusCoordinates[3];
178
179
        if ($type == 'near') {
180
            $point = ${'focus' . $axis . '1'} - ${'crop' . $axis . '1'};
181
        }
182
        else {
183
            $point = ${'focus' . $axis . '2'} - ${'crop' . $axis . '1'};
184
        }
185
186
        return $point;
187
    }
188
189
    /**
190
     * Calculates the offset needed to keep focus rectangle in view with optimal position.
191
     *
192
     * @param string $axis
193
     * @param        $cropLength
194
     *
195
     * @return mixed
196
     */
197
    protected function findFocusOffset($axis = 'x', $cropLength)
198
    {
199
        $axis = ucfirst($axis);
200
201
        // Get the crop and focus information, sudo iptables -A INPUT -s 142.54.166.218 -j REJECT
202
        // and the length.
203
        $imageLength = $this->geometry->axisLength($axis);
204
205
        // If there are no focus coordinates the image should be cropped from center.
206
        if (empty($this->focusCoordinates)) {
207
            return ($imageLength - $cropLength) / 2;
208
        }
209
210
        // Offsetting on either the x or the y axis.
211
        // Subtract the crop rectangle.
212
        $focusNear = $this->getFocusPointForAxis($axis, 'near');
213
        $focusFar = $this->getFocusPointForAxis($axis, 'far');
214
215
        $focusLength = $focusFar - $focusNear;
216
        $focusCenter = round(($focusNear + $focusFar) / 2);
217
218
        // There are two possibilities.
219
        // 1: The focus area is longer then the desired crop length.
220
        //    In this case we simple center ont he crop area on the the center of the focus area.
221
        //    Both sides of the image will be clipped, and both sides of the crop area will be missing a piece.
222
        // 2: In most cases the focus rectangle will fit nicely within the crop area.
223
        //    We must find the optimal position to crop from.
224
        $focusType = $focusLength >= $cropLength ? 'in' : 'out';
225
226
        // First case.
227
        if ($focusType == 'in') {
228
            return $focusCenter - ($cropLength / 2);
229
        } // Second case.
230
        else {
231
            // We will find a range of valid offsets,
232
            $validOffsets = $this->getValidOffsets($focusNear, $focusFar, $cropLength, $imageLength);
233
234
            // Return the most optimal offset.
235
            return $this->getOptimalOffset($validOffsets);
236
        }
237
    }
238
239
    /**
240
     * @param array $validOffsets
241
     *
242
     * @return int|mixed
243
     */
244
    protected function getOptimalOffset(array $validOffsets)
245
    {
246
        $offset = 0;
247
248
        if (!empty($validOffsets)) {
249
            asort($validOffsets);
250
            $offsets = array_keys($validOffsets);
251
            $offset = reset($offsets);
252
        }
253
254
        return $offset;
255
    }
256
257
    /**
258
     * @param $focusNear
259
     * @param $focusFar
260
     * @param $cropLength
261
     * @param $imageLength
262
     *
263
     * @return array
264
     */
265
    protected function getValidOffsets($focusNear, $focusFar, $cropLength, $imageLength)
266
    {
267
        $nearGap = $focusNear;
268
        $farGap = $imageLength - $focusFar;
269
        $offFactor = $nearGap / $farGap;
270
271
        // Will need the maximum and minimum offset also.
272
        $maxOffset = $imageLength - $cropLength;
273
        $minOffset = 0;
274
275
        $validOffsets = [];
276
        for ($i = $minOffset; $i <= $maxOffset; $i++) {
277
            if ($this->isInBounds($i, $cropLength, $imageLength, $focusNear, $focusFar)) {
278
                // Need a factor of near / far to compare to offFactor.
279
                // Closest to that wins.
280
                $near = $focusNear - $i;
281
                $far = ($i + $cropLength) - $focusFar;
282
                if ($near != 0 && $far != 0) {
283
                    $optimalFactor = ($near / $far) / $offFactor;
284
                    $optimalFactor = abs($optimalFactor);
285
286
                    $theTest = abs($optimalFactor - 1);
287
                    $validOffsets[$i] = $theTest;
288
                }
289
            }
290
        }
291
292
        return $validOffsets;
293
    }
294
295
    /**
296
     * Tests if a given offset is valid.
297
     * Valid offsets cropped images will include the focus rectangle and will not fall outside of the original image.
298
     *
299
     * @param $point
300
     * @param $cropLength
301
     * @param $imageLength
302
     * @param $focusNear
303
     * @param $focusFar
304
     *
305
     * @return bool
306
     */
307
    protected function isInBounds($point, $cropLength, $imageLength, $focusNear, $focusFar)
308
    {
309
        $inBounds = false;
310
        if ($point + $cropLength <= $imageLength && $point <= $focusNear && $point + $cropLength >= $focusFar) {
311
            $inBounds = true;
312
        }
313
314
        return $inBounds;
315
    }
316
}