diff --git a/CHANGELOG.md b/CHANGELOG.md
index 394eb5179..7ced3b678 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -150,6 +150,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Added move account API
- Enable remote users to interact with posts
- Possibility to discover users like `user@example.org`, while Pleroma is working on `pleroma.example.org`. Additional configuration required.
+- Added Pleroma.Upload.Filter.HeifToJpeg to automate converting .heic files from Apple devices to JPEGs which can be viewed in browsers.
### Fixed
- Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies
diff --git a/lib/pleroma/upload/filter/heif_to_jpeg.ex b/lib/pleroma/upload/filter/heif_to_jpeg.ex
new file mode 100644
index 000000000..a2095ba01
--- /dev/null
+++ b/lib/pleroma/upload/filter/heif_to_jpeg.ex
@@ -0,0 +1,36 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.Filter.HeifToJpeg do
+ @behaviour Pleroma.Upload.Filter
+ alias Pleroma.Upload
+ alias Vix.Vips.Operation
+
+ @type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()}
+ @type conversions :: conversion() | [conversion()]
+
+ @spec filter(Pleroma.Upload.t()) :: {:ok, :atom} | {:error, String.t()}
+ def filter(%Pleroma.Upload{content_type: "image/avif"} = upload), do: apply_filter(upload)
+ def filter(%Pleroma.Upload{content_type: "image/heic"} = upload), do: apply_filter(upload)
+ def filter(%Pleroma.Upload{content_type: "image/heif"} = upload), do: apply_filter(upload)
+
+ def filter(_), do: {:ok, :noop}
+
+ defp apply_filter(%Pleroma.Upload{name: name, path: path, tempfile: tempfile} = upload) do
+ ext = String.split(path, ".") |> List.last()
+
+ try do
+ name = name |> String.replace_suffix(ext, "jpg")
+ path = path |> String.replace_suffix(ext, "jpg")
+ {:ok, {vixdata, _vixflags}} = Operation.heifload(tempfile)
+ {:ok, jpegdata} = Operation.jpegsave_buffer(vixdata)
+ :ok = File.write(tempfile, jpegdata)
+
+ {:ok, :filtered, %Upload{upload | name: name, path: path, content_type: "image/jpeg"}}
+ rescue
+ e in ErlangError ->
+ {:error, "#{__MODULE__}: #{inspect(e)}"}
+ end
+ end
+end
diff --git a/test/fixtures/image.heic b/test/fixtures/image.heic
new file mode 100644
index 000000000..efd119a0e
Binary files /dev/null and b/test/fixtures/image.heic differ
diff --git a/test/pleroma/upload/filter/heif_to_jpeg_test.exs b/test/pleroma/upload/filter/heif_to_jpeg_test.exs
new file mode 100644
index 000000000..7627d18ce
--- /dev/null
+++ b/test/pleroma/upload/filter/heif_to_jpeg_test.exs
@@ -0,0 +1,38 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.Filter.HeifToJpegTest do
+ use Pleroma.DataCase, async: true
+ alias Pleroma.Upload.Filter
+
+ test "apply HeicToJpeg filter" do
+ File.cp!(
+ "test/fixtures/image.heic",
+ "test/fixtures/heictmp"
+ )
+
+ upload = %Pleroma.Upload{
+ name: "image.heic",
+ content_type: "image/heic",
+ path: Path.absname("test/fixtures/image.heic"),
+ tempfile: Path.absname("test/fixtures/heictmp")
+ }
+
+ {:ok, :filtered, result} = Filter.HeifToJpeg.filter(upload)
+
+ assert result.content_type == "image/jpeg"
+ assert result.name == "image.jpg"
+ assert String.ends_with?(result.path, "jpg")
+
+ assert {:ok,
+ %Majic.Result{
+ content:
+ "JPEG image data, JFIF standard 1.02, resolution (DPI), density 96x96, segment length 16, progressive, precision 8, 1024x768, components 3",
+ encoding: "binary",
+ mime_type: "image/jpeg"
+ }} == Majic.perform(result.path, pool: Pleroma.MajicPool)
+
+ on_exit(fn -> File.rm!("test/fixtures/heictmp") end)
+ end
+end