Skip to content

Commit

Permalink
Clean up docs and typespecs
Browse files Browse the repository at this point in the history
  • Loading branch information
GregMefford committed Jan 23, 2018
1 parent 5d38a19 commit ad437f2
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 69 deletions.
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
# 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
[{:zbar, "~> 0.1.0"}]
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
```
89 changes: 54 additions & 35 deletions lib/symbol.ex
Original file line number Diff line number Diff line change
@@ -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
116 changes: 103 additions & 13 deletions lib/zbar.ex
Original file line number Diff line number Diff line change
@@ -1,34 +1,53 @@
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

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([
Expand All @@ -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}} ->
Expand All @@ -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
19 changes: 5 additions & 14 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 4 additions & 1 deletion mix.lock
Original file line number Diff line number Diff line change
@@ -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"}}

0 comments on commit ad437f2

Please sign in to comment.