Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85831892
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
14 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/pleroma/upload/filter/exif/read_description.ex b/lib/pleroma/upload/filter/exif/read_description.ex
new file mode 100644
index 000000000..654422b1b
--- /dev/null
+++ b/lib/pleroma/upload/filter/exif/read_description.ex
@@ -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, then XMP. 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
diff --git a/lib/pleroma/upload/filter/exif/strip_location.ex b/lib/pleroma/upload/filter/exif/strip_location.ex
new file mode 100644
index 000000000..2e2b79f4c
--- /dev/null
+++ b/lib/pleroma/upload/filter/exif/strip_location.ex
@@ -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 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
diff --git a/test/fixtures/strip_location_gps.heic b/test/fixtures/strip_location_gps.heic
new file mode 100644
index 000000000..f27961d83
Binary files /dev/null and b/test/fixtures/strip_location_gps.heic differ
diff --git a/test/fixtures/strip_location_gps.webp b/test/fixtures/strip_location_gps.webp
new file mode 100644
index 000000000..9c7724529
Binary files /dev/null and b/test/fixtures/strip_location_gps.webp differ
diff --git a/test/pleroma/upload/filter/exif/read_description_test.exs b/test/pleroma/upload/filter/exif/read_description_test.exs
new file mode 100644
index 000000000..15d2944cd
--- /dev/null
+++ b/test/pleroma/upload/filter/exif/read_description_test.exs
@@ -0,0 +1,85 @@
+# 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.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
diff --git a/test/pleroma/upload/filter/exif/strip_location_test.exs b/test/pleroma/upload/filter/exif/strip_location_test.exs
new file mode 100644
index 000000000..01ca37711
--- /dev/null
+++ b/test/pleroma/upload/filter/exif/strip_location_test.exs
@@ -0,0 +1,49 @@
+# 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"}
+ ] do
+ test "strips GPS data from #{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 "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 "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
diff --git a/test/pleroma/upload/filter/exiftool/read_description_test.exs b/test/pleroma/upload/filter/exiftool/read_description_test.exs
deleted file mode 100644
index 9a1bd61d7..000000000
--- a/test/pleroma/upload/filter/exiftool/read_description_test.exs
+++ /dev/null
@@ -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
diff --git a/test/support/exif_helpers.ex b/test/support/exif_helpers.ex
new file mode 100644
index 000000000..3a8a3b84e
--- /dev/null
+++ b/test/support/exif_helpers.ex
@@ -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
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Nov 1, 11:49 PM (11 h, 47 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1803436
Default Alt Text
(14 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline