Blurhash.php 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. <?php
  2. namespace App\Util\Blurhash;
  3. use InvalidArgumentException;
  4. class Blurhash {
  5. public static function encode(array $image, int $components_x = 4, int $components_y = 4, bool $linear = false): string {
  6. if (($components_x < 1 || $components_x > 9) || ($components_y < 1 || $components_y > 9)) {
  7. throw new InvalidArgumentException("x and y component counts must be between 1 and 9 inclusive.");
  8. }
  9. $height = count($image);
  10. $width = count($image[0]);
  11. $image_linear = $image;
  12. if (!$linear) {
  13. $image_linear = [];
  14. for ($y = 0; $y < $height; $y++) {
  15. $line = [];
  16. for ($x = 0; $x < $width; $x++) {
  17. $pixel = $image[$y][$x];
  18. $line[] = [
  19. Color::toLinear($pixel[0]),
  20. Color::toLinear($pixel[1]),
  21. Color::toLinear($pixel[2])
  22. ];
  23. }
  24. $image_linear[] = $line;
  25. }
  26. }
  27. $components = [];
  28. $scale = 1 / ($width * $height);
  29. for ($y = 0; $y < $components_y; $y++) {
  30. for ($x = 0; $x < $components_x; $x++) {
  31. $normalisation = $x == 0 && $y == 0 ? 1 : 2;
  32. $r = $g = $b = 0;
  33. for ($i = 0; $i < $width; $i++) {
  34. for ($j = 0; $j < $height; $j++) {
  35. $color = $image_linear[$j][$i];
  36. $basis = $normalisation
  37. * cos(M_PI * $i * $x / $width)
  38. * cos(M_PI * $j * $y / $height);
  39. $r += $basis * $color[0];
  40. $g += $basis * $color[1];
  41. $b += $basis * $color[2];
  42. }
  43. }
  44. $components[] = [
  45. $r * $scale,
  46. $g * $scale,
  47. $b * $scale
  48. ];
  49. }
  50. }
  51. $dc_value = DC::encode(array_shift($components) ?: []);
  52. $max_ac_component = 0;
  53. foreach ($components as $component) {
  54. $component[] = $max_ac_component;
  55. $max_ac_component = max ($component);
  56. }
  57. $quant_max_ac_component = (int) max(0, min(82, floor($max_ac_component * 166 - 0.5)));
  58. $ac_component_norm_factor = ($quant_max_ac_component + 1) / 166;
  59. $ac_values = [];
  60. foreach ($components as $component) {
  61. $ac_values[] = AC::encode($component, $ac_component_norm_factor);
  62. }
  63. $blurhash = Base83::encode($components_x - 1 + ($components_y - 1) * 9, 1);
  64. $blurhash .= Base83::encode($quant_max_ac_component, 1);
  65. $blurhash .= Base83::encode($dc_value, 4);
  66. foreach ($ac_values as $ac_value) {
  67. $blurhash .= Base83::encode((int) $ac_value, 2);
  68. }
  69. return $blurhash;
  70. }
  71. public static function decode (string $blurhash, int $width, int $height, float $punch = 1.0, bool $linear = false): array {
  72. if (empty($blurhash) || strlen($blurhash) < 6) {
  73. throw new InvalidArgumentException("Blurhash string must be at least 6 characters");
  74. }
  75. $size_info = Base83::decode($blurhash[0]);
  76. $size_y = floor($size_info / 9) + 1;
  77. $size_x = ($size_info % 9) + 1;
  78. $length = (int) strlen($blurhash);
  79. $expected_length = (int) (4 + (2 * $size_y * $size_x));
  80. if ($length !== $expected_length) {
  81. throw new InvalidArgumentException("Blurhash length mismatch: length is {$length} but it should be {$expected_length}");
  82. }
  83. $colors = [DC::decode(Base83::decode(substr($blurhash, 2, 4)))];
  84. $quant_max_ac_component = Base83::decode($blurhash[1]);
  85. $max_value = ($quant_max_ac_component + 1) / 166;
  86. for ($i = 1; $i < $size_x * $size_y; $i++) {
  87. $value = Base83::decode(substr($blurhash, 4 + $i * 2, 2));
  88. $colors[$i] = AC::decode($value, $max_value * $punch);
  89. }
  90. $pixels = [];
  91. for ($y = 0; $y < $height; $y++) {
  92. $row = [];
  93. for ($x = 0; $x < $width; $x++) {
  94. $r = $g = $b = 0;
  95. for ($j = 0; $j < $size_y; $j++) {
  96. for ($i = 0; $i < $size_x; $i++) {
  97. $color = $colors[$i + $j * $size_x];
  98. $basis =
  99. cos((M_PI * $x * $i) / $width) *
  100. cos((M_PI * $y * $j) / $height);
  101. $r += $color[0] * $basis;
  102. $g += $color[1] * $basis;
  103. $b += $color[2] * $basis;
  104. }
  105. }
  106. $row[] = $linear ? [$r, $g, $b] : [
  107. Color::toSRGB($r),
  108. Color::toSRGB($g),
  109. Color::toSRGB($b)
  110. ];
  111. }
  112. $pixels[] = $row;
  113. }
  114. return $pixels;
  115. }
  116. }