Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 107
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Tonesque
0.00% covered (danger)
0.00%
0 / 106
0.00% covered (danger)
0.00%
0 / 7
992
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 imagecreatefromurl
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
182
 color
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 grab_points
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
20
 grab_color
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
 get_color
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 contrast
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Tonesque
4 * Grab an average color representation from an image.
5 *
6 * @author Automattic
7 * @author Matias Ventura
8 * @package automattic/jetpack
9 */
10
11if ( ! class_exists( 'Tonesque' ) ) {
12    /**
13     * Color representation class.
14     */
15    class Tonesque {
16        /**
17         * Image URL.
18         *
19         * @var string
20         */
21        private $image_url = '';
22        /**
23         * Image identifier representing the image.
24         *
25         * @var null|object
26         */
27        private $image_obj = null;
28        /**
29         * Color code.
30         *
31         * @var string
32         */
33        private $color = '';
34
35        /**
36         * Constructor.
37         *
38         * @param string $image_url Image URL.
39         */
40        public function __construct( $image_url ) {
41            if ( ! class_exists( 'Jetpack_Color' ) ) {
42                require_once __DIR__ . '/class.color.php';
43            }
44
45            $this->image_url = esc_url_raw( $image_url );
46            $this->image_url = trim( $this->image_url );
47            /**
48             * Allows any image URL to be passed in for $this->image_url.
49             *
50             * @module theme-tools
51             *
52             * @since 2.5.0
53             *
54             * @param string $image_url The URL to any image
55             */
56            $this->image_url = apply_filters( 'tonesque_image_url', $this->image_url );
57
58            $this->image_obj = self::imagecreatefromurl( $this->image_url );
59        }
60
61        /**
62         * Get an image object from a URL.
63         *
64         * @param string $image_url Image URL.
65         *
66         * @return object|bool Image object or false if the image could not be loaded.
67         */
68        public static function imagecreatefromurl( $image_url ) {
69            $data = null;
70
71            // If it's a URL.
72            if ( preg_match( '#^https?://#i', $image_url ) ) {
73                // If it's a url pointing to a local media library url.
74                $content_url = content_url();
75                $_image_url  = set_url_scheme( $image_url );
76                if ( str_starts_with( $_image_url, $content_url ) ) {
77                    $_image_path = str_replace( $content_url, WP_CONTENT_DIR, $_image_url );
78                    if ( file_exists( $_image_path ) ) {
79                        $filetype = wp_check_filetype( $_image_path );
80                        $type     = $filetype['type'];
81
82                        if ( str_starts_with( $type, 'image/' ) ) {
83                            $data = file_get_contents( $_image_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
84                        }
85                    }
86                }
87
88                if ( empty( $data ) ) {
89                    $response      = wp_safe_remote_get( $image_url );
90                    $response_code = wp_remote_retrieve_response_code( $response );
91                    if (
92                        is_wp_error( $response )
93                        || ! $response_code
94                        || $response_code < 200
95                        || $response_code >= 300
96                    ) {
97                        return false;
98                    }
99                    $data = wp_remote_retrieve_body( $response );
100                }
101            }
102
103            // If it's a local path in our WordPress install.
104            if ( file_exists( $image_url ) ) {
105                $filetype = wp_check_filetype( $image_url );
106                $type     = $filetype['type'];
107
108                if ( str_starts_with( $type, 'image/' ) ) {
109                    $data = file_get_contents( $image_url ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
110                }
111            }
112
113            if ( null === $data ) {
114                return false;
115            }
116
117            // Now turn it into an image and return it.
118            return imagecreatefromstring( $data );
119        }
120
121        /**
122         * Construct object from image.
123         *
124         * @param string $type Type (hex, rgb, hsv) (optional).
125         *
126         * @return string|bool color as a string formatted as $type or false if the image could not be loaded.
127         */
128        public function color( $type = 'hex' ) {
129            // Bail if there is no image to work with.
130            if ( ! $this->image_obj ) {
131                return false;
132            }
133
134            // Finds dominant color.
135            $color = self::grab_color();
136            // Passes value to Color class.
137            return self::get_color( $color, $type );
138        }
139
140        /**
141         * Grabs the color index for each of five sample points of the image
142         *
143         * @param string $type can be 'index' or 'hex'.
144         *
145         * @return array|false color indices or false if the image could not be loaded.
146         */
147        public function grab_points( $type = 'index' ) {
148            $img = $this->image_obj;
149            if ( ! $img ) {
150                return false;
151            }
152
153            $height = imagesy( $img );
154            $width  = imagesx( $img );
155
156            /*
157            * Sample five points in the image
158            * based on rule of thirds and center.
159            */
160            $topy    = (int) round( $height / 3 );
161            $bottomy = (int) round( ( $height / 3 ) * 2 );
162            $leftx   = (int) round( $width / 3 );
163            $rightx  = (int) round( ( $width / 3 ) * 2 );
164            $centery = (int) round( $height / 2 );
165            $centerx = (int) round( $width / 2 );
166
167            // Cast those colors into an array.
168            $points = array(
169                imagecolorat( $img, $leftx, $topy ),
170                imagecolorat( $img, $rightx, $topy ),
171                imagecolorat( $img, $leftx, $bottomy ),
172                imagecolorat( $img, $rightx, $bottomy ),
173                imagecolorat( $img, $centerx, $centery ),
174            );
175
176            if ( 'hex' === $type ) {
177                foreach ( $points as $i => $p ) {
178                    $c            = imagecolorsforindex( $img, $p );
179                    $points[ $i ] = self::get_color(
180                        array(
181                            'r' => $c['red'],
182                            'g' => $c['green'],
183                            'b' => $c['blue'],
184                        ),
185                        'hex'
186                    );
187                }
188            }
189
190            return $points;
191        }
192
193        /**
194         * Finds the average color of the image based on five sample points
195         *
196         * @return array|bool array with rgb color or false if the image could not be loaded.
197         */
198        public function grab_color() {
199            $img = $this->image_obj;
200            if ( ! $img ) {
201                return false;
202            }
203
204            $rgb = self::grab_points();
205
206            $r = array();
207            $g = array();
208            $b = array();
209
210            /*
211            * Process the color points
212            * Find the average representation
213            */
214            foreach ( $rgb as $color ) {
215                $index = imagecolorsforindex( $img, $color );
216                $r[]   = $index['red'];
217                $g[]   = $index['green'];
218                $b[]   = $index['blue'];
219            }
220            $red   = round( array_sum( $r ) / 5 );
221            $green = round( array_sum( $g ) / 5 );
222            $blue  = round( array_sum( $b ) / 5 );
223
224            // The average color of the image as rgb array.
225            $color = array(
226                'r' => $red,
227                'g' => $green,
228                'b' => $blue,
229            );
230
231            return $color;
232        }
233
234        /**
235         * Get a Color object using /lib class.color
236         * Convert to appropriate type
237         *
238         * @param string $color Color code.
239         * @param string $type  Color type (rgb, hex, hsv).
240         *
241         * @return string
242         */
243        public function get_color( $color, $type ) {
244            $c           = new Jetpack_Color( $color, 'rgb' );
245            $this->color = $c;
246
247            switch ( $type ) {
248                case 'rgb':
249                    $color = implode( ',', $c->toRgbInt() );
250                    break;
251                case 'hex':
252                    $color = $c->toHex();
253                    break;
254                case 'hsv':
255                    $color = implode( ',', $c->toHsvInt() );
256                    break;
257                default:
258                    return $c->toHex();
259            }
260
261            return $color;
262        }
263
264        /**
265         *
266         * Checks contrast against main color
267         * Gives either black or white for using with opacity
268         *
269         * @return string|bool Returns black or white or false if the image could not be loaded.
270         */
271        public function contrast() {
272            if ( ! $this->color ) {
273                return false;
274            }
275
276            $c = $this->color->getMaxContrastColor();
277            return implode( ',', $c->toRgbInt() );
278        }
279    }
280}