From ad437f2e2413115ba309c370838b2b6278165a2d Mon Sep 17 00:00:00 2001 From: Greg Mefford Date: Mon, 22 Jan 2018 22:24:17 -0500 Subject: [PATCH] Clean up docs and typespecs --- README.md | 29 ++++++++++--- lib/symbol.ex | 89 +++++++++++++++++++++++--------------- lib/zbar.ex | 116 ++++++++++++++++++++++++++++++++++++++++++++------ mix.exs | 19 +++------ mix.lock | 5 ++- 5 files changed, 189 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 032c97f..443f214 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,26 @@ # Zbar -**TODO: Add description** +Scan one or more barcodes from a JPEG image. + +The API for this library is very simple: + +```elixir +iex> File.read!("QR_REF1.jpg") |> Zbar.scan() +%Zbar.Symbol{ + data: "REF1", + points: [{40, 40}, {40, 250}, {250, 250}, {250, 40}], + quality: 1, + type: "QR-Code" +} +``` + +More detailed API documentation can be found at +[https://hexdocs.pm/zbar](https://hexdocs.pm/zbar). ## Installation -If [available in Hex](https://hex.pm/docs/publish), the package can be installed -by adding `zbar` to your list of dependencies in `mix.exs`: +This library can be installed by adding `zbar` to your list of dependencies in +`mix.exs`: ```elixir def deps do @@ -13,7 +28,9 @@ def deps do end ``` -Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) -and published on [HexDocs](https://hexdocs.pm). Once published, the docs can -be found at [https://hexdocs.pm/zbar](https://hexdocs.pm/zbar). +You will also need to have `zbar` and `libjpeg` installed in order to compile +the required native code. On OSX with Homebrew, this is as simple as: +```bash +$ brew install zbar libjpeg +``` diff --git a/lib/symbol.ex b/lib/symbol.ex index 44b9f5e..4c6e2d0 100644 --- a/lib/symbol.ex +++ b/lib/symbol.ex @@ -1,37 +1,56 @@ defmodule Zbar.Symbol do - alias __MODULE__ - - defstruct [:type, :quality, :points, :data] - - def parse(string) do - string - |> String.split(" ") - |> Enum.reduce(%Symbol{}, fn item, acc -> - [key, value] = String.split(item, ":", parts: 2) - case key do - "type" -> - %Symbol{acc | type: value} - - "quality" -> - %Symbol{acc | quality: String.to_integer(value)} - - "points" -> - %Symbol{acc | points: parse_points(value)} - - "data" -> - %Symbol{acc | data: Base.decode64!(value)} - end - end) - end - - defp parse_points(string) do - string - |> String.split(";") - |> Enum.map(& parse_point/1) - end - - defp parse_point(string) do - [x, y] = String.split(string, ",", parts: 2) - {String.to_integer(x), String.to_integer(y)} - end + @moduledoc """ + The `Zbar.Symbol` struct represents a barcode that has been detected by `zbar`. + + It has the following fields: + + * `type`: The type of barcode that has been detected, as a string. Possible + values are listed in `t:type_enum/0` + + * `quality`: An integer metric representing barcode scan confidence. + + Larger values are better than smaller values, but only the ordered + relationship between two values is meaningful. The values themselves + are not defined and may change in future versions of the library. + + * `points`: The list of coordinates (encoded as `{x, y}` tuples) where a + barcode was located within the source image. + + The structure of the points depends on the type of barcode being scanned. + For example, for the `QR-Code` type, the points represent the bounding + rectangle, with the first point indicating the top-left positioning pattern + of the QR-Code if it had not been rotated. + + * `data`: The actual barcode data, as a binary string. + + Note that this is a string that may contain arbitrary binary data, + including non-printable characters. + """ + + defstruct type: :UNKNOWN, quality: 0, points: [], data: nil + + @type type_enum :: + :CODE_39 + | :CODE_128 + | :EAN_8 + | :EAN_13 + | :I2_5 + | :ISBN_10 + | :ISBN_13 + | :PDF417 + | :QR_Code + | :UPC_A + | :UPC_E + | :UNKNOWN + + @type point :: {non_neg_integer(), non_neg_integer()} + + @typedoc @moduledoc + @type t :: %__MODULE__{ + type: type_enum(), + quality: non_neg_integer(), + points: [point()], + data: binary() + } + end diff --git a/lib/zbar.ex b/lib/zbar.ex index d716aca..841e186 100644 --- a/lib/zbar.ex +++ b/lib/zbar.ex @@ -1,6 +1,6 @@ defmodule Zbar do @moduledoc """ - Scan all barcodes in a JPEG image using the zbar library. + Scan all barcodes in a JPEG image using the `zbar` library. """ alias Zbar.Symbol @@ -8,27 +8,46 @@ defmodule Zbar do require Logger @doc """ + Scan all barcode data in a JPEG-encoded image. + + * `jpeg_data` should be a binary containing JPEG-encoded image data. + * `timeout` is the time in milliseconds to allow for the processing of the image + (default 5000 milliseconds). + Returns: - {:ok, [%Zbar.Symbol{}, ...]} on success - {:error, :timeout} if the zbar process hung for some reason - {:error, string} if there was an error in the scanning process + * `{:ok, [%Zbar.Symbol{}]}` on success + * `{:error, :timeout}` if the zbar process hung for some reason + * `{:error, binary()}` if there was an error in the scanning process """ - def scan(jpeg_data, timeout \\ 2000) do + @spec scan(binary(), pos_integer()) :: + {:ok, list(Zbar.Symbol.t())} + | {:error, :timeout} + | {:error, binary()} + | no_return() + def scan(jpeg_data, timeout \\ 5000) do # We run this is a `Task` so that `collect_output` can use `receive` # without interfering with the caller's mailbox. - Task.async(fn -> - write_image_to_temp_file(jpeg_data) - open_zbar_port() - |> collect_output(timeout) - |> format_result() - end) + Task.async(fn -> do_scan(jpeg_data, timeout) end) |> Task.await(:infinity) end + @spec do_scan(binary(), pos_integer()) :: + {:ok, [Zbar.Symbol.t()]} + | {:error, :timeout} + | {:error, binary()} + defp do_scan(jpeg_data, timeout) do + write_image_to_temp_file(jpeg_data) + open_zbar_port() + |> collect_output(timeout) + |> format_result() + end + + @spec write_image_to_temp_file(binary()) :: :ok | no_return() defp write_image_to_temp_file(jpeg_data) do File.open!(temp_file(), [:write, :binary], & IO.binwrite(&1, jpeg_data)) end + @spec open_zbar_port() :: port() defp open_zbar_port do {:spawn_executable, zbar_binary()} |> Port.open([ @@ -41,10 +60,17 @@ defmodule Zbar do ]) end + @spec temp_file() :: binary() defp temp_file, do: Path.join(System.tmp_dir!(), "zbar_image.jpg") + @spec zbar_binary() :: binary() defp zbar_binary, do: Path.join(:code.priv_dir(:zbar), "zbar_scanner") + @spec collect_output(port(), pos_integer(), binary()) :: + {:ok, binary()} + | {:error, :timeout} + | {:error, binary()} + | no_return() defp collect_output(port, timeout, buffer \\ "") do receive do {^port, {:data, data}} -> @@ -53,17 +79,81 @@ defmodule Zbar do {:ok, buffer} {^port, {:exit_status, _}} -> {:error, buffer} - after timeout -> {:error, :timeout} + after timeout -> + Port.close(port) + {:error, :timeout} end end + # `output` is the complete multi-line output collected from the `port`. + @spec format_result({:ok | :error, binary()}) :: + {:ok, [Zbar.Symbol.t()]} + | {:error, binary()} defp format_result({:ok, output}) do symbols = output |> String.split("\n", trim: true) - |> Enum.map(&Symbol.parse/1) + |> Enum.map(&parse_symbol/1) {:ok, symbols} end defp format_result({:error, reason}), do: {:error, reason} + # Accepts strings like: + # type:QR-Code quality:1 points:40,40;40,250;250,250;250,40 data:UkVGMQ== + # + # Returns structs like: + # %Zbar.Symbol{ + # data: "REF1", + # points: [{40, 40}, {40, 250}, {250, 250}, {250, 40}], + # quality: 1, + # type: "QR-Code" + # } + @spec parse_symbol(binary()) :: Zbar.Symbol.t() + defp parse_symbol(string) do + string + |> String.split(" ") + |> Enum.reduce(%Symbol{}, fn item, acc -> + [key, value] = String.split(item, ":", parts: 2) + case key do + "type" -> + %Symbol{acc | type: parse_type(value)} + + "quality" -> + %Symbol{acc | quality: String.to_integer(value)} + + "points" -> + %Symbol{acc | points: parse_points(value)} + + "data" -> + %Symbol{acc | data: Base.decode64!(value)} + end + end) + end + + @spec parse_type(binary()) :: Zbar.Symbol.type_enum() + defp parse_type("CODE-39"), do: :CODE_39 + defp parse_type("CODE-128"), do: :CODE_128 + defp parse_type("EAN-8"), do: :EAN_8 + defp parse_type("EAN-13"), do: :EAN_13 + defp parse_type("I2/5"), do: :I2_5 + defp parse_type("ISBN-10"), do: :ISBN_10 + defp parse_type("ISBN-13"), do: :ISBN_13 + defp parse_type("PDF417"), do: :PDF417 + defp parse_type("QR-Code"), do: :QR_Code + defp parse_type("UPC-A"), do: :UPC_A + defp parse_type("UPC-E"), do: :UPC_E + defp parse_type(_), do: :UNKNOWN + + @spec parse_points(binary()) :: [Zbar.Symbol.point()] + defp parse_points(string) do + string + |> String.split(";") + |> Enum.map(& parse_point/1) + end + + @spec parse_point(binary()) :: Zbar.Symbol.point() + defp parse_point(string) do + [x, y] = String.split(string, ",", parts: 2) + {String.to_integer(x), String.to_integer(y)} + end end diff --git a/mix.exs b/mix.exs index 91513b7..22c2095 100644 --- a/mix.exs +++ b/mix.exs @@ -14,24 +14,15 @@ defmodule Zbar.Mixfile do deps: deps()] end - # Configuration for the OTP application - # - # Type "mix help compile.app" for more information def application do - # Specify extra applications you'll use from Erlang/Elixir [extra_applications: [:logger]] end - # Dependencies can be Hex packages: - # - # {:my_dep, "~> 0.3.0"} - # - # Or git/path repositories: - # - # {:my_dep, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} - # - # Type "mix help deps" for more examples and options defp deps do - [{:elixir_make, "~> 0.4", runtime: false}] + [ + {:elixir_make, "~> 0.4", runtime: false}, + {:dialyxir, "~> 0.5", only: :dev, runtime: false}, + {:ex_doc, "~> 0.16", only: :dev, runtime: false}, + ] end end diff --git a/mix.lock b/mix.lock index 6320dc4..a26ffbc 100644 --- a/mix.lock +++ b/mix.lock @@ -1 +1,4 @@ -%{"elixir_make": {:hex, :elixir_make, "0.4.0", "992f38fabe705bb45821a728f20914c554b276838433349d4f2341f7a687cddf", [], [], "hexpm"}} +%{"dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [], [], "hexpm"}, + "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [], [], "hexpm"}, + "elixir_make": {:hex, :elixir_make, "0.4.0", "992f38fabe705bb45821a728f20914c554b276838433349d4f2341f7a687cddf", [], [], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}}