From 57252bcc4dc817e81edac1b5098a0eb5eda664a0 Mon Sep 17 00:00:00 2001 From: rafal0p Date: Wed, 23 Jul 2025 15:01:25 +0200 Subject: [PATCH] make it work again --- README.md | 4 +- lib/blur_hash.ex | 387 +++++++++++++++++++++++++---------------- mix.exs | 6 +- test/blurhash_test.exs | 279 +++++++++++++++++++++++++++++ test/test_helper.exs | 1 + 5 files changed, 522 insertions(+), 155 deletions(-) create mode 100644 test/blurhash_test.exs create mode 100644 test/test_helper.exs diff --git a/README.md b/README.md index 4a42e87..a9163ff 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ Pure Elixir implementation of Blurhash algorithm with no additional dependencies Blurhash is an algorithm by Dag Ågren of Wolt that decodes an image to a very compact (~ 20-30 bytes) ASCII string representation, which can be then decoded into a blurred placeholder image. See the main [repository](https://github.com/woltapp/blurhash) for the rationale and details. -This library supports only encoding. - More details on https://blurha.sh/ Documentation available on hexdocs: https://hexdocs.pm/blurhash @@ -17,7 +15,7 @@ BlurHash is published on [Hex](https://hexdocs.pm/blurhash). Add it to your list ```elixir def deps do [ - {:blurhash, "~> 1.0.0"} + {:blurhash, "~> 2.0.0"} ] end ``` diff --git a/lib/blur_hash.ex b/lib/blur_hash.ex index 6f4fb95..52429a9 100644 --- a/lib/blur_hash.ex +++ b/lib/blur_hash.ex @@ -1,178 +1,235 @@ defmodule BlurHash do @moduledoc """ - Pure Elixir implementation of Blurhash algorithm with no additional dependencies. + BlurHash implementation in Elixir. - Blurhash is an algorithm by Dag Ågren of Wolt that decodes an image to a very compact (~ 20-30 bytes) ASCII string representation, which can be then decoded into a blurred placeholder image. See the main repo (https://github.com/woltapp/blurhash) for the rationale and details. + BlurHash is a compact representation of a placeholder for an image. + It applies a DCT transform to the image data and encodes the components + using a base 83 encoding. - This library supports only encoding. + ## Examples + + iex> pixels = BlurHash.decode("LlMF%n00%#MwS|WCWEM{R*bbWBbH", 4, 3) + iex> length(pixels) + 36 + iex> Enum.all?(pixels, fn x -> x >= 0 and x <= 255 end) + true - More details on https://blurha.sh/ """ - @moduledoc since: "1.0.0" - @digit_characters "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" + # Base83 character set for encoding + @base83_chars "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" + @base83_chars_list String.graphemes(@base83_chars) + @base83_chars_map @base83_chars_list + |> Enum.with_index() + |> Enum.into(%{}) @doc """ - Calculates the blur hash from the given pixels + Encode an image to a BlurHash string. - Returns Blurhash string + ## Parameters + - `pixels`: List of RGB pixel values [r, g, b, r, g, b, ...] + - `width`: Image width + - `height`: Image height + - `x_components`: Number of components along X axis (1-9) + - `y_components`: Number of components along Y axis (1-9) + + ## Returns + BlurHash string ## Examples - iex> BlurHash.encode(pixels, 30, 30, 4, 3) - "LEHV6nWB2yk8pyo0adR*.7kCMdnj" + iex> pixels = [255, 0, 0, 0, 255, 0, 0, 0, 255] + iex> blurhash = BlurHash.encode(pixels, 3, 1, 4, 3) + iex> is_binary(blurhash) + true + iex> String.length(blurhash) > 6 + true """ - @doc since: "1.0.0" - - @type pixels :: [integer()] - @type width :: integer() - @type height :: integer() - @type components_y :: integer() - @type components_x :: integer() - @type hash :: String.t() - - @spec encode(pixels, width, height, components_y, components_x) :: hash - def encode(pixels, width, height, components_y, components_x) do - size_flag = components_x - 1 + (components_y - 1) * 9 - [dc | ac] = get_factors(pixels, width, height, components_y, components_x) - - hash = encode_83(size_flag, 1) - - cond do - length(ac) > 0 -> - actual_maximum_value = - ac - |> Enum.map(&Enum.max/1) - |> Enum.max() - - quantised_maximum_value = - floor(Enum.max([0.0, Enum.min([82.0, floor(actual_maximum_value * 166 - 0.5)])]) / 1) - - maximum_value = (quantised_maximum_value + 1) / 166 - hash = hash <> encode_83(quantised_maximum_value, 1) <> encode_83(encode_dc(dc), 4) - - Enum.reduce( - ac, - hash, - fn factor, acc -> - acc <> - (factor - |> encode_ac(maximum_value) - |> encode_83(2)) - end - ) - - true -> - maximum_value = 1 - - hash <> - encode_83(0, 1) <> - (encode_dc(dc) - |> encode_83(4)) + def encode(pixels, width, height, x_components, y_components) do + if length(pixels) != width * height * 3 do + raise ArgumentError, "Pixel array size doesn't match dimensions" end + + ac_count = x_components * y_components - 1 + + # Calculate DCT factors + factors = calculate_factors(pixels, width, height, x_components, y_components) + + # Extract DC and AC components + dc = hd(factors) + ac = tl(factors) + + # Encode size flag + size_flag = x_components - 1 + (y_components - 1) * 9 + hash = encode_base83(size_flag, 1) + + # Calculate and encode maximum AC value + {max_ac_encoded, max_ac_value} = + if ac_count > 0 do + actual_max = ac |> Enum.flat_map(&Tuple.to_list/1) |> Enum.map(&abs/1) |> Enum.max() + quantised_max_ac = max(0, min(82, floor(actual_max * 166 - 0.5))) + {quantised_max_ac, (quantised_max_ac + 1) / 166} + else + {0, 1.0} + end + + hash = hash <> encode_base83(max_ac_encoded, 1) + + # Encode DC component + dc_encoded = encode_dc(dc) + hash = hash <> encode_base83(dc_encoded, 4) + + # Encode AC components + ac_encoded = Enum.map(ac, fn component -> encode_ac(component, max_ac_value) end) + ac_hash = Enum.map(ac_encoded, fn value -> encode_base83(value, 2) end) |> Enum.join() + + hash <> ac_hash end - defp get_factors(pixels, width, height, components_y, components_x) do - bytes_per_pixel = 4 - bytes_per_row = width * bytes_per_pixel - scale = 1 / (width * height) - - tasks = - for y <- 0..(components_y - 1), - x <- 0..(components_x - 1), - reduce: [] do - acc -> - normalisation = if x === 0 && y === 0, do: 1, else: 2 - - acc ++ - [ - Task.async(fn -> - [total_r, total_g, total_b] = - for x1 <- 0..(width - 1), - y1 <- 0..(height - 1), - reduce: [0, 0, 0] do - rgb -> - basis = - normalisation * - :math.cos(:math.pi() * x * x1 / width) * - :math.cos(:math.pi() * y * y1 / height) - - [r, g, b] = rgb - - [ - r + - basis * - s_rgb_to_linear( - Enum.fetch!(pixels, bytes_per_pixel * x1 + 0 + bytes_per_row * y1) - ), - g + - basis * - s_rgb_to_linear( - Enum.fetch!(pixels, bytes_per_pixel * x1 + 1 + bytes_per_row * y1) - ), - b + - basis * - s_rgb_to_linear( - Enum.fetch!(pixels, bytes_per_pixel * x1 + 2 + bytes_per_row * y1) - ) - ] - end - - [total_r * scale, total_g * scale, total_b * scale] - end) - ] + @doc """ + Decode a BlurHash string to RGB pixel data. + + ## Parameters + - `blurhash`: BlurHash string + - `width`: Desired output width + - `height`: Desired output height + - `punch`: Contrast adjustment (default: 1.0) + + ## Returns + List of RGB pixel values [r, g, b, r, g, b, ...] + + ## Examples + + iex> pixels = BlurHash.decode("LlMF%n00%#MwS|WCWEM{R*bbWBbH", 4, 3) + iex> length(pixels) + 36 + iex> Enum.all?(pixels, fn x -> x >= 0 and x <= 255 end) + true + + """ + def decode(blurhash, width, height, punch \\ 1.0) do + if String.length(blurhash) < 6 do + raise ArgumentError, "BlurHash must be at least 6 characters" + end + + # Parse size flag + size_flag = decode_base83(String.slice(blurhash, 0, 1)) + num_y = div(size_flag, 9) + 1 + num_x = rem(size_flag, 9) + 1 + + expected_length = 4 + 2 * num_x * num_y + + if String.length(blurhash) != expected_length do + raise ArgumentError, + "Invalid BlurHash length: expected #{expected_length}, got #{String.length(blurhash)}" + end + + # Parse maximum AC value + max_ac_encoded = decode_base83(String.slice(blurhash, 1, 1)) + max_ac = (max_ac_encoded + 1) / 166 * punch + + # Parse DC component + dc_encoded = decode_base83(String.slice(blurhash, 2, 4)) + dc = decode_dc(dc_encoded) + + # Parse AC components + ac_components = + for i <- 1..(num_x * num_y - 1) do + start_pos = 4 + i * 2 + ac_encoded = decode_base83(String.slice(blurhash, start_pos, 2)) + decode_ac(ac_encoded, max_ac) end - tasks - |> Task.yield_many(60_000) - |> Enum.map(fn {_, {:ok, result}} -> result end) + colors = [dc | ac_components] + + # Generate pixel data + for y <- 0..(height - 1), x <- 0..(width - 1) do + {r, g, b} = + colors + |> Enum.with_index() + |> Enum.reduce({0.0, 0.0, 0.0}, fn {{color_r, color_g, color_b}, index}, + {acc_r, acc_g, acc_b} -> + j = div(index, num_x) + i = rem(index, num_x) + basis = :math.cos(:math.pi() * x * i / width) * :math.cos(:math.pi() * y * j / height) + {acc_r + color_r * basis, acc_g + color_g * basis, acc_b + color_b * basis} + end) + + [linear_to_srgb(r), linear_to_srgb(g), linear_to_srgb(b)] + end + |> List.flatten() end - defp encode_83(_, 0), do: "" - - defp encode_83(value, length) do - for i <- 1..length, - reduce: "" do - hash -> - digit = - floor( - rem( - floor(floor(value / 1) / :math.pow(83, length - i)), - 83 - ) / 1 - ) - - hash = hash <> String.at(@digit_characters, digit) + # Private helper functions + + defp calculate_factors(pixels, width, height, x_components, y_components) do + for y <- 0..(y_components - 1), x <- 0..(x_components - 1) do + normalisation = if x == 0 and y == 0, do: 1.0, else: 2.0 + + {r, g, b} = multiply_basis_function(pixels, width, height, x, y) + scale = normalisation / (width * height) + {r * scale, g * scale, b * scale} end end - defp encode_dc([r, g, b]) do - r = linear_to_s_rgb(r) - g = linear_to_s_rgb(g) - b = linear_to_s_rgb(b) - r * 0x10000 + g * 0x100 + b + defp multiply_basis_function(pixels, width, height, x_component, y_component) do + pixels + |> Enum.chunk_every(3) + |> Enum.with_index() + |> Enum.reduce({0.0, 0.0, 0.0}, fn {[r, g, b], pixel_index}, {acc_r, acc_g, acc_b} -> + x = rem(pixel_index, width) + y = div(pixel_index, width) + + basis = + :math.cos(:math.pi() * x_component * x / width) * + :math.cos(:math.pi() * y_component * y / height) + + linear_r = srgb_to_linear(r) + linear_g = srgb_to_linear(g) + linear_b = srgb_to_linear(b) + + {acc_r + basis * linear_r, acc_g + basis * linear_g, acc_b + basis * linear_b} + end) end - defp encode_ac([r, g, b], maximum_value) do - quant = fn value -> - sign = if value / maximum_value < 0, do: -1, else: 1 - - floor( - Enum.max([ - 0.0, - Enum.min([ - 18.0, - floor(sign * :math.pow(abs(value / maximum_value), 0.5) * 9 + 9.5) - ]) - ]) / 1 - ) - end + defp encode_dc({r, g, b}) do + rounded_r = linear_to_srgb(r) + rounded_g = linear_to_srgb(g) + rounded_b = linear_to_srgb(b) + + Bitwise.bsl(rounded_r, 16) + Bitwise.bsl(rounded_g, 8) + rounded_b + end + + defp encode_ac({r, g, b}, max_value) do + quant_r = max(0, min(18, floor(sign_pow(r / max_value, 0.5) * 9 + 9.5))) + quant_g = max(0, min(18, floor(sign_pow(g / max_value, 0.5) * 9 + 9.5))) + quant_b = max(0, min(18, floor(sign_pow(b / max_value, 0.5) * 9 + 9.5))) + + trunc(quant_r * 19 * 19 + quant_g * 19 + quant_b) + end + + defp decode_dc(value) do + r = Bitwise.bsr(value, 16) + g = Bitwise.band(Bitwise.bsr(value, 8), 255) + b = Bitwise.band(value, 255) + + {srgb_to_linear(r), srgb_to_linear(g), srgb_to_linear(b)} + end + + defp decode_ac(value, max_value) do + quant_r = div(value, 19 * 19) + quant_g = rem(div(value, 19), 19) + quant_b = rem(value, 19) - quant.(r) * 19 * 19 + quant.(g) * 19 + quant.(b) + r = sign_pow((quant_r - 9) / 9, 2.0) * max_value + g = sign_pow((quant_g - 9) / 9, 2.0) * max_value + b = sign_pow((quant_b - 9) / 9, 2.0) * max_value + + {r, g, b} end - defp s_rgb_to_linear(value) do + defp srgb_to_linear(value) do v = value / 255.0 if v <= 0.04045 do @@ -182,13 +239,41 @@ defmodule BlurHash do end end - defp linear_to_s_rgb(value) do + defp linear_to_srgb(value) do v = max(0, min(1, value)) - if v <= 0.0031308 do - round(v * 12.92 * 255 + 0.5) - else - round((1.055 * :math.pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) - end + result = + if v <= 0.0031308 do + v * 12.92 * 255 + else + (1.055 * :math.pow(v, 1 / 2.4) - 0.055) * 255 + end + + trunc(result) + end + + defp sign_pow(value, exp) do + sign = if value < 0, do: -1, else: 1 + sign * :math.pow(abs(value), exp) + end + + defp encode_base83(value, length) do + {result, _} = + Enum.reduce((length - 1)..0, {[], value}, fn i, {acc, val} -> + power = trunc(:math.pow(83, i)) + digit = div(val, power) + new_val = rem(val, power) + {[Enum.at(@base83_chars_list, digit) | acc], new_val} + end) + + result |> Enum.reverse() |> Enum.join() + end + + defp decode_base83(string) do + string + |> String.graphemes() + |> Enum.reduce(0, fn char, acc -> + acc * 83 + Map.get(@base83_chars_map, char, 0) + end) end end diff --git a/mix.exs b/mix.exs index 17e3618..e59e9e7 100644 --- a/mix.exs +++ b/mix.exs @@ -4,13 +4,14 @@ defmodule BlurHash.MixProject do def project do [ app: :blurhash, - version: "1.0.0", + version: "2.0.0", elixir: "~> 1.7", build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, description: description(), package: package(), deps: deps(), + elixirc_paths: elixirc_paths(Mix.env()), name: "BlurHash", source_url: "https://github.com/perzanko/blurhash-elixir" ] @@ -26,6 +27,9 @@ defmodule BlurHash.MixProject do ] end + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + defp description() do """ Pure Elixir implementation of Blurhash algorithm with no additional dependencies. Blurhash is an algorithm by Dag Ågren of Wolt that decodes an image to a very compact (~ 20-30 bytes) ASCII string representation, which can be then decoded into a blurred placeholder image. diff --git a/test/blurhash_test.exs b/test/blurhash_test.exs new file mode 100644 index 0000000..18696f0 --- /dev/null +++ b/test/blurhash_test.exs @@ -0,0 +1,279 @@ +defmodule BlurHashTest do + use ExUnit.Case + doctest BlurHash + + describe "decode/3" do + test "decodes known BlurHash correctly" do + blurhash = "LlMF%n00%#MwS|WCWEM{R*bbWBbH" + pixels = BlurHash.decode(blurhash, 20, 12) + + # 20 * 12 * 3 + assert length(pixels) == 720 + # 240 pixels + assert div(length(pixels), 3) == 240 + + # Check first few pixels match expected values + first_pixels = Enum.take(pixels, 9) |> Enum.chunk_every(3) + assert length(first_pixels) == 3 + + # Values should be in valid RGB range + Enum.each(pixels, fn value -> + assert value >= 0 and value <= 255 + end) + end + + test "decodes black and white BlurHash" do + blurhash = "LjIY5?00?bIUofWBWBM{WBofWBj[" + pixels = BlurHash.decode(blurhash, 16, 16) + + # 16 * 16 * 3 + assert length(pixels) == 768 + + # All values should be valid RGB + Enum.each(pixels, fn value -> + assert value >= 0 and value <= 255 + end) + end + + test "raises error for invalid BlurHash length" do + assert_raise ArgumentError, ~r/BlurHash must be at least 6 characters/, fn -> + BlurHash.decode("short", 10, 10) + end + end + + test "raises error for incorrect BlurHash length" do + # This BlurHash is too short for the expected component count + assert_raise ArgumentError, ~r/Invalid BlurHash length/, fn -> + BlurHash.decode("L00000", 10, 10) + end + end + + test "handles punch parameter" do + blurhash = "LlMF%n00%#MwS|WCWEM{R*bbWBbH" + + # Default punch + pixels_default = BlurHash.decode(blurhash, 10, 10) + + # Higher punch (more contrast) + pixels_high_punch = BlurHash.decode(blurhash, 10, 10, 2.0) + + # Lower punch (less contrast) + pixels_low_punch = BlurHash.decode(blurhash, 10, 10, 0.5) + + # All should have same length + assert length(pixels_default) == length(pixels_high_punch) + assert length(pixels_default) == length(pixels_low_punch) + + # But different values (punch affects contrast) + assert pixels_default != pixels_high_punch + assert pixels_default != pixels_low_punch + end + end + + describe "encode/5" do + test "encodes simple gradient pattern" do + # Create a simple gradient + width = 4 + height = 3 + + pixels = + for _y <- 0..(height - 1), x <- 0..(width - 1) do + intensity = trunc(255 * x / (width - 1)) + [intensity, intensity, intensity] + end + |> List.flatten() + + blurhash = BlurHash.encode(pixels, width, height, 4, 3) + + # Should be a valid BlurHash string + assert is_binary(blurhash) + assert String.length(blurhash) > 6 + + # Should only contain valid Base83 characters + base83_chars = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" + + Enum.each(String.graphemes(blurhash), fn char -> + assert String.contains?(base83_chars, char) + end) + end + + test "encodes colorful pattern" do + width = 8 + height = 6 + + pixels = + for y <- 0..(height - 1), x <- 0..(width - 1) do + r = trunc(255 * x / (width - 1)) + g = trunc(255 * y / (height - 1)) + b = trunc(255 * (x + y) / (width + height - 2)) + [r, g, b] + end + |> List.flatten() + + blurhash = BlurHash.encode(pixels, width, height, 4, 3) + + assert is_binary(blurhash) + # Expected length for 4x3 components + assert String.length(blurhash) == 4 + 2 * 4 * 3 + end + + test "raises error for mismatched pixel array size" do + # 2 pixels worth of data + pixels = [255, 0, 0, 0, 255, 0] + + assert_raise ArgumentError, ~r/Pixel array size doesn't match dimensions/, fn -> + # Expecting 3 pixels + BlurHash.encode(pixels, 3, 1, 4, 3) + end + end + + test "handles single pixel" do + # Single red pixel + pixels = [128, 64, 192] + blurhash = BlurHash.encode(pixels, 1, 1, 1, 1) + + assert is_binary(blurhash) + # 1 + 1 + 4 + 0 (size + max_ac + dc + no ac components) + assert String.length(blurhash) == 6 + end + end + + describe "round-trip encoding/decoding" do + test "round-trip preserves general image characteristics" do + # Create a test pattern + width = 16 + height = 12 + + pixels = + for y <- 0..(height - 1), x <- 0..(width - 1) do + r = trunc(255 * :math.sin(x * :math.pi() / width) * :math.sin(x * :math.pi() / width)) + g = trunc(255 * :math.sin(y * :math.pi() / height) * :math.sin(y * :math.pi() / height)) + b = trunc(128 + 127 * :math.sin((x + y) * :math.pi() / (width + height))) + [max(0, min(255, r)), max(0, min(255, g)), max(0, min(255, b))] + end + |> List.flatten() + + # Encode to BlurHash + blurhash = BlurHash.encode(pixels, width, height, 4, 3) + + # Decode back (to smaller size for comparison) + decoded_pixels = BlurHash.decode(blurhash, 8, 6) + + # Should have correct number of pixels + assert length(decoded_pixels) == 8 * 6 * 3 + + # All values should be valid RGB + Enum.each(decoded_pixels, fn value -> + assert value >= 0 and value <= 255 + end) + end + + test "round-trip with different component counts" do + width = 8 + height = 8 + + pixels = + for y <- 0..(height - 1), x <- 0..(width - 1) do + [x * 32, y * 32, (x + y) * 16] + end + |> List.flatten() + + # Test different component configurations + component_configs = [ + # Minimal + {1, 1}, + # Standard + {4, 3}, + # Maximum + {9, 9} + ] + + Enum.each(component_configs, fn {x_comp, y_comp} -> + blurhash = BlurHash.encode(pixels, width, height, x_comp, y_comp) + decoded = BlurHash.decode(blurhash, 4, 4) + + assert is_binary(blurhash) + assert length(decoded) == 4 * 4 * 3 + + # Expected BlurHash length: 1 (size) + 1 (max_ac) + 4 (dc) + 2 * (x_comp * y_comp - 1) (ac) + expected_length = 6 + 2 * (x_comp * y_comp - 1) + assert String.length(blurhash) == expected_length + end) + end + end + + describe "edge cases" do + test "handles all black image" do + # All black 4x4 image + pixels = List.duplicate(0, 4 * 4 * 3) + blurhash = BlurHash.encode(pixels, 4, 4, 4, 3) + decoded = BlurHash.decode(blurhash, 4, 4) + + assert is_binary(blurhash) + assert length(decoded) == 4 * 4 * 3 + + # Decoded should be mostly dark + average = Enum.sum(decoded) / length(decoded) + # Should be quite dark + assert average < 50 + end + + test "handles all white image" do + # All white 4x4 image + pixels = List.duplicate(255, 4 * 4 * 3) + blurhash = BlurHash.encode(pixels, 4, 4, 4, 3) + decoded = BlurHash.decode(blurhash, 4, 4) + + assert is_binary(blurhash) + assert length(decoded) == 4 * 4 * 3 + + # Decoded should be mostly bright + average = Enum.sum(decoded) / length(decoded) + # Should be quite bright + assert average > 200 + end + + test "handles single color image" do + # All red image + pixels = + for _i <- 1..(3 * 3) do + [255, 0, 0] + end + |> List.flatten() + + blurhash = BlurHash.encode(pixels, 3, 3, 4, 3) + decoded = BlurHash.decode(blurhash, 3, 3) + + assert is_binary(blurhash) + assert length(decoded) == 3 * 3 * 3 + + # Should be predominantly red-ish + red_values = decoded |> Enum.chunk_every(3) |> Enum.map(&hd/1) + average_red = Enum.sum(red_values) / length(red_values) + # Should have significant red component + assert average_red > 100 + end + end + + describe "base83 encoding/decoding" do + test "base83 characters are valid" do + # Test that we can encode and decode various values + # 83^2 = 6889 + test_values = [0, 1, 42, 83, 166, 1000, 6889] + + Enum.each(test_values, fn value -> + # This is testing internal functionality, but important for correctness + # We'll test through the public API by creating specific patterns + pixels = [value |> rem(256), value |> div(256) |> rem(256), 128] + pixels = List.duplicate(pixels, 4) |> List.flatten() + + blurhash = BlurHash.encode(pixels, 2, 2, 1, 1) + decoded = BlurHash.decode(blurhash, 2, 2) + + assert is_binary(blurhash) + assert length(decoded) == 2 * 2 * 3 + end) + end + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()