Packages

A gleam implementation of blurhash.

Current section

Files

Jump to
blush src blush.gleam
Raw

src/blush.gleam

import gleam/int
import gleam/list
import gleam/result
pub type EncodingError {
InvalidComponentsSize(message: String)
InvalidImageSize(message: String)
InvalidPixelArraySize(message: String)
InvalidPixelValue(message: String)
}
/// Data representation of an image - width and height cannot be zero or negeative.
/// Pixels is a list of values from 0 to 255 inclusive. Each pixel is represented by 3 values in this order: [r, g, b].
///
/// For example - an image that's 320x240 pixels should contain 320x240x3 = 230400 values.
pub type ImageData {
ImageData(width: Int, height: Int, pixels: List(Int))
}
/// Components size - both x and y should be an integer between 1 and 9 inclusive.
/// Higher values produce "better" hash image, but the resulting blurhash string is longer.
/// A good default would be something like 4x4.
pub type Components {
Components(x: Int, y: Int)
}
/// Encodes the provided image into a BlurHash - that can be later decoded into thumbnail image or used with other BlurHash-compatible libraries.
pub fn encode(
image: ImageData,
components: Components,
) -> Result(String, EncodingError) {
use _ <- result.try(validate_components(components))
use _ <- result.try(validate_image_data(image))
let blurhash =
encode_blurhash_via_ffi(
image.pixels,
image.width,
image.height,
components.x,
components.y,
)
Ok(blurhash)
}
@external(erlang, "Elixir.BlurHash", "encode")
@external(javascript, "./blush_ffi.mjs", "encode")
fn encode_blurhash_via_ffi(
pixels: List(Int),
width: Int,
height: Int,
components_x: Int,
components_y: Int,
) -> String
fn validate_components(components: Components) -> Result(Nil, EncodingError) {
use _ <- result.try(validate_component_size(components.x, "x"))
use _ <- result.try(validate_component_size(components.y, "y"))
Ok(Nil)
}
fn validate_component_size(
value: Int,
label: String,
) -> Result(Nil, EncodingError) {
case value {
valid if valid > 0 && valid < 10 -> Ok(Nil)
_invalid -> {
let message =
"Invalid component size: "
<> label
<> ". Expected an integer value that's between 1 and 9 inclusive."
Error(InvalidComponentsSize(message))
}
}
}
fn validate_image_data(image: ImageData) -> Result(Nil, EncodingError) {
use _ <- result.try(validate_image_size(image.width, "width"))
use _ <- result.try(validate_image_size(image.height, "height"))
use _ <- result.try(validate_pixels(image))
[
validate_image_size(image.width, "width"),
validate_image_size(image.height, "height"),
]
|> result.all()
|> result.replace(Nil)
}
fn validate_image_size(value: Int, label: String) -> Result(Nil, EncodingError) {
case value {
valid if valid > 0 -> Ok(Nil)
_invalid -> {
let message =
"Invalid image size. Expected " <> label <> " to be greater than zero."
Error(InvalidImageSize(message))
}
}
}
fn validate_pixels(image: ImageData) -> Result(Nil, EncodingError) {
let expected_items = image.width * image.height * 3
let actual_items = list.length(image.pixels)
case actual_items == expected_items {
False -> {
let message =
"Expected "
<> int.to_string(image.width)
<> " * "
<> int.to_string(image.height)
<> " * 3 (r, g, b) = "
<> int.to_string(expected_items)
<> " items in the pixel array, but the pixel array size contains "
<> int.to_string(actual_items)
<> " items instead."
Error(InvalidPixelArraySize(message))
}
True -> validate_pixel_values(image)
}
}
fn validate_pixel_values(image: ImageData) -> Result(Nil, EncodingError) {
let offending_pixel_value =
list.find(image.pixels, fn(value) {
case value {
valid if valid >= 0 && valid <= 255 -> False
_invalid -> True
}
})
case offending_pixel_value {
Ok(pixel) -> {
let message =
"All pixel values must be integers between 0 and 255 inclusive. Encountered pixel with value "
<> int.to_string(pixel)
<> " which is not valid."
Error(InvalidPixelValue(message))
}
_ -> Ok(Nil)
}
}