defmodule Moul do
@moduledoc """
Opinionated Image Resizing Library
> This package requires a static binary, `https://github.com/moul-co/moul/releases` >= `v5.0.0-rc.5` to be in `$PATH`.
There are two resizing options:
- `moulify/2` For general use. This option maintains the aspect ratio and creates different sizes for `srcset`.
- `avatarize/2` For resizing avatars with square crop and center anchor. This option also creates different sizes for `srcset`.
In addition, there are helper functions available for converting to data URLs: `decode_blurhash/3`, `decode_thumbhash/1`, and `to_data_url!/1`.
There are two hashing options available:
- BlurHash [https://github.com/woltapp/blurhash](https://github.com/woltapp/blurhash)
- ThumbHash [https://github.com/evanw/thumbhash](https://github.com/evanw/thumbhash)
By default, `moul` uses `ThumbHash`, but it can also be configured to use `BlurHash`.
```elixir
config :moul,
hash: "blurhash"
```
"""
require Logger
@doc """
Resize image at max 4096px at the long end with few additional sizes
xl:4096
lg:2048
md:1024
xs:32
xs:32 is decoded hash
## Examples
iex> Moul.moulify("test/images/dscf3742-4k-by-phearak-s-tha.jpeg")
{:ok,
%{
hash: "2DgSJYhld3d/d4eDeJh4eGZ2YHYH",
height: "2731",
width: "4096"
}}
"""
def moulify(in_file, out_path \\ "") do
resize(in_file, out_path, :moulify)
end
@doc """
Crop square image at max 512px with few additional sizes
xl:512
lg:256
md:128
xs:32
xs:32 is decoded hash
## Examples
iex> Moul.avatarize("test/images/profile.png")
{:ok,
%{
hash: "GwgSFwKLZYZ/eHZHZ1aWZ4iHyAgpjaAC",
height: "1190",
width: "1190"
}}
"""
def avatarize(in_file, out_path \\ "") do
resize(in_file, out_path, :avatarize)
end
def decode_blurhash(hash, width, height) when is_nil(hash) or is_nil(width) or is_nil(height),
do: {:error, nil}
@doc """
Decode blurhash and return data URL
## Examples
iex> Moul.decode_blurhash("VUGQ:~EMI:s:WB_2R*WBoLay1O$$snWVjt$jsnWVWWaz", 4096, 2731)
{:ok,
"data:image/jpeg;charset=utf-8;base64,/9j/2wCEAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAMEBgUGBgYFBgYGBwkIBgcJBwYGCAsICQoKCgoKBggLDAsKDAkKCgoBAgICAgICBQMDBQoHBgcKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCv/AABEIABUAIAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AKGg+KYo5hzXdaD4hW62qK+XPCXx18M3s426rEee0gr1Lwp8YvDcZRjqMf8A32K+Iq5/XjHSDufo+E4JrOp70j6G0mEzwbh3FYnivTHYmuf8P/HLw0tsB/aEfT+8KzPFfx68MR5J1CL8XFedU4kx0I3UGetV4Lw1rOWp+Bej/Erx5o8mdN8VXkOTyUmIrqtM/aY+NmlqFg8eXbgdPMIP9K85i/1gq1X7JllGhWotVIJ69Umfl39r5rTknGvNf9vP/M9g0r9tf9oHT4/KXxUkg9ZYf8CKyvFH7XPx81vLzeOZYQR923jUD9QTXncPX8agvvufhXv43Kssp5c6kaMOa2/Kv8jN51m1Wp71eX3s/9k="}
"""
def decode_blurhash(hash, width, height) do
args = ["decode", "--blurhash", "#{hash}", "--width", "#{width}", "--height", "#{height}"]
case System.cmd("moul", args) do
{result, 0} ->
{:ok, result}
{_, exit_status} ->
{:error, exit_status}
end
end
def decode_thumbhash(hash) when is_nil(hash), do: {:error, nil}
@doc """
Decode thumbhash and return data URL
## Examples
iex> Moul.decode_thumbhash("GwgSFwKLZYZ/eHZHZ1aWZ4iHyAgpjaAC")
{:ok,
"data:image/jpeg;charset=utf-8;base64,/9j/2wCEAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAMEBgUGBgYFBgYGBwkIBgcJBwYGCAsICQoKCgoKBggLDAsKDAkKCgoBAgICAgICBQMDBQoHBgcKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCv/AABEIACAAIAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/APcP+COn7HviXwnMPttk6/Va7P8A4Ksfsf6r4vTZHAxJHpX0n+z78aPhx8LbV5bbyEwOxArxL9ub9vbwTd3jQm4iIXpyKrB8RxUjfE5RJo+OPgP+xle+CLA3d2hULzyK+iPgzZab4fvDbPcqCvBGa+c/ir/wUU8NeH/DEwsLiNTtOMNXz94G/wCCjupax4klNpdMRvPQ19Rh+JoKO587XyWTke0+Ev2wZfEcDQ2urk7h2krwH9rL4l6rfTPcPqDnPT5q8P8A2OfE+u+Ir9Y57piCe5r0r9qHwxdjSPO3nO3rX5Lh8JJM/Qa2Ji0fPPxE8ZXGpaW8Et+xB6/NV79mDS9HudUaW7uRknu1cL4q8P6jLayfvDxnvTPg9NrGi6kwjuGHPrXtU8PNLc8ydWLZ/9k="}
"""
def decode_thumbhash(hash) do
args = ["decode", "--thumbhash", "#{hash}"]
case System.cmd("moul", args) do
{result, 0} ->
{:ok, result}
{_, exit_status} ->
{:error, exit_status}
end
end
@doc """
Tranform image to data URL
## Examples
iex> Moul.to_data_url!("test/images/DSCF0527/xs-blurhash.jpeg")
"data:image/jpeg;charset=utf-8;base64,/9j/2wCEAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAMEBgUGBgYFBgYGBwkIBgcJBwYGCAsICQoKCgoKBggLDAsKDAkKCgoBAgICAgICBQMDBQoHBgcKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCv/AABEIACAAFQMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/APwI07VbrTLxby3I3L2PQivfvhB+11ZaPZppPiO1aJlXAmAyp/LpXJ2v7HPxQnvks2SMb2A3BTivoH4Of8EtrnVbBNV18TXUpGQrAhR+A6/jXz2Y8UZBRjbEy5/Jbr/L5n2OW5PxLSpONJKMX3aav8r/AKGDfftIeGrqXzlvFwR61B/w0P4b/wCf1Pzr0vWv+Cchs7vyY9HbA9Fqn/w7ul/6A7/lXzf+ufCi05ZHof2dxkusPxPdo4PCdpqsRbyfvDjivrP9n9fB93ocanyj8o9K/DfVP2x/jHf3aXNvqEMITBClC2T9civa/gp/wVi8X+ALBLHxRoFxKyYHmWUgIb3IYjH0ya5sZwbiardTfyDAcXxjh1GS5X2f/A0P2O1nwl4Onuy4SH8hVT/hDPB//POD8hX5gyf8FpLeQ7hoWpj6ov8A8VTf+H0EH/QE1L/vhf8AGvEfBOMb+Bnrx4nptfHH/wACX+Z//9k="
"""
def to_data_url!(in_file) when is_nil(in_file), do: nil
def to_data_url!(in_file) do
b64 =
in_file
|> File.read!()
|> Base.encode64()
"data:image/jpeg;charset=utf-8;base64,#{b64}"
end
defp resize(in_file, out_path, type) do
default_args = ["photo", "--hash", Application.get_env(:moul, :hash, "thumbhash")]
out_args = get_out_path(in_file, out_path)
avatar_args =
if type == :avatarize do
["--avatar", "true"]
else
[]
end
args = default_args ++ ["--in", in_file] ++ avatar_args ++ out_args
case System.cmd("moul", args) do
{result, 0} ->
meta = String.split(result, "\n", trim: true)
dimension = meta |> List.first() |> String.split(":", trim: true)
{:ok,
%{hash: List.last(meta), width: List.first(dimension), height: List.last(dimension)}}
{_, exit_status} ->
{:error, exit_status}
end
end
defp get_out_path(in_file, out_path) do
if out_path != "" do
["--out", out_path]
else
["--out", Path.rootname(in_file)]
end
end
end