Packages

Idiomatic Elixir interface to the Tesseract OCR engine via a NIF, accepting Vix.Vips.Image structs, file paths, or in-memory image binaries.

Current section

Files

Jump to
image_ocr lib image ocr input.ex
Raw

lib/image/ocr/input.ex

defmodule Image.OCR.Input do
@moduledoc """
Normalises supported OCR inputs into a `Vix.Vips.Image.t()` and an associated
raw pixel buffer suitable for handing to the Tesseract NIF.
Supported inputs:
* `%Vix.Vips.Image{}` — used directly.
* A binary that is an existing file path — loaded with
`Vix.Vips.Image.new_from_file/1`.
* A binary containing encoded image data (PNG, JPEG, TIFF, …) — loaded
with `Vix.Vips.Image.new_from_buffer/1`.
"""
alias Vix.Vips.Image, as: VImage
alias Vix.Vips.Operation
@typedoc """
An accepted OCR input. See the moduledoc for resolution rules.
"""
@type t :: VImage.t() | Path.t() | binary()
@doc """
Loads `input` as a `Vix.Vips.Image.t()`.
### Arguments
* `input` is one of the values described in the moduledoc.
### Returns
* `{:ok, image}` on success.
* `{:error, reason}` if the input cannot be interpreted as an image.
"""
@spec to_vimage(t()) :: {:ok, VImage.t()} | {:error, term()}
def to_vimage(%VImage{} = image), do: {:ok, image}
def to_vimage(binary) when is_binary(binary) do
cond do
looks_like_path?(binary) and File.exists?(binary) ->
VImage.new_from_file(binary)
byte_size(binary) >= 4 ->
VImage.new_from_buffer(binary)
true ->
{:error, {:unsupported_input, :binary_too_small}}
end
end
def to_vimage(charlist) when is_list(charlist) do
to_vimage(List.to_string(charlist))
rescue
ArgumentError -> {:error, {:unsupported_input, :invalid_charlist}}
end
def to_vimage(other), do: {:error, {:unsupported_input, other}}
@doc """
Returns a tightly-packed 8-bit pixel buffer for `image`, ready to feed to
the Tesseract NIF.
The image is normalised in two ways:
* Down-cast to 8 bits per band when needed.
* Constrained to 1 band (grayscale) or 3 bands (RGB). 4-band RGBA images
are flattened against an opaque white background; 2-band images are
reduced to grayscale.
### Arguments
* `image` is a `Vix.Vips.Image.t()`.
### Returns
* `{:ok, %{pixels: binary, width: pos_integer, height: pos_integer,
bytes_per_pixel: 1 | 3, bytes_per_line: pos_integer}}` on success.
* `{:error, reason}` on failure.
"""
@spec to_pixel_buffer(VImage.t()) :: {:ok, map()} | {:error, term()}
def to_pixel_buffer(%VImage{} = image) do
with {:ok, normalised} <- normalise(image),
{:ok, binary} <- VImage.write_to_binary(normalised) do
width = VImage.width(normalised)
height = VImage.height(normalised)
bands = VImage.bands(normalised)
{:ok,
%{
pixels: binary,
width: width,
height: height,
bytes_per_pixel: bands,
bytes_per_line: width * bands
}}
end
end
defp normalise(image) do
image
|> ensure_8bit()
|> ensure_supported_bands()
end
defp ensure_8bit(image) do
case VImage.format(image) do
:VIPS_FORMAT_UCHAR -> {:ok, image}
_ -> Operation.cast(image, :VIPS_FORMAT_UCHAR)
end
end
defp ensure_supported_bands({:ok, image}) do
case VImage.bands(image) do
1 ->
{:ok, image}
3 ->
{:ok, image}
4 ->
Operation.flatten(image, background: [255.0, 255.0, 255.0])
2 ->
Operation.extract_band(image, 0, n: 1)
n when n > 4 ->
Operation.extract_band(image, 0, n: 3)
_ ->
{:error, :unsupported_band_count}
end
end
defp ensure_supported_bands({:error, _} = error), do: error
defp looks_like_path?(binary) do
byte_size(binary) < 4096 and not String.contains?(binary, <<0>>) and
String.printable?(binary)
end
end