add processor skeleton

This commit is contained in:
byte[] 2019-11-25 21:57:47 -05:00
parent 813ff87f9e
commit 2159fbd9c7
10 changed files with 378 additions and 110 deletions

View file

@ -87,14 +87,9 @@ defmodule Philomena.Images.Image do
field :removed_tags, {:array, :any}, default: [], virtual: true
field :added_tags, {:array, :any}, default: [], virtual: true
timestamps(inserted_at: :created_at)
end
field :uploaded_image, :string, virtual: true
@doc false
def changeset(image, attrs) do
image
|> cast(attrs, [])
|> validate_required([])
timestamps(inserted_at: :created_at)
end
def interaction_data(image) do
@ -106,6 +101,32 @@ defmodule Philomena.Images.Image do
}
end
@doc false
def changeset(image, attrs) do
image
|> cast(attrs, [])
|> validate_required([])
end
def image_changeset(image, attrs) do
image
|> cast(attrs, [
:image, :image_name, :image_width, :image_height, :image_size,
:image_format, :image_mime_type, :image_aspect_ratio,
:image_orig_sha512_hash, :image_sha512_hash, :uploaded_image
])
|> validate_required([
:image, :image_width, :image_height, :image_size,
:image_format, :image_mime_type, :image_aspect_ratio,
:image_orig_sha512_hash, :image_sha512_hash, :uploaded_image
])
|> validate_number(:image_size, greater_than: 0, less_than_or_equal_to: 26214400)
|> validate_number(:image_width, greater_than: 0, less_than_or_equal_to: 32767)
|> validate_number(:image_height, greater_than: 0, less_than_or_equal_to: 32767)
|> validate_length(:image_name, max: 255, count: :bytes)
|> validate_inclusion(:image_mime_type, ~W(image/gif image/jpeg image/png image/svg+xml video/webm))
end
def source_changeset(image, attrs) do
image
|> cast(attrs, [:source_url])

View file

@ -0,0 +1,98 @@
defmodule Philomena.Processors do
# alias Philomena.Images.Image
# alias Philomena.Repo
alias Philomena.Sha512
@mimes %{
"image/gif" => "image/gif",
"image/jpeg" => "image/jpeg",
"image/png" => "image/png",
"image/svg+xml" => "image/svg+xml",
"video/webm" => "video/webm",
"image/svg" => "image/svg+xml",
"audio/webm" => "video/webm"
}
@analyzers %{
"image/gif" => Philomena.Analyzers.Gif,
"image/jpeg" => Philomena.Analyzers.Jpeg,
"image/png" => Philomena.Analyzers.Png,
"image/svg+xml" => Philomena.Analyzers.Svg,
"video/webm" => Philomena.Analyzers.Webm
}
@processors %{
"image/gif" => Philomena.Processors.Gif,
"image/jpeg" => Philomena.Processors.Jpeg,
"image/png" => Philomena.Processors.Png,
"image/svg+xml" => Philomena.Processors.Svg,
"video/webm" => Philomena.Processors.Webm
}
def analysis_to_changes(analysis, file, upload_name) do
{width, height} = analysis.dimensions
%{size: size} = File.stat(file)
sha512 = Sha512.file(file)
filename = build_filename(analysis.extension)
%{
"image" => filename,
"image_name" => upload_name,
"image_width" => width,
"image_height" => height,
"image_size" => size,
"image_format" => analysis.extension,
"image_mime_type" => analysis.mime_type,
"image_aspect_ratio" => aspect_ratio(width, height),
"image_orig_sha512_hash" => sha512,
"image_sha512_hash" => sha512,
"uploaded_image" => file
}
end
def after_upload(image) do
File.cp(image.uploaded_image, Path.join([image_file_root(), image.image]))
end
defp aspect_ratio(_, 0), do: 0.0
defp aspect_ratio(w, h), do: w / h
defp build_filename(extension) do
[
time_identifier(),
"/",
usec_identifier(),
pid_identifier(),
".",
extension
]
|> Enum.join()
end
defp time_identifier do
now = DateTime.utc_now()
Enum.join([now.year, now.month, now.day], "/")
end
defp usec_identifier do
DateTime.utc_now()
|> DateTime.to_unix(:microsecond)
|> to_string()
end
defp pid_identifier do
self()
|> :erlang.pid_to_list()
|> to_string()
|> String.replace(~r/[^0-9]/, "")
end
defp image_file_root do
Application.get_env(:philomena, :image_file_root)
end
defp image_thumbnail_root do
image_file_root() <> "/thumbs"
end
end

View file

@ -1,59 +1,93 @@
defmodule Philomena.Processors.Gif do
alias Philomena.Processors.Gif
alias Philomena.Intensities
defstruct [:duration, :file, :palette]
def process(analysis, file, versions) do
dimensions = analysis.dimensions
duration = analysis.duration
preview = preview(duration, file)
palette = palette(file)
def new(analysis, file) do
%Gif{
file: file,
duration: analysis.duration,
palette: nil
{:ok, intensities} = Intensities.file(preview)
scaled = Enum.flat_map(versions, &scale_if_smaller(palette, file, dimensions, &1))
%{
intensities: intensities,
thumbnails: scaled ++ [{:copy, preview, "rendered.png"}]
}
end
def strip(processor) do
{processor, processor.file}
def post_process(_analysis, file) do
%{replace_original: optimize(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
defp optimize(file) do
optimized = Briefly.create!(extname: ".gif")
{_output, 0} =
System.cmd("gifsicle", ["--careful", "-O2", processor.file, "-o", optimized])
System.cmd("gifsicle", ["--careful", "-O2", file, "-o", optimized])
{processor, optimized}
optimized
end
def scale(processor, {width, height}) do
processor = generate_palette(processor)
scaled = Briefly.create!(extname: ".gif")
defp preview(duration, file) do
preview = Briefly.create!(extname: ".png")
{_output, 0} =
System.cmd("ffmpeg", ["-loglevel", "0", "-y", "-i", file, "-ss", to_string(duration / 2), "-frames:v", "1", preview])
preview
end
defp palette(file) do
palette = Briefly.create!(extname: ".png")
{_output, 0} =
System.cmd("ffmpeg", ["-loglevel", "0", "-y", "-i", file, "-vf", "palettegen=stats_mode=diff", palette])
palette
end
# Generate full version, and WebM and MP4 previews
defp scale_if_smaller(_palette, file, _dimensions, {:full, _target_dim}) do
[{:symlink_original, "full.gif"}] ++ generate_videos(file)
end
defp scale_if_smaller(palette, file, {width, height}, {thumb_name, {target_width, target_height}}) do
if width > target_width or height > target_height do
scaled = scale(palette, file, {target_width, target_height})
[{:copy, scaled, "#{thumb_name}.gif"}]
else
[{:symlink_original, "#{thumb_name}.gif"}]
end
end
defp scale(palette, file, {width, height}) do
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])
System.cmd("ffmpeg", ["-loglevel", "0", "-y", "-i", file, "-i", palette, "-lavfi", filter_graph, scaled])
{processor, scaled}
scaled
end
defp generate_palette(%{palette: nil} = processor) do
palette = Briefly.create!(extname: ".png")
defp generate_videos(file) do
webm = Briefly.create!(extname: ".webm")
mp4 = Briefly.create!(extname: ".mp4")
{_output, 0} =
System.cmd("ffmpeg", ["-y", "-i", processor.file, "-vf", "palettegen=stats_mode=diff", palette])
%{processor | palette: palette}
System.cmd("ffmpeg", ["-loglevel", "0", "-y", "-i", file, "-pix_fmt", "yuv420p", "-c:v", "libvpx", "-quality", "good", "-b:v", "5M", webm])
{_output, 0} =
System.cmd("ffmpeg", ["-loglevel", "0", "-y", "-i", file, "-vf", "scale=ceil(iw/2)*2:ceil(ih/2)*2", "-c:v", "libx264", "-preset", "medium", "-crf", "18", "-b:v", "5M", mp4])
[
{:copy, webm, "full.webm"},
{:copy, mp4, "full.mp4"}
]
end
defp generate_palette(processor), do: processor
end

View file

@ -1,43 +1,64 @@
defmodule Philomena.Processors.Jpeg do
alias Philomena.Processors.Jpeg
alias Philomena.Intensities
defstruct [:file]
def process(analysis, file, versions) do
dimensions = analysis.dimensions
stripped = optimize(strip(file))
def new(_analysis, file) do
%Jpeg{file: file}
{:ok, intensities} = Intensities.file(stripped)
scaled = Enum.flat_map(versions, &scale_if_smaller(stripped, dimensions, &1))
%{
replace_original: stripped,
intensities: intensities,
thumbnails: scaled
}
end
def strip(processor) do
def post_process(_analysis, _file), do: %{}
defp strip(file) do
stripped = Briefly.create!(extname: ".jpg")
{_output, 0} =
System.cmd("convert", [processor.file, "-auto-orient", "-strip", stripped])
System.cmd("convert", [file, "-auto-orient", "-strip", stripped])
processor = %{processor | file: stripped}
{processor, stripped}
stripped
end
def preview(processor) do
{processor, processor.file}
end
def optimize(processor) do
defp optimize(file) do
optimized = Briefly.create!(extname: ".jpg")
{_output, 0} =
System.cmd("jpegtran", ["-optimize", "-outfile", optimized, processor.file])
System.cmd("jpegtran", ["-optimize", "-outfile", optimized, file])
optimized
end
def scale(processor, {width, height}) do
defp scale_if_smaller(_file, _dimensions, {:full, _target_dim}) do
[{:symlink_original, "full.jpg"}]
end
defp scale_if_smaller(file, {width, height}, {thumb_name, {target_width, target_height}}) do
if width > target_width or height > target_height do
scaled = scale(file, {target_width, target_height})
[{:copy, scaled, "#{thumb_name}.jpg"}]
else
[{:symlink_original, "#{thumb_name}.jpg"}]
end
end
defp scale(file, {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])
System.cmd("ffmpeg", ["-loglevel", "0", "-y", "-i", file, "-vf", scale_filter, scaled])
{_output, 0} =
System.cmd("jpegtran", ["-optimize", "-outfile", scaled, scaled])
{processor, scaled}
scaled
end
end

View file

@ -1,38 +1,55 @@
defmodule Philomena.Processors.Png do
alias Philomena.Processors.Png
alias Philomena.Intensities
defstruct [:file]
def process(analysis, file, versions) do
dimensions = analysis.dimensions
def new(_analysis, file) do
%Png{file: file}
{:ok, intensities} = Intensities.file(file)
scaled = Enum.flat_map(versions, &scale_if_smaller(file, dimensions, &1))
%{
intensities: intensities,
thumbnails: scaled
}
end
def strip(processor) do
{processor, processor.file}
def post_process(_analysis, file) do
%{replace_original: optimize(file)}
end
def preview(processor) do
{processor, processor.file}
end
def optimize(processor) do
defp optimize(file) do
optimized = Briefly.create!(extname: ".png")
{_output, 0} =
System.cmd("optipng", ["-fix", "-i0", "-o2", processor.file, "-out", optimized])
System.cmd("optipng", ["-fix", "-i0", "-o2", file, "-out", optimized])
{processor, optimized}
optimized
end
def scale(processor, {width, height}) do
defp scale_if_smaller(_file, _dimensions, {:full, _target_dim}) do
[{:symlink_original, "full.jpg"}]
end
defp scale_if_smaller(file, {width, height}, {thumb_name, {target_width, target_height}}) do
if width > target_width or height > target_height do
scaled = scale(file, {target_width, target_height})
[{:copy, scaled, "#{thumb_name}.jpg"}]
else
[{:symlink_original, "#{thumb_name}.jpg"}]
end
end
defp scale(file, {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])
System.cmd("ffmpeg", ["-loglevel", "0", "-y", "-i", file, "-vf", scale_filter, scaled])
{_output, 0} =
System.cmd("optipng", ["-i0", "-o1", scaled])
{processor, scaled}
scaled
end
end

View file

@ -1,41 +1,49 @@
defmodule Philomena.Processors.Svg do
alias Philomena.Processors.Svg
alias Philomena.Intensities
defstruct [:file, :preview]
def process(_analysis, file, versions) do
preview = preview(file)
def new(_analysis, file) do
%Svg{file: file, preview: nil}
{:ok, intensities} = Intensities.file(preview)
scaled = Enum.flat_map(versions, &scale_if_smaller(file, preview, &1))
%{
intensities: intensities,
thumbnails: scaled ++ [{:copy, preview, "rendered.png"}]
}
end
# FIXME
def strip(processor) do
{processor, processor.file}
end
def post_process(_analysis, _file), do: %{}
def preview(processor) do
defp preview(file) do
preview = Briefly.create!(extname: ".png")
{_output, 0} =
System.cmd("inkscape", [processor.file, "--export-png", preview])
processor = %{processor | preview: preview}
System.cmd("inkscape", [file, "--export-png", preview])
{processor, preview}
preview
end
def optimize(processor) do
{processor, processor.file}
defp scale_if_smaller(_file, preview, {:full, _target_dim}) do
[{:symlink_original, "full.svg"}, {:copy, preview, "full.png"}]
end
def scale(processor, {width, height}) do
defp scale_if_smaller(_file, preview, {thumb_name, {target_width, target_height}}) do
scaled = scale(preview, {target_width, target_height})
[{:copy, scaled, "#{thumb_name}.png"}]
end
defp scale(preview, {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])
System.cmd("ffmpeg", ["-y", "-i", preview, "-vf", scale_filter, scaled])
{_output, 0} =
System.cmd("optipng", ["-i0", "-o1", scaled])
{processor, scaled}
scaled
end
end

View file

@ -1,39 +1,97 @@
defmodule Philomena.Processors.Webm do
alias Philomena.Processors.Webm
alias Philomena.Intensities
import Bitwise
defstruct [:duration, :file]
def process(analysis, file, versions) do
dimensions = analysis.dimensions
duration = analysis.duration
preview = preview(duration, file)
palette = gif_palette(file)
def new(analysis, file) do
%Webm{duration: analysis.duration, file: file}
{:ok, intensities} = Intensities.file(preview)
scaled = Enum.flat_map(versions, &scale_if_smaller(file, palette, dimensions, &1))
%{
intensities: intensities,
thumbnails: scaled ++ [{:copy, preview, "rendered.png"}]
}
end
def strip(processor) do
{processor, processor.file}
end
def post_process(_analysis, _file), do: %{}
def preview(processor) do
defp preview(duration, file) 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}
System.cmd("ffmpeg", ["-loglevel", "0", "-y", "-i", file, "-ss", to_string(duration / 2), "-frames:v", "1", preview])
preview
end
def optimize(processor) do
{processor, processor.file}
defp scale_if_smaller(palette, file, dimensions, {:full, _target_dim}) do
{webm, mp4} = scale_videos(file, palette, dimensions)
[
{:copy, webm, "full.webm"},
{:copy, mp4, "full.mp4"}
]
end
def scale(processor, dimensions) do
defp scale_if_smaller(palette, file, _dimensions, {thumb_name, {target_width, target_height}}) do
{webm, mp4} = scale_videos(file, palette, {target_width, target_height})
cond do
thumb_name in [:thumb, :thumb_small, :thumb_tiny] ->
gif = scale_gif(file, palette, {target_width, target_height})
[
{:copy, webm, "#{thumb_name}.webm"},
{:copy, mp4, "#{thumb_name}.mp4"},
{:copy, gif, "#{thumb_name}.gif"}
]
true ->
[
{:copy, webm, "#{thumb_name}.webm"},
{:copy, mp4, "#{thumb_name}.mp4"}
]
end
end
defp scale_videos(file, _palette, dimensions) do
{width, height} = normalize_dimensions(dimensions)
scaled = Briefly.create!(extname: ".webm")
webm = Briefly.create!(extname: ".webm")
mp4 = Briefly.create!(extname: ".mp4")
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])
System.cmd("ffmpeg", ["-loglevel", "0", "-y", "-i", file, "-c:v", "libvpx", "-crf", "10", "-b:v", "5M", "-vf", scale_filter, webm])
{_output, 0} =
System.cmd("ffmpeg", ["-loglevel", "0", "-y", "-i", file, "-c:v", "libx264", "-preset", "medium", "-crf", "18", "-b:v", "5M", "-vf", scale_filter, mp4])
{processor, scaled}
{webm, mp4}
end
defp scale_gif(file, palette, {width, height}) do
gif = 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", ["-loglevel", "0", "-y", "-i", file, "-i", palette, "-lavfi", filter_graph, gif])
gif
end
defp gif_palette(file) do
palette = Briefly.create!(extname: ".png")
{_output, 0} =
System.cmd("ffmpeg", ["-loglevel", "0", "-y", "-i", file, "-vf", "palettegen=stats_mode=diff", palette])
palette
end
# Force dimensions to be a multiple of 2. This is required by the

11
lib/philomena/sha512.ex Normal file
View file

@ -0,0 +1,11 @@
defmodule Philomena.Sha512 do
@spec file(String.t()) :: String.t()
def file(file) do
hash_ref = :crypto.hash_init(:sha512)
File.stream!(file, [], 10_485_760)
|> Enum.reduce(hash_ref, &:crypto.hash_update(&2, &1))
|> :crypto.hash_final()
|> Base.encode16(case: :lower)
end
end

View file

@ -9,7 +9,7 @@ defmodule PhilomenaWeb.Image.SourceChangeController do
plug PhilomenaWeb.CanaryMapPlug, index: :show
plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true
def index(conn, params) do
def index(conn, _params) do
image = conn.assigns.image
source_changes =

View file

@ -55,7 +55,7 @@ if ! install_packages build-essential postgresql-11 libpq-dev nginx nodejs \
elasticsearch esl-erlang elixir inotify-tools git \
redis-server automake libtool zlib1g-dev ffmpeg \
libavutil-dev libavcodec-dev libavformat-dev \
libmagic-dev libpng-dev; then
libmagic-dev libpng-dev gifsicle optipng libjpeg-progs; then
>&2 echo "Installation of dependencies failed."
exit 1
fi