Current section
Files
Jump to
Current section
Files
lib/sac_thumbnails.ex
defmodule SacThumbnails do
@moduledoc """
Create thumbnails from images and videos.
"""
alias SacThumbnails.Animations
alias SacThumbnails.ExtractFrame
alias SacThumbnails.Gifs
alias SacThumbnails.Draw
import Mogrify
@doc """
Create a thumbnail image.
Image format is inferred from output path file extension.
To override, pass the `:format` option.
Options:
* `:width` - Width of the thumbnail. Defaults to input width.
* `:height` - Height of the thumbnail. Defaults to input height.
* `:max_width` - Maximum width of the thumbnail.
* `:max_height` - Maximum height of the thumbnail.
* `:circle` - Rounded or not.
* `:format` - Output format for the thumbnail. Defaults to `output_path` extension, or `"png"` if indeterminate.
* `:time_offset` - Timestamp in seconds at which to take screenshot, for videos and GIFs.
By default picks a time near the beginning, based on video duration.
"""
def scale(%{width: width, height: height} = image, path)
when height != 0 and not is_nil(width) do
scale_xy =
case width > height do
true -> width / height
false -> height / width
end
image |> resize("#{width * scale_xy}x#{height * scale_xy}") |> save(path: path)
image
end
def scale(image, _path), do: image
def create_thumbnail(input_path, output_path, opts \\ []) do
input_path = Path.expand(input_path)
output_path = Path.expand(output_path)
max_width = number_opt(opts, :max_width, 1_000_000_000_000)
max_height = number_opt(opts, :max_height, 1_000_000_000_000)
format = normalize_format(Keyword.get(opts, :format, image_format_from_path(output_path)))
circle_shape = Keyword.get(opts, :circle, false)
duration = Animations.duration(input_path)
frame_time = number_opt(opts, :time_offset, frame_time(duration))
replace = Keyword.get(opts, :replace, false)
desired_width = number_opt(opts, :width, nil)
|> IO.inspect()
IO.inspect(opts)
desired_height = number_opt(opts, :height, nil)
single_frame_path =
ExtractFrame.single_frame(input_path, frame_time, output_ext: ".#{format}")
single_frame_path
|> Mogrify.open()
|> verbose()
|> resize_if_different(desired_width, desired_height)
|> resize_to_limit("#{max_width}x#{max_height}")
|> optimize_image()
|> save(path: output_path, in_place: replace)
if(circle_shape) do
circle_thumb_generator(output_path)
end
:ok = File.rm!(single_frame_path)
end
@doc """
Create an animated GIF preview.
Options:
* `:width` - Width of the thumbnail. Defaults to input width.
* `:height` - Height of the thumbnail. Defaults to input height.
* `:max_width` - Maximum width of the thumbnail.
* `:max_height` - Maximum height of the thumbnail.
* `:frame_count` - Number of frames to output. Default 4.
* `:fps` - Frames per second of output GIF. Default 1.
* `:optimize` - Add mogrify options to reduce output size. Default true.
"""
@spec animated_gif_thumbnail(binary, binary, Keyword.t()) :: :ok
def animated_gif_thumbnail(input_path, output_path, opts \\ []) do
input_path = Path.expand(input_path)
output_path = Path.expand(output_path)
max_width = number_opt(opts, :max_width, 1_000_000_000_000)
max_height = number_opt(opts, :max_height, 1_000_000_000_000)
desired_width = number_opt(opts, :width, nil)
desired_height = number_opt(opts, :height, nil)
frame_count = number_opt(opts, :frame_count, 4)
fps = number_opt(opts, :fps, 1)
optimize = Keyword.get(opts, :optimize, true)
multi_frame_path =
ExtractFrame.multiple_frames(input_path, frame_count, fps, output_ext: ".gif")
multi_frame_path
|> Mogrify.open()
|> verbose()
|> resize_if_different(desired_width, desired_height)
|> resize_to_limit("#{max_width}x#{max_height}")
|> optimize_mogrify_image(optimize)
|> save(path: output_path)
:ok = File.rm!(multi_frame_path)
end
defp image_format_from_path(path) do
case Path.extname(path) do
"" -> "png"
# remove "."
extname -> String.slice(extname, 1..-1)
end
end
defp normalize_format(format) do
if String.starts_with?(format, "."), do: String.slice(format, 1..-1), else: format
end
defp frame_time(:no_duration), do: 0
defp frame_time(short) when short < 4, do: 0
defp frame_time(medium) when medium < 10, do: 1
defp frame_time(long), do: 0.1 * long
defp resize_if_different(image, nil, nil), do: image
defp resize_if_different(%{width: width, height: height} = image, desired_width, desired_height) do
if width != desired_width or height != desired_height do
image |> resize("#{desired_width}x#{desired_height}")
else
image
end
end
defp draw_circle(%_{width: origin_x, height: origin_y} = image) do
perim_y = perim(origin_y, origin_x)
perim_x = perim(origin_x, origin_y)
size = max(origin_x, origin_y)
image
|> gravity("center")
|> resize_if_different(size, size)
|> resize_to_limit("#{10_000_000}x#{10_000_000}")
# |> Draw.circle(origin_y, origin_y, perim_x, perim_y)
|> Draw.circle(size, size, perim_x, perim_y)
# image |> Draw.circle(200, 200, 200, 0)
end
defp perim(a, b) when a > b, do: a
defp perim(_a, _b), do: 0
def circle_thumb_generator(output_path) do
tmp = ExtractFrame.single_frame(output_path, 0)
%Mogrify.Image{path: tmp, ext: "png"}
|> verbose()
|> scale(output_path)
|> canvas(:none)
|> custom(:fill, Path.expand(output_path))
|> draw_circle()
|> create(path: tmp)
tmp
|> Mogrify.open()
|> optimize_image()
|> save(path: output_path)
Path.dirname(output_path)
|> (fn x -> Path.wildcard("#{x}/*.*g~*") end).()
|> Enum.map(&File.rm(&1))
:ok = File.rm!(tmp)
end
defp optimize_mogrify_image(image, true = _optimize) do
Gifs.optimize_mogrify_image(image)
end
defp optimize_mogrify_image(image, false = _optimize), do: image
defp optimize_image(image) do
image
# |> quality("50")
|> custom(:interlace, :line)
|> image_operator(:strip)
# |> custom(:density, "1000")
|> custom("fuzz", "10%")
|> custom("layers", "Optimize")
end
defp number_opt(opts, key, default) do
value = Keyword.get(opts, key)
if is_number(value), do: value, else: default
end
end