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

@ -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