lib/image/qrcode.ex

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