Packages
evision
1.0.1-rc.0
1.0.1-rc.0
1.0.0
1.0.0-rc.0
0.2.17
0.2.17-rc2
0.2.17-rc1
0.2.16
0.2.16-pre.2
0.2.16-pre
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.2-rc2
0.2.1
0.2.0
0.1.39
0.1.38
0.1.37
0.1.36
0.1.35
0.1.34
0.1.33
0.1.32
retired
0.1.31
0.1.30
0.1.29
0.1.28
0.1.27
0.1.26
0.1.26-rc3
0.1.26-rc2
0.1.26-rc1
0.1.26-rc0
0.1.25
0.1.24
0.1.23
0.1.22
0.1.21
0.1.20
0.1.19
0.1.18
0.1.17
0.1.16
0.1.15
0.1.14
0.1.13
0.1.12
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
OpenCV-Erlang/Elixir binding.
Current section
Files
Jump to
Current section
Files
py_src/evision_js_attr.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Resolve evision ↔ opencv.js correspondences for the codegen ``@js`` tag.
The whitelist source of truth is OpenCV's own
``platforms/js/opencv_js.config.py`` (parsed by ``opencv_js_whitelist``).
This module turns that flat data into an ``(classname, fn_name) → entry``
index keyed by the C++ shape evision's ``FuncInfo`` exposes, and renders
Elixir/Erlang persistent-attribute syntax for the codegen to splice in.
If the OpenCV source tree is not on disk (e.g. when building against a
precompiled binary), the lookup degrades to "no matches" — the codegen
still produces output, just without ``@js`` tags.
See ``projects/evision/context/js-correspondence-tag.md`` in the
``cocoa/ai-native`` notebook for the full design.
"""
from __future__ import annotations
import sys
from typing import Dict, Iterable, List, Optional, Tuple
from opencv_js_whitelist import (
ConfigNotFoundError,
entries,
namespace_prefix_override,
)
if sys.version_info[0] >= 3:
from io import StringIO
else:
from cStringIO import StringIO
JsEntry = Dict[str, object]
LookupKey = Tuple[Optional[str], str]
RuntimeEntry = Dict[str, object]
_INDEX: Optional[Dict[LookupKey, JsEntry]] = None
def _build_index() -> Dict[LookupKey, JsEntry]:
overrides = namespace_prefix_override()
index: Dict[LookupKey, JsEntry] = {}
for _module, classname, fn_name, kind in entries():
key: LookupKey = (classname, fn_name)
if classname is None:
index[key] = {"js_kind": "function", "js_name": f"cv.{fn_name}"}
elif kind == "constructor":
index[key] = {
"js_kind": "constructor",
"js_class": f"cv.{_js_class_for(classname, overrides)}",
}
else:
index[key] = {
"js_kind": "method",
"js_class": f"cv.{_js_class_for(classname, overrides)}",
"js_method": fn_name,
}
return index
def _ensure_loaded() -> Dict[LookupKey, JsEntry]:
global _INDEX
if _INDEX is None:
try:
_INDEX = _build_index()
except ConfigNotFoundError:
_INDEX = {}
return _INDEX
def lookup_for_func(func) -> Optional[JsEntry]:
"""Return the opencv.js entry matching an evision ``FuncInfo``, or ``None``.
Lookup uses C++ shapes (``func.name``, ``func.classname``) per CCD-18's
closing refinements: the post-folded Elixir name lowercases the first
letter so a match against ``Canny`` would miss.
"""
index = _ensure_loaded()
if not index:
return None
if func.isconstructor:
# The whitelist names the constructor as the bare class segment
# (``aruco_Dictionary`` → ``Dictionary``), not the full path-joined
# name ``func.name`` carries.
bare = func.classname.rsplit("_", 1)[-1] if func.classname else ""
key: LookupKey = (func.classname, bare)
elif func.classname:
key = (func.classname, func.name)
else:
ns = func.namespace
if ns == "cv":
key = (None, func.name)
elif ns.startswith("cv."):
# ``cv::fisheye::undistortImage`` lives in ``calib3d['']`` as
# ``fisheye_undistortImage`` — the C++ namespace below ``cv`` is
# flattened into the fn_name with an underscore separator.
ns_below = ns[3:].replace(".", "_")
key = (None, f"{ns_below}_{func.name}")
else:
return None
return index.get(key)
def _js_class_for(class_key: str, overrides: Dict[str, str]) -> str:
"""Project a whitelist class key onto its opencv.js class name.
embindgen's ``namespace_prefix_override`` strips ``dnn_``/``aruco_``
prefixes when the whitelist is a Python file (defaults live in
``opencv_js_whitelist.DEFAULT_NAMESPACE_PREFIX_OVERRIDE``).
"""
if "_" in class_key:
prefix, rest = class_key.split("_", 1)
if prefix in overrides:
override = overrides[prefix]
return f"{override}.{rest}" if override else rest
return class_key
# ----- Source emitters ----------------------------------------------------
def elixir_register_attribute() -> str:
"""Single line that opts a module into accumulating ``@js`` attributes."""
return (
" Module.register_attribute(__MODULE__, :js, persist: true, accumulate: true)\n"
)
def elixir_attribute_line(entry: JsEntry, fun: str, arity: int) -> str:
"""`` @js %{...}\\n`` line ready to splice before a ``def`` clause."""
fields = [
f"fun: {_elixir_atom(fun)}",
f"arity: {arity}",
f"js_kind: :{entry['js_kind']}",
]
if "js_name" in entry:
fields.append(f'js_name: "{entry["js_name"]}"')
if "js_class" in entry:
fields.append(f'js_class: "{entry["js_class"]}"')
if "js_method" in entry:
fields.append(f'js_method: "{entry["js_method"]}"')
if "arg_plan" in entry:
fields.append(f"arg_plan: {_elixir_arg_plan(entry['arg_plan'])}")
return f" @js %{{{', '.join(fields)}}}\n"
def erlang_attribute_line(entry: JsEntry, fun: str, arity: int) -> str:
"""``-js(#{...}).\\n`` line ready to splice before a function clause."""
fields = [
f"fun => {_erlang_atom(fun)}",
f"arity => {arity}",
f"js_kind => {entry['js_kind']}",
]
if "js_name" in entry:
fields.append(f'js_name => <<"{entry["js_name"]}">>')
if "js_class" in entry:
fields.append(f'js_class => <<"{entry["js_class"]}">>')
if "js_method" in entry:
fields.append(f'js_method => <<"{entry["js_method"]}">>')
if "arg_plan" in entry:
fields.append(f"arg_plan => {_erlang_arg_plan(entry['arg_plan'])}")
return f"-js(#{{{', '.join(fields)}}}).\n"
def _elixir_atom(name: str) -> str:
# All evision-emitted def names are valid bare Elixir atoms (letter,
# then letters/digits/underscore). The quoted form would still work but
# would clutter generated source.
return f":{name}"
def _erlang_atom(name: str) -> str:
# Erlang atoms must start with lowercase. The evision codegen lowercases
# the first character of every emitted name (`map_argname_elixir`,
# `get_module_func_name`), so this is always safe without quoting.
return name
def _elixir_arg_plan(plan) -> str:
# opencv.js positional arg-plan as an Elixir atom list: [:in, :out, :in].
return "[" + ", ".join(f":{slot}" for slot in plan) + "]"
def _erlang_arg_plan(plan) -> str:
# opencv.js positional arg-plan as an Erlang atom list: [in, out, in].
return "[" + ", ".join(plan) + "]"
# ----- Runtime module emission (Evision.JS / evision_js) ------------------
def collect_runtime_entries(module_generators: Iterable, kind: str) -> List[RuntimeEntry]:
"""Flatten ``js_attrs[kind]`` across every emitted ``ModuleGenerator``.
Each ``ModuleGenerator`` carries its own
``js_attrs[kind] = {fun_name: {arity: js_entry}}``. The runtime module
needs a single flat list of entries keyed by
``(module, fun, arity)`` — the Elixir module name is reconstructed
from the generator's ``module_name`` (the root ``Evision`` keeps that
bare name; everything else is prefixed with ``Evision.``).
Output is sorted by ``(module, fun, arity)`` so the generated source
is reproducible across runs.
"""
out: List[RuntimeEntry] = []
for mg in module_generators:
mod_name = mg.module_name
full_module = mod_name if mod_name == "Evision" else f"Evision.{mod_name}"
kind_attrs = mg.js_attrs.get(kind) or {}
for fun_name, by_arity in kind_attrs.items():
for arity, js_entry in by_arity.items():
entry: RuntimeEntry = {
"module": full_module,
"fun": fun_name,
"arity": arity,
"js_kind": js_entry["js_kind"],
}
for optional_key in ("js_name", "js_class", "js_method", "arg_plan"):
if optional_key in js_entry:
entry[optional_key] = js_entry[optional_key]
out.append(entry)
out.sort(key=lambda e: (e["module"], e["fun"], e["arity"]))
return out
def elixir_runtime_module(entries_list: List[RuntimeEntry]) -> str:
"""Render ``lib/generated/evision_js.ex`` from a flat entry list."""
out = StringIO()
out.write("defmodule Evision.JS do\n")
out.write(' @moduledoc """\n')
out.write(" Generated map of evision bindings to their opencv.js counterparts.\n")
out.write("\n")
out.write(" Each entry carries the Elixir `(module, fun, arity)` plus the\n")
out.write(" opencv.js call shape: `:js_kind` discriminates between\n")
out.write(" `:function`, `:method` and `:constructor`; `:js_name` is set\n")
out.write(" for free functions, `:js_class` for class methods and\n")
out.write(" constructors, and `:js_method` for methods only.\n")
out.write("\n")
out.write(" `opencv_js_url/0` returns where to fetch the matching CSP-clean\n")
out.write(" `opencv.js` runtime for these entries; `opencv_js_path/0` returns its\n")
out.write(" local path when embedded via `config :evision, Evision.JS, embed_js: true`.\n")
out.write(' """\n\n')
# Mirrors Evision.MixProject.Metadata.github_url/0, which is build-time only.
out.write(' @github_url "https://github.com/cocoa-xu/evision"\n\n')
out.write(' @typedoc "An opencv.js correspondence entry."\n')
out.write(" @type entry :: %{\n")
out.write(" required(:module) => module(),\n")
out.write(" required(:fun) => atom(),\n")
out.write(" required(:arity) => arity(),\n")
out.write(" required(:js_kind) => :function | :method | :constructor,\n")
out.write(" optional(:arg_plan) => [:in | :out],\n")
out.write(" optional(:js_name) => String.t(),\n")
out.write(" optional(:js_class) => String.t(),\n")
out.write(" optional(:js_method) => String.t()\n")
out.write(" }\n\n")
if entries_list:
out.write(" @whitelist [\n")
for entry in entries_list:
out.write(" " + _elixir_entry_literal(entry) + ",\n")
out.write(" ]\n\n")
else:
out.write(" @whitelist []\n\n")
out.write(" @index Map.new(@whitelist, fn entry -> {{entry.module, entry.fun, entry.arity}, entry} end)\n\n")
out.write(' @doc "Every opencv.js correspondence entry emitted by evision\'s codegen."\n')
out.write(" @spec whitelist() :: [entry()]\n")
out.write(" def whitelist, do: @whitelist\n\n")
out.write(' @doc "Return `{:ok, entry}` for a known `(module, fun, arity)`; `:error` otherwise."\n')
out.write(" @spec lookup(module(), atom(), arity()) :: {:ok, entry()} | :error\n")
out.write(" def lookup(module, fun, arity), do: Map.fetch(@index, {module, fun, arity})\n\n")
out.write(' @doc "Whether the given `(module, fun, arity)` has an opencv.js counterpart."\n')
out.write(" @spec runnable?(module(), atom(), arity()) :: boolean()\n")
out.write(" def runnable?(module, fun, arity), do: Map.has_key?(@index, {module, fun, arity})\n\n")
out.write(' @doc """\n')
out.write(" URL of the CSP-clean `opencv.js` published with this evision release.\n")
out.write("\n")
out.write(" The asset is attached to the `v<version>` release tag. `version` defaults to\n")
out.write(" the running evision version (`Application.spec(:evision, :vsn)`), so the URL\n")
out.write(" points at the `opencv.js` that matches these correspondence entries; pass a\n")
out.write(" version to target another release. The pinned checksum lives beside it at the\n")
out.write(" same URL with `opencv.sha256` in place of `opencv.js`.\n")
out.write(' """\n')
out.write(" @spec opencv_js_url(String.t() | nil) :: String.t()\n")
out.write(" def opencv_js_url(version \\\\ nil) do\n")
out.write(" version = version || to_string(Application.spec(:evision, :vsn))\n")
out.write(' "#{@github_url}/releases/download/v#{version}/opencv.js"\n')
out.write(" end\n\n")
out.write(' @doc """\n')
out.write(" Filesystem path of the `opencv.js` embedded in `priv`, or `:error`.\n")
out.write("\n")
out.write(" Present when evision was built with `config :evision, Evision.JS,\n")
out.write(" embed_js: true` (or `EVISION_EMBED_JS=true`), which downloads or builds the\n")
out.write(" matching `opencv.js` into `priv` at compile time.\n")
out.write(' """\n')
out.write(" @spec opencv_js_path() :: {:ok, String.t()} | :error\n")
out.write(" def opencv_js_path do\n")
out.write(" case :code.priv_dir(:evision) do\n")
out.write(" dir when is_list(dir) or is_binary(dir) ->\n")
out.write(' path = Path.join(to_string(dir), "opencv.js")\n')
out.write(" if File.exists?(path), do: {:ok, path}, else: :error\n")
out.write("\n")
out.write(" _ ->\n")
out.write(" :error\n")
out.write(" end\n")
out.write(" end\n")
out.write("end\n")
return out.getvalue()
def erlang_runtime_module(entries_list: List[RuntimeEntry]) -> str:
"""Render ``src/generated/evision_js.erl`` from a flat entry list.
``fun`` is a reserved word in Erlang's expression / type-spec grammar,
so every map key referring to that field is quoted as ``'fun'``.
Inside ``-attribute(#{...}).`` declarations (CCD-19's ``-js(...)``) the
attribute parser is more permissive and bare ``fun`` works there — that
permissiveness does NOT extend to function bodies or ``-spec``s.
"""
out = StringIO()
out.write("-module(evision_js).\n")
out.write("-compile(nowarn_export_all).\n")
out.write("-compile([export_all]).\n\n")
out.write("-type entry() :: #{\n")
out.write(" module := module(),\n")
out.write(" 'fun' := atom(),\n")
out.write(" arity := arity(),\n")
out.write(" js_kind := function | method | constructor,\n")
out.write(" arg_plan => [in | out],\n")
out.write(" js_name => binary(),\n")
out.write(" js_class => binary(),\n")
out.write(" js_method => binary()\n")
out.write("}.\n\n")
out.write("-spec whitelist() -> [entry()].\n")
out.write("whitelist() ->\n")
if entries_list:
out.write(" [\n")
for i, entry in enumerate(entries_list):
sep = "," if i < len(entries_list) - 1 else ""
out.write(f" {_erlang_entry_literal(entry)}{sep}\n")
out.write(" ].\n\n")
else:
out.write(" [].\n\n")
out.write("-spec lookup(module(), atom(), arity()) -> {ok, entry()} | error.\n")
out.write("lookup(Module, Fun, Arity) ->\n")
out.write(" maps:find({Module, Fun, Arity}, index()).\n\n")
out.write("-spec runnable(module(), atom(), arity()) -> boolean().\n")
out.write("runnable(Module, Fun, Arity) ->\n")
out.write(" maps:is_key({Module, Fun, Arity}, index()).\n\n")
# opencv_js_url/0,1 mirror Evision.JS.opencv_js_url; github_url matches
# Evision.MixProject.Metadata.github_url/0 (build-time only).
out.write("-spec opencv_js_url() -> binary().\n")
out.write("opencv_js_url() ->\n")
out.write(" opencv_js_url(evision_vsn()).\n\n")
out.write("-spec opencv_js_url(binary() | string()) -> binary().\n")
out.write("opencv_js_url(Version) ->\n")
out.write(' iolist_to_binary([<<"https://github.com/cocoa-xu/evision/releases/download/v">>, Version, <<"/opencv.js">>]).\n\n')
out.write("evision_vsn() ->\n")
out.write(" case application:get_key(evision, vsn) of\n")
out.write(" {ok, Vsn} -> Vsn;\n")
out.write(' undefined -> ""\n')
out.write(" end.\n\n")
out.write("-spec opencv_js_path() -> {ok, binary()} | error.\n")
out.write("opencv_js_path() ->\n")
out.write(" case code:priv_dir(evision) of\n")
out.write(" {error, _} -> error;\n")
out.write(" Dir ->\n")
out.write(' Path = filename:join(Dir, "opencv.js"),\n')
out.write(" case filelib:is_file(Path) of\n")
out.write(" true -> {ok, unicode:characters_to_binary(Path)};\n")
out.write(" false -> error\n")
out.write(" end\n")
out.write(" end.\n\n")
out.write("index() ->\n")
if entries_list:
out.write(" #{\n")
for i, entry in enumerate(entries_list):
sep = "," if i < len(entries_list) - 1 else ""
out.write(f" {_erlang_key(entry)} => {_erlang_entry_literal(entry)}{sep}\n")
out.write(" }.\n")
else:
out.write(" #{}.\n")
return out.getvalue()
def _elixir_entry_literal(entry: RuntimeEntry) -> str:
fields = [
f"module: {entry['module']}",
f"fun: :{entry['fun']}",
f"arity: {entry['arity']}",
f"js_kind: :{entry['js_kind']}",
]
for optional_key in ("js_name", "js_class", "js_method"):
if optional_key in entry:
fields.append(f'{optional_key}: "{entry[optional_key]}"')
if "arg_plan" in entry:
fields.append(f"arg_plan: {_elixir_arg_plan(entry['arg_plan'])}")
return "%{" + ", ".join(fields) + "}"
def _erlang_module_atom(elixir_module_name: str) -> str:
return f"'Elixir.{elixir_module_name}'"
def _erlang_key(entry: RuntimeEntry) -> str:
return f"{{{_erlang_module_atom(str(entry['module']))}, {entry['fun']}, {entry['arity']}}}"
def _erlang_entry_literal(entry: RuntimeEntry) -> str:
fields = [
f"module => {_erlang_module_atom(str(entry['module']))}",
f"'fun' => {entry['fun']}",
f"arity => {entry['arity']}",
f"js_kind => {entry['js_kind']}",
]
for optional_key in ("js_name", "js_class", "js_method"):
if optional_key in entry:
fields.append(f'{optional_key} => <<"{entry[optional_key]}">>')
if "arg_plan" in entry:
fields.append(f"arg_plan => {_erlang_arg_plan(entry['arg_plan'])}")
return "#{" + ", ".join(fields) + "}"