From 817aa22c063b4e3fb7b201bcc29405591f47d0df Mon Sep 17 00:00:00 2001 From: Alex Lion Date: Fri, 26 Jun 2026 11:20:39 +0200 Subject: [PATCH] Detect ImageMagick and add to Docker images --- Dockerfile | 2 +- Dockerfile.dev | 1 + lib/claper/tasks/converter.ex | 21 ++++-- test/claper/tasks/converter_test.exs | 104 +++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 test/claper/tasks/converter_test.exs diff --git a/Dockerfile b/Dockerfile index 7d006ae..bcfc921 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,7 +64,7 @@ RUN mix release # the compiled release and other runtime necessities FROM ${RUNNER_IMAGE} -RUN apt-get update -y && apt-get install -y curl libstdc++6 openssl locales ghostscript default-jre libreoffice-java-common \ +RUN apt-get update -y && apt-get install -y curl libstdc++6 openssl locales ghostscript imagemagick default-jre libreoffice-java-common \ && apt-get install -y libreoffice --no-install-recommends && apt-get clean && rm -f /var/lib/apt/lists/*_* # RUN apk add --no-cache curl libstdc++ openssl ncurses ghostscript openjdk11-jre diff --git a/Dockerfile.dev b/Dockerfile.dev index 2a3a655..91c27f8 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -37,6 +37,7 @@ RUN apt-get update && apt-get install -y \ libtool \ openjdk-11-jdk \ ghostscript \ + imagemagick \ libreoffice \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/lib/claper/tasks/converter.ex b/lib/claper/tasks/converter.ex index 36203cb..151b68a 100644 --- a/lib/claper/tasks/converter.ex +++ b/lib/claper/tasks/converter.ex @@ -8,6 +8,8 @@ defmodule Claper.Tasks.Converter do alias Claper.Presentations.PresentationFile alias Porcelain.Result + @imagemagick_commands ~w(magick convert convert-im6.q16) + @doc """ Convert the presentation file to images. We use original hash :erlang.phash2(code-name) where the files are uploaded to send it to another folder with a new hash. This last stored in db. @@ -157,17 +159,14 @@ defmodule Claper.Tasks.Converter do files = Path.wildcard("#{path}/*.jpg") - if Enum.empty?(files) do - {:error, :missing_slides} - else + with [_ | _] <- files, + {:ok, imagemagick_command} <- get_imagemagick_command() do result = Enum.reduce_while(files, :ok, fn file, _acc -> thumb_path = Path.join(thumbs_dir, Path.basename(file)) - # Generate thumbnail with 200px width, maintaining aspect ratio - # Using "magick" for ImageMagick v7+ compatibility case Porcelain.exec( - "magick", + imagemagick_command, [ file, "-resize", @@ -187,6 +186,16 @@ defmodule Claper.Tasks.Converter do end result + else + [] -> {:error, :missing_slides} + {:error, _reason} = error -> error + end + end + + defp get_imagemagick_command do + case Enum.find(@imagemagick_commands, &System.find_executable/1) do + nil -> {:error, :missing_imagemagick} + command -> {:ok, command} end end diff --git a/test/claper/tasks/converter_test.exs b/test/claper/tasks/converter_test.exs new file mode 100644 index 0000000..907e99f --- /dev/null +++ b/test/claper/tasks/converter_test.exs @@ -0,0 +1,104 @@ +defmodule Claper.Tasks.ConverterTest do + use ExUnit.Case, async: false + + import ExUnit.CaptureIO + + alias Claper.Presentations.PresentationFile + alias Claper.Tasks.Converter + + describe "regenerate_thumbnails/1" do + test "uses convert when magick is unavailable" do + storage_dir = unique_tmp_dir() + hash = "convert-fallback" + slide_dir = Path.join([storage_dir, "uploads", hash]) + bin_dir = Path.join(storage_dir, "bin") + + File.mkdir_p!(slide_dir) + File.write!(Path.join(slide_dir, "1.jpg"), "slide") + File.mkdir_p!(bin_dir) + write_fake_imagemagick(bin_dir, "convert") + + put_local_storage_config(storage_dir) + put_path(bin_dir) + + presentation_file = %PresentationFile{hash: hash, length: 1} + + output = + capture_io(fn -> assert :ok = Converter.regenerate_thumbnails(presentation_file) end) + + assert output =~ "Generated 1 thumbnails" + assert File.read!(Path.join([slide_dir, "thumbs", "1.jpg"])) == "fake-thumbnail" + end + + test "returns a clear error when ImageMagick is unavailable" do + storage_dir = unique_tmp_dir() + hash = "missing-imagemagick" + slide_dir = Path.join([storage_dir, "uploads", hash]) + empty_bin_dir = Path.join(storage_dir, "empty-bin") + + File.mkdir_p!(slide_dir) + File.write!(Path.join(slide_dir, "1.jpg"), "slide") + File.mkdir_p!(empty_bin_dir) + + put_local_storage_config(storage_dir) + put_path(empty_bin_dir) + + assert {:error, :missing_imagemagick} = + Converter.regenerate_thumbnails(%PresentationFile{hash: hash, length: 1}) + end + end + + defp write_fake_imagemagick(bin_dir, command) do + path = Path.join(bin_dir, command) + + File.write!(path, """ + #!/bin/sh + last="" + + for arg in "$@"; do + last="$arg" + done + + printf fake-thumbnail > "$last" + """) + + File.chmod!(path, 0o755) + end + + defp put_local_storage_config(storage_dir) do + previous_storage_dir = Application.get_env(:claper, :storage_dir) + previous_presentations = Application.get_env(:claper, :presentations) + + Application.put_env(:claper, :storage_dir, storage_dir) + + Application.put_env( + :claper, + :presentations, + Keyword.merge(previous_presentations || [], storage: "local") + ) + + on_exit(fn -> + File.rm_rf!(storage_dir) + Application.put_env(:claper, :storage_dir, previous_storage_dir) + Application.put_env(:claper, :presentations, previous_presentations) + end) + end + + defp put_path(path) do + previous_path = System.get_env("PATH") + + System.put_env("PATH", path) + + on_exit(fn -> + if previous_path do + System.put_env("PATH", previous_path) + else + System.delete_env("PATH") + end + end) + end + + defp unique_tmp_dir do + Path.join(System.tmp_dir!(), "claper-converter-test-#{System.unique_integer([:positive])}") + end +end