From 813ff87f9ef6e948f4feeeb2c28691e3b3561aa7 Mon Sep 17 00:00:00 2001 From: "byte[]" Date: Mon, 25 Nov 2019 19:06:40 -0500 Subject: [PATCH] processors --- lib/philomena/analyzers/gif.ex | 59 ++++++++++++++++++++++++++++++++ lib/philomena/analyzers/jpeg.ex | 28 +++++++++++++++ lib/philomena/analyzers/png.ex | 45 ++++++++++++++++++++++++ lib/philomena/analyzers/svg.ex | 29 ++++++++++++++++ lib/philomena/analyzers/webm.ex | 39 +++++++++++++++++++++ lib/philomena/processors/gif.ex | 59 ++++++++++++++++++++++++++++++++ lib/philomena/processors/jpeg.ex | 43 +++++++++++++++++++++++ lib/philomena/processors/png.ex | 38 ++++++++++++++++++++ lib/philomena/processors/svg.ex | 41 ++++++++++++++++++++++ lib/philomena/processors/webm.ex | 44 ++++++++++++++++++++++++ mix.exs | 1 + mix.lock | 1 + 12 files changed, 427 insertions(+) create mode 100644 lib/philomena/analyzers/gif.ex create mode 100644 lib/philomena/analyzers/jpeg.ex create mode 100644 lib/philomena/analyzers/png.ex create mode 100644 lib/philomena/analyzers/svg.ex create mode 100644 lib/philomena/analyzers/webm.ex create mode 100644 lib/philomena/processors/gif.ex create mode 100644 lib/philomena/processors/jpeg.ex create mode 100644 lib/philomena/processors/png.ex create mode 100644 lib/philomena/processors/svg.ex create mode 100644 lib/philomena/processors/webm.ex diff --git a/lib/philomena/analyzers/gif.ex b/lib/philomena/analyzers/gif.ex new file mode 100644 index 00000000..e40ba18f --- /dev/null +++ b/lib/philomena/analyzers/gif.ex @@ -0,0 +1,59 @@ +defmodule Philomena.Analyzers.Gif do + def analyze(file) do + animated? = animated?(file) + duration = duration(animated?, file) + + %{ + extension: "gif", + mime_type: "image/gif", + animated?: animated?, + duration: duration, + dimensions: dimensions(file) + } + end + + defp animated?(file) do + System.cmd("identify", [file]) + |> case do + {output, 0} -> + len = + output + |> String.split("\n", parts: 2, trim: true) + |> length() + + len > 1 + + _error -> + nil + end + end + + defp duration(false, _file), do: 0.0 + defp duration(true, file) do + with {output, 0} <- System.cmd("ffprobe", ["-i", file, "-show_entries", "format=duration", "-v", "quiet", "-of", "csv=p=0"]), + {duration, _} <- Float.parse(output) + do + duration + else + _ -> + 0.0 + end + end + + defp dimensions(file) do + System.cmd("identify", ["-format", "%W %H\n", file]) + |> case do + {output, 0} -> + [width, height] = + output + |> String.trim() + |> String.split(" ") + |> Enum.map(&String.to_integer/1) + + {width, height} + + _error -> + {0, 0} + end + end +end \ No newline at end of file diff --git a/lib/philomena/analyzers/jpeg.ex b/lib/philomena/analyzers/jpeg.ex new file mode 100644 index 00000000..fbff77d4 --- /dev/null +++ b/lib/philomena/analyzers/jpeg.ex @@ -0,0 +1,28 @@ +defmodule Philomena.Analyzers.Jpeg do + def analyze(file) do + %{ + extension: "jpg", + mime_type: "image/jpeg", + animated?: false, + duration: 0.0, + dimensions: dimensions(file) + } + end + + defp dimensions(file) do + System.cmd("identify", ["-format", "%W %H\n", file]) + |> case do + {output, 0} -> + [width, height] = + output + |> String.trim() + |> String.split(" ") + |> Enum.map(&String.to_integer/1) + + {width, height} + + _error -> + {0, 0} + end + end +end \ No newline at end of file diff --git a/lib/philomena/analyzers/png.ex b/lib/philomena/analyzers/png.ex new file mode 100644 index 00000000..83c88c6d --- /dev/null +++ b/lib/philomena/analyzers/png.ex @@ -0,0 +1,45 @@ +defmodule Philomena.Analyzers.Png do + def analyze(file) do + animated? = animated?(file) + duration = duration(animated?, file) + + %{ + extension: "png", + mime_type: "image/png", + animated?: animated?, + duration: duration, + dimensions: dimensions(file) + } + end + + defp animated?(file) do + System.cmd("ffprobe", ["-i", file, "-v", "quiet", "-show_entries", "stream=codec_name", "-of", "csv=p=0"]) + |> case do + {"apng\n", 0} -> + true + + _other -> + false + end + end + + # No tooling available for this yet. + defp duration(_animated?, _file), do: 0.0 + + defp dimensions(file) do + System.cmd("identify", ["-format", "%W %H\n", file]) + |> case do + {output, 0} -> + [width, height] = + output + |> String.trim() + |> String.split(" ") + |> Enum.map(&String.to_integer/1) + + {width, height} + + _error -> + {0, 0} + end + end +end \ No newline at end of file diff --git a/lib/philomena/analyzers/svg.ex b/lib/philomena/analyzers/svg.ex new file mode 100644 index 00000000..cc1c2eff --- /dev/null +++ b/lib/philomena/analyzers/svg.ex @@ -0,0 +1,29 @@ +defmodule Philomena.Analyzers.Svg do + def analyze(file) do + %{ + extension: "svg", + mime_type: "image/svg+xml", + animated?: false, + duration: 0.0, + dimensions: dimensions(file) + } + end + + # Force use of MSVG to prevent invoking inkscape + defp dimensions(file) do + System.cmd("identify", ["-format", "%W %H\n", "msvg:#{file}"]) + |> case do + {output, 0} -> + [width, height] = + output + |> String.trim() + |> String.split(" ") + |> Enum.map(&String.to_integer/1) + + {width, height} + + _error -> + {0, 0} + end + end +end \ No newline at end of file diff --git a/lib/philomena/analyzers/webm.ex b/lib/philomena/analyzers/webm.ex new file mode 100644 index 00000000..f3f88514 --- /dev/null +++ b/lib/philomena/analyzers/webm.ex @@ -0,0 +1,39 @@ +defmodule Philomena.Analyzers.Webm do + def analyze(file) do + %{ + extension: "webm", + mime_type: "video/webm", + animated?: true, + duration: duration(file), + dimensions: dimensions(file) + } + end + + defp duration(file) do + with {output, 0} <- System.cmd("ffprobe", ["-i", file, "-show_entries", "format=duration", "-v", "quiet", "-of", "csv=p=0"]), + {duration, _} <- Float.parse(output) + do + duration + else + _ -> + 0.0 + end + end + + defp dimensions(file) do + System.cmd("ffprobe", ["-i", file, "-show_entries", "stream=width,height", "-v", "quiet", "-of", "csv=p=0"]) + |> case do + {output, 0} -> + [width, height] = + output + |> String.trim() + |> String.split(",") + |> Enum.map(&String.to_integer/1) + + {width, height} + + _error -> + {0, 0} + end + end +end \ No newline at end of file diff --git a/lib/philomena/processors/gif.ex b/lib/philomena/processors/gif.ex new file mode 100644 index 00000000..3c909de0 --- /dev/null +++ b/lib/philomena/processors/gif.ex @@ -0,0 +1,59 @@ +defmodule Philomena.Processors.Gif do + alias Philomena.Processors.Gif + + defstruct [:duration, :file, :palette] + + def new(analysis, file) do + %Gif{ + file: file, + duration: analysis.duration, + palette: nil + } + end + + def strip(processor) do + {processor, processor.file} + end + + def preview(processor) do + preview = Briefly.create!(extname: ".png") + + {_output, 0} = + System.cmd("ffmpeg", ["-y", "-i", processor.file, "-ss", to_string(processor.duration / 2), "-frames:v", "1", preview]) + + {processor, preview} + end + + def optimize(processor) do + optimized = Briefly.create!(extname: ".gif") + + {_output, 0} = + System.cmd("gifsicle", ["--careful", "-O2", processor.file, "-o", optimized]) + + {processor, optimized} + end + + def scale(processor, {width, height}) do + processor = generate_palette(processor) + scaled = Briefly.create!(extname: ".gif") + + scale_filter = "scale=w=#{width}:h=#{height}:force_original_aspect_ratio=decrease" + palette_filter = "paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle" + filter_graph = "#{scale_filter} [x]; [x][1:v] #{palette_filter}" + + {_output, 0} = + System.cmd("ffmpeg", ["-y", "-i", processor.file, "-i", processor.palette, "-lavfi", filter_graph, scaled]) + + {processor, scaled} + end + + defp generate_palette(%{palette: nil} = processor) do + palette = Briefly.create!(extname: ".png") + + {_output, 0} = + System.cmd("ffmpeg", ["-y", "-i", processor.file, "-vf", "palettegen=stats_mode=diff", palette]) + + %{processor | palette: palette} + end + defp generate_palette(processor), do: processor +end \ No newline at end of file diff --git a/lib/philomena/processors/jpeg.ex b/lib/philomena/processors/jpeg.ex new file mode 100644 index 00000000..301336a9 --- /dev/null +++ b/lib/philomena/processors/jpeg.ex @@ -0,0 +1,43 @@ +defmodule Philomena.Processors.Jpeg do + alias Philomena.Processors.Jpeg + + defstruct [:file] + + def new(_analysis, file) do + %Jpeg{file: file} + end + + def strip(processor) do + stripped = Briefly.create!(extname: ".jpg") + + {_output, 0} = + System.cmd("convert", [processor.file, "-auto-orient", "-strip", stripped]) + + processor = %{processor | file: stripped} + + {processor, stripped} + end + + def preview(processor) do + {processor, processor.file} + end + + def optimize(processor) do + optimized = Briefly.create!(extname: ".jpg") + + {_output, 0} = + System.cmd("jpegtran", ["-optimize", "-outfile", optimized, processor.file]) + end + + def scale(processor, {width, height}) do + scaled = Briefly.create!(extname: ".jpg") + scale_filter = "scale=w=#{width}:h=#{height}:force_original_aspect_ratio=decrease" + + {_output, 0} = + System.cmd("ffmpeg", ["-y", "-i", processor.file, "-vf", scale_filter, scaled]) + {_output, 0} = + System.cmd("jpegtran", ["-optimize", "-outfile", scaled, scaled]) + + {processor, scaled} + end +end \ No newline at end of file diff --git a/lib/philomena/processors/png.ex b/lib/philomena/processors/png.ex new file mode 100644 index 00000000..22e66bbb --- /dev/null +++ b/lib/philomena/processors/png.ex @@ -0,0 +1,38 @@ +defmodule Philomena.Processors.Png do + alias Philomena.Processors.Png + + defstruct [:file] + + def new(_analysis, file) do + %Png{file: file} + end + + def strip(processor) do + {processor, processor.file} + end + + def preview(processor) do + {processor, processor.file} + end + + def optimize(processor) do + optimized = Briefly.create!(extname: ".png") + + {_output, 0} = + System.cmd("optipng", ["-fix", "-i0", "-o2", processor.file, "-out", optimized]) + + {processor, optimized} + end + + def scale(processor, {width, height}) do + scaled = Briefly.create!(extname: ".png") + scale_filter = "scale=w=#{width}:h=#{height}:force_original_aspect_ratio=decrease" + + {_output, 0} = + System.cmd("ffmpeg", ["-y", "-i", processor.file, "-vf", scale_filter, scaled]) + {_output, 0} = + System.cmd("optipng", ["-i0", "-o1", scaled]) + + {processor, scaled} + end +end \ No newline at end of file diff --git a/lib/philomena/processors/svg.ex b/lib/philomena/processors/svg.ex new file mode 100644 index 00000000..67255412 --- /dev/null +++ b/lib/philomena/processors/svg.ex @@ -0,0 +1,41 @@ +defmodule Philomena.Processors.Svg do + alias Philomena.Processors.Svg + + defstruct [:file, :preview] + + def new(_analysis, file) do + %Svg{file: file, preview: nil} + end + + # FIXME + def strip(processor) do + {processor, processor.file} + end + + def preview(processor) do + preview = Briefly.create!(extname: ".png") + + {_output, 0} = + System.cmd("inkscape", [processor.file, "--export-png", preview]) + + processor = %{processor | preview: preview} + + {processor, preview} + end + + def optimize(processor) do + {processor, processor.file} + end + + def scale(processor, {width, height}) do + scaled = Briefly.create!(extname: ".png") + scale_filter = "scale=w=#{width}:h=#{height}:force_original_aspect_ratio=decrease" + + {_output, 0} = + System.cmd("ffmpeg", ["-y", "-i", processor.preview, "-vf", scale_filter, scaled]) + {_output, 0} = + System.cmd("optipng", ["-i0", "-o1", scaled]) + + {processor, scaled} + end +end \ No newline at end of file diff --git a/lib/philomena/processors/webm.ex b/lib/philomena/processors/webm.ex new file mode 100644 index 00000000..205d223a --- /dev/null +++ b/lib/philomena/processors/webm.ex @@ -0,0 +1,44 @@ +defmodule Philomena.Processors.Webm do + alias Philomena.Processors.Webm + import Bitwise + + defstruct [:duration, :file] + + def new(analysis, file) do + %Webm{duration: analysis.duration, file: file} + end + + def strip(processor) do + {processor, processor.file} + end + + def preview(processor) do + preview = Briefly.create!(extname: ".png") + + {_output, 0} = + System.cmd("ffmpeg", ["-y", "-i", processor.file, "-ss", to_string(processor.duration / 2), "-frames:v", "1", preview]) + + {processor, preview} + end + + def optimize(processor) do + {processor, processor.file} + end + + def scale(processor, dimensions) do + {width, height} = normalize_dimensions(dimensions) + scaled = Briefly.create!(extname: ".webm") + scale_filter = "scale=w=#{width}:h=#{height}:force_original_aspect_ratio=decrease" + + {_output, 0} = + System.cmd("ffmpeg", ["-y", "-i", processor.file, "-c:v", "libvpx", "-crf", "10", "-b:v", "5M", "-vf", scale_filter, scaled]) + + {processor, scaled} + end + + # Force dimensions to be a multiple of 2. This is required by the + # libvpx and x264 encoders. + defp normalize_dimensions({width, height}) do + {width &&& (~~~1), height &&& (~~~1)} + end +end \ No newline at end of file diff --git a/mix.exs b/mix.exs index db23e91a..08c1ee6f 100644 --- a/mix.exs +++ b/mix.exs @@ -59,6 +59,7 @@ defmodule Philomena.MixProject do {:bamboo, "~> 1.2"}, {:bamboo_smtp, "~> 1.7"}, {:remote_ip, "~> 0.2.0"}, + {:briefly, "~> 0.3.0"} ] end diff --git a/mix.lock b/mix.lock index ad36e1f3..61f416c8 100644 --- a/mix.lock +++ b/mix.lock @@ -2,6 +2,7 @@ "bamboo": {:hex, :bamboo, "1.3.0", "9ab7c054f1c3435464efcba939396c29c5e1b28f73c34e1f169e0881297a3141", [:mix], [{:hackney, ">= 1.13.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "bamboo_smtp": {:hex, :bamboo_smtp, "1.7.0", "f0d213e18ced1f08b551a72221e9b8cfbf23d592b684e9aa1ef5250f4943ef9b", [:mix], [{:bamboo, "~> 1.2", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 0.14.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "2.0.3", "64e0792d5b5064391927bf3b8e436994cafd18ca2d2b76dea5c76e0adcf66b7c", [:make, :mix], [{:comeonin, "~> 5.1", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, + "briefly": {:hex, :briefly, "0.3.0", "16e6b76d2070ebc9cbd025fa85cf5dbaf52368c4bd896fb482b5a6b95a540c2f", [:mix], [], "hexpm"}, "canada": {:hex, :canada, "1.0.2", "040e4c47609b0a67d5773ac1fbe5e99f840cef173d69b739beda7c98453e0770", [:mix], [], "hexpm"}, "canary": {:hex, :canary, "1.1.1", "4138d5e05db8497c477e4af73902eb9ae06e49dceaa13c2dd9f0b55525ded48b", [:mix], [{:canada, "~> 1.0.1", [hex: :canada, repo: "hexpm", optional: false]}, {:ecto, ">= 1.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},