array( 0, 0, 0 ), 'white' => array( 255, 255, 255 ), 'red' => array( 255, 0, 0 ), 'green' => array( 0, 128, 0 ), 'blue' => array( 0, 0, 255 ), 'yellow' => array( 255, 255, 0 ), 'orange' => array( 255, 165, 0 ), 'gray' => array( 128, 128, 128 ), 'grey' => array( 128, 128, 128 ), 'silver' => array( 192, 192, 192 ), 'navy' => array( 0, 0, 128 ), 'purple' => array( 128, 0, 128 ), 'lime' => array( 0, 255, 0 ), 'aqua' => array( 0, 255, 255 ), 'cyan' => array( 0, 255, 255 ), 'maroon' => array( 128, 0, 0 ), 'teal' => array( 0, 128, 128 ), 'olive' => array( 128, 128, 0 ), 'fuchsia' => array( 255, 0, 255 ), 'magenta' => array( 255, 0, 255 ), 'pink' => array( 255, 192, 203 ), ); /** * Reads one colour value. * * @param string $value CSS colour, e.g. "#777", "#777777", "rgb(119,119,119)" or "white". * @return array{0: int, 1: int, 2: int}|null Red, green, blue; null when not readable or see-through. */ public static function parse( string $value ): ?array { $value = strtolower( trim( $value ) ); $value = trim( (string) preg_replace( '/\s*!important$/', '', $value ) ); if ( isset( self::NAMES[ $value ] ) ) { return self::NAMES[ $value ]; } if ( 1 === preg_match( '/^#([0-9a-f]{3})$/', $value, $m ) ) { return array( (int) hexdec( $m[1][0] . $m[1][0] ), (int) hexdec( $m[1][1] . $m[1][1] ), (int) hexdec( $m[1][2] . $m[1][2] ) ); } if ( 1 === preg_match( '/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/', $value, $m ) ) { return array( (int) hexdec( $m[1] ), (int) hexdec( $m[2] ), (int) hexdec( $m[3] ) ); } if ( 1 === preg_match( '/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/', $value, $m ) ) { return array( min( 255, (int) $m[1] ), min( 255, (int) $m[2] ), min( 255, (int) $m[3] ) ); } return null; } /** * Contrast ratio between two colours, 1 to 21. * * @param array{0: int, 1: int, 2: int} $a First colour. * @param array{0: int, 1: int, 2: int} $b Second colour. */ public static function ratio( array $a, array $b ): float { $la = self::luminance( $a ); $lb = self::luminance( $b ); return ( max( $la, $lb ) + 0.05 ) / ( min( $la, $lb ) + 0.05 ); } /** * Relative luminance of a colour. * * @param array{0: int, 1: int, 2: int} $rgb Colour. */ private static function luminance( array $rgb ): float { $channels = array(); foreach ( $rgb as $value ) { $c = $value / 255; $channels[] = $c <= 0.03928 ? $c / 12.92 : ( ( $c + 0.055 ) / 1.055 ) ** 2.4; } return 0.2126 * $channels[0] + 0.7152 * $channels[1] + 0.0722 * $channels[2]; } /** * Judges the text and background colours of a style attribute. * * @param string $style Style attribute. * @return array{ratio: float, text: string, background: string, limit: float}|null Null when the pair is fine or not readable. */ public static function check_style( string $style ): ?array { $props = array(); foreach ( explode( ';', $style ) as $declaration ) { $parts = explode( ':', $declaration, 2 ); if ( 2 === count( $parts ) ) { $props[ strtolower( trim( $parts[0] ) ) ] = trim( $parts[1] ); } } $text = isset( $props['color'] ) ? self::parse( $props['color'] ) : null; $back = self::parse( $props['background-color'] ?? ( str_contains( $props['background'] ?? '', ' ' ) ? '' : ( $props['background'] ?? '' ) ) ); if ( null === $text || null === $back ) { return null; } $large = 1 === preg_match( '/^(\d+(?:\.\d+)?)px$/', $props['font-size'] ?? '', $m ) && (float) $m[1] >= 24.0; $limit = $large ? self::LARGE : self::NORMAL; $ratio = self::ratio( $text, $back ); return $ratio < $limit ? array( 'ratio' => round( $ratio, 2 ), 'text' => $props['color'], 'background' => $props['background-color'] ?? $props['background'], 'limit' => $limit, ) : null; } }