diff --git a/changelog.d/4178-strip-gps-without-exiftool.change b/changelog.d/4178-strip-gps-without-exiftool.change new file mode 100644 index 000000000..7944b1267 --- /dev/null +++ b/changelog.d/4178-strip-gps-without-exiftool.change @@ -0,0 +1 @@ +Strip GPS data of JPGs and PNGs without exiftool. diff --git a/lib/pleroma/upload/filter/exiftool/strip_location.ex b/lib/pleroma/upload/filter/exiftool/strip_location.ex index 1744a286d..fee5632c8 100644 --- a/lib/pleroma/upload/filter/exiftool/strip_location.ex +++ b/lib/pleroma/upload/filter/exiftool/strip_location.ex @@ -15,18 +15,109 @@ defmodule Pleroma.Upload.Filter.Exiftool.StripLocation do 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", ["-overwrite_original", "-gps:all=", "-png:all=", file], - parallelism: true - ) do - {_response, 0} -> {:ok, :filtered} - {error, 1} -> {:error, error} - end - rescue - e in ErlangError -> - {:error, "#{__MODULE__}: #{inspect(e)}"} + case ExifGpsStripper.strip_gps_data(file, file) do + :ok -> {:ok, :filtered} + {:error, reason} -> {:error, reason} end end def filter(_), do: {:ok, :noop} end + +defmodule ExifGpsStripper do + @exif_header <<0xFF, 0xE1>> + @gps_ifd_tag 0x8825 + + def strip_gps_data(input_path, output_path) do + case File.read(input_path) do + {:ok, data} -> + case process_file(data) do + {:ok, stripped_data} -> File.write(output_path, stripped_data) + {:error, reason} -> {:error, reason} + end + + {:error, reason} -> + {:error, "Failed to read file: #{reason}"} + end + end + + defp process_file(<<0x89, "PNG", 0x0D, 0x0A, 0x1A, 0x0A, rest::binary>>) do + # PNG file + {:ok, <<0x89, "PNG", 0x0D, 0x0A, 0x1A, 0x0A>> <> strip_png_gps(rest)} + end + + defp process_file(<<0xFF, 0xD8, rest::binary>>) do + # JPEG file + {:ok, <<0xFF, 0xD8>> <> strip_jpeg_gps(rest)} + end + + defp process_file(_), do: {:error, "Unsupported file format"} + + defp strip_png_gps(data) do + strip_png_chunks(data, []) + end + + defp strip_png_chunks(<<>>, acc), do: IO.iodata_to_binary(Enum.reverse(acc)) + + defp strip_png_chunks( + <>, + acc + ) do + case chunk_type do + "eXIf" -> + strip_png_chunks(rest, acc) + + "tEXt" -> + strip_png_chunks(rest, acc) + + _ -> + chunk = + <> + + strip_png_chunks(rest, [chunk | acc]) + end + end + + defp strip_jpeg_gps(data) do + case :binary.match(data, @exif_header) do + :nomatch -> + data + + {pos, _} -> + <> = data + + stripped_exif = strip_gps_from_exif(exif_data) + new_size = byte_size(stripped_exif) + 2 + before <> @exif_header <> <> <> stripped_exif <> rest + end + end + + defp strip_gps_from_exif(<<"Exif", 0, 0, tiff_header::binary-size(8), ifd_data::binary>>) do + {stripped_ifd, _} = strip_gps_from_ifd(ifd_data) + <<"Exif", 0, 0, tiff_header::binary-size(8), stripped_ifd::binary>> + end + + defp strip_gps_from_ifd(<>) do + {entries, remaining} = strip_gps_from_entries(rest, count, []) + {<> <> IO.iodata_to_binary(entries), remaining} + end + + defp strip_gps_from_entries(data, 0, acc), do: {Enum.reverse(acc), data} + + defp strip_gps_from_entries( + <>, + entries_left, + acc + ) do + entry = <> + + if tag == @gps_ifd_tag do + strip_gps_from_entries(rest, entries_left - 1, acc) + else + strip_gps_from_entries(rest, entries_left - 1, [entry | acc]) + end + end +end