if Image.evision_configured?() do
defmodule Image.QRcode do
@moduledoc """
Detects and decodes QRcodes.
Note that the implementation, which is based upon
[eVision](https://github.com/cocoa-xu/evision) requires that
the image be a 3-channel image in order to support
detection.
Images that are in different formats must be converted
first.
### Note
This module is only available if the optional dependency
[eVision](https://hex.pm/packages/evision) is configured in
`mix.exs`.
"""
alias Vix.Vips.Image, as: Vimage
alias Evision.QRCodeDetector, as: Detector
alias Evision.QRCodeEncoder, as: Encoder
import Detector, only: [qrCodeDetector: 0]
@dialyzer {:nowarn_function, {:decode, 1}}
@doc """
Encodes a string as a QRCode.
### Arguments
* `string` is any string to be encoded,
* `options` is a keyword list of options. The
default is `size: :auto`.
## Options
* `:size` is the size in pixels of the QRcode
dimensions. The default is `:auto` in which
the generated QRcode will be the minimum dimensions
necessary to encode the `string`.
### Returns
* `{:ok, image}` or
* `{:error, reason}`
"""
@doc since: "0.13.0"
def encode(string, options \\ []) when is_binary(string) do
size = Keyword.get(options, :size, :auto)
with %Evision.Mat{} = mat <- Encoder.encode(Encoder.create(), string) do
case size do
:auto ->
Image.from_evision(mat)
size when is_integer(size) and size > 0 ->
{:ok, image} = Image.from_evision(mat)
scale = scale_from_size(image, size)
Image.resize(image, scale, interpolate: :nearest)
other ->
{
:error,
"Invalid `:size` option. `:size` must be a positive " <>
"integer or `:auto`. Found #{inspect(other)}."
}
end
end
end
defp scale_from_size(image, size) do
width = Image.width(image)
max(size / width, 1)
end
@doc """
Detects and decodes a QR code in an image.
### Arguments
* `image` is any `t:Vix.Vips.Image.t/0` that
has three bands (for example, a typical srgb
image).
### Returns
* `{:ok, string}` or
* `{:error, reason}`
### Note
Only images with three bands (channels) are
supported. This restriction may be lifted in
a future release.
"""
@doc since: "0.9.0"
def decode(%Vimage{} = image) do
with {:ok, evision} <- Image.to_evision(image) do
decode(evision)
end
end
# The QRcode encoder will encode the smallest possible image
# as a result, its often not recognised by the decoder. So we
# resize to a minimum size. Sizes less than 300px do not
# reliably decode (based upon informal testing).
@minumum_dimension 300
@dimensions {@minumum_dimension, @minumum_dimension}
def decode(%Evision.Mat{shape: {height, width, _}} = evision)
when height < @minumum_dimension or width < @minumum_dimension do
resized =
Evision.resize(evision, @dimensions, interpolate: Evision.Constant.cv_INTER_NEAREST())
decode(resized)
end
def decode(%Evision.Mat{} = evision) do
case Detector.detectAndDecode(qrCodeDetector(), evision) do
{string, %Evision.Mat{} = _points, %Evision.Mat{} = _rectified} ->
{:ok, string}
{"", %Evision.Mat{}, {:error, "empty matrix"}} ->
{:error, "QRcode detected but could not be decoded"}
{"", {:error, "empty matrix"}, {:error, "empty matrix"}} ->
{:error, "No QRcode detected in the image"}
end
end
end
end