@@ -60,7 +60,185 @@ final class Image
6060 */
6161 public static function resize (\Imagick $ source , int $ boxWidth , int $ boxHeight , array $ options = []) : \Imagick
6262 {
63- $ results = self ::resizeMulti ($ source , [['width ' => $ boxWidth , 'height ' => $ boxHeight ]], $ options );
63+ $ boxSizes = [['width ' => $ boxWidth , 'height ' => $ boxHeight ]];
64+ $ options += self ::DEFAULT_OPTIONS ;
65+
66+ //algorithm inspired from http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html
67+ //use of 2x2 binning is arguably the best quality one will get downsizing and is what lots of hardware does in
68+ //the photography field, while being reasonably fast. Upsizing is more subjective but you can't get much
69+ //better than bicubic which is what is used here.
70+
71+ $ color = $ options ['color ' ];
72+ Util::ensure (true , is_string ($ color ), InvalidArgumentException::class, ['$options["color"] was not a string ' ]);
73+
74+ $ upsize = $ options ['upsize ' ];
75+ Util::ensure (true , is_bool ($ upsize ), InvalidArgumentException::class, ['$options["upsize"] was not a bool ' ]);
76+
77+ $ bestfit = $ options ['bestfit ' ];
78+ Util::ensure (true , is_bool ($ bestfit ), InvalidArgumentException::class, ['$options["bestfit"] was not a bool ' ]);
79+
80+ $ blurBackground = $ options ['blurBackground ' ];
81+ Util::ensure (
82+ true ,
83+ is_bool ($ blurBackground ),
84+ InvalidArgumentException::class,
85+ ['$options["blurBackground"] was not a bool ' ]
86+ );
87+
88+ $ blurValue = $ options ['blurValue ' ];
89+ Util::ensure (
90+ true ,
91+ is_float ($ blurValue ),
92+ InvalidArgumentException::class,
93+ ['$options["blurValue"] was not a float ' ]
94+ );
95+ $ maxWidth = $ options ['maxWidth ' ];
96+ Util::ensure (true , is_int ($ maxWidth ), InvalidArgumentException::class, ['$options["maxWidth"] was not an int ' ]);
97+
98+ $ maxHeight = $ options ['maxHeight ' ];
99+ Util::ensure (
100+ true ,
101+ is_int ($ maxHeight ),
102+ InvalidArgumentException::class,
103+ ['$options["maxHeight"] was not an int ' ]
104+ );
105+
106+ foreach ($ boxSizes as $ boxSizeKey => $ boxSize ) {
107+ if (!isset ($ boxSize ['width ' ]) || !is_int ($ boxSize ['width ' ])) {
108+ throw new InvalidArgumentException ('a width in a $boxSizes value was not an int ' );
109+ }
110+
111+ if (!isset ($ boxSize ['height ' ]) || !is_int ($ boxSize ['height ' ])) {
112+ throw new InvalidArgumentException ('a height in a $boxSizes value was not an int ' );
113+ }
114+
115+ if ($ boxSize ['width ' ] > $ maxWidth || $ boxSize ['width ' ] <= 0 ) {
116+ throw new InvalidArgumentException ('a $boxSizes width was not between 0 and $options["maxWidth"] ' );
117+ }
118+
119+ if ($ boxSize ['height ' ] > $ maxHeight || $ boxSize ['height ' ] <= 0 ) {
120+ throw new InvalidArgumentException ('a $boxSizes height was not between 0 and $options["maxHeight"] ' );
121+ }
122+ }
123+
124+ $ results = [];
125+ $ cloneCache = [];
126+ foreach ($ boxSizes as $ boxSizeKey => $ boxSize ) {
127+ $ boxWidth = $ boxSize ['width ' ];
128+ $ boxHeight = $ boxSize ['height ' ];
129+
130+ $ clone = clone $ source ;
131+
132+ self ::rotateImage ($ clone );
133+
134+ $ width = $ clone ->getImageWidth ();
135+ $ height = $ clone ->getImageHeight ();
136+
137+ //ratio over 1 is horizontal, under 1 is vertical
138+ $ boxRatio = $ boxWidth / $ boxHeight ;
139+ //height should be positive since I didnt find a way you could get zero into imagick
140+ $ originalRatio = $ width / $ height ;
141+
142+ $ targetWidth = null ;
143+ $ targetHeight = null ;
144+ $ targetX = null ;
145+ $ targetY = null ;
146+ if ($ width < $ boxWidth && $ height < $ boxHeight && !$ upsize ) {
147+ $ targetWidth = $ width ;
148+ $ targetHeight = $ height ;
149+ $ targetX = ($ boxWidth - $ width ) / 2 ;
150+ $ targetY = ($ boxHeight - $ height ) / 2 ;
151+ } else {
152+ //if box is more vertical than original
153+ if ($ boxRatio < $ originalRatio ) {
154+ $ targetWidth = $ boxWidth ;
155+ $ targetHeight = (int )((double )$ boxWidth / $ originalRatio );
156+ $ targetX = 0 ;
157+ $ targetY = ($ boxHeight - $ targetHeight ) / 2 ;
158+ } else {
159+ $ targetWidth = (int )((double )$ boxHeight * $ originalRatio );
160+ $ targetHeight = $ boxHeight ;
161+ $ targetX = ($ boxWidth - $ targetWidth ) / 2 ;
162+ $ targetY = 0 ;
163+ }
164+ }
165+
166+ //do iterative downsize by halfs (2x2 binning is a common name) on dimensions that are bigger than target
167+ //width and height
168+ while (true ) {
169+ $ widthReduced = false ;
170+ $ widthIsHalf = false ;
171+ if ($ width > $ targetWidth ) {
172+ $ width = (int )($ width / 2 );
173+ $ widthReduced = true ;
174+ $ widthIsHalf = true ;
175+ if ($ width < $ targetWidth ) {
176+ $ width = $ targetWidth ;
177+ $ widthIsHalf = false ;
178+ }
179+ }
180+
181+ $ heightReduced = false ;
182+ $ heightIsHalf = false ;
183+ if ($ height > $ targetHeight ) {
184+ $ height = (int )($ height / 2 );
185+ $ heightReduced = true ;
186+ $ heightIsHalf = true ;
187+ if ($ height < $ targetHeight ) {
188+ $ height = $ targetHeight ;
189+ $ heightIsHalf = false ;
190+ }
191+ }
192+
193+ if (!$ widthReduced && !$ heightReduced ) {
194+ break ;
195+ }
196+
197+ $ cacheKey = "{$ width }x {$ height }" ;
198+ if (isset ($ cloneCache [$ cacheKey ])) {
199+ $ clone = clone $ cloneCache [$ cacheKey ];
200+ continue ;
201+ }
202+
203+ if ($ clone ->resizeImage ($ width , $ height , \Imagick::FILTER_BOX , 1.0 ) !== true ) {
204+ //cumbersome to test
205+ throw new \Exception ('Imagick::resizeImage() did not return true ' );//@codeCoverageIgnore
206+ }
207+
208+ if ($ widthIsHalf && $ heightIsHalf ) {
209+ $ cloneCache [$ cacheKey ] = clone $ clone ;
210+ }
211+ }
212+
213+ if ($ upsize && ($ width < $ targetWidth || $ height < $ targetHeight )) {
214+ if ($ clone ->resizeImage ($ targetWidth , $ targetHeight , \Imagick::FILTER_CUBIC , 1.0 , $ bestfit ) !== true ) {
215+ //cumbersome to test
216+ throw new \Exception ('Imagick::resizeImage() did not return true ' );//@codeCoverageIgnore
217+ }
218+ }
219+
220+ if ($ clone ->getImageHeight () === $ boxHeight && $ clone ->getImageWidth () === $ boxWidth ) {
221+ $ results [$ boxSizeKey ] = $ clone ;
222+ continue ;
223+ }
224+
225+ //put image in box
226+ $ canvas = self ::getBackgroundCanvas ($ source , $ color , $ blurBackground , $ blurValue , $ boxWidth , $ boxHeight );
227+ if ($ canvas ->compositeImage ($ clone , \Imagick::COMPOSITE_ATOP , $ targetX , $ targetY ) !== true ) {
228+ //cumbersome to test
229+ throw new \Exception ('Imagick::compositeImage() did not return true ' );//@codeCoverageIgnore
230+ }
231+
232+ //reason we are not supporting the options in self::write() here is because format, and strip headers are
233+ //only relevant once written Imagick::stripImage() doesnt even have an effect until written
234+ //also the user can just call that function with the resultant $canvas
235+ $ results [$ boxSizeKey ] = $ canvas ;
236+ }
237+
238+ foreach ($ cloneCache as $ clone ) {
239+ $ clone ->destroy ();
240+ }
241+
64242 return $ results [0 ];
65243 }
66244
0 commit comments