Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
14 KB
Referenced Files
None
Subscribers
None
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

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)

Event Timeline