PluginProbe
ActivityPub / 9.3.0
ActivityPub v9.3.0
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
activitypub / includes / class-blurhash-encoder.php

class-blurhash-encoder.php in ActivityPub 9.3.0, at includes/class-blurhash-encoder.php

188 lines 5.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Blurhash encoder.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
10 /**
11 * Encodes pixel data into a Blurhash placeholder string.
12 *
13 * A first-party port of the public Blurhash encode algorithm
14 * (https://github.com/woltapp/blurhash, MIT). Adapted from
15 * Automattic/FOSSE (https://github.com/Automattic/fosse), which used the
16 * kornrunner/blurhash library; this replaces that runtime dependency so
17 * the plugin ships Blurhash support out of the box.
18 *
19 * @since 9.0.0
20 */
21 class Blurhash_Encoder {
22
23 /**
24 * Base83 alphabet defined by the Blurhash spec.
25 *
26 * @var string
27 */
28 const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~';
29
30 /**
31 * Encode a pixel array into a Blurhash string.
32 *
33 * @param array $pixels Row-major `[r,g,b][][]` array (0-255 per channel), indexed `[$y][$x]`.
34 * @param int $components_x Horizontal component count (1-9).
35 * @param int $components_y Vertical component count (1-9).
36 * @return string Blurhash string, or '' on invalid input.
37 */
38 public static function encode( $pixels, $components_x, $components_y ) {
39 $components_x = (int) $components_x;
40 $components_y = (int) $components_y;
41
42 if ( $components_x < 1 || $components_x > 9 || $components_y < 1 || $components_y > 9 ) {
43 return '';
44 }
45
46 $height = \count( $pixels );
47 if ( $height < 1 || ! isset( $pixels[0] ) || ! \is_array( $pixels[0] ) ) {
48 return '';
49 }
50 $width = \count( $pixels[0] );
51 if ( $width < 1 ) {
52 return '';
53 }
54
55 $factors = array();
56 for ( $y = 0; $y < $components_y; $y++ ) {
57 for ( $x = 0; $x < $components_x; $x++ ) {
58 $normalisation = ( 0 === $x && 0 === $y ) ? 1.0 : 2.0;
59 $r = 0.0;
60 $g = 0.0;
61 $b = 0.0;
62 for ( $i = 0; $i < $width; $i++ ) {
63 for ( $j = 0; $j < $height; $j++ ) {
64 $basis = $normalisation
65 * \cos( \pi() * $x * $i / $width )
66 * \cos( \pi() * $y * $j / $height );
67 $pixel = $pixels[ $j ][ $i ];
68 $r += $basis * self::srgb_to_linear( (int) $pixel[0] );
69 $g += $basis * self::srgb_to_linear( (int) $pixel[1] );
70 $b += $basis * self::srgb_to_linear( (int) $pixel[2] );
71 }
72 }
73 $scale = 1.0 / ( $width * $height );
74 $factors[] = array( $r * $scale, $g * $scale, $b * $scale );
75 }
76 }
77
78 $dc = $factors[0];
79 $ac = \array_slice( $factors, 1 );
80
81 $hash = self::encode83( ( $components_x - 1 ) + ( $components_y - 1 ) * 9, 1 );
82 $max_value = 1.0;
83
84 if ( \count( $ac ) > 0 ) {
85 $actual_max = 0.0;
86 foreach ( $ac as $factor ) {
87 $actual_max = \max( $actual_max, \abs( $factor[0] ), \abs( $factor[1] ), \abs( $factor[2] ) );
88 }
89 $quantised_max = (int) \max( 0, \min( 82, \floor( $actual_max * 166 - 0.5 ) ) );
90 $max_value = ( $quantised_max + 1 ) / 166;
91 $hash .= self::encode83( $quantised_max, 1 );
92 } else {
93 $hash .= self::encode83( 0, 1 );
94 }
95
96 $hash .= self::encode83( self::encode_dc( $dc ), 4 );
97 foreach ( $ac as $factor ) {
98 $hash .= self::encode83( self::encode_ac( $factor, $max_value ), 2 );
99 }
100
101 return $hash;
102 }
103
104 /**
105 * Encode an integer to a fixed-length base83 string.
106 *
107 * @param int $value Value.
108 * @param int $length Output length.
109 * @return string
110 */
111 private static function encode83( $value, $length ) {
112 $value = (int) $value;
113 $result = '';
114 for ( $i = 1; $i <= $length; $i++ ) {
115 $digit = (int) ( $value / ( 83 ** ( $length - $i ) ) ) % 83;
116 $result .= self::ALPHABET[ $digit ];
117 }
118 return $result;
119 }
120
121 /**
122 * Convert an sRGB 0-255 channel to linear 0-1.
123 *
124 * @param int $value Channel value.
125 * @return float
126 */
127 private static function srgb_to_linear( $value ) {
128 $v = $value / 255.0;
129 if ( $v <= 0.04045 ) {
130 return $v / 12.92;
131 }
132 return \pow( ( $v + 0.055 ) / 1.055, 2.4 );
133 }
134
135 /**
136 * Convert a linear 0-1 channel to sRGB 0-255.
137 *
138 * @param float $value Linear value.
139 * @return int
140 */
141 private static function linear_to_srgb( $value ) {
142 $v = \max( 0.0, \min( 1.0, $value ) );
143 if ( $v <= 0.0031308 ) {
144 return (int) ( $v * 12.92 * 255 + 0.5 );
145 }
146 return (int) ( ( 1.055 * \pow( $v, 1 / 2.4 ) - 0.055 ) * 255 + 0.5 );
147 }
148
149 /**
150 * Encode the DC (average color) factor.
151 *
152 * @param array $factor `[r,g,b]` linear floats.
153 * @return int
154 */
155 private static function encode_dc( $factor ) {
156 $r = self::linear_to_srgb( $factor[0] );
157 $g = self::linear_to_srgb( $factor[1] );
158 $b = self::linear_to_srgb( $factor[2] );
159 return ( $r << 16 ) + ( $g << 8 ) + $b;
160 }
161
162 /**
163 * Encode an AC factor against the maximum value.
164 *
165 * @param array $factor `[r,g,b]` linear floats.
166 * @param float $max_value Quantisation maximum.
167 * @return int
168 */
169 private static function encode_ac( $factor, $max_value ) {
170 $quant_r = (int) \max( 0, \min( 18, \floor( self::sign_pow( $factor[0] / $max_value, 0.5 ) * 9 + 9.5 ) ) );
171 $quant_g = (int) \max( 0, \min( 18, \floor( self::sign_pow( $factor[1] / $max_value, 0.5 ) * 9 + 9.5 ) ) );
172 $quant_b = (int) \max( 0, \min( 18, \floor( self::sign_pow( $factor[2] / $max_value, 0.5 ) * 9 + 9.5 ) ) );
173 return $quant_r * 19 * 19 + $quant_g * 19 + $quant_b;
174 }
175
176 /**
177 * Sign-preserving power.
178 *
179 * @param float $value Base.
180 * @param float $exp Exponent.
181 * @return float
182 */
183 private static function sign_pow( $value, $exp ) {
184 $result = \pow( \abs( $value ), $exp );
185 return $value < 0 ? -$result : $result;
186 }
187 }
188