-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
464 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
defmodule Image.Blurhash do | ||
@moduledoc """ | ||
BlurHash is an algorithm developed by [Wolt](https://github.com/woltapp) | ||
that allows encoding of an image into a compact string representation | ||
called a blurhash. This string can then be decoded back into | ||
an image, providing a low-resolution placeholder that can be | ||
displayed quickly while the actual image is being loaded. | ||
It combines the benefits of data compression and perceptual | ||
hashing to create visually pleasing representations of images. | ||
The blurhash string consists of a short sequence of characters | ||
that represents the image's colors and their distribution. By | ||
adjusting the length of the blurhash, you can control the level | ||
of detail and the amount of data required to represent the image. | ||
The encode and decoder in this implementation are a fork of | ||
the [rinpatch_blurhash](https://github.com/rinpatch/blurhash) library | ||
by @rinpatch. | ||
""" | ||
|
||
alias Vix.Vips.Image, as: Vimage | ||
|
||
@doc """ | ||
Encodes an image as a [blurhash](https://blurha.sh). | ||
`Image.Blurhash.encode/2` takes an image and returns a short string | ||
(only 20-30 characters) that represents the placeholder | ||
for this image. | ||
It is intended that calculating a blurhash is performed | ||
in a background process and stored for retrieval on demand | ||
when rendering a page. | ||
### Arguments | ||
* `image` is any `t:Vix.Vips.Image.t/0`. | ||
* `options` is a keyword list of options. The default is | ||
`[x_components: 4, y_components: 3]`. | ||
### Options | ||
* `:x_components` represents the number of horizontal blocks used | ||
to calculate the blurhash. | ||
* `:y_components` represents the number of vertical blocks used | ||
to calculate the blurhash. | ||
### Returns | ||
* `{:ok, blurhash}` or | ||
* `{:error, reason}` | ||
### Selecting the number of X and Y components | ||
A higher `:x_components` and `:y_components` value will result in | ||
more details in the blurhash in the X and Y direction respectively. | ||
A lower value will create a more abstract representation. | ||
By adjusting the X and Y components, you can control the level of | ||
granularity and complexity in the generated blurhash. However, it's | ||
important to note that increasing the X and Y values also increases | ||
the size of the blurhash string, which may impact performance and | ||
bandwidth usage. | ||
The default of `[x_components: 4, y_components: 3]` is a good starting | ||
points but if the the image aspect ratio is portrait, a higher | ||
`:y_compnents` value may be appropriate. | ||
### Example | ||
iex> image = Image.open!("./test/support/images/Kip_small.jpg") | ||
iex> Image.Blurhash.encode(image) | ||
{:ok, "LBA,zk9F00~qofWBt7t700%M?bD%"} | ||
""" | ||
@doc subject: "Operation", since: "0.44.0" | ||
|
||
@spec encode(image :: Vimage.t(), options :: Keyword.t()) :: | ||
{:ok, String.t} | {:error, Image.error_message()} | ||
|
||
def encode(%Vimage{} = image, options \\ []) do | ||
with {:ok, options} <- Image.Options.Blurhash.validate_options(image, options), | ||
{:ok, binary} <- Vimage.write_to_binary(image) do | ||
{width, height, _bands} = Image.shape(image) | ||
Image.Blurhash.Encoder.encode(binary, width, height, options.x_components, options.y_components) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
defmodule Image.Blurhash.Base83 do | ||
@moduledoc false | ||
|
||
alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~' | ||
|
||
for {encoded, value} <- Enum.with_index(alphabet) do | ||
def encode_char(unquote(value)) do | ||
unquote(encoded) | ||
end | ||
|
||
def decode_char(unquote(encoded)) do | ||
unquote(value) | ||
end | ||
end | ||
|
||
def decode_number(string, length, acc \\ 0) | ||
|
||
def decode_number(rest, 0, acc) do | ||
{:ok, acc, rest} | ||
end | ||
|
||
def decode_number(<<>>, _, _) do | ||
{:error, :unexpected_end} | ||
end | ||
|
||
def decode_number(<<char, rest::binary>>, length, acc) do | ||
decode_number(rest, length - 1, acc * 83 + decode_char(char)) | ||
end | ||
|
||
def encode_number(_, 0), do: "" | ||
|
||
def encode_number(number, length) do | ||
divisor = floor(:math.pow(83, length - 1)) | ||
remainder = rem(number, divisor) | ||
quotient = floor(number / divisor) | ||
|
||
<<encode_char(quotient)>> <> encode_number(remainder, length - 1) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
defmodule Image.Blurhash.Decoder do | ||
@moduledoc false | ||
|
||
import Image.Blurhash.Utils | ||
alias Image.Blurhash.Base83 | ||
|
||
import Bitwise | ||
|
||
defp size_flag(blurhash) do | ||
with {:ok, encoded_flag, rest} <- Base83.decode_number(blurhash, 1) do | ||
x = rem(encoded_flag, 9) + 1 | ||
y = floor(encoded_flag / 9) + 1 | ||
{:ok, {x, y}, rest} | ||
end | ||
end | ||
|
||
defp max_ac(blurhash) do | ||
with {:ok, quantized_max, rest} <- Base83.decode_number(blurhash, 1) do | ||
{:ok, (quantized_max + 1) / 166, rest} | ||
end | ||
end | ||
|
||
defp average_color_and_dc(blurhash) do | ||
with {:ok, raw, rest} <- Base83.decode_number(blurhash, 4) do | ||
{r, g, b} = color = {bsr(raw, 16), band(bsr(raw, 8), 255), band(raw, 255)} | ||
dc = {srgb_to_linear(r), srgb_to_linear(g), srgb_to_linear(b)} | ||
{:ok, {color, dc}, rest} | ||
end | ||
end | ||
|
||
def construct_matrix(encoded_ac, max_ac, x, y, dc) do | ||
size = x * y - 1 | ||
|
||
try do | ||
# We start with 1 because {0, 0} is the DC | ||
{ac_values, rest} = | ||
Enum.map_reduce(1..size, encoded_ac, fn index, rest -> | ||
case Base83.decode_number(rest, 2) do | ||
{:ok, value, rest} -> | ||
# add matrix position with the color since we will need it for | ||
# inverse dct later | ||
matrix_pos = {rem(index, x), floor(index / x)} | ||
|
||
quantized_r = floor(value / (19 * 19)) | ||
quantized_g = floor(rem(floor(value / 19), 19)) | ||
quantized_b = rem(value, 19) | ||
|
||
r = unquantize_color(quantized_r, max_ac) | ||
g = unquantize_color(quantized_g, max_ac) | ||
b = unquantize_color(quantized_b, max_ac) | ||
|
||
{{matrix_pos, {r, g, b}}, rest} | ||
|
||
# Haven't found a more elegant solution to throwing in this case | ||
error -> | ||
throw(error) | ||
end | ||
end) | ||
|
||
if rest != "" do | ||
{:error, :unexpected_components} | ||
else | ||
{r, g, b} = dc | ||
matrix = [{{0, 0}, {r, g, b}} | ac_values] | ||
{:ok, matrix} | ||
end | ||
catch | ||
error -> error | ||
end | ||
end | ||
|
||
def construct_pixel_iodata(width, height, matrix) do | ||
Enum.reduce((height - 1)..0, [], fn y, acc -> | ||
pixel_row = | ||
Enum.reduce((width - 1)..0, [], fn x, acc -> | ||
{linear_r, linear_g, linear_b} = | ||
Enum.reduce(matrix, {0, 0, 0}, fn {{component_x, component_y}, | ||
{current_red, current_green, current_blue}}, | ||
{red, green, blue} -> | ||
idct_basis = | ||
:math.cos(:math.pi() * x * component_x / width) * | ||
:math.cos(:math.pi() * y * component_y / height) | ||
|
||
{red + current_red * idct_basis, green + current_green * idct_basis, | ||
blue + current_blue * idct_basis} | ||
end) | ||
|
||
r = linear_to_srgb(linear_r) | ||
g = linear_to_srgb(linear_g) | ||
b = linear_to_srgb(linear_b) | ||
|
||
[<<r::8, g::8, b::8>> | acc] | ||
end) | ||
|
||
[pixel_row | acc] | ||
end) | ||
end | ||
|
||
def decode(blurhash, width, height) do | ||
with {:ok, {components_x, components_y}, rest} <- size_flag(blurhash), | ||
{:ok, max_ac, rest} <- max_ac(rest), | ||
{:ok, {average_color, dc}, rest} <- average_color_and_dc(rest), | ||
{:ok, matrix} <- construct_matrix(rest, max_ac, components_x, components_y, dc) do | ||
pixels = construct_pixel_iodata(width, height, matrix) | ||
|
||
{:ok, pixels, average_color} | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
defmodule Image.Blurhash.Encoder do | ||
@moduledoc false | ||
|
||
import Image.Blurhash.Utils | ||
alias Image.Blurhash.Base83 | ||
|
||
import Bitwise | ||
|
||
def encode(pixels, width, height, components_x, components_y) do | ||
components = calculate_components(pixels, components_x, components_y, width, height) | ||
{:ok, encode_blurhash(components, components_x, components_y)} | ||
end | ||
|
||
defp encode_blurhash([dc | ac], x, y) do | ||
size_flag = encode_size_flag(x, y) | ||
dc = encode_dc(dc) | ||
{max_ac, ac} = encode_ac(ac) | ||
|
||
size_flag <> max_ac <> dc <> ac | ||
end | ||
|
||
defp calculate_component(pixels, component_x, component_y, width, height, acc \\ {{0, 0, 0}, 0}) | ||
|
||
defp calculate_component( | ||
<<r::8, g::8, b::8, rest::binary>>, | ||
component_x, | ||
component_y, | ||
width, | ||
height, | ||
{{acc_r, acc_g, acc_b}, index} | ||
) do | ||
pixel_x = rem(index, width) | ||
pixel_y = floor(index / width) | ||
|
||
# DC | ||
normalization_factor = | ||
unless component_x == 0 and component_y == 0, | ||
do: 2, | ||
else: 1 | ||
|
||
basis = | ||
normalization_factor * :math.cos(:math.pi() * pixel_x * component_x / width) * | ||
:math.cos(:math.pi() * pixel_y * component_y / height) | ||
|
||
linear_r = srgb_to_linear(r) | ||
linear_g = srgb_to_linear(g) | ||
linear_b = srgb_to_linear(b) | ||
|
||
acc_r = acc_r + basis * linear_r | ||
acc_g = acc_g + basis * linear_g | ||
acc_b = acc_b + basis * linear_b | ||
|
||
acc = {{acc_r, acc_g, acc_b}, index + 1} | ||
calculate_component(rest, component_x, component_y, width, height, acc) | ||
end | ||
|
||
defp calculate_component(_, _, _, width, height, {{r, g, b}, _}) do | ||
scale = 1 / (width * height) | ||
{r * scale, g * scale, b * scale} | ||
end | ||
|
||
defp calculate_components(pixels, x, y, width, height) do | ||
for y <- 0..(y - 1), | ||
x <- 0..(x - 1) do | ||
{{x, y}, calculate_component(pixels, x, y, width, height)} | ||
end | ||
end | ||
|
||
defp encode_size_flag(x, y) do | ||
Base83.encode_number(x - 1 + (y - 1) * 9, 1) | ||
end | ||
|
||
defp encode_dc({_, {linear_r, linear_g, linear_b}}) do | ||
r = linear_to_srgb(linear_r) | ||
g = linear_to_srgb(linear_g) | ||
b = linear_to_srgb(linear_b) | ||
|
||
Base83.encode_number(bsl(r, 16) + bsl(g, 8) + b, 4) | ||
end | ||
|
||
defp encode_ac([]) do | ||
{Base83.encode_number(0, 1), ""} | ||
end | ||
|
||
defp encode_ac(ac) do | ||
max_ac = Enum.reduce(ac, -2, fn {_, {r, g, b}}, max_ac -> Enum.max([max_ac, r, g, b]) end) | ||
|
||
quantized_max_ac = floor(max(0, min(82, floor(max_ac * 166 - 0.5)))) | ||
max_ac_for_quantization = (quantized_max_ac + 1) / 166 | ||
|
||
encoded_max_ac = Base83.encode_number(quantized_max_ac, 1) | ||
|
||
encoded_components = | ||
Enum.reduce(ac, "", fn {_, {r, g, b}}, hash -> | ||
r = quantize_color(r, max_ac_for_quantization) | ||
g = quantize_color(g, max_ac_for_quantization) | ||
b = quantize_color(b, max_ac_for_quantization) | ||
hash <> Base83.encode_number(r * 19 * 19 + g * 19 + b, 2) | ||
end) | ||
|
||
{encoded_max_ac, encoded_components} | ||
end | ||
end |
Oops, something went wrong.