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

@ -42,7 +42,7 @@ ARG HOME=/opt/pleroma
ARG DATA=/var/lib/pleroma
RUN apk update &&\
apk add exiftool ffmpeg vips libmagic ncurses postgresql-client &&\
apk add ffmpeg vips libmagic ncurses postgresql-client &&\
adduser --system --shell /bin/false --home ${HOME} pleroma &&\
mkdir -p ${DATA}/uploads &&\
mkdir -p ${DATA}/static &&\

View file

@ -41,7 +41,7 @@ ARG HOME=/opt/pleroma
ARG DATA=/var/lib/pleroma
RUN apk update &&\
apk add exiftool ffmpeg vips libmagic ncurses postgresql-client &&\
apk add ffmpeg vips libmagic ncurses postgresql-client &&\
adduser --system --shell /bin/false --home ${HOME} pleroma &&\
mkdir -p ${DATA}/uploads &&\
mkdir -p ${DATA}/static &&\

View file

@ -0,0 +1 @@
Strip GPS data from WebP and HEIC/AVIF uploads; the Exiftool.StripLocation filter used to skip them; GPS data in XMP (including DJI drone positions) is stripped too

View file

@ -0,0 +1 @@
Exiftool upload filters are replaced by the built-in Exif.StripLocation and Exif.ReadDescription filters; exiftool is no longer required, old filter names still work but are deprecated

View file

@ -37,8 +37,8 @@ If any of the options are left unspecified, you will be prompted interactively.
- `--static-dir <path>` - the directory custom public files should be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)
- `--listen-ip <ip>` - the ip the app should listen to, defaults to 127.0.0.1
- `--listen-port <port>` - the port the app should listen to, defaults to 4000
- `--strip-uploads-location <Y|N>` - use ExifTool to strip uploads of sensitive location data
- `--read-uploads-description <Y|N>` - use ExifTool to read image descriptions from uploads
- `--strip-uploads-location <Y|N>` - strip sensitive location data from uploaded images
- `--read-uploads-description <Y|N>` - read image descriptions from upload metadata
- `--anonymize-uploads <Y|N>` - randomize uploaded filenames
- `--dedupe-uploads <Y|N>` - store files based on their hash to reduce data storage requirements if duplicates are uploaded with different filenames
- `--skip-release-env` - skip generation the release environment file

View file

@ -705,15 +705,19 @@ This filter replaces the filename (not the path) of an upload. For complete obfu
No specific configuration.
#### Pleroma.Upload.Filter.Exiftool.StripLocation
#### Pleroma.Upload.Filter.Exif.StripLocation
This filter only strips the GPS and location metadata with Exiftool leaving color profiles and attributes intact.
This filter strips GPS and location metadata from JPEG, WebP, HEIC, AVIF and TIFF images, leaving color profiles and attributes intact. For PNG images it removes all text and EXIF metadata chunks. Images are not re-encoded, and other formats are left unchanged.
It replaces the deprecated `Pleroma.Upload.Filter.Exiftool.StripLocation`, which no longer needs exiftool.
No specific configuration.
#### Pleroma.Upload.Filter.Exiftool.ReadDescription
#### Pleroma.Upload.Filter.Exif.ReadDescription
This filter reads the ImageDescription and iptc:Caption-Abstract fields with Exiftool so clients can prefill the media description field.
This filter reads the ImageDescription and iptc:Caption-Abstract fields so clients can prefill the media description field.
It replaces the deprecated `Pleroma.Upload.Filter.Exiftool.ReadDescription`, which no longer needs exiftool.
No specific configuration.

View file

@ -68,7 +68,7 @@ sudo rc-update add postgresql
### Install media / graphics packages (optional, see [`docs/installation/optional/media_graphics_packages.md`](../installation/optional/media_graphics_packages.md))
```shell
sudo apk add ffmpeg imagemagick exiftool
sudo apk add ffmpeg imagemagick
```
### Install PleromaBE

View file

@ -36,7 +36,7 @@ sudo apt install elixir erlang-dev erlang-nox
### Optional packages: [`docs/installation/optional/media_graphics_packages.md`](../installation/optional/media_graphics_packages.md)
```shell
sudo apt install imagemagick ffmpeg libimage-exiftool-perl
sudo apt install imagemagick ffmpeg
```
### Install PleromaBE

View file

@ -28,7 +28,6 @@ Note: This article is potentially outdated because at this time we may not have
- `certbot` (または何らかのLet's Encrypt向けACMEクライアント)
- `ImageMagick`
- `ffmpeg`
- `exiftool`
### システムを準備する
@ -60,7 +59,7 @@ sudo apt install elixir erlang-dev erlang-nox
### オプションパッケージ: [`docs/installation/optional/media_graphics_packages.md`](../installation/optional/media_graphics_packages.md)
```shell
sudo apt install imagemagick ffmpeg libimage-exiftool-perl
sudo apt install imagemagick ffmpeg
```
### Pleroma BE (バックエンド) をインストールします

View file

@ -31,7 +31,7 @@ Setup the required services to automatically start at boot, using `sysrc(8)`.
### Install media / graphics packages (optional, see [`docs/installation/optional/media_graphics_packages.md`](../installation/optional/media_graphics_packages.md))
```shell
# pkg install imagemagick ffmpeg p5-Image-ExifTool vips
# pkg install imagemagick ffmpeg vips
```
## Configuring Pleroma

View file

@ -12,7 +12,6 @@
* ImageMagick
* FFmpeg
* exiftool
!!!info
**Source installs only**

View file

@ -41,7 +41,6 @@ Gentoo quite pointedly does not come with a cron daemon installed, and as such i
* `app-crypt/certbot-nginx` (nginx certbot plugin that allows use of the all-powerful `--nginx` flag on certbot)
* `media-gfx/imagemagick`
* `media-video/ffmpeg`
* `media-libs/exiftool`
### Prepare the system
@ -97,7 +96,7 @@ Not only does this make it much easier to deploy changes you make, as you can co
### Install media / graphics packages (optional, see [`docs/installation/optional/media_graphics_packages.md`](docs/installation/optional/media_graphics_packages.md))
```shell
# emerge --ask media-video/ffmpeg media-gfx/imagemagick media-libs/exiftool
# emerge --ask media-video/ffmpeg media-gfx/imagemagick
```
### Install PleromaBE

View file

@ -25,7 +25,6 @@ Gentoo quite pointedly does not come with a cron daemon installed, and as such i
* `app-crypt/certbot-nginx` (nginx certbot plugin that allows use of the all-powerful `--nginx` flag on certbot)
* `media-gfx/imagemagick`
* `media-video/ffmpeg`
* `media-libs/exiftool`
### Prepare the system
@ -68,7 +67,7 @@ The output from emerging postgresql should give you a command for initializing t
See [Optional software packages needed for specific functionality](optional/media_graphics_packages.md) for details.
```shell
# emerge --ask media-video/ffmpeg media-gfx/imagemagick media-libs/exiftool
# emerge --ask media-video/ffmpeg media-gfx/imagemagick
```
### Setup PleromaBE

View file

@ -26,12 +26,11 @@ Independently of the reverse proxy, [acme-client(1)](https://man.openbsd.org/acm
* ImageMagick
* ffmpeg
* exiftool
To install the above:
```
# pkg_add ImageMagick ffmpeg p5-Image-ExifTool
# pkg_add ImageMagick ffmpeg
```
For more information read [`docs/installation/optional/media_graphics_packages.md`](../installation/optional/media_graphics_packages.md):

View file

@ -25,11 +25,10 @@ Asenna tarvittava ohjelmisto:
[`docs/installation/optional/media_graphics_packages.md`](../installation/optional/media_graphics_packages.md):
* ImageMagick
* ffmpeg
* exiftool
Asenna tarvittava ohjelmisto:
`# pkg_add ImageMagick ffmpeg p5-Image-ExifTool`
`# pkg_add ImageMagick ffmpeg`
Luo postgresql-tietokanta:

View file

@ -2,9 +2,8 @@
For specific Pleroma functionality (which is disabled by default) some or all of the below packages are required:
* `ImageMagic`
* `ImageMagick`
* `ffmpeg`
* `exiftool`
Please refer to documentation in `docs/installation` on how to install them on specific OS.
@ -26,12 +25,3 @@ It is required for the following Pleroma features:
It is required for the following Pleroma features:
* Media preview proxy for videos (related config: `media_preview_proxy/enabled` in `config/config.exs`)
## `exiftool`
`exiftool` is media files metadata reader/writer.
It is required for the following Pleroma features:
* `Pleroma.Upload.Filters.Exiftool.StripLocation` upload filter (related config: `Pleroma.Upload/filters` in `config/config.exs`)
* `Pleroma.Upload.Filters.Exiftool.ReadDescription` upload filter (related config: `Pleroma.Upload/filters` in `config/config.exs`)

View file

@ -61,17 +61,16 @@ Other than things bundled in the OTP release Pleroma depends on:
Per [`docs/installation/optional/media_graphics_packages.md`](optional/media_graphics_packages.md):
* ImageMagick
* ffmpeg
* exiftool
=== "Alpine"
```
apk update
apk add imagemagick ffmpeg exiftool
apk add imagemagick ffmpeg
```
=== "Debian/Ubuntu"
```
apt install imagemagick ffmpeg libimage-exiftool-perl
apt install imagemagick ffmpeg
```
## Setup

View file

@ -12,7 +12,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
openssl libssl3 \
libmagic1t64 file \
postgresql-client \
ffmpeg imagemagick libimage-exiftool-perl \
ffmpeg imagemagick \
libvips42t64 \
unzip \
curl \

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

View file

@ -205,6 +205,7 @@ defmodule Pleroma.Mixfile do
{:majic, "~> 1.2"},
{:open_api_spex, "~> 3.22"},
{:ecto_psql_extras, "~> 0.8"},
{:exif_stripper, "~> 0.1.3"},
{:vix, "~> 0.36"},
{:elixir_make, "~> 0.7.8", override: true},
{:blurhash, "~> 0.1.0", hex: :rinpatch_blurhash},

View file

@ -47,6 +47,7 @@
"ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"},
"ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"},
"ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"},
"exif_stripper": {:hex, :exif_stripper, "0.1.3", "b3d89a623bb49c09fbe45fe5d4f1becee0ffe011bcade174c3bb91cdae59745e", [:mix], [], "hexpm", "e1ab26d766205ae54be57c19ec70d408b020f3c31855b49d53a55460de81c6e1"},
"exile": {:hex, :exile, "0.10.0", "b69e2d27a9af670b0f0a0898addca0eda78f6f5ba95ccfbc9bc6ccdd04925436", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "c62ee8fee565b5ac4a898d0dcd58d2b04fb5eec1655af1ddcc9eb582c6732c33"},
"expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"},
"fast_html": {:hex, :fast_html, "2.5.0", "23578c1c1fa03db6a3786d78218b2d944df34b0fc3729e72de912f390913c80f", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "69eb46ed98a5d9cca1ccd4a5ac94ce5dd626fc29513fbaa0a16cd8b2da67ae3e"},

View file

@ -0,0 +1,41 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Repo.Migrations.UploadFilterExiftoolToExif do
use Ecto.Migration
alias Pleroma.ConfigDB
@up %{
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
}
@down %{
Pleroma.Upload.Filter.Exif.StripLocation => Pleroma.Upload.Filter.Exiftool.StripLocation,
Pleroma.Upload.Filter.Exif.ReadDescription => Pleroma.Upload.Filter.Exiftool.ReadDescription
}
def up, do: rename_filters(@up)
def down, do: rename_filters(@down)
# Only touch configs that list filters: adding an empty list would hide the
# filters from the config file.
defp rename_filters(renames) do
with %{value: value} <- ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Upload}),
filters when is_list(filters) <- value[:filters],
new_filters when new_filters != filters <-
filters |> Enum.map(&Map.get(renames, &1, &1)) |> Enum.uniq() do
ConfigDB.update_or_create(%{
group: :pleroma,
key: Pleroma.Upload,
value: Keyword.put(value, :filters, new_filters)
})
else
_ -> nil
end
end
end

BIN
test/fixtures/dji_gps.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
test/fixtures/strip_location_gps.heic vendored Normal file

Binary file not shown.

BIN
test/fixtures/strip_location_gps.tiff vendored Normal file

Binary file not shown.

BIN
test/fixtures/strip_location_gps.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

BIN
test/fixtures/xmp_gps.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -95,7 +95,7 @@ defmodule Mix.Tasks.Pleroma.InstanceTest do
assert generated_config =~ "http: [ip: {127, 0, 0, 1}, port: 4000]"
assert generated_config =~
"filters: [Pleroma.Upload.Filter.Exiftool.StripLocation, Pleroma.Upload.Filter.Exiftool.ReadDescription]"
"filters: [Pleroma.Upload.Filter.Exif.StripLocation, Pleroma.Upload.Filter.Exif.ReadDescription]"
assert File.read!(tmp_path() <> "setup.psql") == generated_setup_psql()
assert File.exists?(Path.expand("./test/instance/static/robots.txt"))

View file

@ -13,40 +13,39 @@ defmodule Pleroma.Config.DeprecationWarningsTest do
describe "filter exiftool" do
test "gives warning when still used" do
clear_config(
[Pleroma.Upload, :filters],
[Pleroma.Upload.Filter.Exiftool]
)
assert capture_log(fn -> DeprecationWarnings.check_exiftool_filter() end) =~
"""
!!!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:
```
config :pleroma, Pleroma.Upload,
filters: [Pleroma.Upload.Filter.Exiftool]
```
Is now
```
config :pleroma, Pleroma.Upload,
filters: [Pleroma.Upload.Filter.Exiftool.StripLocation]
```
"""
end
test "changes setting to exiftool strip location" do
clear_config(
[Pleroma.Upload, :filters],
[Pleroma.Upload.Filter.Exiftool, Pleroma.Upload.Filter.Exiftool.ReadDescription]
)
log = capture_log(fn -> DeprecationWarnings.check_exiftool_filter() end)
assert log =~ "!!!DEPRECATION WARNING!!!"
assert log =~
"* `Pleroma.Upload.Filter.Exiftool` is now `Pleroma.Upload.Filter.Exif.StripLocation`"
assert log =~
"* `Pleroma.Upload.Filter.Exiftool.ReadDescription` is now `Pleroma.Upload.Filter.Exif.ReadDescription`"
refute log =~ "Exiftool.StripLocation`"
end
test "changes setting to the Exif filters" do
clear_config(
[Pleroma.Upload, :filters],
[
Pleroma.Upload.Filter.Dedupe,
Pleroma.Upload.Filter.Exiftool,
Pleroma.Upload.Filter.Exiftool.StripLocation,
Pleroma.Upload.Filter.Exiftool.ReadDescription
]
)
expected_config = [
Pleroma.Upload.Filter.Exiftool.StripLocation,
Pleroma.Upload.Filter.Exiftool.ReadDescription
Pleroma.Upload.Filter.Dedupe,
Pleroma.Upload.Filter.Exif.StripLocation,
Pleroma.Upload.Filter.Exif.ReadDescription
]
capture_log(fn -> DeprecationWarnings.warn() end)
@ -58,8 +57,8 @@ defmodule Pleroma.Config.DeprecationWarningsTest do
clear_config(
[Pleroma.Upload, :filters],
[
Pleroma.Upload.Filter.Exiftool.StripLocation,
Pleroma.Upload.Filter.Exiftool.ReadDescription
Pleroma.Upload.Filter.Exif.StripLocation,
Pleroma.Upload.Filter.Exif.ReadDescription
]
)

View file

@ -0,0 +1,73 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Repo.Migrations.UploadFilterExiftoolToExifTest do
use Pleroma.DataCase
import Pleroma.Factory
import Pleroma.Tests.Helpers
alias Pleroma.ConfigDB
setup_all do: require_migration("20261027000000_upload_filter_exiftool_to_exif")
defp filters do
ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Upload}).value[:filters]
end
test "up/0 renames the Exiftool filters", %{migration: migration} do
insert(:config,
group: :pleroma,
key: Pleroma.Upload,
value: [
filters: [
Pleroma.Upload.Filter.Dedupe,
Pleroma.Upload.Filter.Exiftool,
Pleroma.Upload.Filter.Exiftool.StripLocation,
Pleroma.Upload.Filter.Exiftool.ReadDescription
]
]
)
migration.up()
assert filters() == [
Pleroma.Upload.Filter.Dedupe,
Pleroma.Upload.Filter.Exif.StripLocation,
Pleroma.Upload.Filter.Exif.ReadDescription
]
end
test "down/0 restores the Exiftool filters", %{migration: migration} do
insert(:config,
group: :pleroma,
key: Pleroma.Upload,
value: [
filters: [
Pleroma.Upload.Filter.Exif.StripLocation,
Pleroma.Upload.Filter.Exif.ReadDescription
]
]
)
migration.down()
assert filters() == [
Pleroma.Upload.Filter.Exiftool.StripLocation,
Pleroma.Upload.Filter.Exiftool.ReadDescription
]
end
test "up/0 does nothing without a database config", %{migration: migration} do
assert migration.up() == nil
assert ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Upload}) == nil
end
test "up/0 does not add filters to a database config without them", %{migration: migration} do
insert(:config, group: :pleroma, key: Pleroma.Upload, value: [uploader: Pleroma.Uploaders.S3])
migration.up()
value = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Upload}).value
assert value == [uploader: Pleroma.Uploaders.S3]
end
end

View file

@ -0,0 +1,85 @@
# 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.ReadDescriptionTest do
use Pleroma.DataCase, async: true
alias Pleroma.Upload.Filter.Exif.ReadDescription
defp upload(fixture, attrs \\ []) do
path = Path.absname(Path.join("test/fixtures", fixture))
struct!(
%Pleroma.Upload{
name: fixture,
content_type: "image/" <> String.trim_leading(Path.extname(fixture), "."),
path: path,
tempfile: path,
description: nil
},
attrs
)
end
test "keeps description when not empty" do
upload =
upload("image_with_imagedescription_and_caption-abstract.jpg",
description: "Some description"
)
assert ReadDescription.filter(upload) == {:ok, :noop}
end
test "otherwise returns ImageDescription when present" do
upload = upload("image_with_imagedescription_and_caption-abstract.jpg")
assert ReadDescription.filter(upload) ==
{:ok, :filtered, %{upload | description: "a descriptive white pixel"}}
end
test "ignores stray data after the image" do
upload = upload("image_with_imagedescription_and_caption-abstract_and_stray_data_after.png")
assert {:ok, :filtered, %{description: "a descriptive white pixel"}} =
ReadDescription.filter(upload)
assert ReadDescription.filter(upload("image_with_stray_data_after.png")) == {:ok, :noop}
end
test "otherwise returns iptc:Caption-Abstract when present" do
upload = upload("image_with_caption-abstract.jpg")
assert ReadDescription.filter(upload) ==
{:ok, :filtered, %{upload | description: "an abstract white pixel"}}
end
test "reads ImageDescription from WebP and HEIC" do
assert {:ok, :filtered, %{description: "a webp with a location"}} =
ReadDescription.filter(upload("strip_location_gps.webp"))
assert {:ok, :filtered, %{description: "a heic with a location"}} =
ReadDescription.filter(upload("strip_location_gps.heic"))
end
test "does nothing when there is no description" do
assert ReadDescription.filter(upload("image_with_no_description.jpg")) == {:ok, :noop}
end
test "ignores descriptions that exceed the maximum length" do
clear_config([:instance, :description_limit], 5)
upload = upload("image_with_imagedescription_and_caption-abstract.jpg")
assert ReadDescription.filter(upload) == {:ok, :noop}
end
test "ignores content with only whitespace" do
upload = upload("image_with_imagedescription_and_caption-abstract_whitespaces.jpg")
assert ReadDescription.filter(upload) == {:ok, :noop}
end
test "does nothing when the file can't be read" do
upload = %{upload("image.jpg") | tempfile: Path.absname("test/fixtures/non-existant_tmp.jpg")}
assert ReadDescription.filter(upload) == {:ok, :noop}
end
end

View file

@ -0,0 +1,72 @@
# 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.StripLocationTest do
use Pleroma.DataCase, async: true
import Pleroma.Tests.ExifHelpers
alias Pleroma.Upload.Filter.Exif.StripLocation
for {fixture, content_type} <- [
{"DSCN0010.jpg", "image/jpeg"},
{"DSCN0010.png", "image/png"},
{"strip_location_gps.webp", "image/webp"},
{"strip_location_gps.heic", "image/heic"},
{"strip_location_gps_grid.heic", "image/heic"},
{"strip_location_gps.tiff", "image/tiff"}
] do
test "strips GPS data from #{fixture} (#{content_type})" do
upload = upload_copy(unquote(fixture), unquote(content_type))
assert gps_tags(upload.tempfile) =~ "GPS Latitude"
assert StripLocation.filter(upload) == {:ok, :filtered}
assert gps_tags(upload.tempfile) == ""
end
end
test "strips GPS data from XMP, including DJI drone positions" do
for fixture <- ["xmp_gps.jpg", "dji_gps.jpg"] do
upload = upload_copy(fixture, "image/jpeg")
assert exiftool_xmp(upload.tempfile) =~ "GPSLatitude"
assert StripLocation.filter(upload) == {:ok, :filtered}
refute exiftool_xmp(upload.tempfile) =~ ~r/GPS|AbsoluteAltitude/
end
end
defp exiftool_xmp(path) do
{output, 0} = System.cmd("exiftool", ["-m", "-s", "-xmp:all", path])
output
end
test "keeps other EXIF data in HEIC" do
upload = upload_copy("strip_location_gps.heic", "image/heic")
assert StripLocation.filter(upload) == {:ok, :filtered}
assert tag(upload.tempfile, "ImageDescription") == "a heic with a location"
assert tag(upload.tempfile, "ImageSize") == "64x48"
end
test "does nothing when there is no GPS data" do
upload = upload_copy("image.jpg", "image/jpeg")
original = File.read!(upload.tempfile)
assert StripLocation.filter(upload) == {:ok, :noop}
assert File.read!(upload.tempfile) == original
end
test "accepts PNG files with data after IEND" do
upload = upload_copy("image_with_stray_data_after.png", "image/png")
assert {:ok, _} = StripLocation.filter(upload)
end
test "ignores non-image uploads" do
upload = upload_copy("DSCN0010.jpg", "application/octet-stream")
assert StripLocation.filter(upload) == {:ok, :noop}
assert gps_tags(upload.tempfile) =~ "GPS Latitude"
end
end

View file

@ -1,144 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.Exiftool.ReadDescriptionTest do
use Pleroma.DataCase
alias Pleroma.Upload.Filter
@uploads %Pleroma.Upload{
name: "image_with_imagedescription_and_caption-abstract.jpg",
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_with_imagedescription_and_caption-abstract.jpg"),
tempfile: Path.absname("test/fixtures/image_with_imagedescription_and_caption-abstract.jpg"),
description: nil
}
test "keeps description when not empty" do
uploads = %Pleroma.Upload{
name: "image_with_imagedescription_and_caption-abstract.jpg",
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_with_imagedescription_and_caption-abstract.jpg"),
tempfile:
Path.absname("test/fixtures/image_with_imagedescription_and_caption-abstract.jpg"),
description: "Some description"
}
assert Filter.Exiftool.ReadDescription.filter(uploads) ==
{:ok, :noop}
end
test "otherwise returns ImageDescription when present" do
uploads_after = %Pleroma.Upload{
name: "image_with_imagedescription_and_caption-abstract.jpg",
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_with_imagedescription_and_caption-abstract.jpg"),
tempfile:
Path.absname("test/fixtures/image_with_imagedescription_and_caption-abstract.jpg"),
description: "a descriptive white pixel"
}
assert Filter.Exiftool.ReadDescription.filter(@uploads) ==
{:ok, :filtered, uploads_after}
end
test "Ignores warnings" do
uploads = %Pleroma.Upload{
name: "image_with_imagedescription_and_caption-abstract_and_stray_data_after.png",
content_type: "image/png",
path:
Path.absname(
"test/fixtures/image_with_imagedescription_and_caption-abstract_and_stray_data_after.png"
),
tempfile:
Path.absname(
"test/fixtures/image_with_imagedescription_and_caption-abstract_and_stray_data_after.png"
)
}
assert {:ok, :filtered, %{description: "a descriptive white pixel"}} =
Filter.Exiftool.ReadDescription.filter(uploads)
uploads = %Pleroma.Upload{
name: "image_with_stray_data_after.png",
content_type: "image/png",
path: Path.absname("test/fixtures/image_with_stray_data_after.png"),
tempfile: Path.absname("test/fixtures/image_with_stray_data_after.png")
}
assert {:ok, :filtered, %{description: nil}} = Filter.Exiftool.ReadDescription.filter(uploads)
end
test "otherwise returns iptc:Caption-Abstract when present" do
upload = %Pleroma.Upload{
name: "image_with_caption-abstract.jpg",
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_with_caption-abstract.jpg"),
tempfile: Path.absname("test/fixtures/image_with_caption-abstract.jpg"),
description: nil
}
upload_after = %Pleroma.Upload{
name: "image_with_caption-abstract.jpg",
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_with_caption-abstract.jpg"),
tempfile: Path.absname("test/fixtures/image_with_caption-abstract.jpg"),
description: "an abstract white pixel"
}
assert Filter.Exiftool.ReadDescription.filter(upload) ==
{:ok, :filtered, upload_after}
end
test "otherwise returns nil" do
uploads = %Pleroma.Upload{
name: "image_with_no_description.jpg",
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_with_no_description.jpg"),
tempfile: Path.absname("test/fixtures/image_with_no_description.jpg"),
description: nil
}
assert Filter.Exiftool.ReadDescription.filter(uploads) ==
{:ok, :filtered, uploads}
end
test "Return nil when image description from EXIF data exceeds the maximum length" do
clear_config([:instance, :description_limit], 5)
assert Filter.Exiftool.ReadDescription.filter(@uploads) ==
{:ok, :filtered, @uploads}
end
test "Ignores content with only whitespace" do
uploads = %Pleroma.Upload{
name: "non-existant.jpg",
content_type: "image/jpeg",
path:
Path.absname(
"test/fixtures/image_with_imagedescription_and_caption-abstract_whitespaces.jpg"
),
tempfile:
Path.absname(
"test/fixtures/image_with_imagedescription_and_caption-abstract_whitespaces.jpg"
),
description: nil
}
assert Filter.Exiftool.ReadDescription.filter(uploads) ==
{:ok, :filtered, uploads}
end
test "Return nil when image description from EXIF data can't be read" do
uploads = %Pleroma.Upload{
name: "non-existant.jpg",
content_type: "image/jpeg",
path: Path.absname("test/fixtures/non-existant.jpg"),
tempfile: Path.absname("test/fixtures/non-existant_tmp.jpg"),
description: nil
}
assert Filter.Exiftool.ReadDescription.filter(uploads) ==
{:ok, :filtered, uploads}
end
end

View file

@ -1,51 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.Exiftool.StripLocationTest do
use Pleroma.DataCase, async: true
alias Pleroma.Upload.Filter
test "apply exiftool filter" do
assert Pleroma.Utils.command_available?("exiftool")
~w{jpg png}
|> Enum.map(fn type ->
File.cp!(
"test/fixtures/DSCN0010.#{type}",
"test/fixtures/DSCN0010_tmp.#{type}"
)
upload = %Pleroma.Upload{
name: "image_with_GPS_data.#{type}",
content_type: "image/jpeg",
path: Path.absname("test/fixtures/DSCN0010.#{type}"),
tempfile: Path.absname("test/fixtures/DSCN0010_tmp.#{type}")
}
assert Filter.Exiftool.StripLocation.filter(upload) == {:ok, :filtered}
{exif_original, 0} = System.cmd("exiftool", ["-m", "test/fixtures/DSCN0010.#{type}"])
{exif_filtered, 0} = System.cmd("exiftool", ["-m", "test/fixtures/DSCN0010_tmp.#{type}"])
assert String.match?(exif_original, ~r/GPS/)
refute String.match?(exif_filtered, ~r/GPS/)
end)
end
test "verify webp, heic, svg files are skipped" do
uploads =
~w{webp heic svg svg+xml}
|> Enum.map(fn type ->
%Pleroma.Upload{
name: "sample.#{type}",
content_type: "image/#{type}"
}
end)
uploads
|> Enum.each(fn upload ->
assert Filter.Exiftool.StripLocation.filter(upload) == {:ok, :noop}
end)
end
end

View file

@ -0,0 +1,31 @@
# 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.ExiftoolTest do
# Instances may still configure the deprecated Exiftool filter names.
# They must keep working, including for formats exiftool used to skip.
use Pleroma.DataCase, async: true
import Pleroma.Tests.ExifHelpers
for filter <- [Pleroma.Upload.Filter.Exiftool, Pleroma.Upload.Filter.Exiftool.StripLocation] do
test "#{inspect(filter)} strips GPS data from HEIC uploads" do
upload = upload_copy("strip_location_gps.heic", "image/heic")
assert gps_tags(upload.tempfile) =~ "GPS Latitude"
assert Pleroma.Upload.Filter.filter([unquote(filter)], upload) == {:ok, upload}
assert gps_tags(upload.tempfile) == ""
end
end
test "Exiftool.ReadDescription reads the description" do
upload = upload_copy("strip_location_gps.heic", "image/heic", description: nil)
assert {:ok, %{description: "a heic with a location"}} =
Pleroma.Upload.Filter.filter(
[Pleroma.Upload.Filter.Exiftool.ReadDescription],
upload
)
end
end

View file

@ -0,0 +1,44 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Tests.ExifHelpers do
@moduledoc """
Helpers for upload filter tests that use exiftool as an independent oracle.
"""
@doc "Copies a fixture to a unique temporary file and returns an upload for it."
def upload_copy(fixture, content_type, attrs \\ []) do
source = Path.absname(Path.join("test/fixtures", fixture))
tempfile =
Path.join(
System.tmp_dir!(),
"pleroma_exif_#{System.unique_integer([:positive])}#{Path.extname(fixture)}"
)
File.cp!(source, tempfile)
ExUnit.Callbacks.on_exit(fn -> File.rm(tempfile) end)
struct!(
%Pleroma.Upload{
name: fixture,
content_type: content_type,
path: source,
tempfile: tempfile
},
attrs
)
end
@doc "Returns the GPS tags exiftool finds in a file, as text."
def gps_tags(path), do: exiftool!(["-gps:all", path])
@doc "Returns the value of one tag as exiftool reads it."
def tag(path, tag), do: exiftool!(["-s3", "-#{tag}", path])
defp exiftool!(args) do
{output, 0} = System.cmd("exiftool", ["-m" | args])
String.trim(output)
end
end