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 languages.ex
Raw

lib/image/ocr/languages.ex

defmodule Image.OCR.Languages do
@moduledoc """
Translation between user-facing language identifiers and Tesseract's
trained-data filename codes.
All public `Image.OCR` and mix-task language arguments accept any of:
* An ISO 639-1 two-letter code, as a string or atom: `"en"`, `:en`,
`"fr"`, `:de`.
* A BCP-47 tag for region- or script-specific variants where ISO 639-1
alone is ambiguous: `"zh-Hans"`, `"zh-Hant"`, `"sr-Latn"`,
`"az-Cyrl"`.
* A Tesseract trained-data code passed through verbatim, for languages or
artefacts ISO 639-1 cannot express: `"eng"`, `"chi_sim"`, `"frk"`
(German Fraktur), `"osd"` (orientation/script detection),
`"script/Latin"`.
* A `+`-joined combination of any of the above: `"en+fr"`, `"chi_sim+eng"`.
## Ambiguous codes
ISO 639-1 `zh` does not specify a script and is rejected — use `"zh-Hans"`
for Simplified Chinese or `"zh-Hant"` for Traditional Chinese.
"""
# Unambiguous ISO 639-1 → Tesseract code mappings.
@iso639_1 %{
"af" => "afr",
"am" => "amh",
"ar" => "ara",
"as" => "asm",
"az" => "aze",
"be" => "bel",
"bg" => "bul",
"bn" => "ben",
"bo" => "bod",
"bs" => "bos",
"ca" => "cat",
"cs" => "ces",
"cy" => "cym",
"da" => "dan",
"de" => "deu",
"dz" => "dzo",
"el" => "ell",
"en" => "eng",
"eo" => "epo",
"es" => "spa",
"et" => "est",
"eu" => "eus",
"fa" => "fas",
"fi" => "fin",
"fr" => "fra",
"ga" => "gle",
"gd" => "gla",
"gl" => "glg",
"gu" => "guj",
"he" => "heb",
"hi" => "hin",
"hr" => "hrv",
"hu" => "hun",
"hy" => "hye",
"id" => "ind",
"is" => "isl",
"it" => "ita",
"iu" => "iku",
"ja" => "jpn",
"jv" => "jav",
"ka" => "kat",
"kk" => "kaz",
"km" => "khm",
"kn" => "kan",
"ko" => "kor",
"ku" => "kur",
"ky" => "kir",
"la" => "lat",
"lo" => "lao",
"lt" => "lit",
"lv" => "lav",
"mk" => "mkd",
"ml" => "mal",
"mn" => "mon",
"mr" => "mar",
"ms" => "msa",
"mt" => "mlt",
"my" => "mya",
"ne" => "nep",
"nl" => "nld",
"no" => "nor",
"or" => "ori",
"pa" => "pan",
"pl" => "pol",
"ps" => "pus",
"pt" => "por",
"ro" => "ron",
"ru" => "rus",
"si" => "sin",
"sk" => "slk",
"sl" => "slv",
"sq" => "sqi",
"sr" => "srp",
"sv" => "swe",
"sw" => "swa",
"ta" => "tam",
"te" => "tel",
"tg" => "tgk",
"th" => "tha",
"ti" => "tir",
"tk" => "tuk",
"tl" => "tgl",
"tr" => "tur",
"ug" => "uig",
"uk" => "ukr",
"ur" => "urd",
"uz" => "uzb",
"vi" => "vie",
"yi" => "yid",
"yo" => "yor"
}
# BCP-47 region/script tags for cases where ISO 639-1 alone is ambiguous.
@bcp47 %{
"zh-Hans" => "chi_sim",
"zh-CN" => "chi_sim",
"zh-Hant" => "chi_tra",
"zh-TW" => "chi_tra",
"zh-HK" => "chi_tra",
"sr-Cyrl" => "srp",
"sr-Latn" => "srp_latn",
"az-Cyrl" => "aze_cyrl",
"uz-Cyrl" => "uzb_cyrl"
}
@reverse for {k, v} <- @iso639_1, into: %{}, do: {v, k}
@doc """
Translates a user-supplied language identifier to the corresponding
Tesseract trained-data code (the basename of the `.traineddata` file).
Compound `+`-joined identifiers are translated component-wise.
### Arguments
* `code` is a string or atom describing one or more languages. See the
moduledoc for the accepted forms.
### Returns
* The translated string, e.g. `"eng"`, `"chi_sim+eng"`, or `"frk"`.
### Examples
iex> Image.OCR.Languages.to_tesseract(:en)
"eng"
iex> Image.OCR.Languages.to_tesseract("zh-Hans")
"chi_sim"
iex> Image.OCR.Languages.to_tesseract("en+fr")
"eng+fra"
iex> Image.OCR.Languages.to_tesseract("frk")
"frk"
"""
@spec to_tesseract(atom() | String.t()) :: String.t()
def to_tesseract(code) when is_atom(code), do: to_tesseract(Atom.to_string(code))
def to_tesseract(code) when is_binary(code) do
code
|> String.split("+", trim: true)
|> Enum.map_join("+", &translate_one/1)
end
@doc """
Inverse of `to_tesseract/1`: returns the ISO 639-1 code for a Tesseract
trained-data code when one exists, otherwise the input unchanged.
### Examples
iex> Image.OCR.Languages.from_tesseract("eng")
"en"
iex> Image.OCR.Languages.from_tesseract("frk")
"frk"
"""
@spec from_tesseract(String.t()) :: String.t()
def from_tesseract(tess_code) when is_binary(tess_code) do
Map.get(@reverse, tess_code, tess_code)
end
@doc false
def known_iso639_1, do: Map.keys(@iso639_1)
@doc false
def known_bcp47, do: Map.keys(@bcp47)
defp translate_one(code) do
cond do
Map.has_key?(@bcp47, code) ->
Map.fetch!(@bcp47, code)
Map.has_key?(@iso639_1, code) ->
Map.fetch!(@iso639_1, code)
String.contains?(code, "-") ->
translate_bcp47(code)
String.length(code) == 2 ->
if code == "zh" do
raise ArgumentError,
"ambiguous locale \"zh\" — use \"zh-Hans\" (Simplified) or \"zh-Hant\" (Traditional)"
else
raise ArgumentError, "unknown ISO 639-1 locale: #{inspect(code)}"
end
true ->
# Tesseract code (eng, chi_sim, frk, osd, script/Latin, …) — pass
# through unchanged.
code
end
end
# ----------------------------------------------------------------------
# BCP-47 fallback — compile-fenced on the optional `:localize` dependency.
# When Localize is available we accept any BCP-47 locale (e.g. "en-US",
# "fr-CA", "zh-Hans-CN", "sr-Latn-RS"), parse it with
# `Localize.LanguageTag.parse/1`, and resolve the right Tesseract code
# from the language + script subtags. Without Localize we only know the
# short table at the top of this module.
# ----------------------------------------------------------------------
if Code.ensure_loaded?(Localize.LanguageTag) do
@doc false
def localize_available?, do: true
defp translate_bcp47(code) do
case Localize.LanguageTag.parse(code) do
{:ok, %Localize.LanguageTag{language: language, script: script}} ->
compose_from_subtags(language, script, code)
{:error, _} ->
raise ArgumentError, "unparseable BCP-47 locale: #{inspect(code)}"
end
end
# Map (language, script) atoms returned by Localize into a Tesseract code.
# When script is required to disambiguate (zh, sr, az, uz) we use it;
# otherwise we fall back to the language portion alone.
defp compose_from_subtags(:zh, :Hans, _), do: "chi_sim"
defp compose_from_subtags(:zh, :Hant, _), do: "chi_tra"
defp compose_from_subtags(:zh, nil, code), do: ambiguous_zh!(code)
defp compose_from_subtags(:sr, :Latn, _), do: "srp_latn"
defp compose_from_subtags(:sr, :Cyrl, _), do: "srp"
defp compose_from_subtags(:az, :Cyrl, _), do: "aze_cyrl"
defp compose_from_subtags(:uz, :Cyrl, _), do: "uzb_cyrl"
defp compose_from_subtags(language, _script, code) when is_atom(language) do
lang_str = Atom.to_string(language)
case Map.fetch(@iso639_1, lang_str) do
{:ok, tess_code} ->
tess_code
:error ->
raise ArgumentError,
"no Tesseract trained-data mapping for locale #{inspect(code)} " <>
"(language #{inspect(language)})"
end
end
defp ambiguous_zh!(code) do
raise ArgumentError,
"ambiguous Chinese locale #{inspect(code)} — script subtag required " <>
"(\"zh-Hans\" or \"zh-Hant\")"
end
else
@doc false
def localize_available?, do: false
defp translate_bcp47(code) do
raise ArgumentError,
"unknown BCP-47 locale: #{inspect(code)}. Add `{:localize, \"~> 0.25\"}` " <>
"to your dependencies for full BCP-47 support."
end
end
end