Verified Commit e58bd802 authored by Thomas Citharel's avatar Thomas Citharel 🏃🏻
Browse files

Initial commit


Signed-off-by: Thomas Citharel's avatarThomas Citharel <tcit@tcit.fr>
parents
Pipeline #177618 failed with stages
in 20 seconds
# This file contains the configuration for Credo and you are probably reading
# this after creating it with `mix credo.gen.config`.
#
# If you find anything wrong or unclear in this file, please report an
# issue on GitHub: https://github.com/rrrene/credo/issues
#
%{
#
# You can have as many configs as you like in the `configs:` field.
configs: [
%{
#
# Run any exec using `mix credo -C <name>`. If no exec name is given
# "default" is used.
#
name: "default",
#
# These are the files included in the analysis:
files: %{
#
# You can give explicit globs or simply directories.
# In the latter case `**/*.{ex,exs}` will be used.
#
included: ["lib/", "src/", "test/", "web/", "apps/"],
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
},
#
# Load and configure plugins here:
#
plugins: [],
#
# If you create your own checks, you must specify the source files for
# them here, so they can be loaded by Credo before running the analysis.
#
requires: [],
#
# If you want to enforce a style guide and need a more traditional linting
# experience, you can change `strict` to `true` below:
#
strict: false,
#
# If you want to use uncolored output by default, you can change `color`
# to `false` below:
#
color: true,
#
# You can customize the parameters of any check by adding a second element
# to the tuple.
#
# To disable a check put `false` as second element:
#
# {Credo.Check.Design.DuplicatedCode, false}
#
checks: [
#
## Consistency Checks
#
{Credo.Check.Consistency.ExceptionNames, []},
{Credo.Check.Consistency.LineEndings, []},
{Credo.Check.Consistency.ParameterPatternMatching, []},
{Credo.Check.Consistency.SpaceAroundOperators, []},
{Credo.Check.Consistency.SpaceInParentheses, []},
{Credo.Check.Consistency.TabsOrSpaces, []},
#
## Design Checks
#
# You can customize the priority of any check
# Priority values are: `low, normal, high, higher`
#
{Credo.Check.Design.AliasUsage,
[priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
# You can also customize the exit_status of each check.
# If you don't want TODO comments to cause `mix credo` to fail, just
# set this value to 0 (zero).
#
{Credo.Check.Design.TagTODO, [exit_status: 2]},
{Credo.Check.Design.TagFIXME, []},
#
## Readability Checks
#
{Credo.Check.Readability.AliasOrder, []},
{Credo.Check.Readability.FunctionNames, []},
{Credo.Check.Readability.LargeNumbers, []},
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
{Credo.Check.Readability.ModuleAttributeNames, []},
{Credo.Check.Readability.ModuleDoc, []},
{Credo.Check.Readability.ModuleNames, []},
{Credo.Check.Readability.ParenthesesInCondition, []},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
{Credo.Check.Readability.PredicateFunctionNames, []},
{Credo.Check.Readability.PreferImplicitTry, []},
{Credo.Check.Readability.RedundantBlankLines, []},
{Credo.Check.Readability.Semicolons, []},
{Credo.Check.Readability.SpaceAfterCommas, []},
{Credo.Check.Readability.StringSigils, []},
{Credo.Check.Readability.TrailingBlankLine, []},
{Credo.Check.Readability.TrailingWhiteSpace, []},
# TODO: enable by default in Credo 1.1
{Credo.Check.Readability.UnnecessaryAliasExpansion, false},
{Credo.Check.Readability.VariableNames, []},
#
## Refactoring Opportunities
#
{Credo.Check.Refactor.CondStatements, []},
{Credo.Check.Refactor.CyclomaticComplexity, []},
{Credo.Check.Refactor.FunctionArity, []},
{Credo.Check.Refactor.LongQuoteBlocks, []},
{Credo.Check.Refactor.MapInto, false},
{Credo.Check.Refactor.MatchInCondition, []},
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
{Credo.Check.Refactor.Nesting, []},
{Credo.Check.Refactor.UnlessWithElse, []},
{Credo.Check.Refactor.WithClauses, []},
#
## Warnings
#
{Credo.Check.Warning.BoolOperationOnSameValues, []},
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
{Credo.Check.Warning.IExPry, []},
{Credo.Check.Warning.IoInspect, []},
{Credo.Check.Warning.LazyLogging, false},
{Credo.Check.Warning.OperationOnSameValues, []},
{Credo.Check.Warning.OperationWithConstantResult, []},
{Credo.Check.Warning.RaiseInsideRescue, []},
{Credo.Check.Warning.UnusedEnumOperation, []},
{Credo.Check.Warning.UnusedFileOperation, []},
{Credo.Check.Warning.UnusedKeywordOperation, []},
{Credo.Check.Warning.UnusedListOperation, []},
{Credo.Check.Warning.UnusedPathOperation, []},
{Credo.Check.Warning.UnusedRegexOperation, []},
{Credo.Check.Warning.UnusedStringOperation, []},
{Credo.Check.Warning.UnusedTupleOperation, []},
#
# Controversial and experimental checks (opt-in, just replace `false` with `[]`)
#
{Credo.Check.Consistency.MultiAliasImportRequireUse, false},
{Credo.Check.Consistency.UnusedVariableNames, false},
{Credo.Check.Design.DuplicatedCode, false},
{Credo.Check.Readability.AliasAs, false},
{Credo.Check.Readability.MultiAlias, false},
{Credo.Check.Readability.Specs, false},
{Credo.Check.Readability.SinglePipe, false},
{Credo.Check.Refactor.ABCSize, false},
{Credo.Check.Refactor.AppendSingleItem, false},
{Credo.Check.Refactor.DoubleBooleanNegation, false},
{Credo.Check.Refactor.ModuleDependencies, false},
{Credo.Check.Refactor.PipeChainStart, false},
{Credo.Check.Refactor.VariableRebinding, false},
{Credo.Check.Warning.MapGetUnsafePass, false},
{Credo.Check.Warning.UnsafeToAtom, false}
#
# Custom checks can be created using `mix credo.gen.check`.
#
]
}
]
}
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
ex_optimizer-*.tar
image: elixir:latest
stages:
- check
- test
- deploy
before_script:
- mix local.rebar --force
- mix local.hex --force
- mix deps.get
check:
stage: check
before_script:
- mix deps.get
script:
- mix credo -a
- mix format --check-formatted --dry-run
mix:
stage: test
before_script:
- mix deps.get
script:
- mix coveralls
pages:
stage: deploy
before_script:
- mix deps.get
script:
- mkdir public
- mix docs
- mv doc public
only:
- master
artifacts:
expire_in: 1 hour
paths:
- public
# Changelog
## 0.1.0
2019-10-20
* Initial release
# ExOptimizer
ExOptimizer is a tool to optimize pictures by running them through a chain of various image optimization tools. It's heavily inspired by the tool [ImageOptimizer](https://github.com/spatie/image-optimizer) for PHP.
Here's how you can use it:
```elixir
ExOptimizer.optimize(path_to_image)
```
The image at `path_to_image` will be overwritten by an optimized version which should be smaller. The package will automatically detect which optimization binaries are installed on your system and use them.
## Installation
You can install the package through Hex.pm by adding `ex_optimizer` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:ex_optimizer, "~> 0.1.0"}
]
end
```
Then running `mix deps.get` to fetch the package.
[https://hexdocs.pm/ex_optimizer](https://hexdocs.pm/ex_optimizer).
# Optimization tools
The package will use these optimizers if they are present on your system:
* [JpegOptim](http://freecode.com/projects/jpegoptim)
* [Optipng](http://optipng.sourceforge.net/)
* [Pngquant 2](https://pngquant.org/)
* [SVGO](https://github.com/svg/svgo)
* [Gifsicle](http://www.lcdf.org/gifsicle/)
* [cwebp](https://developers.google.com/speed/webp/docs/precompiled)
Here's how to install all the optimizers on Ubuntu:
```bash
sudo apt-get install jpegoptim
sudo apt-get install optipng
sudo apt-get install pngquant
sudo npm install -g svgo
sudo apt-get install gifsicle
sudo apt-get install webp
```
And here's how to install the binaries on MacOS (using [Homebrew](https://brew.sh/)):
```bash
brew install jpegoptim
brew install optipng
brew install pngquant
brew install svgo
brew install gifsicle
brew install webp
```
# Changelog
Please see [CHANGELOG](changelog.html) for more information what has changed recently.
# Testing
```sh
mix test
```
You can see coverage with
```sh
mix coveralls.html
```
# Support us
[Framasoft](https://framasoft.org/en/) is a French not-for-profit popular educational organisation that lives only through your donations. It provides free online services part of the [De-google-ify Internet](https://degooglisons-internet.org/en/) campaign and leads projects like [PeerTube](https://joinpeertube.org/en/) and [Mobilizon](https://joinmobilizon.org/en/).
defmodule ExOptimizer do
@moduledoc """
ExOptimizer is a tool to optimize pictures by running them through a chain of various image optimization tools.
"""
alias ExOptimizer.Image
alias ExOptimizer.Optimizers.{JpegOptim, Optipng, PngQuant, Gifsicle, Svgo, Cwebp}
@default_optimizers [
JpegOptim,
PngQuant,
Optipng,
Svgo,
Gifsicle,
Cwebp
]
@doc """
Optimize a picture
Options:
* `optimizers` accepts a list of optimizers (see `ExOptimizer.Optimizer`) module names or tuples with the module name and the custom options to pass to the binary command.
"""
@spec optimize(String.t(), List.t()) ::
{:ok, Map.t()} | {:error, Map.t()} | {:error, :file_not_found}
def optimize(path, opts \\ []) do
path = Path.expand(path)
case {:regular_file, File.regular?(path)} do
{:regular_file, true} ->
image = load_image(path)
optimizers =
opts
|> Keyword.get(:optimizers, @default_optimizers)
|> find_optimizers()
res =
Enum.reduce(optimizers, %{}, fn {optimizer_module, _optimizer_options} = optimizer,
acc ->
Map.put(acc, optimizer_module, do_optimize(image, optimizer))
end)
status =
if Enum.all?(res, &is_status_ok/1),
do: :ok,
else: :error
{status, res}
{:regular_file, false} ->
{:error, :file_not_found}
end
end
@spec do_optimize(Image.t(), {atom(), list()}) :: tuple()
defp do_optimize(%Image{path: path} = image, {optimizer, optimizer_options}) do
with {:can_handle, true} <- {:can_handle, optimizer.can_handle(image)},
{:module_not_found, true} <- {:module_not_found, Code.ensure_compiled?(optimizer)},
{:executable_not_found, executable_path} when is_binary(executable_path) <-
{:executable_not_found, System.find_executable(optimizer.binary_name())},
args <- optimizer_options ++ optimizer.extra_args(image) ++ [path],
{:error_command, {_return, 0} = res} <-
{:error_command, System.cmd(optimizer.binary_name(), args)} do
{:ok, res}
else
{:can_handle, false} -> {:ignore, :not_applicable}
{:error_command, res} -> {:error, :command, res}
{:executable_not_found, nil} -> {:ignore, :executable_not_found}
{:module_not_found, false} -> {:error, :module_not_loaded}
end
end
@spec find_optimizers(List.t()) :: List.t()
defp find_optimizers(optimizers), do: Enum.map(optimizers, &make_optimizer_config/1)
@spec make_optimizer_config(tuple()) :: tuple()
defp make_optimizer_config({optimizer, config}), do: {optimizer, config}
@spec make_optimizer_config(atom()) :: tuple()
defp make_optimizer_config(optimizer), do: {optimizer, optimizer.options()}
@spec load_image(String.t()) :: Image.t()
defp load_image(path) do
infos = FileInfo.get_info(path)
%FileInfo.Mime{subtype: subtype, type: type} = Map.get(infos, path)
%Image{path: path, ext: Path.extname(path), mime: "#{type}/#{subtype}"}
end
defp is_status_ok({_, {status, _}}) when status in [:ok, :ignore], do: true
defp is_status_ok({_, {status, _, _}}) when status in [:ok, :ignore], do: true
defp is_status_ok(_), do: false
end
defmodule ExOptimizer.Image do
@moduledoc """
Represents a picture
"""
@type path :: binary
@type ext :: binary
@type mime :: binary
@type t :: %__MODULE__{
path: path,
ext: ext,
mime: mime
}
defstruct path: nil,
ext: nil,
mime: nil
end
defmodule ExOptimizer.Optimizer do
@moduledoc """
Behaviour for creating ExOptimizer optimizers.
The current list of optimizers bundled with ExOptimizer are the following:
* JPEG files
* `ExOptimizer.Optimizers.JpegOptim`
* PNG files
* `ExOptimizer.Optimizers.PngQuant`
* `ExOptimizer.Optimizers.Optipng`
* SVG files
* `ExOptimizer.Optimizers.Svgo`
* GIF files
* `ExOptimizer.Optimizers.Gifsicle`
* WebP files
* `ExOptimizer.Optimizers.Cwebp`
## Example
defmodule ExOptimizer.Optimizers.Custom do
@behaviour ExOptimizer.Optimizer
def binary_name(), do: "some_command"
def can_handle(%Image{path: path, ext: ext, mime: mime} = _image), do: mime == "image/something"
def extra_args(%Image{path: path} = _image), do: ["-o \#{path}"]
def options(), do: ["--force", "--produce 'best'"]
end
"""
alias ExOptimizer.Image
@callback binary_name() :: String.t()
@callback can_handle(Image.t()) :: boolean
@callback extra_args(Image.t()) :: list(String.t())
@callback options() :: list(String.t())
end
defmodule ExOptimizer.Optimizers.Cwebp do
@moduledoc """
WEBPs will be optimized by [Cwebp](https://developers.google.com/speed/webp/docs/cwebp).
These options will be used:
* `-m 6` for the slowest compression method in order to get the best compression.
* `-pass 10` for maximizing the amount of analysis pass.
* `-mt` multithreading for some speed improvements.
* `-q 90` Quality factor that brings the least noticeable changes.
(Settings are original taken from [here](https://medium.com/@vinhlh/how-i-apply-webp-for-optimizing-images-9b11068db349))
> Note: Due to an issue with the `cwebp` binary, options have to be set one by one, for instance the default ones are: `["-m", "6", "-pass", "10", "-mt", "-q", "80"]`
"""
alias ExOptimizer.{Image, Optimizer}
@behaviour Optimizer
@mime "image/webp"
@options ["-m", "6", "-pass", "10", "-mt", "-q", "80"]
@impl Optimizer
def can_handle(%Image{mime: mime}), do: mime == @mime
@impl Optimizer
def binary_name(), do: "cwebp"
@impl Optimizer
def extra_args(%Image{path: path} = _image), do: ["-o", "#{path}"]
@impl Optimizer
def options(), do: @options
end
defmodule ExOptimizer.Optimizers.Gifsicle do
@moduledoc """
GIFs will be optimized by [Gifsicle](http://www.lcdf.org/gifsicle).
These options will be used:
* `-O3`: this sets the optimization level to Gifsicle's maximum, which produces the slowest but best results
"""
alias ExOptimizer.{Image, Optimizer}
@behaviour Optimizer
@mime "image/gif"
@options ["-b", "-O3"]
@impl Optimizer
def can_handle(%Image{mime: mime}), do: mime == @mime
@impl Optimizer
def binary_name(), do: "gifsicle"
@impl Optimizer
def extra_args(%Image{path: path} = _image), do: ["--output=#{path}"]
@impl Optimizer
def options(), do: @options
end
defmodule ExOptimizer.Optimizers.JpegOptim do
@moduledoc """
[JpegOptim](https://github.com/tjko/jpegoptim) is a tool to optimize JPEG files
The following options are used by default:
* `-m85`: this will store the image with 85% quality. This setting [seems to satisfy Google's Pagespeed compression rules](https://webmasters.stackexchange.com/questions/102094/google-pagespeed-how-to-satisfy-the-new-image-compression-rules)
* `--strip-all`: this strips out all text information such as comments and EXIF data
* `--all-progressive`: this will make sure the resulting image is a progressive one, meaning it can be downloaded using multiple passes of progressively higher details.
"""
alias ExOptimizer.{Image, Optimizer}
@behaviour Optimizer
@mime "image/jpeg"
@options ["-m85", "--strip-all", "--all-progressive"]
@impl Optimizer
def can_handle(%Image{mime: mime}), do: mime == @mime
@impl Optimizer
def binary_name(), do: "jpegoptim"
@impl Optimizer
def extra_args(%Image{} = _image), do: []
@impl Optimizer
def options(), do: @options
end
defmodule ExOptimizer.Optimizers.Optipng do
@moduledoc """
[Optipng](http://optipng.sourceforge.net) is another PNG compressor.
The following options are used:
* `-i0`: this will result in a non-interlaced, progressive scanned image
* `-o2`: this set the optimization level to two (multiple IDAT compression trials)
"""
alias ExOptimizer.{Image, Optimizer}
@behaviour Optimizer
@mime "image/png"
@options ["-i0", "-o2", "-quiet"]
@impl Optimizer
def can_handle(%Image{mime: mime}), do: mime == @mime
@impl Optimizer
def binary_name(), do: "optipng"
@impl Optimizer
def extra_args(%Image{} = _image), do: []
@impl Optimizer
def options(), do: @options
end
defmodule ExOptimizer.Optimizers.PngQuant do
@moduledoc """
[PngQuant2](https://pngquant.org/) is a lossy PNG compressor.
We set no extra options, their defaults are used
"""
alias ExOptimizer.{Image, Optimizer}
@behaviour Optimizer
@mime "image/png"