From bc39f39dde1af6c53af9f49afa50adc8d803dea8 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Tue, 18 Apr 2023 06:33:57 -0500 Subject: [PATCH] Port colord to PHP (#49700) * Port colord to PHP * Refactor duotone to use the colord port * Deprecate tinycolor functions * Add deprecation TODO --- lib/block-supports/duotone.php | 98 ++----- lib/class-wp-duotone-gutenberg.php | 431 ++++++++++++++++++++++++++++- 2 files changed, 457 insertions(+), 72 deletions(-) diff --git a/lib/block-supports/duotone.php b/lib/block-supports/duotone.php index e775bc9237697..28bb579efa8fb 100644 --- a/lib/block-supports/duotone.php +++ b/lib/block-supports/duotone.php @@ -11,11 +11,15 @@ * * @see https://github.com/bgrins/TinyColor * + * @deprecated 6.3.0 + * * @param mixed $n Number of unknown type. * @param int $max Upper value of the range to bound to. * @return float Value in the range [0,1]. */ function gutenberg_tinycolor_bound01( $n, $max ) { + _deprecated_function( __FUNCTION__, '6.3.0' ); + if ( 'string' === gettype( $n ) && str_contains( $n, '.' ) && 1 === (float) $n ) { $n = '100%'; } @@ -42,10 +46,14 @@ function gutenberg_tinycolor_bound01( $n, $max ) { * * @see https://github.com/bgrins/TinyColor * + * @deprecated 6.3.0 + * * @param mixed $n Number of unknown type. * @return float Value in the range [0,1]. */ function gutenberg_tinycolor_bound_alpha( $n ) { + _deprecated_function( __FUNCTION__, '6.3.0' ); + if ( is_numeric( $n ) ) { $n = (float) $n; if ( $n >= 0 && $n <= 1 ) { @@ -60,10 +68,14 @@ function gutenberg_tinycolor_bound_alpha( $n ) { * * @see https://github.com/bgrins/TinyColor * + * @deprecated 6.3.0 + * * @param array $rgb_color RGB object. * @return array Rounded and converted RGB object. */ function gutenberg_tinycolor_rgb_to_rgb( $rgb_color ) { + _deprecated_function( __FUNCTION__, '6.3.0' ); + return array( 'r' => gutenberg_tinycolor_bound01( $rgb_color['r'], 255 ) * 255, 'g' => gutenberg_tinycolor_bound01( $rgb_color['g'], 255 ) * 255, @@ -76,12 +88,16 @@ function gutenberg_tinycolor_rgb_to_rgb( $rgb_color ) { * * @see https://github.com/bgrins/TinyColor * + * @deprecated 6.3.0 + * * @param float $p first component. * @param float $q second component. * @param float $t third component. * @return float R, G, or B component. */ function gutenberg_tinycolor_hue_to_rgb( $p, $q, $t ) { + _deprecated_function( __FUNCTION__, '6.3.0' ); + if ( $t < 0 ) { ++$t; } @@ -105,10 +121,14 @@ function gutenberg_tinycolor_hue_to_rgb( $p, $q, $t ) { * * @see https://github.com/bgrins/TinyColor * + * @deprecated 6.3.0 + * * @param array $hsl_color HSL object. * @return array Rounded and converted RGB object. */ function gutenberg_tinycolor_hsl_to_rgb( $hsl_color ) { + _deprecated_function( __FUNCTION__, '6.3.0' ); + $h = gutenberg_tinycolor_bound01( $hsl_color['h'], 360 ); $s = gutenberg_tinycolor_bound01( $hsl_color['s'], 100 ); $l = gutenberg_tinycolor_bound01( $hsl_color['l'], 100 ); @@ -140,10 +160,14 @@ function gutenberg_tinycolor_hsl_to_rgb( $hsl_color ) { * @see https://github.com/bgrins/TinyColor * @see https://github.com/casesandberg/react-color/ * + * @deprecated 6.3.0 + * * @param string $color_str CSS color string. * @return array RGB object. */ function gutenberg_tinycolor_string_to_rgb( $color_str ) { + _deprecated_function( __FUNCTION__, '6.3.0' ); + $color_str = strtolower( trim( $color_str ) ); $css_integer = '[-\\+]?\\d+%?'; @@ -321,80 +345,14 @@ function gutenberg_get_duotone_filter_property( $preset ) { /** * Returns the duotone filter SVG string for the preset. * + * @deprecated 6.3.0 + * * @param array $preset Duotone preset value as seen in theme.json. * @return string Duotone SVG filter. */ function gutenberg_get_duotone_filter_svg( $preset ) { - $filter_id = gutenberg_get_duotone_filter_id( $preset ); - - $duotone_values = array( - 'r' => array(), - 'g' => array(), - 'b' => array(), - 'a' => array(), - ); - - if ( ! isset( $preset['colors'] ) || ! is_array( $preset['colors'] ) ) { - $preset['colors'] = array(); - } - - foreach ( $preset['colors'] as $color_str ) { - $color = gutenberg_tinycolor_string_to_rgb( $color_str ); - - $duotone_values['r'][] = $color['r'] / 255; - $duotone_values['g'][] = $color['g'] / 255; - $duotone_values['b'][] = $color['b'] / 255; - $duotone_values['a'][] = $color['a']; - } - - ob_start(); - - ?> - - - - - - - - - - - - - - - - - <', '><', $svg ); - $svg = trim( $svg ); - } - - return $svg; + _deprecated_function( __FUNCTION__, '6.3.0' ); + return WP_Duotone_Gutenberg::get_filter_svg_from_preset( $preset ); } /** diff --git a/lib/class-wp-duotone-gutenberg.php b/lib/class-wp-duotone-gutenberg.php index 4d15b0b96381c..62532ac9a6d03 100644 --- a/lib/class-wp-duotone-gutenberg.php +++ b/lib/class-wp-duotone-gutenberg.php @@ -2,6 +2,32 @@ /** * WP_Duotone_Gutenberg class * + * Parts of this source were derived and modified from colord, + * released under the MIT license. + * + * https://github.com/omgovich/colord + * + * Copyright (c) 2020 Vlad Shilov omgovich@ya.ru + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * * @package gutenberg * @since 6.3.0 */ @@ -73,6 +99,321 @@ class WP_Duotone_Gutenberg { */ const CSS_VAR_PREFIX = '--wp--preset--duotone--'; + /** + * Direct port of colord's clamp function. Using min/max instead of + * nested ternaries. + * + * @see https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/helpers.ts#L23 + * + * @param float $number The number to clamp. + * @param float $min The minimum value. + * @param float $max The maximum value. + * @return float The clamped value. + */ + private static function colord_clamp( $number, $min = 0, $max = 1 ) { + return $number > $max ? $max : ( $number > $min ? $number : $min ); + } + + /** + * Direct port of colord's clampHue function. + * + * @see https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/helpers.ts#L32 + * + * @param float $degrees The hue to clamp. + * @return float The clamped hue. + */ + private static function colord_clamp_hue( $degrees ) { + $degrees = is_finite( $degrees ) ? $degrees % 360 : 0; + return $degrees > 0 ? $degrees : $degrees + 360; + } + + /** + * Direct port of colord's parseHue function. + * + * @see https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/helpers.ts#L40 + * + * @param float $value The hue value to parse. + * @param string $unit The unit of the hue value. + * @return float The parsed hue value. + */ + private static function colord_parse_hue( $value, $unit = 'deg' ) { + $angle_units = array( + 'grad' => 360 / 400, + 'turn' => 360, + 'rad' => 360 / ( M_PI * 2 ), + ); + + $factor = $angle_units[ $unit ]; + if ( ! $factor ) { + $factor = 1; + } + + return (float) $value * $factor; + } + + /** + * Direct port of colord's parseHex function. + * + * @see https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/hex.ts#L8 + * + * @param string $hex The hex string to parse. + * @return array|null An array of RGBA values or null if the hex string is invalid. + */ + private static function colord_parse_hex( $hex ) { + $is_match = preg_match( + '/^#([0-9a-f]{3,8})$/i', + $hex, + $hex_match + ); + + if ( ! $is_match ) { + return null; + } + + $hex = $hex_match[1]; + + if ( 4 >= strlen( $hex ) ) { + return array( + 'r' => (int) base_convert( $hex[0] . $hex[0], 16, 10 ), + 'g' => (int) base_convert( $hex[1] . $hex[1], 16, 10 ), + 'b' => (int) base_convert( $hex[2] . $hex[2], 16, 10 ), + 'a' => 4 === strlen( $hex ) ? round( base_convert( $hex[3] . $hex[3], 16, 10 ) / 255, 2 ) : 1, + ); + } + + if ( 6 === strlen( $hex ) || 8 === strlen( $hex ) ) { + return array( + 'r' => (int) base_convert( substr( $hex, 0, 2 ), 16, 10 ), + 'g' => (int) base_convert( substr( $hex, 2, 2 ), 16, 10 ), + 'b' => (int) base_convert( substr( $hex, 4, 2 ), 16, 10 ), + 'a' => 8 === strlen( $hex ) ? round( (int) base_convert( substr( $hex, 6, 2 ), 16, 10 ) / 255, 2 ) : 1, + ); + } + + return null; + } + + /** + * Direct port of colord's clampRgba function. + * + * @see https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/rgb.ts#L5 + * + * @param array $rgba The RGBA array to clamp. + * @return array The clamped RGBA array. + */ + private static function colord_clamp_rgba( $rgba ) { + $rgba['r'] = self::colord_clamp( $rgba['r'], 0, 255 ); + $rgba['g'] = self::colord_clamp( $rgba['g'], 0, 255 ); + $rgba['b'] = self::colord_clamp( $rgba['b'], 0, 255 ); + $rgba['a'] = self::colord_clamp( $rgba['a'] ); + + return $rgba; + } + + /** + * Direct port of colord's parseRgbaString function. + * + * @see https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/rgbString.ts#L18 + * + * @param string $input The RGBA string to parse. + * @return array|null An array of RGBA values or null if the RGB string is invalid. + */ + private static function colord_parse_rgba_string( $input ) { + // Functional syntax. + $is_match = preg_match( + '/^rgba?\(\s*([+-]?\d*\.?\d+)(%)?\s*,\s*([+-]?\d*\.?\d+)(%)?\s*,\s*([+-]?\d*\.?\d+)(%)?\s*(?:,\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i', + $input, + $match + ); + + if ( ! $is_match ) { + // Whitespace syntax. + $is_match = preg_match( + '/^rgba?\(\s*([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s*(?:\/\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i', + $input, + $match + ); + } + + if ( ! $is_match ) { + return null; + } + + // For some reason, preg_match doesn't include empty matches at the end + // of the array, so we add them manually to make things easier later. + for ( $i = 1; $i <= 8; $i++ ) { + if ( ! isset( $match[ $i ] ) ) { + $match[ $i ] = ''; + } + } + + if ( $match[2] !== $match[4] || $match[4] !== $match[6] ) { + return null; + } + + return self::colord_clamp_rgba( + array( + 'r' => (float) $match[1] / ( $match[2] ? 100 / 255 : 1 ), + 'g' => (float) $match[3] / ( $match[4] ? 100 / 255 : 1 ), + 'b' => (float) $match[5] / ( $match[6] ? 100 / 255 : 1 ), + 'a' => '' === $match[7] ? 1 : (float) $match[7] / ( $match[8] ? 100 : 1 ), + ) + ); + } + + /** + * Direct port of colord's clampHsla function. + * + * @see https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/hsl.ts#L6 + * + * @param array $hsla The HSLA array to clamp. + * @return array The clamped HSLA array. + */ + private static function colord_clamp_hsla( $hsla ) { + $hsla['h'] = self::colord_clamp_hue( $hsla['h'] ); + $hsla['s'] = self::colord_clamp( $hsla['s'], 0, 100 ); + $hsla['l'] = self::colord_clamp( $hsla['l'], 0, 100 ); + $hsla['a'] = self::colord_clamp( $hsla['a'] ); + + return $hsla; + } + + /** + * Direct port of colord's hsvaToRgba function. + * + * @see https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/hsv.ts#L52 + * + * @param array $hsva The HSVA array to convert. + * @return array The RGBA array. + */ + private static function colord_hsva_to_rgba( $hsva ) { + $h = ( $hsva['h'] / 360 ) * 6; + $s = $hsva['s'] / 100; + $v = $hsva['v'] / 100; + $a = $hsva['a']; + + $hh = floor( $h ); + $b = $v * ( 1 - $s ); + $c = $v * ( 1 - ( $h - $hh ) * $s ); + $d = $v * ( 1 - ( 1 - $h + $hh ) * $s ); + $module = $hh % 6; + + return array( + 'r' => array( $v, $c, $b, $b, $d, $v )[ $module ] * 255, + 'g' => array( $d, $v, $v, $c, $b, $b )[ $module ] * 255, + 'b' => array( $b, $b, $d, $v, $v, $c )[ $module ] * 255, + 'a' => $a, + ); + } + + /** + * Direct port of colord's hslaToHsva function. + * + * @see https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/hsl.ts#L33 + * + * @param array $hsla The HSLA array to convert. + * @return array The HSVA array. + */ + private static function colord_hsla_to_hsva( $hsla ) { + $h = $hsla['h']; + $s = $hsla['s']; + $l = $hsla['l']; + $a = $hsla['a']; + + $s *= ( $l < 50 ? $l : 100 - $l ) / 100; + + return array( + 'h' => $h, + 's' => $s > 0 ? ( ( 2 * $s ) / ( $l + $s ) ) * 100 : 0, + 'v' => $l + $s, + 'a' => $a, + ); + } + + /** + * Direct port of colord's hslaToRgba function. + * + * @see https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/hsl.ts#L55 + * + * @param array $hsla The HSLA array to convert. + * @return array The RGBA array. + */ + private static function colord_hsla_to_rgba( $hsla ) { + return self::colord_hsva_to_rgba( self::colord_hsla_to_hsva( $hsla ) ); + } + + /** + * Direct port of colord's parseHslaString function. + * + * @see https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/colorModels/hslString.ts#L17 + * + * @param string $input The HSLA string to parse. + * @return array|null An array of RGBA values or null if the RGB string is invalid. + */ + private static function colord_parse_hsla_string( $input ) { + // Functional syntax. + $is_match = preg_match( + '/^hsla?\(\s*([+-]?\d*\.?\d+)(deg|rad|grad|turn)?\s*,\s*([+-]?\d*\.?\d+)%\s*,\s*([+-]?\d*\.?\d+)%\s*(?:,\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i', + $input, + $match + ); + + if ( ! $is_match ) { + // Whitespace syntax. + $is_match = preg_match( + '/^hsla?\(\s*([+-]?\d*\.?\d+)(deg|rad|grad|turn)?\s+([+-]?\d*\.?\d+)%\s+([+-]?\d*\.?\d+)%\s*(?:\/\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i', + $input, + $match + ); + } + + if ( ! $is_match ) { + return null; + } + + // For some reason, preg_match doesn't include empty matches at the end + // of the array, so we add them manually to make things easier later. + for ( $i = 1; $i <= 6; $i++ ) { + if ( ! isset( $match[ $i ] ) ) { + $match[ $i ] = ''; + } + } + + $hsla = self::colord_clamp_hsla( + array( + 'h' => self::colord_parse_hue( $match[1], $match[2] ), + 's' => (float) $match[3], + 'l' => (float) $match[4], + 'a' => '' === $match[5] ? 1 : (float) $match[5] / ( $match[6] ? 100 : 1 ), + ) + ); + + return self::colord_hsla_to_rgba( $hsla ); + } + + /** + * Direct port of colord's parse function simplified for our use case. This + * version only supports string parsing and only returns RGBA values. + * + * @see https://github.com/omgovich/colord/blob/3f859e03b0ca622eb15480f611371a0f15c9427f/src/parse.ts#L37 + * + * @param string $input The string to parse. + * @return array|null An array of RGBA values or null if the string is invalid. + */ + private static function colord_parse( $input ) { + $result = self::colord_parse_hex( $input ); + + if ( ! $result ) { + $result = self::colord_parse_rgba_string( $input ); + } + + if ( ! $result ) { + $result = self::colord_parse_hsla_string( $input ); + } + + return $result; + } + /** * Get all possible duotone presets from global and theme styles and store as slug => [ colors array ] * We only want to process this one time. On block render we'll access and output only the needed presets for that page. @@ -160,6 +501,80 @@ private static function get_css_custom_property_name( $slug ) { return self::CSS_VAR_PREFIX . $slug; } + /** + * Gets the SVG for the duotone filter definition. + * + * @param string $filter_id The ID of the filter. + * @param array $colors An array of color strings. + * @return string An SVG with a duotone filter definition. + */ + private static function get_filter_svg( $filter_id, $colors ) { + $duotone_values = array( + 'r' => array(), + 'g' => array(), + 'b' => array(), + 'a' => array(), + ); + + foreach ( $colors as $color_str ) { + $color = self::colord_parse( $color_str ); + + $duotone_values['r'][] = $color['r'] / 255; + $duotone_values['g'][] = $color['g'] / 255; + $duotone_values['b'][] = $color['b'] / 255; + $duotone_values['a'][] = $color['a']; + } + + ob_start(); + + ?> + + + + + + + + + + + + + + + + + <', '><', $svg ); + $svg = trim( $svg ); + } + + return $svg; + } + /** * Get the CSS variable for a duotone preset. * @@ -190,7 +605,7 @@ public static function output_footer_assets() { foreach ( self::$output as $filter_data ) { // SVG will be output on the page later. - $filter_svg = gutenberg_get_duotone_filter_svg( $filter_data ); + $filter_svg = self::get_filter_svg_from_preset( $filter_data ); echo $filter_svg; @@ -213,7 +628,7 @@ public static function add_editor_settings( $settings ) { $duotone_svgs = ''; $duotone_css = 'body{'; foreach ( self::$global_styles_presets as $filter_data ) { - $duotone_svgs .= gutenberg_get_duotone_filter_svg( $filter_data ); + $duotone_svgs .= self::get_filter_svg_from_preset( $filter_data ); $duotone_css .= self::get_css_custom_property_declaration( $filter_data ); } $duotone_css .= '}'; @@ -444,4 +859,16 @@ public static function migrate_experimental_duotone_support_flag( $settings, $me return $settings; } + + /** + * Gets the SVG for the duotone filter definition from a preset. + * + * @param array $preset The duotone preset. + * @return string The SVG for the filter definition. + */ + public static function get_filter_svg_from_preset( $preset ) { + // TODO: This function will be refactored out in a follow-up PR where it will be deprecated. + $filter_id = gutenberg_get_duotone_filter_id( $preset ); + return self::get_filter_svg( $filter_id, $preset['colors'] ); + } }