@@ -137,47 +137,29 @@ public static function getPalette($sourceImage, $colorCount = 10, $quality = 10,
137137 throw new \InvalidArgumentException ('The quality argument must be an integer greater than one. ' );
138138 }
139139
140- $ pixelArray = static ::loadImage ($ sourceImage , $ quality , $ area );
141- if (!count ($ pixelArray )) {
140+ $ histo = [];
141+ $ numPixelsAnalyzed = static ::loadImage ($ sourceImage , $ quality , $ histo , $ area );
142+ if ($ numPixelsAnalyzed === 0 ) {
142143 throw new \RuntimeException ('Unable to compute the color palette of a blank or transparent image. ' , 1 );
143144 }
144145
145- // Send array to quantize function which clusters values
146+ // Send histogram to quantize function which clusters values
146147 // using median cut algorithm
147- $ cmap = static ::quantize ($ pixelArray , $ colorCount );
148+ $ cmap = static ::quantize ($ numPixelsAnalyzed , $ colorCount, $ histo );
148149 $ palette = $ cmap ->palette ();
149150
150151 return $ palette ;
151152 }
152153
153- /**
154- * Histo: 1-d array, giving the number of pixels in each quantized region of color space.
155- *
156- * @param array $pixels
157- *
158- * @return array
159- */
160- private static function getHisto ($ pixels )
161- {
162- $ histo = [];
163-
164- foreach ($ pixels as $ rgb ) {
165- list ($ red , $ green , $ blue ) = static ::getColorsFromIndex ($ rgb );
166- $ bucketIndex = static ::getColorIndex ($ red , $ green , $ blue );
167- $ histo [$ bucketIndex ] = (isset ($ histo [$ bucketIndex ]) ? $ histo [$ bucketIndex ] : 0 ) + 1 ;
168- }
169-
170- return $ histo ;
171- }
172-
173154 /**
174155 * @param mixed $sourceImage Path/URL to the image, GD resource, Imagick instance, or image as binary string
175- * @param int $quality
156+ * @param int $quality Analyze every $quality pixels
157+ * @param array $histo Histogram
176158 * @param array|null $area
177159 *
178- * @return SplFixedArray
160+ * @return int
179161 */
180- private static function loadImage ($ sourceImage , $ quality , array $ area = null )
162+ private static function loadImage ($ sourceImage , $ quality , array & $ histo , array $ area = null )
181163 {
182164 $ loader = new ImageLoader ();
183165 $ image = $ loader ->load ($ sourceImage );
@@ -197,25 +179,46 @@ private static function loadImage($sourceImage, $quality, array $area = null)
197179 }
198180 }
199181
200- $ pixelCount = $ width * $ height ;
182+ // Fill a SplArray with zeroes to initialize the 5-bit buckets and avoid having to check isset in the pixel loop.
183+ // There are 32768 buckets because each color is 5 bits (15 bits total for RGB values).
184+ $ totalBuckets = (1 << (3 * self ::SIGBITS ));
185+ $ histoSpl = new SplFixedArray ($ totalBuckets );
186+ for ($ i = 0 ; $ i < $ totalBuckets ; $ i ++) {
187+ $ histoSpl [$ i ] = 0 ;
188+ }
201189
202- // Store the RGB values in an array format suitable for quantize function
203- // SplFixedArray is faster and more memory-efficient than normal PHP array.
204- $ pixelArray = new SplFixedArray (ceil ($ pixelCount / $ quality ));
190+ $ numUsefulPixels = 0 ;
191+ $ pixelCount = $ width * $ height ;
205192
206- $ size = 0 ;
207- for ($ i = 0 ; $ i < $ pixelCount ; $ i = $ i + $ quality ) {
193+ for ($ i = 0 ; $ i < $ pixelCount ; $ i += $ quality ) {
208194 $ x = $ startX + ($ i % $ width );
209195 $ y = (int ) ($ startY + $ i / $ width );
210196 $ color = $ image ->getPixelColor ($ x , $ y );
211197
212- if (static ::isClearlyVisible ($ color ) && static ::isNonWhite ($ color )) {
213- $ pixelArray [$ size ++] = static ::getColorIndex ($ color ->red , $ color ->green , $ color ->blue , 8 );
214- // TODO : Compute directly the histogram here ? (save one iteration over all pixels)
198+ // Pixel is too transparent. Its alpha value is larger (more transparent) than THRESHOLD_ALPHA.
199+ // PHP's transparency range (0-127 opaque-transparent) is reverse that of Javascript (0-255 tranparent-opaque).
200+ if ($ color ->alpha > self ::THRESHOLD_ALPHA ) {
201+ continue ;
202+ }
203+
204+ // Pixel is too white to be useful. Its RGB values all exceed THRESHOLD_WHITE
205+ if ($ color ->red > self ::THRESHOLD_WHITE && $ color ->green > self ::THRESHOLD_WHITE && $ color ->blue > self ::THRESHOLD_WHITE ) {
206+ continue ;
215207 }
208+
209+ // Count this pixel in its histogram bucket.
210+ $ numUsefulPixels ++;
211+ $ bucketIndex = static ::getColorIndex ($ color ->red , $ color ->green , $ color ->blue );
212+ $ histoSpl [$ bucketIndex ] = $ histoSpl [$ bucketIndex ] + 1 ;
216213 }
217214
218- $ pixelArray ->setSize ($ size );
215+ // Copy the histogram buckets that had pixels back to a normal array.
216+ $ histo = [];
217+ foreach ($ histoSpl as $ bucketInt => $ numPixels ) {
218+ if ($ numPixels > 0 ) {
219+ $ histo [$ bucketInt ] = $ numPixels ;
220+ }
221+ }
219222
220223 // Don't destroy a resource passed by the user !
221224 // TODO Add a method in ImageLoader to know if the image should be destroy
@@ -224,31 +227,7 @@ private static function loadImage($sourceImage, $quality, array $area = null)
224227 $ image ->destroy ();
225228 }
226229
227- return $ pixelArray ;
228- }
229-
230- /**
231- * @param object $color
232- *
233- * @return bool
234- */
235- protected static function isClearlyVisible ($ color )
236- {
237- return $ color ->alpha <= self ::THRESHOLD_ALPHA ;
238- }
239-
240- /**
241- * @param object $color
242- *
243- * @return bool
244- */
245- protected static function isNonWhite ($ color )
246- {
247- return !(
248- $ color ->red > self ::THRESHOLD_WHITE &&
249- $ color ->green > self ::THRESHOLD_WHITE &&
250- $ color ->blue > self ::THRESHOLD_WHITE
251- );
230+ return $ numUsefulPixels ;
252231 }
253232
254233 /**
@@ -377,15 +356,20 @@ private static function sumColors($axis, $histo, $vBox)
377356 $ sum = 0 ;
378357 foreach ($ secondRange as $ secondColor ) {
379358 foreach ($ thirdRange as $ thirdColor ) {
380- list ( $ redBucket , $ greenBucket , $ blueBucket ) = static :: rearrangeColors (
381- $ colorIterateOrder ,
382- $ firstColor ,
383- $ secondColor ,
384- $ thirdColor
385- ) ;
359+ // Rearrange color components
360+ $ bucket = [
361+ $ colorIterateOrder [ 0 ] => $ firstColor ,
362+ $ colorIterateOrder [ 1 ] => $ secondColor ,
363+ $ colorIterateOrder [ 2 ] => $ thirdColor,
364+ ] ;
386365
387366 // The getColorIndex function takes RGB values instead of buckets. The left shift converts our bucket into its RGB value.
388- $ bucketIndex = static ::getColorIndex ($ redBucket << self ::RSHIFT , $ greenBucket << self ::RSHIFT , $ blueBucket << self ::RSHIFT , self ::SIGBITS );
367+ $ bucketIndex = static ::getColorIndex (
368+ $ bucket ['r ' ] << self ::RSHIFT ,
369+ $ bucket ['g ' ] << self ::RSHIFT ,
370+ $ bucket ['b ' ] << self ::RSHIFT ,
371+ self ::SIGBITS
372+ );
389373
390374 if (isset ($ histo [$ bucketIndex ])) {
391375 $ sum += $ histo [$ bucketIndex ];
@@ -399,29 +383,6 @@ private static function sumColors($axis, $histo, $vBox)
399383 return [$ total , $ partialSum ];
400384 }
401385
402- /**
403- * @param array $order
404- * @param int $color1
405- * @param int $color2
406- * @param int $color3
407- *
408- * @return array
409- */
410- private static function rearrangeColors (array $ order , $ color1 , $ color2 , $ color3 )
411- {
412- $ data = [
413- $ order [0 ] => $ color1 ,
414- $ order [1 ] => $ color2 ,
415- $ order [2 ] => $ color3 ,
416- ];
417-
418- return [
419- $ data ['r ' ],
420- $ data ['g ' ],
421- $ data ['b ' ],
422- ];
423- }
424-
425386 /**
426387 * @param VBox $vBox
427388 * @param array $order
@@ -490,20 +451,24 @@ private static function quantizeIter(&$priorityQueue, $target, $histo)
490451 }
491452
492453 /**
493- * @param SplFixedArray|array $ pixels
454+ * @param $numPixels Number of image pixels analyzed
494455 * @param $maxColors
456+ * @param array $histo Histogram
495457 *
496458 * @return bool|CMap
497459 */
498- private static function quantize ($ pixels , $ maxColors )
460+ private static function quantize ($ numPixels , $ maxColors, array & $ histo )
499461 {
500- // short-circuit
501- if (!count ($ pixels ) || $ maxColors < 2 || $ maxColors > 256 ) {
502- // echo 'wrong number of maxcolors'."\n";
503- return false ;
462+ // Short-Circuits
463+ if ($ numPixels === 0 ) {
464+ throw new \InvalidArgumentException ('Zero useable pixels found in image. ' );
465+ }
466+ if ($ maxColors < 2 || $ maxColors > 256 ) {
467+ throw new \InvalidArgumentException ('The maxColors parameter must be between 2 and 256 inclusive. ' );
468+ }
469+ if (count ($ histo ) === 0 ) {
470+ throw new \InvalidArgumentException ('Image produced an empty histogram. ' );
504471 }
505-
506- $ histo = static ::getHisto ($ pixels );
507472
508473 // check that we aren't below maxcolors already
509474 //if (count($histo) <= $maxcolors) {
0 commit comments