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 c_src image_ocr_nif.cc
Raw

c_src/image_ocr_nif.cc

// Image.OCR NIF: thin wrapper around tesseract::TessBaseAPI.
//
// Concurrency model:
// * Each %Image.OCR{} owns one TessBaseAPI*. TessBaseAPI is NOT safe for
// concurrent use on the same instance. We hold a per-resource ErlNifMutex
// so accidental sharing degrades to serialization rather than UB.
// * For real parallelism, callers create one instance per worker (see
// Image.OCR.Pool). All recognition entry points are dirty-CPU NIFs.
#include <cstring>
#include <string>
#include <erl_nif.h>
#include <tesseract/baseapi.h>
#include <tesseract/publictypes.h>
#include <tesseract/resultiterator.h>
namespace {
ErlNifResourceType* TESS_RESOURCE_TYPE = nullptr;
struct TessResource {
tesseract::TessBaseAPI* api;
ErlNifMutex* mutex;
};
void tess_resource_dtor(ErlNifEnv*, void* obj) {
auto* r = static_cast<TessResource*>(obj);
if (r->api) {
r->api->End();
delete r->api;
r->api = nullptr;
}
if (r->mutex) {
enif_mutex_destroy(r->mutex);
r->mutex = nullptr;
}
}
ERL_NIF_TERM mk_atom(ErlNifEnv* env, const char* name) {
ERL_NIF_TERM atom;
if (!enif_make_existing_atom(env, name, &atom, ERL_NIF_LATIN1)) {
atom = enif_make_atom(env, name);
}
return atom;
}
ERL_NIF_TERM mk_ok(ErlNifEnv* env, ERL_NIF_TERM value) {
return enif_make_tuple2(env, mk_atom(env, "ok"), value);
}
ERL_NIF_TERM mk_error(ErlNifEnv* env, const char* reason) {
return enif_make_tuple2(env, mk_atom(env, "error"), mk_atom(env, reason));
}
bool get_iolist_string(ErlNifEnv* env, ERL_NIF_TERM term, std::string* out) {
ErlNifBinary bin;
if (enif_inspect_iolist_as_binary(env, term, &bin)) {
out->assign(reinterpret_cast<const char*>(bin.data), bin.size);
return true;
}
return false;
}
tesseract::PageSegMode psm_from_int(int v) {
if (v < 0 || v > tesseract::PSM_COUNT - 1) return tesseract::PSM_AUTO;
return static_cast<tesseract::PageSegMode>(v);
}
// init_nif(language :: binary, datapath :: binary | nil, psm :: integer)
ERL_NIF_TERM init_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (argc != 3) return enif_make_badarg(env);
std::string language;
if (!get_iolist_string(env, argv[0], &language) || language.empty()) {
return mk_error(env, "invalid_language");
}
std::string datapath;
bool have_datapath = false;
if (!enif_is_atom(env, argv[1])) {
if (!get_iolist_string(env, argv[1], &datapath)) {
return mk_error(env, "invalid_datapath");
}
have_datapath = true;
}
int psm_int;
if (!enif_get_int(env, argv[2], &psm_int)) return enif_make_badarg(env);
auto* api = new tesseract::TessBaseAPI();
int rc = api->Init(have_datapath ? datapath.c_str() : nullptr,
language.c_str(),
tesseract::OEM_DEFAULT);
if (rc != 0) {
delete api;
return mk_error(env, "init_failed");
}
api->SetPageSegMode(psm_from_int(psm_int));
// Suppress Tesseract's noisy "Estimating resolution as N" stderr chatter.
// Callers can override via the `:variables` option if they need it back.
api->SetVariable("debug_file", "/dev/null");
auto* res = static_cast<TessResource*>(
enif_alloc_resource(TESS_RESOURCE_TYPE, sizeof(TessResource)));
res->api = api;
res->mutex = enif_mutex_create(const_cast<char*>("image_ocr.api"));
ERL_NIF_TERM term = enif_make_resource(env, res);
enif_release_resource(res);
return mk_ok(env, term);
}
// set_variable_nif(api, key :: binary, value :: binary)
ERL_NIF_TERM set_variable_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (argc != 3) return enif_make_badarg(env);
TessResource* res;
if (!enif_get_resource(env, argv[0], TESS_RESOURCE_TYPE, (void**)&res)) {
return enif_make_badarg(env);
}
std::string key, value;
if (!get_iolist_string(env, argv[1], &key) ||
!get_iolist_string(env, argv[2], &value)) {
return enif_make_badarg(env);
}
enif_mutex_lock(res->mutex);
bool ok = res->api->SetVariable(key.c_str(), value.c_str());
enif_mutex_unlock(res->mutex);
return ok ? mk_atom(env, "ok") : mk_error(env, "unknown_variable");
}
// recognize_nif(api, pixel_binary, width, height, bytes_per_pixel, bytes_per_line)
ERL_NIF_TERM recognize_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (argc != 6) return enif_make_badarg(env);
TessResource* res;
if (!enif_get_resource(env, argv[0], TESS_RESOURCE_TYPE, (void**)&res)) {
return enif_make_badarg(env);
}
ErlNifBinary pixels;
if (!enif_inspect_binary(env, argv[1], &pixels)) return enif_make_badarg(env);
int width, height, bpp, bpl;
if (!enif_get_int(env, argv[2], &width) ||
!enif_get_int(env, argv[3], &height) ||
!enif_get_int(env, argv[4], &bpp) ||
!enif_get_int(env, argv[5], &bpl)) {
return enif_make_badarg(env);
}
if (width <= 0 || height <= 0 || bpp < 1 || bpp > 4 || bpl < width * bpp) {
return mk_error(env, "invalid_image_geometry");
}
if (pixels.size < static_cast<size_t>(bpl) * static_cast<size_t>(height)) {
return mk_error(env, "insufficient_pixel_data");
}
enif_mutex_lock(res->mutex);
res->api->SetImage(pixels.data, width, height, bpp, bpl);
char* utf8 = res->api->GetUTF8Text();
// Drop image-related buffers; keeps memory bounded between calls.
res->api->Clear();
enif_mutex_unlock(res->mutex);
if (!utf8) return mk_error(env, "recognition_failed");
size_t len = std::strlen(utf8);
ERL_NIF_TERM bin;
unsigned char* buf = enif_make_new_binary(env, len, &bin);
std::memcpy(buf, utf8, len);
delete[] utf8;
return mk_ok(env, bin);
}
// recognize_with_boxes_nif(api, pixel_binary, width, height, bpp, bpl)
ERL_NIF_TERM recognize_with_boxes_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
if (argc != 6) return enif_make_badarg(env);
TessResource* res;
if (!enif_get_resource(env, argv[0], TESS_RESOURCE_TYPE, (void**)&res)) {
return enif_make_badarg(env);
}
ErlNifBinary pixels;
if (!enif_inspect_binary(env, argv[1], &pixels)) return enif_make_badarg(env);
int width, height, bpp, bpl;
if (!enif_get_int(env, argv[2], &width) ||
!enif_get_int(env, argv[3], &height) ||
!enif_get_int(env, argv[4], &bpp) ||
!enif_get_int(env, argv[5], &bpl)) {
return enif_make_badarg(env);
}
if (width <= 0 || height <= 0 || bpp < 1 || bpp > 4 || bpl < width * bpp) {
return mk_error(env, "invalid_image_geometry");
}
if (pixels.size < static_cast<size_t>(bpl) * static_cast<size_t>(height)) {
return mk_error(env, "insufficient_pixel_data");
}
enif_mutex_lock(res->mutex);
res->api->SetImage(pixels.data, width, height, bpp, bpl);
int rc = res->api->Recognize(nullptr);
if (rc != 0) {
res->api->Clear();
enif_mutex_unlock(res->mutex);
return mk_error(env, "recognition_failed");
}
ERL_NIF_TERM list = enif_make_list(env, 0);
tesseract::ResultIterator* it = res->api->GetIterator();
if (it != nullptr) {
const tesseract::PageIteratorLevel level = tesseract::RIL_WORD;
do {
char* word = it->GetUTF8Text(level);
if (word == nullptr) continue;
float conf = it->Confidence(level);
int x1, y1, x2, y2;
it->BoundingBox(level, &x1, &y1, &x2, &y2);
size_t wlen = std::strlen(word);
ERL_NIF_TERM word_bin;
unsigned char* wbuf = enif_make_new_binary(env, wlen, &word_bin);
std::memcpy(wbuf, word, wlen);
delete[] word;
ERL_NIF_TERM keys[3] = {
mk_atom(env, "text"),
mk_atom(env, "confidence"),
mk_atom(env, "bbox")};
ERL_NIF_TERM bbox = enif_make_tuple4(
env,
enif_make_int(env, x1),
enif_make_int(env, y1),
enif_make_int(env, x2),
enif_make_int(env, y2));
ERL_NIF_TERM values[3] = {
word_bin,
enif_make_double(env, static_cast<double>(conf)),
bbox};
ERL_NIF_TERM map;
enif_make_map_from_arrays(env, keys, values, 3, &map);
list = enif_make_list_cell(env, map, list);
} while (it->Next(level));
delete it;
}
res->api->Clear();
enif_mutex_unlock(res->mutex);
// Reverse list to preserve reading order.
ERL_NIF_TERM reversed;
enif_make_reverse_list(env, list, &reversed);
return mk_ok(env, reversed);
}
// tesseract_version_nif() :: binary
ERL_NIF_TERM tesseract_version_nif(ErlNifEnv* env, int, const ERL_NIF_TERM[]) {
const char* v = tesseract::TessBaseAPI::Version();
size_t len = std::strlen(v);
ERL_NIF_TERM bin;
unsigned char* buf = enif_make_new_binary(env, len, &bin);
std::memcpy(buf, v, len);
return bin;
}
int load(ErlNifEnv* env, void**, ERL_NIF_TERM) {
ErlNifResourceFlags flags =
static_cast<ErlNifResourceFlags>(ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER);
TESS_RESOURCE_TYPE = enif_open_resource_type(
env, nullptr, "image_ocr_tess_api", tess_resource_dtor, flags, nullptr);
return TESS_RESOURCE_TYPE == nullptr ? -1 : 0;
}
ErlNifFunc nif_funcs[] = {
{"init_nif", 3, init_nif, 0},
{"set_variable_nif", 3, set_variable_nif, 0},
{"recognize_nif", 6, recognize_nif, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"recognize_with_boxes_nif", 6, recognize_with_boxes_nif, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"tesseract_version_nif", 0, tesseract_version_nif, 0},
};
} // namespace
ERL_NIF_INIT(Elixir.Image.OCR.Nif, nif_funcs, load, nullptr, nullptr, nullptr)