Merge pull request 'Uploads: replace exiftool with the built-in exif_stripper' (#8028) from lambadalambda/pleroma:feature/exif-stripper into develop

Reviewed-on: https://git.pleroma.social/pleroma/pleroma/pulls/8028
This commit is contained in:
lain 2026-10-31 07:48:00 +00:00
commit 3e9dc477c9
44 changed files with 518 additions and 381 deletions

View file

@ -162,38 +162,20 @@ defmodule Mix.Tasks.Pleroma.Instance do
)
|> Path.expand()
{strip_uploads_location_message, strip_uploads_location_default} =
if Pleroma.Utils.command_available?("exiftool") do
{"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as installed. (y/n)",
"y"}
else
{"Do you want to strip location (GPS) data from uploaded images? This requires exiftool, it was detected as not installed, please install it if you answer yes. (y/n)",
"n"}
end
strip_uploads_location =
get_option(
options,
:strip_uploads_location,
strip_uploads_location_message,
strip_uploads_location_default
"Do you want to strip location (GPS) data from uploaded images? (y/n)",
"y"
) === "y"
{read_uploads_description_message, read_uploads_description_default} =
if Pleroma.Utils.command_available?("exiftool") do
{"Do you want to read data from uploaded files so clients can use it to prefill fields like image description? This requires exiftool, it was detected as installed. (y/n)",
"y"}
else
{"Do you want to read data from uploaded files so clients can use it to prefill fields like image description? This requires exiftool, it was detected as not installed, please install it if you answer yes. (y/n)",
"n"}
end
read_uploads_description =
get_option(
options,
:read_uploads_description,
read_uploads_description_message,
read_uploads_description_default
"Do you want to read metadata from uploaded images so clients can prefill the media description field? (y/n)",
"y"
) === "y"
anonymize_uploads =
@ -324,14 +306,14 @@ defmodule Mix.Tasks.Pleroma.Instance do
defp upload_filters(filters) when is_map(filters) do
enabled_filters =
if filters.strip_location do
[Pleroma.Upload.Filter.Exiftool.StripLocation]
[Pleroma.Upload.Filter.Exif.StripLocation]
else
[]
end
enabled_filters =
if filters.read_description do
enabled_filters ++ [Pleroma.Upload.Filter.Exiftool.ReadDescription]
enabled_filters ++ [Pleroma.Upload.Filter.Exif.ReadDescription]
else
enabled_filters
end

View file

@ -168,8 +168,6 @@ defmodule Pleroma.ApplicationRequirements do
defp check_system_commands!(:ok) do
filter_commands_statuses = [
check_filter(Pleroma.Upload.Filter.Exiftool.StripLocation, "exiftool"),
check_filter(Pleroma.Upload.Filter.Exiftool.ReadDescription, "exiftool"),
check_filter(Pleroma.Upload.Filter.Mogrify, "mogrify"),
check_filter(Pleroma.Upload.Filter.Mogrifun, "mogrify"),
check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "ffprobe")

View file

@ -22,40 +22,44 @@ defmodule Pleroma.Config.DeprecationWarnings do
@logger_formatter_config_knobs [:colors, :format, :metadata, :truncate, :utc_log]
@exiftool_filter_replacements %{
Pleroma.Upload.Filter.Exiftool => Pleroma.Upload.Filter.Exif.StripLocation,
Pleroma.Upload.Filter.Exiftool.StripLocation => Pleroma.Upload.Filter.Exif.StripLocation,
Pleroma.Upload.Filter.Exiftool.ReadDescription => Pleroma.Upload.Filter.Exif.ReadDescription
}
def check_exiftool_filter do
filters = Config.get([Pleroma.Upload]) |> Keyword.get(:filters, [])
if Pleroma.Upload.Filter.Exiftool in filters do
Logger.warning("""
!!!DEPRECATION WARNING!!!
Your config is using Exiftool as a filter instead of Exiftool.StripLocation. This should work for now, but you are advised to change to the new configuration to prevent possible issues later:
deprecated_filters =
filters
|> Enum.filter(&Map.has_key?(@exiftool_filter_replacements, &1))
|> Enum.uniq()
```
config :pleroma, Pleroma.Upload,
filters: [Pleroma.Upload.Filter.Exiftool]
```
case deprecated_filters do
[] ->
:ok
Is now
deprecated_filters ->
replacements =
Enum.map_join(deprecated_filters, fn filter ->
"\n* `#{inspect(filter)}` is now `#{inspect(@exiftool_filter_replacements[filter])}`"
end)
Logger.warning("""
!!!DEPRECATION WARNING!!!
Your config is using Exiftool upload filters. Pleroma no longer needs exiftool, the filters were replaced by built-in ones. The old names should work for now, but you are advised to change to the new configuration to prevent possible issues later:
#{replacements}
""")
```
config :pleroma, Pleroma.Upload,
filters: [Pleroma.Upload.Filter.Exiftool.StripLocation]
```
""")
new_config =
filters
|> Enum.map(&Map.get(@exiftool_filter_replacements, &1, &1))
|> Enum.uniq()
new_config =
filters
|> Enum.map(fn
Pleroma.Upload.Filter.Exiftool -> Pleroma.Upload.Filter.Exiftool.StripLocation
filter -> filter
end)
Config.put([Pleroma.Upload, :filters], new_config)
Config.put([Pleroma.Upload, :filters], new_config)
:error
else
:ok
:error
end
end

View file

@ -0,0 +1,32 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.Exif.ReadDescription do
@moduledoc """
Reads a description from image metadata when the upload has none yet.
Checks EXIF ImageDescription, then IPTC Caption-Abstract (and XMP for GIF). Blank
values and values longer than `[:instance, :description_limit]` are ignored.
"""
@behaviour Pleroma.Upload.Filter
@impl true
def filter(%Pleroma.Upload{description: description}) when is_binary(description),
do: {:ok, :noop}
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _} = upload) do
max_length = Pleroma.Config.get([:instance, :description_limit])
case ExifStripper.Description.read_file(file, max_length: max_length) do
description when is_binary(description) ->
{:ok, :filtered, %{upload | description: description}}
nil ->
{:ok, :noop}
end
end
def filter(_), do: {:ok, :noop}
end

View file

@ -0,0 +1,25 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.Exif.StripLocation do
@moduledoc """
Strips GPS metadata from uploaded images and overwrites the file in place.
Supports JPEG, PNG, WebP, TIFF and ISO-BMFF images (HEIC, AVIF). This is lossless:
the image data is not re-encoded. Other image types are left untouched.
"""
@behaviour Pleroma.Upload.Filter
@impl true
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
case ExifStripper.GpsStripper.strip_gps_data(file, file) do
:ok -> {:ok, :filtered}
:noop -> {:ok, :noop}
{:error, _} = error -> error
end
end
def filter(_), do: {:ok, :noop}
end

View file

@ -0,0 +1,14 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.Exiftool do
@moduledoc """
Deprecated: use `Pleroma.Upload.Filter.Exif.StripLocation`. This module delegates to it.
"""
@behaviour Pleroma.Upload.Filter
@impl true
defdelegate filter(upload), to: Pleroma.Upload.Filter.Exif.StripLocation
end

View file

@ -4,51 +4,11 @@
defmodule Pleroma.Upload.Filter.Exiftool.ReadDescription do
@moduledoc """
Gets a valid description from the related EXIF tags and provides them in the response if no description is provided yet.
It will first check ImageDescription, when that doesn't probide a valid description, it will check iptc:Caption-Abstract.
A valid description means the fields are filled in and not too long (see `:instance, :description_limit`).
Deprecated: use `Pleroma.Upload.Filter.Exif.ReadDescription`. This module delegates to it.
"""
@behaviour Pleroma.Upload.Filter
def filter(%Pleroma.Upload{description: description})
when is_binary(description),
do: {:ok, :noop}
def filter(%Pleroma.Upload{tempfile: file} = upload),
do: {:ok, :filtered, upload |> Map.put(:description, read_description_from_exif_data(file))}
def filter(_, _), do: {:ok, :noop}
defp read_description_from_exif_data(file) do
nil
|> read_when_empty(file, "-ImageDescription")
|> read_when_empty(file, "-iptc:Caption-Abstract")
end
defp read_when_empty(current_description, _, _) when is_binary(current_description),
do: current_description
defp read_when_empty(_, file, tag) do
if File.exists?(file) do
try do
{tag_content, 0} =
System.cmd("exiftool", ["-m", "-b", "-s3", tag, file],
stderr_to_stdout: false,
parallelism: true
)
tag_content = String.trim(tag_content)
if tag_content != "" and
String.length(tag_content) <=
Pleroma.Config.get([:instance, :description_limit]),
do: tag_content,
else: nil
rescue
_ in ErlangError -> nil
end
else
nil
end
end
@impl true
defdelegate filter(upload), to: Pleroma.Upload.Filter.Exif.ReadDescription
end

View file

@ -4,30 +4,11 @@
defmodule Pleroma.Upload.Filter.Exiftool.StripLocation do
@moduledoc """
Strips GPS related EXIF tags and overwrites the file in place.
Also strips or replaces filesystem metadata e.g., timestamps.
Deprecated: use `Pleroma.Upload.Filter.Exif.StripLocation`. This module delegates to it.
"""
@behaviour Pleroma.Upload.Filter
# Formats not compatible with exiftool at this time
def filter(%Pleroma.Upload{content_type: "image/heic"}), do: {:ok, :noop}
def filter(%Pleroma.Upload{content_type: "image/webp"}), do: {:ok, :noop}
def filter(%Pleroma.Upload{content_type: "image/svg" <> _}), do: {:ok, :noop}
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
try do
case System.cmd("exiftool", ["-m", "-overwrite_original", "-gps:all=", "-png:all=", file],
stderr_to_stdout: true,
parallelism: true
) do
{_response, 0} -> {:ok, :filtered}
{error, _} -> {:error, error}
end
rescue
e in ErlangError ->
{:error, "#{__MODULE__}: #{inspect(e)}"}
end
end
def filter(_), do: {:ok, :noop}
@impl true
defdelegate filter(upload), to: Pleroma.Upload.Filter.Exif.StripLocation
end