feat: media handling
This commit is contained in:
@@ -13,6 +13,8 @@ defmodule BDS.Media do
|
||||
project = Projects.get_project!(attr(attrs, :project_id))
|
||||
source_path = attr(attrs, :source_path)
|
||||
original_name = Path.basename(source_path)
|
||||
mime_type = detect_mime(original_name)
|
||||
{width, height} = image_dimensions(source_path, mime_type)
|
||||
now = System.system_time(:second)
|
||||
file_name = Ecto.UUID.generate() <> Path.extname(original_name)
|
||||
file_path = media_file_path(file_name, now)
|
||||
@@ -28,10 +30,10 @@ defmodule BDS.Media do
|
||||
project_id: project.id,
|
||||
filename: file_name,
|
||||
original_name: original_name,
|
||||
mime_type: detect_mime(original_name),
|
||||
mime_type: mime_type,
|
||||
size: stat.size,
|
||||
width: attr(attrs, :width),
|
||||
height: attr(attrs, :height),
|
||||
width: attr(attrs, :width) || width,
|
||||
height: attr(attrs, :height) || height,
|
||||
title: attr(attrs, :title),
|
||||
alt: attr(attrs, :alt),
|
||||
caption: attr(attrs, :caption),
|
||||
@@ -167,6 +169,18 @@ defmodule BDS.Media do
|
||||
}
|
||||
end
|
||||
|
||||
def regenerate_thumbnails(media_id) do
|
||||
case Repo.get(Media, media_id) do
|
||||
nil ->
|
||||
{:error, :not_found}
|
||||
|
||||
media ->
|
||||
project = Projects.get_project!(media.project_id)
|
||||
:ok = ensure_thumbnails(project, media)
|
||||
{:ok, media}
|
||||
end
|
||||
end
|
||||
|
||||
def rebuild_media_from_files(project_id) do
|
||||
project = Projects.get_project!(project_id)
|
||||
|
||||
@@ -309,20 +323,59 @@ defmodule BDS.Media do
|
||||
if image_mime?(media.mime_type) do
|
||||
source_path = Path.join(Projects.project_data_dir(project), media.file_path)
|
||||
|
||||
Enum.each(thumbnail_paths(media), fn {_size, relative_path} ->
|
||||
destination = Path.join(Projects.project_data_dir(project), relative_path)
|
||||
:ok = File.mkdir_p(Path.dirname(destination))
|
||||
case Image.open(source_path) do
|
||||
{:ok, image} ->
|
||||
image
|
||||
|> Image.autorotate!()
|
||||
|> write_all_thumbnails(project, media)
|
||||
|
||||
case File.read(source_path) do
|
||||
{:ok, contents} -> :ok = File.write(destination, contents)
|
||||
{:error, _reason} -> :ok = File.write(destination, "")
|
||||
end
|
||||
end)
|
||||
{:error, _reason} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp write_all_thumbnails(image, project, media) do
|
||||
thumbnail_paths(media)
|
||||
|> Enum.each(fn {size, relative_path} ->
|
||||
destination = Path.join(Projects.project_data_dir(project), relative_path)
|
||||
:ok = File.mkdir_p(Path.dirname(destination))
|
||||
|
||||
image
|
||||
|> render_thumbnail(size)
|
||||
|> write_thumbnail(destination, size)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp render_thumbnail(image, :small), do: bounded_thumbnail(image, 150, 150)
|
||||
defp render_thumbnail(image, :medium), do: bounded_thumbnail(image, 400, 400)
|
||||
defp render_thumbnail(image, :large), do: bounded_thumbnail(image, 800, 800)
|
||||
|
||||
defp render_thumbnail(image, :ai) do
|
||||
image
|
||||
|> Image.thumbnail!("448x448", fit: :contain, resize: :both, autorotate: false)
|
||||
|> Image.embed!(448, 448, x: :center, y: :center, background_color: :black)
|
||||
end
|
||||
|
||||
defp bounded_thumbnail(image, width, height) do
|
||||
Image.thumbnail!(image, "#{width}x#{height}", fit: :contain, resize: :down, autorotate: false)
|
||||
end
|
||||
|
||||
defp write_thumbnail(image, destination, :ai) do
|
||||
flattened = Image.flatten!(image, background_color: :black)
|
||||
Image.write!(flattened, destination, quality: 85, strip_metadata: true)
|
||||
:ok
|
||||
end
|
||||
|
||||
defp write_thumbnail(image, destination, _size) do
|
||||
Image.write!(image, destination, quality: 80, strip_metadata: true)
|
||||
:ok
|
||||
end
|
||||
|
||||
defp delete_thumbnail_files(project_id, media) do
|
||||
Enum.each(Map.values(thumbnail_paths(media)), fn path ->
|
||||
delete_file_if_present(project_id, path)
|
||||
@@ -347,10 +400,26 @@ defmodule BDS.Media do
|
||||
".png" -> "image/png"
|
||||
".gif" -> "image/gif"
|
||||
".webp" -> "image/webp"
|
||||
".tif" -> "image/tiff"
|
||||
".tiff" -> "image/tiff"
|
||||
".bmp" -> "image/bmp"
|
||||
".heic" -> "image/heic"
|
||||
".heif" -> "image/heif"
|
||||
_ -> "application/octet-stream"
|
||||
end
|
||||
end
|
||||
|
||||
defp image_dimensions(source_path, mime_type) do
|
||||
if image_mime?(mime_type) do
|
||||
case Image.open(source_path) do
|
||||
{:ok, image} -> {Image.width(image), Image.height(image)}
|
||||
{:error, _reason} -> {nil, nil}
|
||||
end
|
||||
else
|
||||
{nil, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp image_mime?(mime_type), do: String.starts_with?(mime_type || "", "image/")
|
||||
|
||||
defp binary_exists_for_sidecar?(sidecar_path) do
|
||||
|
||||
4
mix.exs
4
mix.exs
@@ -24,7 +24,9 @@ defmodule BDS.MixProject do
|
||||
{:ecto_sql, "~> 3.13"},
|
||||
{:ecto_sqlite3, "~> 0.21"},
|
||||
{:luerl, "~> 1.5"},
|
||||
{:jason, "~> 1.4"}
|
||||
{:jason, "~> 1.4"},
|
||||
{:plug, "~> 1.18"},
|
||||
{:image, "~> 0.65"}
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
8
mix.lock
8
mix.lock
@@ -1,5 +1,6 @@
|
||||
%{
|
||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
||||
"color": {:hex, :color, "0.12.0", "f59f9bb6452a460760d44116ec0c1cf86f9d7707c8756c01f83c6d8fe042ae67", [:mix], [{:bandit, "~> 1.5", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "1e17768919dad0bd44f48d0daf294d24bdd5a615bbfe0b4e01a51312203bd294"},
|
||||
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
|
||||
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
|
||||
"ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"},
|
||||
@@ -7,7 +8,14 @@
|
||||
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.22.0", "edab2d0f701b7dd05dcf7e2d97769c106aff62b5cfddc000d1dd6f46b9cbd8c3", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "5af9e031bffcc5da0b7bca90c271a7b1e7c04a93fecf7f6cd35bc1b1921a64bd"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
||||
"exqlite": {:hex, :exqlite, "0.36.0", "07b4f95d61cb82b8d52946d0639497fa7d32117e09b2c8d25e24a38723c295cb", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "cbeca3ce781f9ff07cfa9a87486f3ebd512a143ad6a14ed5c9fca21fe0bf3ae7"},
|
||||
"image": {:hex, :image, "0.65.0", "44908233a1a0dcdbb6ae873ec09fd9ae533d1840d300d8b0b1b186d586b935e6", [:mix], [{:color, "~> 0.4", [hex: :color, repo: "hexpm", optional: false]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "0.11.0", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.11.0", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.33", [hex: :vix, repo: "hexpm", optional: false]}, {:xav, "~> 0.10", [hex: :xav, repo: "hexpm", optional: true]}], "hexpm", "d2060e08d0f42564f49de1ea97a82a5d237f9ac91edb141dece51f1238dd8b4a"},
|
||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
"luerl": {:hex, :luerl, "1.5.1", "f6700420950fc6889137e7a0c11c4a8467dea04a8c23f707a40d83566d14e786", [:rebar3], [], "hexpm", "abf88d849baa0d5dca93b245a8688d4de2ee3d588159bb2faf51e15946509390"},
|
||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
|
||||
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
||||
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
|
||||
"telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"},
|
||||
"vix": {:hex, :vix, "0.38.0", "77529ee4f6ced339c3d5f90a9eacf306f5b7109d3d1b5e3ef391a984ad404f75", [:make, :mix], [{:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "dca58f654922fa678d5df8e028317483d9c0f8acb2e2714076a8468695687aa7"},
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ defmodule BDS.MediaTest do
|
||||
binary_path = Path.join(media_dir, "asset.jpg")
|
||||
sidecar_path = binary_path <> ".meta"
|
||||
|
||||
File.write!(binary_path, "fake-jpeg")
|
||||
File.write!(binary_path, tiny_jpeg_binary())
|
||||
|
||||
File.write!(
|
||||
sidecar_path,
|
||||
@@ -118,9 +118,9 @@ defmodule BDS.MediaTest do
|
||||
"id: media-from-file",
|
||||
"original_name: original.jpg",
|
||||
"mime_type: image/jpeg",
|
||||
"size: 9",
|
||||
"width: 0",
|
||||
"height: 0",
|
||||
"size: #{byte_size(tiny_jpeg_binary())}",
|
||||
"width: 3",
|
||||
"height: 2",
|
||||
"title: Recovered",
|
||||
"alt: Recovered alt",
|
||||
"caption: Recovered caption",
|
||||
@@ -157,7 +157,7 @@ defmodule BDS.MediaTest do
|
||||
assert media.filename == "asset.jpg"
|
||||
assert media.original_name == "original.jpg"
|
||||
assert media.mime_type == "image/jpeg"
|
||||
assert media.size == 9
|
||||
assert media.size == byte_size(tiny_jpeg_binary())
|
||||
assert media.title == "Recovered"
|
||||
assert media.alt == "Recovered alt"
|
||||
assert media.caption == "Recovered caption"
|
||||
@@ -183,7 +183,7 @@ defmodule BDS.MediaTest do
|
||||
|
||||
test "import_media generates the four thumbnail files in bucketed thumbnail paths", %{project: project, temp_dir: temp_dir} do
|
||||
source_path = Path.join(temp_dir, "sample.jpg")
|
||||
File.write!(source_path, "fake-jpeg")
|
||||
File.write!(source_path, tiny_jpeg_binary())
|
||||
|
||||
assert {:ok, media} = BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
||||
|
||||
@@ -198,6 +198,108 @@ defmodule BDS.MediaTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "import_media extracts image dimensions and writes real encoded thumbnails", %{project: project, temp_dir: temp_dir} do
|
||||
source_path = Path.join(temp_dir, "sample.jpg")
|
||||
File.write!(source_path, tiny_jpeg_binary())
|
||||
|
||||
assert {:ok, media} = BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
||||
|
||||
assert media.mime_type == "image/jpeg"
|
||||
assert media.width == 3
|
||||
assert media.height == 2
|
||||
|
||||
thumbnail_paths = BDS.Media.thumbnail_paths(media)
|
||||
|
||||
small = Image.open!(Path.join(temp_dir, thumbnail_paths.small))
|
||||
medium = Image.open!(Path.join(temp_dir, thumbnail_paths.medium))
|
||||
large = Image.open!(Path.join(temp_dir, thumbnail_paths.large))
|
||||
ai = Image.open!(Path.join(temp_dir, thumbnail_paths.ai))
|
||||
|
||||
assert Image.width(small) == 3
|
||||
assert Image.height(small) == 2
|
||||
assert Image.width(medium) == 3
|
||||
assert Image.height(medium) == 2
|
||||
assert Image.width(large) == 3
|
||||
assert Image.height(large) == 2
|
||||
assert Image.width(ai) == 448
|
||||
assert Image.height(ai) == 448
|
||||
|
||||
assert Path.extname(thumbnail_paths.small) == ".webp"
|
||||
assert Path.extname(thumbnail_paths.medium) == ".webp"
|
||||
assert Path.extname(thumbnail_paths.large) == ".webp"
|
||||
assert Path.extname(thumbnail_paths.ai) == ".jpg"
|
||||
end
|
||||
|
||||
test "import_media keeps raw header dimensions but autorotates thumbnails from EXIF orientation", %{project: project, temp_dir: temp_dir} do
|
||||
source_path = Path.join(temp_dir, "rotated.jpg")
|
||||
write_oriented_jpeg!(source_path, 6)
|
||||
|
||||
assert {:ok, media} = BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
||||
|
||||
assert media.width == 2
|
||||
assert media.height == 3
|
||||
|
||||
thumbnail_path = Path.join(temp_dir, BDS.Media.thumbnail_paths(media).small)
|
||||
actual_thumbnail = Image.open!(thumbnail_path)
|
||||
expected_thumbnail = expected_small_thumbnail(source_path)
|
||||
|
||||
assert Image.width(actual_thumbnail) == 3
|
||||
assert Image.height(actual_thumbnail) == 2
|
||||
assert_images_match!(actual_thumbnail, expected_thumbnail)
|
||||
end
|
||||
|
||||
test "regenerate_thumbnails recreates thumbnail files for an existing image media item", %{project: project, temp_dir: temp_dir} do
|
||||
source_path = Path.join(temp_dir, "sample.jpg")
|
||||
File.write!(source_path, tiny_jpeg_binary())
|
||||
|
||||
assert {:ok, media} = BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
||||
|
||||
thumbnail_paths = BDS.Media.thumbnail_paths(media)
|
||||
File.rm!(Path.join(temp_dir, thumbnail_paths.small))
|
||||
File.rm!(Path.join(temp_dir, thumbnail_paths.medium))
|
||||
|
||||
assert {:ok, regenerated} = BDS.Media.regenerate_thumbnails(media.id)
|
||||
assert regenerated.id == media.id
|
||||
|
||||
Enum.each(Map.values(thumbnail_paths), fn path ->
|
||||
assert File.exists?(Path.join(temp_dir, path))
|
||||
end)
|
||||
end
|
||||
|
||||
test "import_media generates thumbnails for png and webp sources", %{project: project, temp_dir: temp_dir} do
|
||||
Enum.each([{ ".png", "image/png"}, {".webp", "image/webp"}], fn {extension, mime_type} ->
|
||||
source_path = Path.join(temp_dir, "sample#{extension}")
|
||||
File.write!(source_path, sample_image_binary(extension))
|
||||
|
||||
assert {:ok, media} = BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
||||
assert media.mime_type == mime_type
|
||||
assert media.width == 2
|
||||
assert media.height == 3
|
||||
|
||||
Enum.each(Map.values(BDS.Media.thumbnail_paths(media)), fn path ->
|
||||
assert File.exists?(Path.join(temp_dir, path))
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
test "import_media detects supported TIFF, BMP, HEIC, and HEIF extensions", %{project: project, temp_dir: temp_dir} do
|
||||
Enum.each([
|
||||
{"asset.tif", "image/tiff"},
|
||||
{"asset.tiff", "image/tiff"},
|
||||
{"asset.bmp", "image/bmp"},
|
||||
{"asset.heic", "image/heic"},
|
||||
{"asset.heif", "image/heif"}
|
||||
], fn {file_name, mime_type} ->
|
||||
source_path = Path.join(temp_dir, file_name)
|
||||
File.write!(source_path, "placeholder")
|
||||
|
||||
assert {:ok, media} = BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
||||
assert media.mime_type == mime_type
|
||||
assert media.width == nil
|
||||
assert media.height == nil
|
||||
end)
|
||||
end
|
||||
|
||||
test "upsert_media_translation persists the row and writes a translated sidecar next to the binary", %{project: project, temp_dir: temp_dir} do
|
||||
source_path = Path.join(temp_dir, "sample.txt")
|
||||
File.write!(source_path, "hello media")
|
||||
@@ -223,4 +325,55 @@ defmodule BDS.MediaTest do
|
||||
assert contents =~ "alt: Alt text\n"
|
||||
assert contents =~ "caption: Bildunterschrift\n"
|
||||
end
|
||||
|
||||
defp tiny_jpeg_binary do
|
||||
Image.new!(3, 2, color: [255, 0, 0])
|
||||
|> Image.write!(:memory, suffix: ".jpg", quality: 85)
|
||||
end
|
||||
|
||||
defp sample_image_binary(extension) do
|
||||
sample_svg_binary()
|
||||
|> Image.from_svg!()
|
||||
|> Image.write!(:memory, suffix: extension, quality: 85)
|
||||
end
|
||||
|
||||
defp sample_svg_binary do
|
||||
"""
|
||||
<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2\" height=\"3\">
|
||||
<rect width=\"1\" height=\"1\" x=\"0\" y=\"0\" fill=\"#ff0000\"/>
|
||||
<rect width=\"1\" height=\"1\" x=\"1\" y=\"0\" fill=\"#00ff00\"/>
|
||||
<rect width=\"1\" height=\"1\" x=\"0\" y=\"1\" fill=\"#0000ff\"/>
|
||||
<rect width=\"1\" height=\"1\" x=\"1\" y=\"1\" fill=\"#ffff00\"/>
|
||||
<rect width=\"1\" height=\"1\" x=\"0\" y=\"2\" fill=\"#00ffff\"/>
|
||||
<rect width=\"1\" height=\"1\" x=\"1\" y=\"2\" fill=\"#ff00ff\"/>
|
||||
</svg>
|
||||
"""
|
||||
end
|
||||
|
||||
defp write_oriented_jpeg!(path, orientation) do
|
||||
image = sample_image_binary(".jpg") |> Image.open!()
|
||||
|
||||
{:ok, oriented_image} =
|
||||
Vix.Vips.Image.mutate(image, fn mutable_image ->
|
||||
:ok = Vix.Vips.MutableImage.update(mutable_image, "orientation", orientation)
|
||||
end)
|
||||
|
||||
Image.write!(oriented_image, path, quality: 85)
|
||||
end
|
||||
|
||||
defp expected_small_thumbnail(source_path) do
|
||||
source_path
|
||||
|> Image.open!()
|
||||
|> Image.autorotate!()
|
||||
|> Image.thumbnail!("150x150", fit: :contain, resize: :down, autorotate: false)
|
||||
|> Image.write!(:memory, suffix: ".webp", quality: 80, strip_metadata: true)
|
||||
|> Image.open!()
|
||||
end
|
||||
|
||||
defp assert_images_match!(left, right) do
|
||||
assert Image.shape(left) == Image.shape(right)
|
||||
|
||||
assert {:ok, score, _difference_image} = Image.compare(left, right)
|
||||
assert score <= 0.0
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user