# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Helpers.MediaHelper do
Handles common media-related operations.
"""
- @ffmpeg_opts [{:sync, true}, {:stdout, true}]
+ alias Pleroma.HTTP
- def ffmpeg_resize_remote(uri, max_width, max_height) do
- cmd = ~s"""
- curl -L "#{uri}" |
- ffmpeg -i pipe:0 -vf \
- "scale='min(#{max_width},iw)':min'(#{max_height},ih)':force_original_aspect_ratio=decrease" \
- -f image2 pipe:1 | \
- cat
- """
+ require Logger
- with {:ok, [stdout: stdout_list]} <- Exexec.run(cmd, @ffmpeg_opts) do
- {:ok, Enum.join(stdout_list)}
+ def missing_dependencies do
+ Enum.reduce([imagemagick: "convert", ffmpeg: "ffmpeg"], [], fn {sym, executable}, acc ->
+ if Pleroma.Utils.command_available?(executable) do
+ acc
+ else
+ [sym | acc]
+ end
+ end)
+ end
+
+ def image_resize(url, options) do
+ with executable when is_binary(executable) <- System.find_executable("convert"),
+ {:ok, args} <- prepare_image_resize_args(options),
+ {:ok, env} <- HTTP.get(url, [], pool: :media),
+ {:ok, fifo_path} <- mkfifo() do
+ args = List.flatten([fifo_path, args])
+ run_fifo(fifo_path, env, executable, args)
+ else
+ nil -> {:error, {:convert, :command_not_found}}
+ {:error, _} = error -> error
end
end
- @doc "Returns a temporary path for an URI"
- def temporary_path_for(uri) do
- name = Path.basename(uri)
- random = rand_uniform(999_999)
- Path.join(System.tmp_dir(), "#{random}-#{name}")
+ defp prepare_image_resize_args(
+ %{max_width: max_width, max_height: max_height, format: "png"} = options
+ ) do
+ quality = options[:quality] || 85
+ resize = Enum.join([max_width, "x", max_height, ">"])
+
+ args = [
+ "-resize",
+ resize,
+ "-quality",
+ to_string(quality),
+ "png:-"
+ ]
+
+ {:ok, args}
end
- @doc "Stores binary content fetched from specified URL as a temporary file."
- @spec store_as_temporary_file(String.t(), binary()) :: {:ok, String.t()} | {:error, atom()}
- def store_as_temporary_file(url, body) do
- path = temporary_path_for(url)
- with :ok <- File.write(path, body), do: {:ok, path}
+ defp prepare_image_resize_args(%{max_width: max_width, max_height: max_height} = options) do
+ quality = options[:quality] || 85
+ resize = Enum.join([max_width, "x", max_height, ">"])
+
+ args = [
+ "-interlace",
+ "Plane",
+ "-resize",
+ resize,
+ "-quality",
+ to_string(quality),
+ "jpg:-"
+ ]
+
+ {:ok, args}
end
- @doc "Modifies image file at specified path by resizing to specified limit dimensions."
- @spec mogrify_resize_to_limit(String.t(), String.t()) :: :ok | any()
- def mogrify_resize_to_limit(path, resize_dimensions) do
- with %Mogrify.Image{} <-
- path
- |> Mogrify.open()
- |> Mogrify.resize_to_limit(resize_dimensions)
- |> Mogrify.save(in_place: true) do
- :ok
+ defp prepare_image_resize_args(_), do: {:error, :missing_options}
+
+ # Note: video thumbnail is intentionally not resized (always has original dimensions)
+ def video_framegrab(url) do
+ with executable when is_binary(executable) <- System.find_executable("ffmpeg"),
+ {:ok, env} <- HTTP.get(url, [], pool: :media),
+ {:ok, fifo_path} <- mkfifo(),
+ args = [
+ "-y",
+ "-i",
+ fifo_path,
+ "-vframes",
+ "1",
+ "-f",
+ "mjpeg",
+ "-loglevel",
+ "error",
+ "-"
+ ] do
+ run_fifo(fifo_path, env, executable, args)
+ else
+ nil -> {:error, {:ffmpeg, :command_not_found}}
+ {:error, _} = error -> error
end
end
- defp rand_uniform(high) do
- Code.ensure_loaded(:rand)
+ defp run_fifo(fifo_path, env, executable, args) do
+ pid =
+ Port.open({:spawn_executable, executable}, [
+ :use_stdio,
+ :stream,
+ :exit_status,
+ :binary,
+ args: args
+ ])
- if function_exported?(:rand, :uniform, 1) do
- :rand.uniform(high)
- else
- # Erlang/OTP < 19
- apply(:crypto, :rand_uniform, [1, high])
+ fifo = Port.open(to_charlist(fifo_path), [:eof, :binary, :stream, :out])
+ fix = Pleroma.Helpers.QtFastStart.fix(env.body)
+ true = Port.command(fifo, fix)
+ :erlang.port_close(fifo)
+ loop_recv(pid)
+ after
+ File.rm(fifo_path)
+ end
+
+ defp mkfifo do
+ path = Path.join(System.tmp_dir!(), "pleroma-media-preview-pipe-#{Ecto.UUID.generate()}")
+
+ case System.cmd("mkfifo", [path]) do
+ {_, 0} ->
+ spawn(fifo_guard(path))
+ {:ok, path}
+
+ {_, err} ->
+ {:error, {:fifo_failed, err}}
+ end
+ end
+
+ defp fifo_guard(path) do
+ pid = self()
+
+ fn ->
+ ref = Process.monitor(pid)
+
+ receive do
+ {:DOWN, ^ref, :process, ^pid, _} ->
+ File.rm(path)
+ end
+ end
+ end
+
+ defp loop_recv(pid) do
+ loop_recv(pid, <<>>)
+ end
+
+ defp loop_recv(pid, acc) do
+ receive do
+ {^pid, {:data, data}} ->
+ loop_recv(pid, acc <> data)
+
+ {^pid, {:exit_status, 0}} ->
+ {:ok, acc}
+
+ {^pid, {:exit_status, status}} ->
+ {:error, status}
+ after
+ 5000 ->
+ :erlang.port_close(pid)
+ {:error, :timeout}
end
end
end