1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-30 07:43:03 +04:00

Merge pull request #29206 from kirtijindal14:doc_optimizations_v3

OpenCV New Documentation #29206

This PR introduces end to end working new Documentation in OpenCV.

Co-authored by: @abhishek-gola @omrope79 @Akansha-977 @Prasadayus @varun-jaiswal17 
### Pull Request Readiness Checklist

See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request

- [x] I agree to contribute to the project under Apache 2 License.
- [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV
- [x] The PR is proposed to the proper branch
- [x] There is a reference to the original bug report and related work
- [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable
      Patch to opencv_extra has the same branch name.
- [x] The feature is well documented and sample code can be built with the project CMake
This commit is contained in:
kirtijindal14
2026-06-02 00:09:16 +05:30
committed by GitHub
parent 4d0b381f03
commit 8756e68bff
17 changed files with 6886 additions and 1943 deletions
+120 -16
View File
@@ -1,9 +1,3 @@
# Sphinx wrapper for the OpenCV doc/ tree.
# Adds a `sphinx` custom target so `cmake --build <build> --target sphinx`
# works the same way `--target doxygen` does.
#
# Gated on BUILD_DOCS so it follows the existing toggle.
if(NOT BUILD_DOCS)
return()
endif()
@@ -20,11 +14,23 @@ if(NOT SPHINX_BUILD)
return()
endif()
find_program(DOT_EXECUTABLE NAMES dot
DOC "Path to graphviz `dot` (required for API collaboration diagrams)")
if(NOT DOT_EXECUTABLE)
message(WARNING
"docs_sphinx: graphviz `dot` not found — API class-page collaboration "
"diagrams will be skipped. Install graphviz (e.g. `sudo apt-get install "
"graphviz`) and re-run cmake so the Doxygen HTML build enables HAVE_DOT.")
else()
message(STATUS
"docs_sphinx: graphviz dot found (${DOT_EXECUTABLE}) — "
"collaboration diagrams enabled")
endif()
set(_SPHINX_CONFDIR "${CMAKE_CURRENT_SOURCE_DIR}")
set(_SPHINX_OUTDIR "${CMAKE_CURRENT_BINARY_DIR}/html")
set(_SPHINX_INPUT_ROOT "${CMAKE_CURRENT_BINARY_DIR}/docs_sphinx_input")
# Stage merged input tree: symlink main + contrib tutorial subtrees into one srcdir.
set(_SPHINX_INPUT_TUTORIALS "${_SPHINX_INPUT_ROOT}/tutorials")
set(_SPHINX_INPUT_CONTRIB "${_SPHINX_INPUT_ROOT}/tutorials_contrib")
file(REMOVE_RECURSE "${_SPHINX_INPUT_ROOT}")
@@ -36,11 +42,6 @@ file(CREATE_LINK
"${_SPHINX_INPUT_TUTORIALS}/tutorials.markdown"
SYMBOLIC COPY_ON_ERROR)
# Top-level standalone pages staged at srcdir root (siblings of tutorials/).
# Add further `file(CREATE_LINK …)` lines here if more top-level pages get
# onboarded. intro.markdown lives inside modules/core/doc/ (it's the core
# module's @page; the legacy Doxygen build pulls it in via the same module
# scan it uses for bib files) — symlink it under a flat name at srcdir root.
file(CREATE_LINK
"${CMAKE_SOURCE_DIR}/doc/faq.markdown"
"${_SPHINX_INPUT_ROOT}/faq.markdown"
@@ -71,7 +72,6 @@ foreach(_root js_tutorials py_tutorials images)
endif()
endforeach()
# Contrib tree: stage when OPENCV_EXTRA_MODULES_PATH is set; unselected modules dropped by conf.py.
if(OPENCV_EXTRA_MODULES_PATH AND EXISTS "${OPENCV_EXTRA_MODULES_PATH}")
file(MAKE_DIRECTORY "${_SPHINX_INPUT_CONTRIB}")
set(_contrib_root_md "${_SPHINX_INPUT_CONTRIB}/contrib_root.markdown")
@@ -97,22 +97,126 @@ if(OPENCV_EXTRA_MODULES_PATH AND EXISTS "${OPENCV_EXTRA_MODULES_PATH}")
endforeach()
endif()
# Forward staging paths via env. OPENCV_CONTRIB_MODULES is not forwarded; set it in shell to override.
set(_LEGACY_DOXYFILE "${CMAKE_BINARY_DIR}/doc/Doxyfile")
set(_DOXYGEN_XML_DIR "${CMAKE_BINARY_DIR}/doc/doxygen/xml")
if(NOT DEFINED SPHINX_API_MODULES)
set(SPHINX_API_MODULES "")
file(GLOB _umbrella_headers "${CMAKE_SOURCE_DIR}/modules/*/include/opencv2/*.hpp")
# Also scan opencv_contrib modules when present.
if(OPENCV_EXTRA_MODULES_PATH AND EXISTS "${OPENCV_EXTRA_MODULES_PATH}")
file(GLOB _contrib_umbrellas "${OPENCV_EXTRA_MODULES_PATH}/*/include/opencv2/*.hpp")
list(APPEND _umbrella_headers ${_contrib_umbrellas})
endif()
foreach(_hdr ${_umbrella_headers})
get_filename_component(_stem "${_hdr}" NAME_WE)
# Only the umbrella header modules/<m>/include/opencv2/<m>.hpp.
if(_hdr MATCHES "/modules/${_stem}/include/opencv2/${_stem}\\.hpp$")
file(READ "${_hdr}" _hdr_contents)
if(_hdr_contents MATCHES "@defgroup")
list(APPEND SPHINX_API_MODULES "${_stem}")
endif()
endif()
endforeach()
list(SORT SPHINX_API_MODULES)
message(STATUS "docs_sphinx: discovered API modules: ${SPHINX_API_MODULES}")
endif()
set(_doxy_input "")
foreach(_m ${SPHINX_API_MODULES})
foreach(_base "${CMAKE_SOURCE_DIR}/modules" "${OPENCV_EXTRA_MODULES_PATH}")
if(EXISTS "${_base}/${_m}/include")
string(APPEND _doxy_input " ${_base}/${_m}/include")
break()
endif()
endforeach()
endforeach()
if(EXISTS "${_LEGACY_DOXYFILE}")
set(_SPHINX_DOXYFILE "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile-xml")
file(WRITE "${_SPHINX_DOXYFILE}"
"@INCLUDE = ${_LEGACY_DOXYFILE}\n"
"GENERATE_HTML = NO\n"
"GENERATE_LATEX = NO\n"
"GENERATE_XML = YES\n"
"XML_OUTPUT = xml\n"
"XML_PROGRAMLISTING = NO\n"
"CREATE_SUBDIRS = NO\n"
"EXCLUDE_PATTERNS += cap_ios.h\n"
"CLASS_GRAPH = NO\n"
"COLLABORATION_GRAPH = NO\n"
"GROUP_GRAPHS = NO\n"
"INCLUDE_GRAPH = NO\n"
"INCLUDED_BY_GRAPH = NO\n"
"DIRECTORY_GRAPH = NO\n"
"INPUT =${_doxy_input}\n"
"RECURSIVE = YES\n"
"EXCLUDE_SYMBOLS = cv::DataType<*> int void CV__* T __CV* cv::gapi::detail*\n"
"MACRO_EXPANSION = YES\n"
"EXPAND_ONLY_PREDEF = YES\n"
"PREDEFINED += __device__= __host__= __forceinline__= __global__= __constant__= __shared__= __restrict__=\n"
)
find_program(DOXYGEN_EXE NAMES doxygen)
if(DOXYGEN_EXE)
# Stamp-based: Doxygen only re-runs when Doxyfile changes, not on every build.
set(_SPHINX_XML_STAMP "${_DOXYGEN_XML_DIR}/.sphinx_xml.stamp")
add_custom_command(
OUTPUT "${_SPHINX_XML_STAMP}"
COMMAND ${CMAKE_COMMAND} -E make_directory ${_DOXYGEN_XML_DIR}
COMMAND ${DOXYGEN_EXE} ${_SPHINX_DOXYFILE}
COMMAND ${CMAKE_COMMAND} -E touch "${_SPHINX_XML_STAMP}"
DEPENDS "${_SPHINX_DOXYFILE}"
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/doc
COMMENT "Doxygen XML (for Sphinx breathe) -> ${_DOXYGEN_XML_DIR}"
VERBATIM)
add_custom_target(sphinx-xml DEPENDS "${_SPHINX_XML_STAMP}")
message(STATUS "docs_sphinx: sphinx-xml target enabled (doxygen: ${DOXYGEN_EXE})")
else()
message(STATUS "docs_sphinx: doxygen not found; `sphinx-xml` disabled")
endif()
else()
message(STATUS "docs_sphinx: legacy Doxyfile not present at ${_LEGACY_DOXYFILE}; "
"`sphinx-xml` disabled (re-run cmake after doc/ subdir configures)")
endif()
set(_SPHINX_ENV
"OPENCV_SPHINX_INPUT_ROOT=${_SPHINX_INPUT_ROOT}"
"OPENCV_CONTRIB_ROOT=${OPENCV_EXTRA_MODULES_PATH}"
"OPENCV_DOXYGEN_XML_DIR=${_DOXYGEN_XML_DIR}"
)
if(OPENCV_PYTHON_SIGNATURES_FILE)
list(APPEND _SPHINX_ENV "OPENCV_PYTHON_SIGNATURES_FILE=${OPENCV_PYTHON_SIGNATURES_FILE}")
endif()
if(NOT SPHINX_JOBS)
set(SPHINX_JOBS "auto")
endif()
set(_SPHINX_WARNINGS "${CMAKE_CURRENT_BINARY_DIR}/sphinx-warnings.log")
add_custom_target(sphinx
COMMAND ${CMAKE_COMMAND} -E make_directory ${_SPHINX_OUTDIR}
COMMAND ${CMAKE_COMMAND} -E env ${_SPHINX_ENV}
${SPHINX_BUILD} -c ${_SPHINX_CONFDIR}
${SPHINX_BUILD} -j ${SPHINX_JOBS} --keep-going
-c ${_SPHINX_CONFDIR}
${_SPHINX_INPUT_ROOT}
${_SPHINX_OUTDIR}
WORKING_DIRECTORY ${_SPHINX_CONFDIR}
COMMENT "Building Sphinx HTML site -> ${_SPHINX_OUTDIR}"
COMMENT "Building Sphinx HTML site -> ${_SPHINX_OUTDIR} (jobs=${SPHINX_JOBS})"
VERBATIM)
# Ensure breathe sees current XML on every `sphinx` build.
if(TARGET sphinx-xml)
add_dependencies(sphinx sphinx-xml)
endif()
# Auto-generate Python binding signatures (needed for Python names in enum tables).
if(TARGET gen_opencv_python_source)
add_dependencies(sphinx gen_opencv_python_source)
endif()
option(SPHINX_BUILD_DIAGRAMS
"Rebuild the Doxygen HTML (collaboration diagrams) as part of the sphinx target" ON)
if(SPHINX_BUILD_DIAGRAMS AND TARGET doxygen)
add_dependencies(sphinx doxygen)
endif()
# Clean rebuild: `cmake --build <build> --target sphinx-clean`
add_custom_target(sphinx-clean
COMMAND ${CMAKE_COMMAND} -E rm -rf ${_SPHINX_OUTDIR}
File diff suppressed because it is too large Load Diff
+127
View File
@@ -0,0 +1,127 @@
{% extends "!layout.html" %}
{%- set _doxy = ('../' * ((pagename or '').count('/') + 2)) ~ 'doc/doxygen/html/' %}
{%- set _search = _doxy ~ 'search/' %}
{% block extrahead %}
{{ super() }}
<link rel="stylesheet" href="{{ _search }}search.css"/>
<script src="{{ pathto('_static/search_map.js', 1) }}"></script>
<script src="{{ _search }}searchdata.js"></script>
<script src="{{ _search }}search.js"></script>
<script>
// SearchBox lives in search.js; guard so a load failure can't abort this
// script (which would leave the modal trigger unwired).
var searchBox;
try { searchBox = new SearchBox("searchBox", "{{ _search }}", '.html'); } catch (e) {}
// Open/close the centered modal. Defined early so the Ctrl+K handler can use
// them, and kept independent of the Doxygen backend so opening always works.
function opencvOpenSearch() {
var ov = document.getElementById("opencvSearchOverlay");
if (!ov) return;
ov.classList.add("opencv-search-open");
var f = document.getElementById("MSearchField");
if (f) { f.focus(); f.select(); }
}
function opencvCloseSearch() {
var ov = document.getElementById("opencvSearchOverlay");
if (ov) ov.classList.remove("opencv-search-open");
if (window.searchBox && searchBox.CloseResultsWindow) searchBox.CloseResultsWindow();
}
// Win Ctrl+K over the theme's deferred handler + the browser (registered
// before the theme's, so stopImmediatePropagation pre-empts it).
window.addEventListener("keydown", function (e) {
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && /^k$/i.test(e.key)) {
e.preventDefault();
e.stopImmediatePropagation();
opencvOpenSearch();
}
}, true);
document.addEventListener("DOMContentLoaded", function () {
// The theme renders navbar_end twice (desktop header + mobile drawer), so
// the search component appears twice → duplicate #opencvSearchTrigger /
// #opencvSearchOverlay IDs. Keep ONE overlay; wire EVERY trigger to it
// (the visible button may be the second copy — getElementById sees only the
// first, which is why clicking did nothing before).
var overlays = document.querySelectorAll("#opencvSearchOverlay");
for (var k = overlays.length - 1; k >= 1; k--) overlays[k].parentNode.removeChild(overlays[k]);
var overlay = overlays[0];
if (overlay && overlay.parentNode !== document.body) document.body.appendChild(overlay);
// 1) Wire opening FIRST so the modal works even if Doxygen init throws.
document.querySelectorAll("#opencvSearchTrigger").forEach(function (btn) {
btn.addEventListener("click", function (e) { e.preventDefault(); opencvOpenSearch(); });
});
if (overlay) overlay.addEventListener("mousedown", function (e) { if (e.target === overlay) opencvCloseSearch(); });
document.addEventListener("keydown", function (e) { if (/^Escape$/i.test(e.key)) opencvCloseSearch(); });
// 2) Doxygen search backend (guarded: a failure must not break typing).
try {
var boxes = document.querySelectorAll("#MSearchBox");
for (var i = 1; i < boxes.length; i++) boxes[i].remove();
var sel = document.createElement("div");
sel.id = "MSearchSelectWindow";
sel.setAttribute("onmouseover", "return searchBox.OnSearchSelectShow()");
sel.setAttribute("onmouseout", "return searchBox.OnSearchSelectHide()");
sel.setAttribute("onkeydown", "return searchBox.OnSearchSelectKey(event)");
document.body.appendChild(sel);
var res = document.createElement("div");
res.id = "MSearchResultsWindow";
res.innerHTML = '<div id="MSearchResults"><div class="SRPage"><div id="SRIndex">' +
'<div id="SRResults"></div>' +
'<div class="SRStatus" id="Loading">Loading...</div>' +
'<div class="SRStatus" id="Searching">Searching...</div>' +
'<div class="SRStatus" id="NoMatches">No Matches</div></div></div></div>';
document.body.appendChild(res);
init_search();
searchBox.OnSelectItem(0); // default to "All" on every page load
// Redirect result clicks: Doxygen filename → Sphinx page via sphinxPageMap.
var sphinxRoot = "{{ '../' * (pagename or '').count('/') }}";
document.body.addEventListener("click", function (e) {
var a = e.target.closest ? e.target.closest("a.SRSymbol, a.SRScope") : null;
if (!a) return;
var href = a.getAttribute("href") || "";
if (!href || href.startsWith("javascript:")) return;
var fname = href.split("/").pop().replace(/#.*$/, ""); // e.g. "group__core.html"
var stem = fname.replace(/\.html$/, ""); // e.g. "group__core"
// Normalise to the Sphinx stem: strip group__/tutorial_ prefix, __ → _
var key = stem.replace(/^group__/, "").replace(/^tutorial_/, "").replace(/__/g, "_");
// Also try exact stem (covers classcv_*, structcv_*, namespacecv_*, etc.)
var sphinxPath = (typeof sphinxPageMap !== "undefined") &&
(sphinxPageMap[stem] || sphinxPageMap[key]);
if (sphinxPath) {
e.preventDefault();
e.stopPropagation();
window.location.href = sphinxRoot + sphinxPath;
}
}, true);
// Pin the Doxygen popups under the (centered) box with position:fixed
// (Doxygen places them absolute at document-top → off-screen here).
var box = boxes[0];
function pin(w, left) {
if (!box) return;
var r = box.getBoundingClientRect();
w.style.setProperty("position", "fixed", "important");
w.style.setProperty("top", (r.bottom + 4) + "px", "important");
var x = left ? r.left : Math.max(8, r.right - (w.offsetWidth || 300));
w.style.setProperty("left", x + "px", "important");
}
[[res, false], [sel, true]].forEach(function (p) {
var w = p[0], opt = { attributes: true, attributeFilter: ["style"] };
var obs = new MutationObserver(function () {
if (w.style.display === "block") { obs.disconnect(); pin(w, p[1]); obs.observe(w, opt); }
});
obs.observe(w, opt);
});
} catch (err) {
if (window.console) console.error("Doxygen search init failed:", err);
}
});
</script>
{% endblock %}
+27 -11
View File
@@ -1,19 +1,35 @@
{# Override of pydata_sphinx_theme's components/navbar-nav.html.
PyData's default renders both top-level toctree items AND the
configured external_links in one bar. For OpenCV we want ONLY the
Doxygen-style external nav (Main Page, Related Pages, Namespaces,
…) in the header — the tutorial module tree belongs in the left
sidebar, not the header. So we render external_links only. #}
PyData's default renders both top-level toctree items AND the configured
external_links in one bar. For OpenCV we want ONLY this header nav (the
tutorial module tree belongs in the left sidebar, not the header), so we
render the `external_links` slot exclusively.
These are NOT external links: conf.py declares them via the theme's
`external_links` key only because that's the data slot for a custom header
nav. Each entry is one of:
* internal -> {"docname": "<docname>", "name": ...}
resolved with `pathto` so the relative URL is correct at ANY page depth
and points at the Sphinx-rendered page (landing, tutorials, module roots);
* external -> {"url": "<abs-url>", "name": ..., "external": true}
rendered verbatim and opened in a new tab (e.g. the Java docs, which
have no Sphinx equivalent).
Internal entries carry no target="_blank" / nav-external glyph since
navigation never leaves the site. #}
<nav>
<ul class="bd-navbar-elements navbar-nav">
{%- for link in (theme_external_links or []) %}
{%- if link.docname %}
{%- set _href = pathto(link.docname) %}
{%- set _external = false %}
{%- else %}
{%- set _href = link.url %}
{%- set _external = link.get('external', false) %}
{%- endif %}
<li class="nav-item">
{# PyData's CSS adds the ↗ icon via .nav-external::after — don't
render a second one inline or it doubles up. #}
<a class="nav-link nav-external"
href="{{ link.url }}"
target="_blank"
rel="noopener noreferrer">{{ link.name }}</a>
<a class="nav-link" href="{{ _href }}"
{%- if _external %} target="_blank" rel="noopener noreferrer"{% endif %}>{{ link.name }}</a>
</li>
{%- endfor %}
</ul>
@@ -0,0 +1,40 @@
{%- set _search = ('../' * ((pagename or '').count('/') + 2)) ~ 'doc/doxygen/html/search/' %}
{# Set magnifier directly; a CSS var here resolves against search.css and 404s. #}
<style>
#MSearchSelect { background-image: url('{{ _search }}mag_sel.svg') !important; }
html[data-theme="dark"] #MSearchSelect { background-image: url('{{ _search }}mag_seld.svg') !important; }
</style>
{# Navbar trigger: native PyData look (.search-button-field), but NOT the
.search-button__button class — that is the theme's own show-modal hook. #}
<button id="opencvSearchTrigger" type="button" class="btn search-button-field"
title="{{ _('Search') }}" aria-label="{{ _('Search') }}">
<i class="fa-solid fa-magnifying-glass"></i>
<span class="search-button__default-text">{{ _('Search') }}</span>
<span class="search-button__kbd-shortcut"><kbd class="kbd-shortcut__modifier">Ctrl</kbd>+<kbd class="kbd-shortcut__modifier">K</kbd></span>
</button>
{# Centered modal overlay (layout.html moves it to <body> and toggles the open
class). Holds the Doxygen search box; results render via Doxygen's search.js. #}
<div id="opencvSearchOverlay" class="opencv-search-overlay" role="dialog"
aria-modal="true" aria-label="{{ _('Search') }}">
<div class="opencv-search-modal">
<div id="MSearchBox" class="MSearchBoxInactive">
<span class="left">
<span id="MSearchSelect"
onmouseover="return searchBox.OnSearchSelectShow()"
onmouseout="return searchBox.OnSearchSelectHide()">&#160;</span>
<input type="text" id="MSearchField" value="" placeholder="Search the docs ..."
accesskey="S"
onfocus="searchBox.OnSearchFieldFocus(true)"
onblur="searchBox.OnSearchFieldFocus(false)"
onkeyup="searchBox.OnSearchFieldChange(event)"/>
</span>
<span class="right">
<span id="MSearchKbd" class="search-button__kbd-shortcut">
<kbd class="kbd-shortcut__modifier">Ctrl</kbd>+<kbd>K</kbd>
</span>
<a id="MSearchClose" href="javascript:searchBox.CloseResultsWindow()">
<img id="MSearchCloseImg" border="0" src="{{ _search }}close.svg" alt=""/></a>
</span>
</div>
</div>
</div>
+98 -1867
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
# This file is part of OpenCV project.
# It is subject to the license terms in the LICENSE file found in the top-level directory
# of this distribution and at http://opencv.org/license.html.
# Copyright (C) 2026, BigVision LLC, all rights reserved.
# Third party copyrights are property of their respective owners.
"""Doc-build engine for the OpenCV Sphinx wrapper.
conf.py stays a thin Sphinx-settings file; everything heavy lives here:
* ``state`` — shared config, paths, tag maps, bib/citation numbering,
redirect map, anchor indexes, constants
* ``xml_render`` — Doxygen XML -> Markdown primitives (incl. enum synopsis)
* ``stubs`` — API-reference stub writers (groups / classes)
* ``translate`` — Doxygen-flavored .markdown -> MyST (the source-read engine)
* ``patches`` — Sphinx C++ domain / breathe warning patches
* ``postprocess`` — build-finished hook that inlines collaboration-diagram SVGs
* ``build`` — import-time orchestration that populates the shared indexes
"""
+413
View File
@@ -0,0 +1,413 @@
# This file is part of OpenCV project.
# It is subject to the license terms in the LICENSE file found in the top-level directory
# of this distribution and at http://opencv.org/license.html.
# Copyright (C) 2026, BigVision LLC, all rights reserved.
# Third party copyrights are property of their respective owners.
"""Import-time orchestration: populate the shared indexes."""
from __future__ import annotations
import pathlib, re, os as _os, shutil as _shutil, textwrap as _textwrap
from .state import *
from .xml_render import _patch_namespace_xml_for_breathe
from .stubs import _generate_api_stubs
# Skip when input root is DOC_ROOT: writing there is forbidden.
if _BIB_ENTRIES_SORTED and SPHINX_INPUT_ROOT != DOC_ROOT:
try:
SPHINX_INPUT_ROOT.mkdir(parents=True, exist_ok=True)
(SPHINX_INPUT_ROOT / "citelist.markdown").write_text(
_bib_render_all(_BIB_ENTRIES_SORTED, _CITE_NUMBER),
encoding="utf-8")
except OSError:
pass
# Internal scan: enabled subtrees + standalone pages.
_scan_internal(SPHINX_INPUT_ROOT / "tutorials" / "tutorials.markdown")
for _m in DOC_MODULES:
_scan_internal(SPHINX_INPUT_ROOT / "tutorials" / _m)
if JS_DOC_MODULES:
_scan_internal(DOC_ROOT / "js_tutorials" / "js_tutorials.markdown",
base=DOC_ROOT)
for _m in JS_DOC_MODULES:
_scan_internal(DOC_ROOT / "js_tutorials" / _m, base=DOC_ROOT)
if PY_DOC_MODULES:
_scan_internal(DOC_ROOT / "py_tutorials" / "py_tutorials.markdown",
base=DOC_ROOT)
for _m in PY_DOC_MODULES:
_scan_internal(DOC_ROOT / "py_tutorials" / _m, base=DOC_ROOT)
_contrib_dir = SPHINX_INPUT_ROOT / "tutorials_contrib"
_contrib_root_md = next(
(p for p in (_contrib_dir / "contrib_root.markdown",
_contrib_dir / "tutorials_contrib.markdown") if p.is_file()),
_contrib_dir / "contrib_root.markdown")
if _contrib_root_md.is_file():
_scan_internal(_contrib_root_md)
for _m in CONTRIB_MODULES:
_scan_internal(SPHINX_INPUT_ROOT / "tutorials_contrib" / _m)
# Standalone top-level pages.
_scan_internal(SPHINX_INPUT_ROOT / "faq.markdown")
_scan_internal(SPHINX_INPUT_ROOT / "citelist.markdown")
_scan_internal(SPHINX_INPUT_ROOT / "intro.markdown")
_IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".svg", ".bmp", ".webp"}
for _root in ((DOC_ROOT / "tutorials").rglob("images/*"),
(DOC_ROOT / "js_tutorials").rglob("images/*"),
(DOC_ROOT / "js_tutorials" / "js_assets").glob("*"),
(DOC_ROOT / "py_tutorials").rglob("images/*"),
(DOC_ROOT / "images").glob("*")):
for _img in _root:
if _img.is_file():
_IMAGE_INDEX.setdefault(_img.name, _img.relative_to(DOC_ROOT).as_posix())
for _m in CONTRIB_MODULES:
# <m>/tutorials/**/images/*
_tut = CONTRIB_ROOT / _m / "tutorials"
if _tut.is_dir():
for _img in _tut.rglob("images/*"):
if _img.is_file():
_rel = _img.relative_to(_tut).as_posix()
_IMAGE_INDEX.setdefault(_img.name,
f"tutorials_contrib/{_m}/{_rel}")
# Contrib images outside <m>/tutorials/.
for _sub in ("doc", "samples"):
_src = CONTRIB_ROOT / _m / _sub
if _src.is_dir():
for _img in _src.rglob("*"):
if _img.is_file() and _img.suffix.lower() in _IMAGE_EXTS:
_rel = _img.relative_to(CONTRIB_ROOT).as_posix()
_IMAGE_INDEX.setdefault(_img.name,
f"contrib_modules/{_rel}")
if API_MODULES:
_api_pics = SPHINX_INPUT_ROOT / "api_pics"
_stage_pics = SPHINX_INPUT_ROOT != DOC_ROOT
if _stage_pics:
_api_pics.mkdir(parents=True, exist_ok=True)
_modules_root = DOC_ROOT.parent / "modules"
if _modules_root.is_dir():
for _doc_dir in sorted(_modules_root.glob("*/doc")):
for _img in _doc_dir.rglob("*"):
if not (_img.is_file() and _img.suffix.lower() in _IMAGE_EXTS):
continue
if _img.name in _IMAGE_INDEX:
continue
_IMAGE_INDEX[_img.name] = f"api_pics/{_img.name}"
if _stage_pics:
_link = _api_pics / _img.name
if not _link.exists():
try:
_os.symlink(_img, _link)
except (OSError, NotImplementedError):
try:
_shutil.copy2(_img, _link)
except OSError:
pass
if _API_XML_DIR.is_dir():
_patch_namespace_xml_for_breathe(_API_XML_DIR, _PATCHED_XML_DIR)
from conf_helpers.state import OPENCV_ROOT, CONTRIB_ROOT
_is_contrib = lambda m: (CONTRIB_ROOT / m).is_dir() and not (
OPENCV_ROOT / "modules" / m).is_dir()
_main_api = [m for m in API_MODULES if not _is_contrib(m)]
_extra_api = [m for m in API_MODULES if _is_contrib(m)]
_generate_api_stubs(_main_api, _API_XML_DIR, SPHINX_INPUT_ROOT / "main_modules",
root_anchor="api_root", root_title="Main modules")
_scan_internal(SPHINX_INPUT_ROOT / "main_modules")
if _extra_api:
_generate_api_stubs(_extra_api, _API_XML_DIR, SPHINX_INPUT_ROOT / "extra_modules",
root_anchor="extra_api_root", root_title="Extra modules")
_scan_internal(SPHINX_INPUT_ROOT / "extra_modules")
def _write_root_index() -> None:
"""Generate the Sphinx landing page at ``index.html``.
The legacy tutorials root remains focused on C++ tutorials. Cross-family
entry points live here so the site root no longer redirects users straight
to ``tutorials/tutorials.html``.
Each entry renders as a section heading (the category) with the page link
on the line beneath it. FAQ and Bibliography are direct links whose heading
*is* the link. A hidden toctree mirrors the same order to drive the sidebar.
"""
if SPHINX_INPUT_ROOT == DOC_ROOT:
return
entries: list[tuple[str, str | None, str]] = []
def add(heading: str, link_text: str | None, docname: str,
condition: bool = True) -> None:
if condition:
entries.append((heading, link_text, docname))
add("Introduction", "Introduction", "intro", "intro" in _ANCHOR_TO_DOC)
add("OpenCV Tutorials", "OpenCV tutorials", "tutorials/tutorials")
add("Python Tutorials", "OpenCV-Python tutorials",
"py_tutorials/py_tutorials", bool(PY_DOC_MODULES))
add("Javascript Tutorials", "OpenCV.js tutorials",
"js_tutorials/js_tutorials", bool(JS_DOC_MODULES))
add("Contrib Tutorials", "Tutorials for contrib module",
f"tutorials_contrib/{_contrib_root_md.stem}",
bool(CONTRIB_MODULES) and _contrib_root_md.is_file())
add("Main modules", "Documentation for main modules",
"main_modules/api_root", "api_root" in _ANCHOR_TO_DOC)
add("Extra modules", "Documentation for extra modules",
"extra_modules/api_root", "extra_api_root" in _ANCHOR_TO_DOC)
add("Frequently Asked Questions", None, "faq", "faq" in _ANCHOR_TO_DOC)
add("Bibliography", None, "citelist", "citelist" in _ANCHOR_TO_DOC)
toctree = "\n".join(
f"{heading} <{docname}>" for heading, _link, docname in entries)
# Body: raw HTML so links resolve correctly relative to index.html.
html_lines = ['<div class="ocv-landing">']
for heading, link_text, docname in entries:
if link_text is None:
html_lines.append(
f'<h2><a href="{docname}.html">{heading}</a></h2>')
else:
html_lines.append(f'<h2>{heading}</h2>')
html_lines.append(f'<p><a href="{docname}.html">{link_text}</a></p>')
html_lines.append("</div>")
body = "\n".join(html_lines)
text = (
"OpenCV modules\n"
"==============\n\n"
"```{toctree}\n"
":hidden:\n"
":maxdepth: 1\n"
":titlesonly:\n\n"
f"{toctree}\n"
"```\n\n"
f"{body}\n"
)
try:
(SPHINX_INPUT_ROOT / "index.markdown").write_text(text, encoding="utf-8")
except OSError:
pass
def _write_related_pages_index() -> None:
"""Generate `related_pages.markdown` — the local analog of Doxygen's
pages.html (the header "Related Pages" target).
Lists every standalone documentation page (\\page) that has a *local*
Sphinx docname, so nothing points off-site. Titles and the canonical set
come from the Doxygen tag page index (`_DOC_PAGE_TITLES`); a page is
emitted only when its name resolves through `_ANCHOR_TO_DOC`, so the list
contains exactly what this build actually rendered and grows automatically
as more modules are enabled. Marked `orphan` — reached via the header link,
not the sidebar toctree (intro/faq/citelist already live in the index toc).
"""
if SPHINX_INPUT_ROOT == DOC_ROOT:
return
rows: list[tuple[str, str]] = [] # (title, docname)
seen: set[str] = set()
def add(anchor: str) -> None:
doc = _ANCHOR_TO_DOC.get(anchor)
if doc and anchor not in seen:
title = (_DOC_PAGE_TITLES.get(anchor)
or _ANCHOR_TO_TITLE.get(anchor) or anchor)
rows.append((title, doc))
seen.add(anchor)
# Core standalone pages first, in a stable, friendly order.
for _a in ("intro", "faq", "citelist"):
add(_a)
# Then every other \page that resolves locally, alphabetical by title.
for _name in sorted(_DOC_PAGE_TITLES,
key=lambda n: (_DOC_PAGE_TITLES.get(n) or n).lower()):
add(_name)
items = "\n".join(f'<li><a href="{_d}.html">{_t}</a></li>' for _t, _d in rows)
text = (
"---\norphan: true\n---\n"
"# Related Pages\n\n"
"All standalone documentation pages available in this build.\n\n"
f'<ul class="ocv-related-pages">\n{items}\n</ul>\n'
)
try:
(SPHINX_INPUT_ROOT / "related_pages.markdown").write_text(
text, encoding="utf-8")
except OSError:
pass
def _write_examples_index() -> None:
"""Generate `examples/examples_root.markdown` — the local analog of
Doxygen's examples.html (the header "Examples" target).
The per-sample example pages are orphan pages reached from class "Examples"
blocks; this index links them all in one place. Sourced from
`_EXAMPLE_PAGES_NEEDED` (populated during API-stub generation), so it lists
exactly the samples this build emitted. Also `orphan` (header-only entry).
"""
if SPHINX_INPUT_ROOT == DOC_ROOT:
return
from .examples import _EXAMPLE_PAGES_NEEDED, _example_pagename
if not _EXAMPLE_PAGES_NEEDED:
return
items = "\n".join(
f'<li><a href="{_example_pagename(_d)}.html">{_d}</a></li>'
for _d in sorted(_EXAMPLE_PAGES_NEEDED))
text = (
"---\norphan: true\n---\n"
"# Examples\n\n"
"All example programs referenced in the API documentation.\n\n"
f'<ul class="ocv-examples-index">\n{items}\n</ul>\n'
)
try:
(SPHINX_INPUT_ROOT / "examples").mkdir(parents=True, exist_ok=True)
(SPHINX_INPUT_ROOT / "examples" / "examples_root.markdown").write_text(
text, encoding="utf-8")
except OSError:
pass
def _esc(s: str) -> str:
"""Minimal HTML escape for brief text injected into the index <li> markup."""
return (s or "").replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
def _write_namespace_list_index() -> None:
"""Generate `namespace_list.markdown` — local analog of Doxygen's
namespaces.html (the header "Namespaces" target).
Renders the namespace tree (cv → cv::cuda → …) as a nested list, each node
linking to its local namespace page with the brief description alongside.
Intermediate namespaces with no page of their own render as plain text.
Sourced from `_ALL_NAMESPACES` (populated during API-stub generation).
"""
if SPHINX_INPUT_ROOT == DOC_ROOT or not _ALL_NAMESPACES:
return
# Nested tree keyed by path component; each node tracks its full name.
tree: dict = {}
for _name in _ALL_NAMESPACES:
node = tree
parts = _name.split("::")
for _i, _part in enumerate(parts):
node = node.setdefault(
_part, {"_full": "::".join(parts[:_i + 1]), "_kids": {}})["_kids"]
def render(node: dict) -> list[str]:
out = ["<ul>"]
for _part in sorted(node, key=str.lower):
child = node[_part]
info = _ALL_NAMESPACES.get(child["_full"])
if info:
label = f'<a href="{info["docname"]}.html">{_part}</a>'
if info.get("brief"):
label += f'{_esc(info["brief"])}'
else:
label = _part
out.append(f"<li>{label}")
if child["_kids"]:
out += render(child["_kids"])
out.append("</li>")
out.append("</ul>")
return out
text = (
"---\norphan: true\n---\n"
"# Namespace List\n\n"
"Here is a list of all documented namespaces with brief descriptions.\n\n"
f'<div class="ocv-namespace-list">\n{chr(10).join(render(tree))}\n</div>\n'
)
try:
(SPHINX_INPUT_ROOT / "namespace_list.markdown").write_text(
text, encoding="utf-8")
except OSError:
pass
def _write_class_list_index() -> None:
"""Generate `class_list.markdown` — local analog of Doxygen's annotated.html
(the header "Classes" target).
Lists every documented class/struct grouped by its enclosing namespace,
each linking to its local page with the brief description. Sourced from
`_ALL_CLASSES` (populated during API-stub generation).
"""
if SPHINX_INPUT_ROOT == DOC_ROOT or not _ALL_CLASSES:
return
by_ns: dict[str, list[tuple[str, dict]]] = {}
for _info in _ALL_CLASSES.values():
qualified = _info.get("qualified", "")
if not qualified:
continue
ns, _, leaf = qualified.rpartition("::")
by_ns.setdefault(ns, []).append((leaf, _info))
body = ['<ul class="ocv-class-list">']
for ns in sorted(by_ns, key=lambda n: (n == "", n.lower())):
heading = ns if ns else "(global namespace)"
ns_info = _ALL_NAMESPACES.get(ns)
if ns_info:
heading = f'<a href="{ns_info["docname"]}.html">{ns}</a>'
body.append(f"<li><b>{heading}</b>")
body.append("<ul>")
for leaf, info in sorted(by_ns[ns], key=lambda t: t[0].lower()):
entry = f'<a href="{info["docname"]}.html">{leaf}</a>'
if info.get("brief"):
entry += f'{_esc(info["brief"])}'
body.append(f"<li>{entry}</li>")
body.append("</ul></li>")
body.append("</ul>")
text = (
"---\norphan: true\n---\n"
"# Class List\n\n"
"Here are the classes, structs and unions with brief descriptions.\n\n"
f'<div class="ocv-class-list-wrap">\n{chr(10).join(body)}\n</div>\n'
)
try:
(SPHINX_INPUT_ROOT / "class_list.markdown").write_text(
text, encoding="utf-8")
except OSError:
pass
_write_root_index()
_write_related_pages_index()
if API_MODULES:
_write_examples_index()
_write_namespace_list_index()
_write_class_list_index()
for _toc in (DOC_ROOT / "tutorials").glob("*/table_of_content_*.markdown"):
if _toc.parent.name not in DOC_MODULES:
_scan_external(_toc)
# Same for js_tutorials (files are named js_table_of_contents_*.markdown there).
for _toc in (DOC_ROOT / "js_tutorials").glob("*/js_table_of_contents_*.markdown"):
if _toc.parent.name not in JS_DOC_MODULES:
_scan_external(_toc)
# py_tutorials uses the `py_table_of_contents_*.markdown` naming variant.
for _toc in (DOC_ROOT / "py_tutorials").glob("*/py_table_of_contents_*.markdown"):
if _toc.parent.name not in PY_DOC_MODULES:
_scan_external(_toc)
_REFERENCED_ANCHORS.update({
"intro", "faq", "citelist",
"tutorial_js_root", "tutorial_py_root", "tutorial_contrib_root",
"api_root", "extra_api_root",
})
# Snippet basename index (mirrors Doxygen EXAMPLE_RECURSIVE lookup).
_SNIPPET_EXTENSIONS = {
".cpp", ".hpp", ".h", ".c", ".cc", ".cxx",
".py", ".java", ".kt", ".scala", ".clj", ".groovy",
".sh", ".bash", ".bat", ".ps1",
".cmake", ".gradle",
".xml", ".yaml", ".yml", ".json", ".html", ".css",
".js", ".ts", ".rb",
}
_snippet_scan_roots = [OPENCV_ROOT / "samples", OPENCV_ROOT / "apps"] + [
CONTRIB_ROOT / _m / "samples" for _m in CONTRIB_MODULES]
for _root in _snippet_scan_roots:
if _root.is_dir():
for _f in _root.rglob("*"):
if _f.is_file() and _f.suffix.lower() in _SNIPPET_EXTENSIONS:
_SNIPPET_INDEX.setdefault(_f.name, _f)
+435
View File
@@ -0,0 +1,435 @@
# This file is part of OpenCV project.
# It is subject to the license terms in the LICENSE file found in the top-level directory
# of this distribution and at http://opencv.org/license.html.
# Copyright (C) 2026, BigVision LLC, all rights reserved.
# Third party copyrights are property of their respective owners.
"""Per-class "Examples" cross-reference system."""
from __future__ import annotations
import re, pathlib
from .state import *
_EXAMPLE_SOURCE_EXTENSIONS = {
# Program file types only; headers excluded.
".cpp", ".cc", ".cxx", ".c",
".py", ".java", ".js", ".ts",
}
_EXAMPLE_LANGUAGE = {
".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp", ".c": "c",
".py": "python", ".java": "java",
".js": "javascript", ".ts": "typescript",
}
_EXAMPLE_INCLUDE_SUBTREES = (
"samples/cpp/tutorial_code/",
"samples/python/",
"samples/java/",
"samples/dnn/",
"samples/gpu/",
)
def _is_canonical_example(rel_path: str) -> bool:
"""True iff this repo-relative path is a canonical example."""
if any(rel_path.startswith(p) for p in _EXAMPLE_INCLUDE_SUBTREES):
return True
if rel_path.startswith("samples/cpp/"):
rest = rel_path[len("samples/cpp/"):]
return "/" not in rest # direct child only, not nested
if re.match(r"opencv_contrib/modules/[^/]+/samples/", rel_path):
return True
return False
def _example_pagename(display_path: str) -> str:
"""`samples/cpp/pca.cpp` → `samples_cpp_pca_cpp` (Sphinx-safe basename)."""
return re.sub(r"[^A-Za-z0-9]+", "_", display_path).strip("_").lower()
_EXAMPLE_FILES: list[tuple[str, pathlib.Path, frozenset[str]]] = []
_EXAMPLE_TOKEN_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
# Not scoped to CONTRIB_MODULES: examples surface for all modules.
_example_scan_roots: list[tuple[pathlib.Path, str]] = [
(OPENCV_ROOT / "samples", "samples"),
]
if CONTRIB_ROOT.is_dir():
for _module_dir in sorted(CONTRIB_ROOT.iterdir()):
if not _module_dir.is_dir():
continue
_contrib_samples = _module_dir / "samples"
if _contrib_samples.is_dir():
_example_scan_roots.append((
_contrib_samples,
f"opencv_contrib/modules/{_module_dir.name}/samples",
))
for _root, _display_prefix in _example_scan_roots:
if not _root.is_dir():
continue
for _f in _root.rglob("*"):
if not _f.is_file() or _f.suffix.lower() not in _EXAMPLE_SOURCE_EXTENSIONS:
continue
_rel = _f.relative_to(_root).as_posix()
_display = f"{_display_prefix}/{_rel}"
if not _is_canonical_example(_display):
continue
try:
_text = _f.read_text(encoding="utf-8", errors="ignore")
except OSError:
continue
_EXAMPLE_FILES.append((
_display,
_f,
frozenset(_EXAMPLE_TOKEN_RE.findall(_text)),
))
_EXAMPLE_FILES.sort(key=lambda t: t[0])
# Display path → source path; only referenced samples (avoids orphan pages).
_EXAMPLE_PAGES_NEEDED: dict[str, pathlib.Path] = {}
_TUTORIAL_LINK_RE = re.compile(
r"\[([^\]]+)\]\(([^)]*?samples/[^)]+?\.(?:cpp|cc|cxx|c|py|java|js|ts))\)",
re.IGNORECASE,
)
# Trailing connector phrase, stripped from end of description.
_TUTORIAL_LINK_TRAILER_RE = re.compile(
r"\s*(?:"
r"can be found at|can be found in|"
r"are available (?:at|in)|is available (?:at|in)|"
r"located at|located in|found at|found in|"
r"see also|see|here|"
r"is at|is in|at|in"
r")\s*$",
re.IGNORECASE,
)
_TUTORIAL_LEADING_DIRECTIVE_RE = re.compile(
r"^@(?:note|see|sa|warning|attention|remark|brief|todo|deprecated)\s+",
re.IGNORECASE,
)
_ANOTHER_LEAD_RE = re.compile(r"^Another\s+(\w)", re.IGNORECASE)
def _extract_link_lead_in(text: str, link_start: int) -> str:
"""Pull the cleaned-up prose right before a tutorial hyperlink."""
LOOKBACK = 400
before = text[max(0, link_start - LOOKBACK):link_start]
cut = max(
before.rfind("\n\n"),
before.rfind(". "),
before.rfind("! "),
before.rfind("? "),
before.rfind(":\n"),
)
if cut >= 0:
before = before[cut + 2:] # skip past the 2-char boundary
before = _TUTORIAL_LEADING_DIRECTIVE_RE.sub("", before.lstrip())
before = re.sub(r"\s+", " ", before).strip()
before = _TUTORIAL_LINK_TRAILER_RE.sub("", before).strip()
m = _ANOTHER_LEAD_RE.match(before)
if m:
article = "An " if m.group(1).lower() in "aeiou" else "A "
before = article + before[m.start(1):]
return before
def _scan_tutorial_sample_refs() -> dict[str, str]:
"""Walk every tutorial markdown for sample-file hyperlinks."""
refs: dict[str, str] = {}
tutorial_roots = [
DOC_ROOT / "tutorials",
DOC_ROOT / "js_tutorials",
DOC_ROOT / "py_tutorials",
]
for tut_root in tutorial_roots:
if not tut_root.is_dir():
continue
for md in tut_root.rglob("*.markdown"):
try:
text = md.read_text(encoding="utf-8", errors="ignore")
except OSError:
continue
for m in _TUTORIAL_LINK_RE.finditer(text):
sample_m = re.search(
r"samples/.+\.(?:cpp|cc|cxx|c|py|java|js|ts)",
m.group(2), re.IGNORECASE,
)
if not sample_m:
continue
sample = sample_m.group(0)
brief = _extract_link_lead_in(text, m.start())
if not brief:
continue
if sample not in refs or len(brief) > len(refs[sample]):
refs[sample] = brief
return refs
_TUTORIAL_SAMPLE_REFS: dict[str, str] = _scan_tutorial_sample_refs()
_DOXY_EXAMPLE_RE = re.compile(
r"[@\\]example\s+(\S+)" # declared path
r"[^\n]*\n"
r"(?P<desc>"
r"(?:"
r"(?!\s*\*/)" # not comment closer
r"(?!\s*\*?\s*[@\\]\w+)" # not a new directive
r"[^\n]*\n"
r")*"
r")"
)
_DOXY_PERCENT_ESCAPE_RE = re.compile(r"%(\w+)")
def _resolve_example_path(decl_path: str, header_path: pathlib.Path) -> str | None:
"""Resolve a `@example <path>` to our repo-relative display path."""
# Case 1: repo-relative.
abs_main = OPENCV_ROOT / decl_path
if abs_main.is_file():
return abs_main.relative_to(OPENCV_ROOT).as_posix()
# Case 2: module-relative — module root is parent of include/.
module_root: pathlib.Path | None = None
for p in header_path.parents:
if p.name == "include":
module_root = p.parent
break
if module_root is None:
return None
abs_mod = module_root / decl_path
if not abs_mod.is_file():
return None
# is_relative_to not startswith: opencv prefixes opencv_contrib.
if abs_mod.is_relative_to(OPENCV_ROOT):
return abs_mod.relative_to(OPENCV_ROOT).as_posix()
contrib_parent = CONTRIB_ROOT.parent.parent
if abs_mod.is_relative_to(contrib_parent):
return abs_mod.relative_to(contrib_parent).as_posix()
return f"opencv_contrib/modules/{module_root.name}/{decl_path}"
def _scan_doxygen_example_decls() -> dict[str, tuple[str, str]]:
"""Walk every module header for `@example` declarations."""
refs: dict[str, tuple[str, str]] = {}
roots = [OPENCV_ROOT / "modules"]
if CONTRIB_ROOT.is_dir():
roots.append(CONTRIB_ROOT)
for root in roots:
if not root.is_dir():
continue
for ext in ("*.hpp", "*.h"):
for header in root.rglob(ext):
# Only <module>/include/.
if "/include/" not in header.as_posix():
continue
try:
text = header.read_text(encoding="utf-8", errors="ignore")
except OSError:
continue
for m in _DOXY_EXAMPLE_RE.finditer(text):
declared = m.group(1).strip().rstrip("*/").strip()
resolved = _resolve_example_path(declared, header)
if not resolved:
continue
raw = m.group("desc") or ""
cleaned = re.sub(r"\n\s*\*\s?", " ", raw)
cleaned = cleaned.replace("*/", "").strip()
cleaned = re.sub(r"\s+", " ", cleaned)
cleaned = _DOXY_PERCENT_ESCAPE_RE.sub(r"\1", cleaned)
# Keep longest if declared in multiple headers.
existing = refs.get(resolved)
if existing is None or len(cleaned) > len(existing[1]):
refs[resolved] = (declared, cleaned)
return refs
_DOXY_EXAMPLE_DECLS: dict[str, tuple[str, str]] = _scan_doxygen_example_decls()
def _find_examples_for_class(class_simple: str) -> list[tuple[str, str]]:
"""Canonical sample files mentioning the class name."""
if not class_simple:
return []
candidates = {class_simple}
if class_simple.startswith("_") and len(class_simple) > 1:
candidates.add(class_simple[1:]) # _InputArray → InputArray alias
out: list[tuple[str, str]] = []
for display, source_path, tokens in _EXAMPLE_FILES:
# Stage 1: must be declared with @example.
decl = _DOXY_EXAMPLE_DECLS.get(display)
if decl is None:
continue
# Stage 2: must mention the class.
if any(c in tokens for c in candidates):
_EXAMPLE_PAGES_NEEDED[display] = source_path
declared, _desc = decl
out.append((declared, _example_pagename(display)))
return out
def _render_examples_block(examples: list[tuple[str, str]]) -> list[str]:
"""HTML lines for the "Examples" footer; empty list if no matches."""
if not examples:
return []
import html as _html_pkg
parts = [
f'<a class="opencv-example-link" '
f'href="../examples/{_html_pkg.escape(page, quote=True)}.html">'
f'{_html_pkg.escape(display)}</a>'
for display, page in examples
]
if len(parts) == 1:
joined = parts[0]
else:
joined = ", ".join(parts[:-1]) + ", and " + parts[-1]
return [
'<dl class="opencv-examples">',
'<dt>Examples</dt>',
f'<dd>{joined}.</dd>',
'</dl>',
"",
]
# Boilerplate-paragraph filter for _extract_sample_brief.
_SAMPLE_BRIEF_SKIP_RE = re.compile(
r"^(?:"
r"author|date|file|copyright|special\s+thanks|see\s+also|maintainer|"
r"created|modified|version|license|brief"
r")\b"
r"|^\w+\.(?:cpp|cc|cxx|c|hpp|h|py|java|js|ts)\s*$",
re.IGNORECASE,
)
def _extract_sample_brief(text: str) -> str:
"""Best-effort one-line description of a sample file."""
# 1) Explicit @brief / \brief — preferred when present.
m = re.search(
r"[@\\]brief\s+(.*?)(?:"
r"\n\s*[*/]?\s*\n" # paragraph break
r"|\n\s*[*/]?\s*[@\\]\w+" # next Doxygen tag
r"|\*/" # end of comment
r")",
text, re.DOTALL,
)
if m:
brief = re.sub(r"\n\s*\*?\s*", " ", m.group(1))
brief = re.sub(r"\s+", " ", brief).strip()
brief = re.split(r"(?<=[.!?])\s", brief, maxsplit=1)[0].strip()
if brief:
return brief
# 2) First /* ... */ block, split into paragraphs.
cm = re.match(r"\s*/\*+([\s\S]*?)\*+/", text)
if not cm:
return ""
# .strip() not .rstrip(): leading space would bypass skip regex.
norm_lines = [
re.sub(r"^\s*\*+\s?", "", raw).strip()
for raw in cm.group(1).splitlines()
]
paragraphs: list[list[str]] = [[]]
for line in norm_lines:
if line.strip():
paragraphs[-1].append(line)
elif paragraphs[-1]:
paragraphs.append([])
paragraphs = [p for p in paragraphs if p]
for para in paragraphs:
if _SAMPLE_BRIEF_SKIP_RE.match(para[0]):
continue
joined = " ".join(line.strip() for line in para if line.strip())
first = re.split(r"(?<=[.!?])\s", joined, maxsplit=1)[0].strip()
if first:
return first
return ""
_BRIEF_REF_RE = re.compile(r'@ref\s+(?P<anchor>[\w:-]+)(?:\s+"(?P<label>[^"]+)")?')
_BRIEF_IMG_RE = re.compile(r'!\[(?P<alt>[^\]]*)\]\((?P<src>[^)]+)\)')
def _resolve_brief_markup(brief: str) -> str:
"""Resolve `@ref` links and bare-filename images in a brief to Markdown."""
def _ref(m: "re.Match") -> str:
anchor = _resolve_redirect(m.group("anchor"))
label = m.group("label")
target = _ANCHOR_TO_DOC.get(anchor)
if target:
return f'[{label or _ANCHOR_TO_TITLE.get(anchor) or anchor}](/{target})'
if anchor in _TAG_FILENAMES:
return f'[{label or _TAG_TITLES.get(anchor, anchor)}]({_doxygen_url(anchor)})'
return f'[{label or anchor}](#{anchor})'
brief = _BRIEF_REF_RE.sub(_ref, brief)
def _img(m: "re.Match") -> str:
alt, src = m.group("alt"), m.group("src")
# Leave already-pathed / absolute / URL images for MyST to handle.
if "/" in src or "://" in src:
return m.group(0)
hit = _IMAGE_INDEX.get(src)
if not hit:
return m.group(0)
# Hard break after image so following prose drops to a new line.
return f'![{alt}](/{hit})\\\n'
return _BRIEF_IMG_RE.sub(_img, brief).strip()
def _generate_example_pages(examples_dir: pathlib.Path) -> None:
"""Write one Sphinx page per sample referenced by an Examples block.
Uses MyST colon-fence `:::` not backticks: source backticks can
close a backtick fence prematurely.
"""
if not _EXAMPLE_PAGES_NEEDED:
return
examples_dir.mkdir(parents=True, exist_ok=True)
for display, source in _EXAMPLE_PAGES_NEEDED.items():
try:
body = source.read_text(encoding="utf-8", errors="ignore")
except OSError:
continue
language = _EXAMPLE_LANGUAGE.get(source.suffix.lower(), "text")
# Brief cascade: @example desc, tutorial prose, top comment.
decl = _DOXY_EXAMPLE_DECLS.get(display)
declared_path = decl[0] if decl else display
decl_desc = decl[1] if decl else ""
brief = (
decl_desc
or _TUTORIAL_SAMPLE_REFS.get(display)
or _extract_sample_brief(body)
)
lines = [
"---",
"orphan: true",
"---",
f"# {declared_path}",
"",
]
if brief:
# MyST paragraph (not raw <p>) so embedded @ref / image renders.
lines.append("{.opencv-example-brief}")
lines.append(_resolve_brief_markup(brief))
lines.append("")
lines.extend([
f":::{{code-block}} {language}",
":linenos:",
"",
])
lines.extend(body.splitlines())
lines.append(":::")
lines.append("")
(examples_dir / f"{_example_pagename(display)}.md").write_text(
"\n".join(lines), encoding="utf-8")
__all__ = [
"_find_examples_for_class",
"_render_examples_block",
"_generate_example_pages",
]
+171
View File
@@ -0,0 +1,171 @@
# This file is part of OpenCV project.
# It is subject to the license terms in the LICENSE file found in the top-level directory
# of this distribution and at http://opencv.org/license.html.
# Copyright (C) 2026, BigVision LLC, all rights reserved.
# Third party copyrights are property of their respective owners.
"""Runtime patches for Sphinx C++ domain and breathe; applied at import."""
from __future__ import annotations
def _patch_cpp_xref_resolver():
"""Work around Sphinx 8.1.x parentSymbol assert in _resolve_xref_inner."""
try:
from sphinx.domains.cpp import CPPDomain
except ImportError:
return
original = CPPDomain._resolve_xref_inner
def guarded(self, env, fromdocname, builder, typ, target, node, contnode):
try:
return original(self, env, fromdocname, builder, typ, target,
node, contnode)
except AssertionError:
return None, None
CPPDomain._resolve_xref_inner = guarded
# Drop breathe unresolvable-xref log noise; text still renders.
import logging
_UNRESOLVED_XREF_PATTERNS = (
"Unable to resolve function",
"Unable to resolve class",
"Cannot find function",
"Cannot find class",
"Cannot find variable",
"Cannot find typedef",
"Cannot find enum",
"Cannot find enumerator",
"Cannot find define",
"Duplicate C++ declaration",
)
class _UnresolvedXrefFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
msg = record.getMessage()
return not any(p in msg for p in _UNRESOLVED_XREF_PATTERNS)
_filt = _UnresolvedXrefFilter()
for _logger_name in ("sphinx", "docutils"):
logging.getLogger(_logger_name).addFilter(_filt)
_patch_cpp_xref_resolver()
def _silence_breathe_anon_enum_warning():
"""Mute Sphinx parser warning on Doxygen's anonymous nested enums."""
import logging
class _AnonEnumFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
msg = record.getMessage()
return not (
"Invalid C++ declaration" in msg
and "Expected identifier in nested name" in msg
)
for _name in ("sphinx", "docutils"):
logging.getLogger(_name).addFilter(_AnonEnumFilter())
_silence_breathe_anon_enum_warning()
def _patch_breathe_operator_signatures():
"""Fix breathe {doxygenfunction} mis-splitting operator overloads."""
try:
import breathe.directives.function as _bf
except ImportError:
return
def _split_operator(s: str):
rp = s.rfind(")")
if rp == -1:
return None
depth, j = 0, rp
while j >= 0:
if s[j] == ")":
depth += 1
elif s[j] == "(":
depth -= 1
if depth == 0:
break
j -= 1
if j < 0:
return None
func_part, args_part = s[:j].strip(), s[j:]
k = func_part.find("::operator")
if k != -1:
return func_part[:k], func_part[k + 2:], args_part
if "::" in func_part:
ns, fn = func_part.rsplit("::", 1)
return ns, fn, args_part
return "", func_part, args_part
class _Shim:
__slots__ = ("_g",)
def __init__(self, g1, g2, g3):
self._g = (None, g1, g2, g3)
def group(self, i=0):
return self._g[i]
class _OperatorAwareRe:
def __init__(self, real):
object.__setattr__(self, "_real", real)
def __getattr__(self, name):
return getattr(self._real, name)
def match(self, pattern, string, *args, **kwargs):
m = self._real.match(pattern, string, *args, **kwargs)
if (m is not None and getattr(m.re, "groups", 0) >= 3
and "::operator" in string):
res = _split_operator(string)
if res is not None:
ns, fn, ar = res
return _Shim(ns or None, fn, ar)
return m
if not isinstance(_bf.re, _OperatorAwareRe):
_bf.re = _OperatorAwareRe(_bf.re)
_patch_breathe_operator_signatures()
def _patch_breathe_docsect():
"""Render title-less docSectN nodes breathe 4.36 drops."""
try:
from breathe.renderer import sphinxrenderer as _bsr
except ImportError:
return
_methods = _bsr.SphinxRenderer.methods
if getattr(_methods.get("docsect1"), "_opencv_docsect_patch", False):
return
_orig_visit = _methods["docsect1"]
def _visit_docsectN(self, node):
if not getattr(node, "title", None):
return self.render_iterable(node.content_)
return _orig_visit(self, node)
_visit_docsectN._opencv_docsect_patch = True
for _kind in ("docsect1", "docsect2", "docsect3"):
_methods[_kind] = _visit_docsectN
_patch_breathe_docsect()
def _silence_orphan_toctree_warning():
"""Mute toctree-orphan warning for intentionally unlinked external pages."""
import logging
class _OrphanFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
return "included in any toctree" not in record.getMessage()
for _name in ("sphinx", "docutils"):
logging.getLogger(_name).addFilter(_OrphanFilter())
_silence_orphan_toctree_warning()
+154
View File
@@ -0,0 +1,154 @@
# This file is part of OpenCV project.
# It is subject to the license terms in the LICENSE file found in the top-level directory
# of this distribution and at http://opencv.org/license.html.
# Copyright (C) 2026, BigVision LLC, all rights reserved.
# Third party copyrights are property of their respective owners.
"""build-finished hook: inline coll-diagram SVGs, strip Breathe clutter, re-theme Doxygen."""
from __future__ import annotations
import pathlib, re
from .state import _doxy_page_to_local, _DOXY_ANCHOR_TO_MEMBER
def _doxy_parent_page(page: str, api_dir: pathlib.Path) -> str:
"""Nested types (e.g. `structcv_1_1SparseMat_1_1Hdr`) get no standalone
Sphinx page — they're documented inline on the enclosing class. Walk up the
`_1_1`-separated scope to the nearest ancestor page that DOES exist locally.
Returns "" if no ancestor page exists."""
stem = page[:-5] if page.endswith(".html") else page
rest = None
for pref in ("class", "struct", "union"):
if stem.startswith(pref):
rest = stem[len(pref):]
break
if rest is None:
return ""
while "_1_1" in rest:
rest = rest.rsplit("_1_1", 1)[0]
for pref in ("class", "struct", "union"):
cand = f"{pref}{rest}.html"
if (api_dir / cand).is_file():
return cand
return ""
def _inline_collaboration_svgs(api_dir: pathlib.Path,
image_dir: pathlib.Path) -> None:
"""Inline coll-diagram SVGs so their links work; idempotent."""
import re
if not api_dir.is_dir():
return
img_re = re.compile(
r'<img alt="(?P<alt>[^"]*)" '
r'class="(?P<cls>opencv-coll-graph[^"]*)" '
r'src="\.\./_images/(?P<file>[^"]+\.svg)"\s*/?>')
href_re = re.compile(r'xlink:href="(?P<path>[^"]+)"')
def _rewrite_href(m: "re.Match") -> str:
path = m.group("path")
if "://" in path:
return m.group(0)
base = path.rsplit("/", 1)[-1]
page, _, frag = base.partition("#")
local = _doxy_page_to_local(page)
if not (api_dir / local).is_file():
# Nested type with no own page -> enclosing class page (inline docs).
parent = _doxy_parent_page(page, api_dir)
if parent:
local = parent
member = _DOXY_ANCHOR_TO_MEMBER.get(frag) if frag else None
if member:
return f'xlink:href="{local}#{member}"'
return f'xlink:href="{local}"'
for html in api_dir.glob("*.html"):
text = html.read_text(encoding="utf-8")
if "opencv-coll-graph" not in text:
continue
def _inline(m: "re.Match") -> str:
svg_path = image_dir / m.group("file")
if not svg_path.is_file():
return m.group(0)
svg = svg_path.read_text(encoding="utf-8")
start = svg.find("<svg")
if start < 0:
return m.group(0)
svg = href_re.sub(_rewrite_href, svg[start:])
# carry theme classes + alt for dark mode / a11y
return svg.replace(
"<svg ",
f'<svg class="{m.group("cls")}" role="img" '
f'aria-label="{m.group("alt")}" ', 1)
new = img_re.sub(_inline, text)
if new != text:
html.write_text(new, encoding="utf-8")
def _strip_breathe_class_clutter(api_dir: pathlib.Path) -> None:
"""Drop Breathe's duplicate class signature header; idempotent."""
import re
if not api_dir.is_dir():
return
section_re = re.compile(
r'(<section id="detailed-description"[^>]*>)'
r'(?P<body>[\s\S]*?)'
r'(</section>)'
)
dl_re = re.compile(
r'<dl[^>]*\bclass="[^"]*\bclass\b[^"]*"[^>]*>\s*'
r'<dt[^>]*>[\s\S]*?</dt>\s*'
r'<dd>(?P<dd>[\s\S]*?)</dd>\s*'
r'</dl>'
)
subclassed_re = re.compile(r'<p>Subclassed by[\s\S]*?</p>\s*')
for h in api_dir.glob("classcv*.html"):
text = h.read_text(encoding="utf-8")
if "detailed-description" not in text:
continue
def _strip_section(sm):
head, body, tail = sm.group(1), sm.group("body"), sm.group(3)
def _strip_dl(dm):
dd_body = dm.group("dd").strip()
dd_body = subclassed_re.sub("", dd_body).strip()
return dd_body
new_body = dl_re.sub(_strip_dl, body, count=1)
return head + new_body + tail
new = section_re.sub(_strip_section, text, count=1)
if new != text:
h.write_text(new, encoding="utf-8")
def _generate_search_map(out_dir: pathlib.Path) -> None:
"""Write _static/search_map.js: stem→Sphinx-path for every built HTML page."""
import json
skip = {"_static", "_sources", "_images", "_sphinx_design_static"}
mapping = {}
for f in out_dir.rglob("*.html"):
rel = f.relative_to(out_dir)
if rel.parts[0] in skip:
continue
mapping[f.stem] = rel.as_posix()
lines = ["var sphinxPageMap = {"]
for k, v in sorted(mapping.items()):
lines.append(f" {json.dumps(k)}: {json.dumps(v)},")
lines.append("};")
(out_dir / "_static" / "search_map.js").write_text("\n".join(lines), encoding="utf-8")
def _inline_coll_graphs_on_finish(app, exception):
"""build-finished entry point."""
if exception is not None:
return
out = pathlib.Path(app.outdir)
for _api in ("main_modules", "extra_modules"):
_inline_collaboration_svgs(out / _api, out / "_images")
_strip_breathe_class_clutter(out / _api)
_generate_search_map(out)
+888
View File
@@ -0,0 +1,888 @@
# This file is part of OpenCV project.
# It is subject to the license terms in the LICENSE file found in the top-level directory
# of this distribution and at http://opencv.org/license.html.
# Copyright (C) 2026, BigVision LLC, all rights reserved.
# Third party copyrights are property of their respective owners.
"""Shared configuration & state for the OpenCV Sphinx wrapper."""
from __future__ import annotations
import pathlib, re, os as _os, shutil as _shutil, textwrap as _textwrap
HERE = pathlib.Path(__file__).resolve().parent.parent
DOC_ROOT = (HERE.parent / "doc").resolve()
OPENCV_ROOT = HERE.parent.resolve()
# SCOPE — tutorial module folders; env override or auto-discover per tree
import os as _os
def _discover_doc_modules(subdir: str, toc_glob: str) -> list[str]:
"""Folder names under DOC_ROOT/<subdir> that carry a TOC page (sorted)."""
root = DOC_ROOT / subdir
if not root.is_dir():
return []
return sorted(p.name for p in root.iterdir()
if p.is_dir() and any(p.glob(toc_glob)))
def _module_list(env_var: str, subdir: str, toc_glob: str) -> list[str]:
"""Env-var override (comma list) when set & non-empty, else auto-discover."""
val = _os.environ.get(env_var)
if val:
return [m.strip() for m in val.split(",") if m.strip()]
return _discover_doc_modules(subdir, toc_glob)
DOC_MODULES = _module_list(
"OPENCV_DOC_MODULES", "tutorials", "table_of_content_*.markdown")
JS_DOC_MODULES = _module_list(
"OPENCV_JS_DOC_MODULES", "js_tutorials", "js_table_of_contents_*.markdown")
PY_DOC_MODULES = _module_list(
"OPENCV_PY_DOC_MODULES", "py_tutorials", "py_table_of_contents_*.markdown")
# SCOPE — env OPENCV_CONTRIB_MODULES; empty/unset auto-discovers
CONTRIB_ROOT = pathlib.Path(
_os.environ.get("OPENCV_CONTRIB_ROOT")
or str(HERE.parent.parent / "opencv_contrib" / "modules")
).resolve()
def _discover_contrib_modules() -> list[str]:
"""Contrib module folders carrying a `tutorials/` subtree (CMake's gate)."""
if not CONTRIB_ROOT.is_dir():
return []
return sorted(p.name for p in CONTRIB_ROOT.iterdir()
if p.is_dir() and (p / "tutorials").is_dir())
_contrib_env = _os.environ.get("OPENCV_CONTRIB_MODULES")
CONTRIB_MODULES = ([m.strip() for m in _contrib_env.split(",") if m.strip()]
if _contrib_env else _discover_contrib_modules())
# SCOPE — env OPENCV_API_MODULES (comma/semicolon); empty disables API pages
def _discover_api_modules() -> list[str]:
"""Main + contrib modules whose umbrella header declares `@defgroup`."""
found = []
# Scan both the main tree and opencv_contrib/modules.
_roots = [OPENCV_ROOT / "modules"]
if CONTRIB_ROOT.is_dir():
_roots.append(CONTRIB_ROOT)
for _root in _roots:
for _hdr in _root.glob("*/include/opencv2/*.hpp"):
if _hdr.stem != _hdr.parents[2].name: # only the umbrella header
continue
try:
if "@defgroup" in _hdr.read_text(encoding="utf-8", errors="ignore"):
found.append(_hdr.stem)
except OSError:
pass
return sorted(found)
# Default = discovered full set; override (comma/semicolon), empty disables
API_MODULES = [
m.strip()
for m in re.split(r"[,;]", _os.environ.get("OPENCV_API_MODULES")
or ",".join(_discover_api_modules()))
if m.strip()
]
# Sphinx srcdir; env OPENCV_SPHINX_INPUT_ROOT (`or` treats empty as unset)
SPHINX_INPUT_ROOT = pathlib.Path(
_os.environ.get("OPENCV_SPHINX_INPUT_ROOT") or str(DOC_ROOT)
).resolve()
# sphinx_design availability
try:
import sphinx_design as _sphinx_design # noqa: F401
HAVE_SPHINX_DESIGN = True
except ImportError:
HAVE_SPHINX_DESIGN = False
# -- Breathe (Doxygen XML -> Sphinx C++ domain) -----------------------------
# env OPENCV_DOXYGEN_XML_DIR
_API_XML_DIR = pathlib.Path(
_os.environ.get("OPENCV_DOXYGEN_XML_DIR")
or str(HERE.parent.parent / "build_doc" / "doc" / "doxygen" / "xml")
).resolve()
# Patched namespace XML for breathe; see _patch_namespace_xml_for_breathe
_PATCHED_XML_DIR = _API_XML_DIR.parent / "xml_for_sphinx"
def _module_group_stem(m: str) -> str:
# Doxygen names a group after its @defgroup, which can differ from the
# module folder (folder `3d` -> `@defgroup _3d`). Return that real stem.
if (_API_XML_DIR / f"group__{m.replace('_', '__')}.xml").is_file():
return m
for _root in (OPENCV_ROOT / "modules", CONTRIB_ROOT):
_hdr = _root / m / "include" / "opencv2" / f"{m}.hpp"
if _hdr.is_file():
_mm = re.search(r"@defgroup\s+(\S+)",
_hdr.read_text(encoding="utf-8", errors="ignore"))
return _mm.group(1) if _mm else m
return m
# -- Python enum/constant signatures ----------------------------------------
# C++ enumerator FQN -> cv2.* name; env OPENCV_PYTHON_SIGNATURES_FILE
_PY_SIGNATURES: dict = {}
import json as _json
for _pysigs_candidate in (
_API_XML_DIR.parents[2] / "modules" / "python_bindings_generator"
/ "pyopencv_signatures.json",
_os.environ.get("OPENCV_PYTHON_SIGNATURES_FILE") or None,
):
if not _pysigs_candidate:
continue
_pysigs_path = pathlib.Path(str(_pysigs_candidate))
if _pysigs_path.is_file():
try:
_PY_SIGNATURES = _json.loads(_pysigs_path.read_text(encoding="utf-8"))
except (ValueError, OSError):
_PY_SIGNATURES = {}
break
def _python_enum_name(enum_qualified: str, value_name: str,
strong: bool) -> str | None:
"""Return the cv2.* Python name for one C++ enumerator, or None."""
if not _PY_SIGNATURES:
return None
if strong:
scope = enum_qualified
elif "::" in enum_qualified:
scope = enum_qualified.rsplit("::", 1)[0]
else:
scope = ""
cpp_key = f"{scope}::{value_name}" if scope else value_name
entries = _PY_SIGNATURES.get(cpp_key)
if entries:
return entries[0].get("name")
return None
# -- Breathe availability ----------------------------------------------------
# Absence empties API_MODULES
HAVE_BREATHE = False
if API_MODULES:
try:
import breathe # noqa: F401
HAVE_BREATHE = True
except ImportError:
API_MODULES = []
# -- Doxygen integration -----------------------------------------------------
DOXYGEN_BASE_URL = (
_os.environ.get("OPENCV_DOXYGEN_BASE_URL", "https://docs.opencv.org/5.x/")
.rstrip("/") + "/")
# First existing wins; env OPENCV_DOXYGEN_TAGFILE overrides
_TAG_CANDIDATES = (
HERE.parent / "build" / "doc" / "doxygen" / "html" / "opencv.tag",
HERE.parent.parent / "build" / "doc" / "doxygen" / "html" / "opencv.tag",
# extra build-dir layouts (vanilla, contrib, nested CI)
HERE.parent.parent / "build" / "doc" / "opencv.tag",
HERE.parent.parent / "build_contrib" / "doc" / "doxygen" / "html" / "opencv.tag",
HERE.parent.parent / "build_contrib" / "doc" / "opencv.tag",
HERE.parent.parent / "build" / "build_contrib" / "build_contrib"
/ "doc" / "doxygen" / "html" / "opencv.tag",
)
_TAG_FILE = pathlib.Path(_os.environ.get(
"OPENCV_DOXYGEN_TAGFILE",
str(next((p for p in _TAG_CANDIDATES if p.is_file()), _TAG_CANDIDATES[0])),
))
# anchor -> doxygen URL filename
_TAG_FILENAMES: dict[str, str] = {}
# anchor -> title
_TAG_TITLES: dict[str, str] = {}
# Page-only subset of _TAG_TITLES (kind="page" compounds only — NOT groups/files),
# i.e. Doxygen's "Related Pages" set: \page docs like intro, faq, cuda_intro, …
_DOC_PAGE_TITLES: dict[str, str] = {}
# (compound-stem, member-name, normalized-args) -> Doxygen HTML anchor.
# Bridges our XML-driven members to the HTML anchors that name the call/caller
# graph SVGs (XML memberdef ids and HTML anchors live in disjoint hash spaces).
_CALL_GRAPH_ANCHORS: dict[tuple[str, str, str], str] = {}
# Doxygen member anchor -> Sphinx anchor (lowercased member name). Lets diagram
# cross-links jump to the exact member, as the original Doxygen pages did.
_DOXY_ANCHOR_TO_MEMBER: dict[str, str] = {}
def _norm_args(arglist: str) -> str:
"""Normalize a C++ arg-list so an XML `argsstring` matches a tag `arglist`."""
import html as _html
return re.sub(r"\s+", "", _html.unescape(arglist or ""))
# cv-namespace short-name -> doxygen URL; used by step 7c
_CV_SYMBOL_URL: dict[str, str] = {}
# include-path -> Doxygen HTML file URL; linkifies enum `#include` lines.
_FILE_URL: dict[str, str] = {}
if _TAG_FILE.is_file():
try:
import xml.etree.ElementTree as _ET
_tag_root = _ET.parse(str(_TAG_FILE)).getroot()
for _c in _tag_root.iter("compound"):
_kind = _c.get("kind")
# Call/caller-graph anchors: every compound's function members,
# keyed by the page they're documented on (the SVG filename prefix).
for _fm in _c.findall("member"):
if _fm.get("kind") != "function":
continue
_fn = _fm.findtext("name")
_faf = _fm.findtext("anchorfile") or ""
_fan = _fm.findtext("anchor") or ""
if not (_fn and _faf and _fan):
continue
_fstem = pathlib.Path(_faf).stem
_CALL_GRAPH_ANCHORS.setdefault(
(_fstem, _fn, _norm_args(_fm.findtext("arglist") or "")), _fan)
# Doxygen anchor -> Sphinx member anchor (lowercased name).
_DOXY_ANCHOR_TO_MEMBER.setdefault(_fan, _fn.lower())
if _kind == "page":
_n, _f = _c.findtext("name"), _c.findtext("filename")
_t = _c.findtext("title")
if _n and _f:
_TAG_FILENAMES[_n] = _f if _f.endswith(".html") else _f + ".html"
if _n and _t:
_TAG_TITLES[_n] = _t
_DOC_PAGE_TITLES[_n] = _t
elif _kind == "namespace" and _c.findtext("name") == "cv":
for _m in _c.findall("member"):
_n = _m.findtext("name")
_af = _m.findtext("anchorfile")
_an = _m.findtext("anchor") or ""
if not (_n and _af):
continue
_CV_SYMBOL_URL.setdefault(
_n, DOXYGEN_BASE_URL + _af + (f"#{_an}" if _an else "")
)
elif _kind in ("class", "struct"):
_full = _c.findtext("name") or ""
if _full.startswith("cv::"):
_short = _full.split("::")[-1]
_af = _c.findtext("filename")
if _short and _af:
_CV_SYMBOL_URL.setdefault(_short, DOXYGEN_BASE_URL + _af)
elif _kind == "group":
# module pages are kind="group"
_n = _c.findtext("name")
_f = _c.findtext("filename")
_t = _c.findtext("title")
if _n and _f:
_TAG_FILENAMES[_n] = _f if _f.endswith(".html") else _f + ".html"
if _n and _t:
_TAG_TITLES[_n] = _t
elif _kind == "file":
# Header file -> Doxygen page; key by include path.
_n = _c.findtext("name") or ""
_p = _c.findtext("path") or ""
_f = _c.findtext("filename") or ""
if _n and _f and not _p.startswith("/"):
_key = (_p + _n) if _p.endswith("/") or not _p else f"{_p}/{_n}"
_FILE_URL.setdefault(
_key, _f if _f.endswith(".html") else _f + ".html")
else:
# CV_* macros (kind="define") re-exported as cv.CV_*
for _m in _c.findall("member"):
if _m.get("kind") != "define":
continue
_n = _m.findtext("name") or ""
if not _n.startswith("CV_"):
continue
_af = _m.findtext("anchorfile")
_an = _m.findtext("anchor") or ""
if _af:
_CV_SYMBOL_URL.setdefault(
_n, DOXYGEN_BASE_URL + _af + (f"#{_an}" if _an else "")
)
except Exception:
pass
def _doxygen_url(page: str) -> str:
return DOXYGEN_BASE_URL + _TAG_FILENAMES.get(page, page)
# -- Live (docs.opencv.org) tagfile for API stub URL construction ----------
# CREATE_SUBDIRS=NO flat names 404 live; live tag has subdir prefixes
_LIVE_TAG_FILE = pathlib.Path(_os.environ.get(
"OPENCV_DOXYGEN_LIVE_TAGFILE",
str(HERE.parent.parent / "build" / "doc" / "doxygen" / "opencv-live.tag"),
))
if not _LIVE_TAG_FILE.is_file():
for _alt in (
HERE.parent.parent / "build" / "build_contrib" / "build_contrib"
/ "doc" / "doxygen" / "opencv-live.tag",
HERE.parent.parent / "build_contrib" / "doc" / "doxygen" / "opencv-live.tag",
):
if _alt.is_file():
_LIVE_TAG_FILE = _alt
break
_LIVE_GROUP_URL: dict[str, str] = {} # 'group__core__basic' -> live URL
_LIVE_CLASS_URL: dict[str, str] = {} # 'Matx' -> live URL
_LIVE_TYPEDEF_URL: dict[str, str] = {} # 'uchar' -> live URL (group anchor)
if _LIVE_TAG_FILE.is_file():
try:
import xml.etree.ElementTree as _ET
for _c in _ET.parse(str(_LIVE_TAG_FILE)).getroot().iter("compound"):
_kind = _c.get("kind")
_n = _c.findtext("name") or ""
_f = _c.findtext("filename") or ""
if not (_n and _f):
continue
_fn = _f if _f.endswith(".html") else _f + ".html"
if _kind == "group":
# key by filename basename
_basename = pathlib.PurePosixPath(_fn).name[:-5] # strip .html
_LIVE_GROUP_URL[_basename] = DOXYGEN_BASE_URL + _fn
elif _kind == "class":
_short = _n.split("::")[-1]
_LIVE_CLASS_URL.setdefault(_short, DOXYGEN_BASE_URL + _fn)
# typedef members -> live anchor URLs
for _mem in _c.findall("member"):
if _mem.get("kind") != "typedef":
continue
_mn = (_mem.findtext("name") or "").strip()
_maf = (_mem.findtext("anchorfile") or "").strip()
_man = (_mem.findtext("anchor") or "").strip()
if _mn and _maf and _man:
_LIVE_TYPEDEF_URL.setdefault(
_mn, f"{DOXYGEN_BASE_URL}{_maf}#{_man}")
except Exception:
pass
# -- Local-link variants of the maps above (step 8g) ------------------------
_LOCAL_SRC_TAG = _TAG_FILE if _TAG_FILE.is_file() else _LIVE_TAG_FILE
_LOCAL_CLASS_URL: dict[str, str] = {
# _Tp template-parameter placeholder stub
"_Tp": "class_Tp.html",
}
_LOCAL_TYPEDEF_URL: dict[str, str] = {} # 'uchar' -> 'core_hal_interface.html#_CPPv45uchar'
# Doxygen template-param placeholder pages -> (display name, Sphinx page).
# Sphinx mirrors these as stubs (stubs._write_placeholder_stubs) so diagram
# cross-links resolve instead of 404ing.
_PLACEHOLDER_STUBS: dict[str, tuple[str, str]] = {
"class__Tp.html": ("_Tp", "class_Tp.html"),
"classfloat__type.html": ("float_type", "classfloat_type.html"),
}
# Doxygen compound filename -> Sphinx page filename (class/struct compounds).
_LOCAL_PAGE_BY_DOXY_FILE: dict[str, str] = {
_doxy: _page for _doxy, (_disp, _page) in _PLACEHOLDER_STUBS.items()}
if _LOCAL_SRC_TAG.is_file():
try:
import xml.etree.ElementTree as _ET
for _c in _ET.parse(str(_LOCAL_SRC_TAG)).getroot().iter("compound"):
if _c.get("kind") in ("class", "struct"):
_n = _c.findtext("name") or ""
_f = _c.findtext("filename") or ""
if _n and _f:
_short = _n.split("::")[-1]
_fn = _f if _f.endswith(".html") else _f + ".html"
_doxy_base = pathlib.PurePosixPath(_fn).name
_LOCAL_CLASS_URL.setdefault(_short, _doxy_base)
# Sphinx mirrors Doxygen's filename except the few remapped.
_LOCAL_PAGE_BY_DOXY_FILE.setdefault(
_doxy_base, _LOCAL_CLASS_URL.get(_short, _doxy_base))
for _mem in _c.findall("member"):
# variable only from namespaces; class-member vars poison the map
_mk = _mem.get("kind")
if _mk == "typedef":
pass
elif _mk == "enumeration":
pass # enum types like cv::DataLayout — linkable
elif _mk == "variable" and _c.get("kind") == "namespace":
pass
else:
continue
_mn = (_mem.findtext("name") or "").strip()
_maf = (_mem.findtext("anchorfile") or "").strip()
if not (_mn and _maf):
continue
if _mn in _LOCAL_TYPEDEF_URL:
continue # first-occurrence wins
_bn = pathlib.PurePosixPath(_maf).name
if _bn.startswith("group__"):
# group__core__hal__interface.html -> core_hal_interface.html
_local_page = (_bn[len("group__"):]
.replace(".html", "")
.replace("__", "_")
+ ".html")
elif _bn.startswith("namespacecv"):
_local_page = "core_basic.html"
else:
_local_page = _bn
# HAL typedefs are global C; else cv::-scoped (cpp-domain v4 anchor)
if "hal_interface" in _local_page:
_anchor = f"_CPPv4{len(_mn)}{_mn}"
else:
_anchor = f"_CPPv4N2cv{len(_mn)}{_mn}E"
_LOCAL_TYPEDEF_URL[_mn] = f"{_local_page}#{_anchor}"
except Exception:
pass
def _doxy_page_to_local(basename: str) -> str:
"""Map a Doxygen compound page filename to the Sphinx page for the same
symbol. Pure name transform — the caller verifies the file exists.
group__core__utils.html -> core_utils.html
namespacecv*.html -> core_basic.html
class*/struct*/union* -> unchanged (or remapped, e.g. the _Tp stub)
"""
if basename.startswith("group__"):
return (basename[len("group__"):].replace(".html", "")
.replace("__", "_") + ".html")
if basename.startswith("namespace"):
return "core_basic.html"
return _LOCAL_PAGE_BY_DOXY_FILE.get(basename, basename)
# -- Class template-parameter display map (step 8e) -------------------------
# class short name -> template-param list e.g. '< _Tp, cn >'
_CLASS_TEMPLATE_DISPLAY: dict[str, str] = {}
if _API_XML_DIR.is_dir():
try:
import xml.etree.ElementTree as _ET
for _xml in _API_XML_DIR.glob("classcv_1_1*.xml"):
try:
_cd = _ET.parse(_xml).getroot().find("compounddef")
except _ET.ParseError:
continue
if _cd is None:
continue
_tpl = _cd.find("templateparamlist")
if _tpl is None:
continue
_names = []
for _p in _tpl.findall("param"):
_decl = (_p.findtext("declname")
or _p.findtext("defname") or "").strip()
_type = (_p.findtext("type") or "").strip()
if _decl:
_names.append(_decl)
elif _type in ("typename", "class"):
_names.append("_Tp")
elif _type:
_names.append(_type)
if _names:
_name = (_cd.findtext("compoundname") or "").split("::")[-1]
_CLASS_TEMPLATE_DISPLAY[_name] = f"< {', '.join(_names)} >"
except Exception:
pass
# Punctuation -> alpha token so operator overloads get distinct slugs
_FUNC_SLUG_PUNCT = {
"=": "eq", "!": "ne", "<": "lt", ">": "gt", "+": "plus", "-": "minus",
"*": "mul", "/": "div", "&": "amp", "|": "or", "%": "mod", "^": "xor",
"~": "tilde", "[": "lbr", "]": "rbr",
}
def _func_slug(name: str) -> str:
"""In-page anchor slug for a function name; shared by stub gen and translator."""
parts = []
for ch in name.lower():
if ch.isalnum() or ch == "_":
parts.append(ch)
elif ch in _FUNC_SLUG_PUNCT:
parts.append("-" + _FUNC_SLUG_PUNCT[ch])
else:
parts.append("-")
s = re.sub(r"-+", "-", "".join(parts)).strip("-")
return f"cv-{s}" if s else "cv"
# ---- Citation numbering --------------------------------------------------
# @cite KEY -> [N], N = bibtex-plain sort position
def _bib_parse(text: str) -> list[dict]:
"""Walk a BibTeX file into a list of {_type, _key, field: value, ...}."""
out: list[dict] = []
n, i = len(text), 0
while i < n:
m = re.search(r"@(\w+)\s*\{\s*([^\s,]+)\s*,", text[i:])
if not m:
break
kind, key = m.group(1), m.group(2)
i += m.end()
depth, body_start = 1, i
while i < n and depth > 0:
c = text[i]
if c == "{":
depth += 1
elif c == "}":
depth -= 1
i += 1
if depth != 0:
break # malformed entry
out.append({"_type": kind.lower(), "_key": key,
**_bib_fields(text[body_start:i - 1])})
return out
def _bib_fields(body: str) -> dict[str, str]:
fields: dict[str, str] = {}
n, j = len(body), 0
while j < n:
while j < n and body[j] in " \t\n\r,":
j += 1
if j >= n:
break
ns = j
while j < n and (body[j].isalnum() or body[j] == "_"):
j += 1
name = body[ns:j].lower()
if not name:
break
while j < n and body[j] in " \t\n\r":
j += 1
if j >= n or body[j] != "=":
break
j += 1
while j < n and body[j] in " \t\n\r":
j += 1
if j >= n:
break
if body[j] == "{":
j += 1
depth, vs = 1, j
while j < n and depth > 0:
if body[j] == "{":
depth += 1
elif body[j] == "}":
depth -= 1
j += 1
value = body[vs:j - 1]
elif body[j] == '"':
j += 1
vs = j
while j < n and body[j] != '"':
if body[j] == "\\" and j + 1 < n:
j += 1
j += 1
value = body[vs:j]
if j < n:
j += 1
else:
vs = j
while j < n and body[j] not in ",\n":
j += 1
value = body[vs:j].strip()
fields[name] = value
return fields
# LaTeX accent + special-char cleanup
_LATEX_ACCENT_RE = re.compile(r"\\([\"'`^~.])\s*\{?\s*([A-Za-z])\s*\}?")
_LATEX_ACCENT_MAP = {
('"', 'a'): 'ä', ('"', 'e'): 'ë', ('"', 'i'): 'ï', ('"', 'o'): 'ö',
('"', 'u'): 'ü', ('"', 'A'): 'Ä', ('"', 'O'): 'Ö', ('"', 'U'): 'Ü',
("'", 'a'): 'á', ("'", 'e'): 'é', ("'", 'i'): 'í', ("'", 'o'): 'ó',
("'", 'u'): 'ú', ("'", 'c'): 'ć', ("'", 'n'): 'ń', ("'", 'A'): 'Á',
("'", 'E'): 'É', ("'", 'I'): 'Í', ("'", 'O'): 'Ó', ("'", 'U'): 'Ú',
("`", 'a'): 'à', ("`", 'e'): 'è', ("`", 'i'): 'ì', ("`", 'o'): 'ò',
("`", 'u'): 'ù',
('^', 'a'): 'â', ('^', 'e'): 'ê', ('^', 'i'): 'î', ('^', 'o'): 'ô',
('^', 'u'): 'û',
('~', 'a'): 'ã', ('~', 'n'): 'ñ', ('~', 'o'): 'õ',
('.', 'c'): 'ċ', ('.', 'e'): 'ė',
}
_LATEX_SPECIAL = {
r"\&": "&", r"\%": "%", r"\#": "#", r"\$": "$",
r"\_": "_", r"\{": "{", r"\}": "}",
r"\textendash": "", r"\textemdash": "",
r"\ldots": "", r"\dots": "",
r"\o": "ø", r"\O": "Ø", r"\ss": "ß",
r"\aa": "å", r"\AA": "Å", r"\ae": "æ", r"\AE": "Æ",
"---": "", "--": "",
}
def _bib_clean(s: str) -> str:
s = re.sub(r"\s+", " ", s or "").strip()
s = _LATEX_ACCENT_RE.sub(
lambda m: _LATEX_ACCENT_MAP.get((m.group(1), m.group(2)), m.group(2)), s)
for k, v in _LATEX_SPECIAL.items():
s = s.replace(k, v)
return s.replace("{", "").replace("}", "").strip()
def _bib_join_authors(field: str) -> str:
"""Render an author/editor list bibtex-plain style."""
if not field:
return ""
parts = re.split(r"\s+and\s+", field)
out: list[str] = []
for p in parts:
p = _bib_clean(p)
if "," in p: # "Last, First" → "First Last"
last, first = p.split(",", 1)
p = f"{first.strip()} {last.strip()}"
out.append(p)
if len(out) <= 1:
return out[0] if out else ""
if len(out) == 2:
return f"{out[0]} and {out[1]}"
return ", ".join(out[:-1]) + f", and {out[-1]}"
def _bib_render_entry(e: dict, num: int | None) -> str:
key = e["_key"]
bracket = f"[{num}]" if num is not None else f"[{key}]"
authors = _bib_join_authors(e.get("author") or e.get("editor") or "")
title = _bib_clean(e.get("title", ""))
year = _bib_clean(e.get("year", ""))
month = _bib_clean(e.get("month", ""))
pages = _bib_clean(e.get("pages", ""))
volume = _bib_clean(e.get("volume", ""))
number = _bib_clean(e.get("number", ""))
doi = _bib_clean(e.get("doi", ""))
url = _bib_clean(e.get("url", ""))
journal = _bib_clean(e.get("journal", ""))
booktitle = _bib_clean(e.get("booktitle", ""))
publisher = _bib_clean(e.get("publisher") or e.get("howpublished")
or e.get("institution") or "")
# title links to url, else doi.org
title_url = url or (f"https://doi.org/{doi}" if doi else "")
title_md = f"[{title}]({title_url})" if (title and title_url) else title
date = (f"{month} {year}".strip()) if (month or year) else ""
bits: list[str] = []
if authors:
bits.append(authors)
if title_md:
bits.append(title_md)
# venue formatting by entry kind
kind = e.get("_type", "")
if kind == "article" and journal:
seg = f"*{journal}*"
if volume:
seg += f", {volume}" + (f"({number})" if number else "")
if pages:
seg += f":{pages}"
elif pages:
seg += f", {pages}"
if date:
seg += f", {date}"
bits.append(seg)
elif kind in ("inproceedings", "incollection") and booktitle:
seg = f"In *{booktitle}*"
if pages:
seg += f", pages {pages}"
if publisher:
seg += f". {publisher}"
if date:
seg += f", {date}"
bits.append(seg)
else:
# @book / @misc / fallback
tail = []
if publisher:
tail.append(publisher)
if booktitle and not publisher:
tail.append(f"*{booktitle}*")
if date:
tail.append(date)
if tail:
bits.append(", ".join(tail))
body = ". ".join(bits)
if body and not body.endswith("."):
body += "."
# keep CITEREF_<Key> case so cached Doxygen links resolve
return f'<a id="CITEREF_{key}"></a>\n\n**{bracket}** {body}'
def _bib_render_all(entries: list[dict], numbering: dict[str, int]) -> str:
out = ["Bibliography {#citelist}", "============", ""]
for e in entries:
out.append(_bib_render_entry(e, numbering.get(e["_key"])))
out.append("")
return "\n".join(out)
def _bib_sort_key(e: dict) -> tuple:
"""bibtex `plain` sort: author last name, year, title (matches live site)."""
authors = e.get("author") or e.get("editor") or "zzz"
first = re.split(r"\s+and\s+", authors)[0]
first = _bib_clean(first)
if "," in first: # "Last, First"
last = first.split(",", 1)[0].strip()
else: # "First Middle Last"
toks = first.split()
last = toks[-1] if toks else "zzz"
return (last.lower(),
_bib_clean(e.get("year", "")),
_bib_clean(e.get("title", "")).lower())
# opencv.bib + per-module + contrib bibs (contrib affects sort order)
_BIB_FILES: list[pathlib.Path] = []
if (DOC_ROOT / "opencv.bib").is_file():
_BIB_FILES.append(DOC_ROOT / "opencv.bib")
_BIB_FILES += sorted((OPENCV_ROOT / "modules").glob("*/doc/*.bib"))
if CONTRIB_ROOT.is_dir():
_BIB_FILES += sorted(CONTRIB_ROOT.glob("*/doc/*.bib"))
_CITE_NUMBER: dict[str, int] = {}
# reused by citelist generator for the same sort order
_BIB_ENTRIES_SORTED: list[dict] = []
_seen_keys: set[str] = set() # dedupe; first wins
_all_entries: list[dict] = []
for _bf in _BIB_FILES:
try:
_txt = _bf.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for _e in _bib_parse(_txt):
if _e["_key"] in _seen_keys:
continue
_seen_keys.add(_e["_key"])
_all_entries.append(_e)
_BIB_ENTRIES_SORTED = sorted(_all_entries, key=_bib_sort_key)
for _i, _e in enumerate(_BIB_ENTRIES_SORTED, 1):
_CITE_NUMBER[_e["_key"]] = _i
_REDIRECT_MAP: dict[str, str] = {}
_REDIRECT_RE = re.compile(
r"\{#(?P<src>[\w-]+)\}\s*\n[=\-]+\s*\n+"
r"\s*(?:Content|Tutorial\s+content)\s+has\s+been\s+moved\b"
r"[^@]{0,300}@ref\s+(?P<dst>[\w-]+)",
re.IGNORECASE,
)
for _scan_dir in ("tutorials", "py_tutorials", "js_tutorials"):
_root = DOC_ROOT / _scan_dir
if not _root.is_dir():
continue
for _md in _root.rglob("*.markdown"):
try:
_t = _md.read_text(encoding="utf-8", errors="replace")[:2000]
except OSError:
continue
_m = _REDIRECT_RE.search(_t)
if _m:
_REDIRECT_MAP[_m.group("src")] = _m.group("dst")
def _resolve_redirect(anchor: str) -> str:
"""Follow `_REDIRECT_MAP` transitively (cycle-safe)."""
seen: set[str] = set()
while anchor in _REDIRECT_MAP and anchor not in seen:
seen.add(anchor)
anchor = _REDIRECT_MAP[anchor]
return anchor
# Anchor maps
_ANCHOR_TO_DOC: dict[str, str] = {} # anchor -> docname (internal)
_ANCHOR_TO_EXTERNAL: dict[str, tuple[str, str]] = {} # anchor -> (title, url)
_ANCHOR_TO_TITLE: dict[str, str] = {} # anchor -> first-heading title
# anchors reachable via @subpage/@ref; rest are orphans
_REFERENCED_ANCHORS: set[str] = set()
_HEAD_RE = re.compile(
r"^(?P<title1>[^\n]+?)\s*\{#(?P<anchor1>[\w-]+)\}\s*\n[=\-]{3,}\s*$"
r"|"
r"^#+\s+(?P<title2>[^\n]+?)\s*\{#(?P<anchor2>[\w-]+)\}\s*$",
re.MULTILINE)
def _scan_internal(path: pathlib.Path, base: pathlib.Path | None = None) -> None:
"""Add every {#anchor} and `@anchor NAME` in `path` to _ANCHOR_TO_DOC."""
base = base or SPHINX_INPUT_ROOT
_SUFFIXES = (".markdown", ".md")
if path.is_file():
files = [path] if path.suffix in _SUFFIXES else []
elif path.is_dir():
# skip _old/** (not compiled)
files = [p for s in _SUFFIXES for p in path.rglob(f"*{s}")
if "_old" not in p.parts]
else:
files = []
for md in files:
try:
body = md.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
# unresolved path so staged symlinks give staging-root docnames
try:
rel = md.relative_to(base).with_suffix("").as_posix()
except ValueError:
# file outside base; fall back to DOC_ROOT-relative
rel = md.relative_to(DOC_ROOT).with_suffix("").as_posix()
for m in re.finditer(r"\{#([\w-]+)\}", body):
_ANCHOR_TO_DOC[m.group(1)] = rel
for m in re.finditer(r"^@anchor\s+([\w-]+)\s*$", body, re.MULTILINE):
_ANCHOR_TO_DOC[m.group(1)] = rel
# anchors this file links to (orphan detection)
for m in re.finditer(r"@(?:subpage|ref)\s+([\w-]+)", body):
_REFERENCED_ANCHORS.add(m.group(1))
# first heading title
tm = _HEAD_RE.search(body[:4000])
if tm:
anchor = tm.group("anchor1") or tm.group("anchor2")
title = (tm.group("title1") or tm.group("title2") or "").strip()
if anchor and title:
_ANCHOR_TO_TITLE[anchor] = title
def _scan_external(toc_file: pathlib.Path) -> None:
"""Add a module TOC file's top (title, anchor) to _ANCHOR_TO_EXTERNAL."""
try:
head = toc_file.read_text(encoding="utf-8", errors="replace")[:4000]
except OSError:
return
m = _HEAD_RE.search(head)
if not m:
return
anchor = m.group("anchor1") or m.group("anchor2")
title = (m.group("title1") or m.group("title2") or "").strip()
if not anchor:
return
url = DOXYGEN_BASE_URL + _TAG_FILENAMES.get(anchor, "index.html")
_ANCHOR_TO_EXTERNAL[anchor] = (title, url)
# shared dict objects populated by the build module
_IMAGE_INDEX: dict[str, str] = {}
_SNIPPET_INDEX: dict[str, pathlib.Path] = {}
_ALL_CLASSES: dict[str, dict] = {} # refid -> {qualified, kind, brief, docname}
_ALL_NAMESPACES: dict[str, dict] = {} # name -> {refid, brief, docname}
_TOGGLE_LABELS = {"cpp": "C++", "java": "Java", "python": "Python"}
# Mirrors Doxygen EXAMPLE_PATH; OPENCV_ROOT first (order matters)
_SNIPPET_BASES = [
OPENCV_ROOT,
OPENCV_ROOT / "samples",
OPENCV_ROOT / "apps",
] + [CONTRIB_ROOT / _m / "samples" for _m in CONTRIB_MODULES]
# Remap Doxygen language names Pygments doesn't know
_LANG_ALIASES = {
"none": "text",
"unparsed": "text",
"guess": "text",
"gradle": "groovy",
"run": "bash",
# `m` = Objective-C in the iOS tutorials (.m sources); Pygments uses `objc`.
"m": "objc",
# No Pygments lexer for these fence tags used by ios/app/face tutorials —
# render as plain text instead of warning "lexer name is not known".
"csv": "text",
"plaintext": "text",
}
USE_INDEX_LANDING = True
__all__ = [
"USE_INDEX_LANDING",
"HERE", "DOC_ROOT", "OPENCV_ROOT",
"DOC_MODULES", "JS_DOC_MODULES", "PY_DOC_MODULES",
"CONTRIB_MODULES", "CONTRIB_ROOT", "SPHINX_INPUT_ROOT", "API_MODULES",
"_API_XML_DIR", "_PATCHED_XML_DIR", "_module_group_stem",
"_PY_SIGNATURES", "_python_enum_name",
"HAVE_SPHINX_DESIGN", "HAVE_BREATHE",
"DOXYGEN_BASE_URL", "_doxygen_url",
"_TAG_FILE", "_TAG_FILENAMES", "_TAG_TITLES", "_DOC_PAGE_TITLES",
"_CV_SYMBOL_URL", "_FILE_URL",
"_CALL_GRAPH_ANCHORS", "_DOXY_ANCHOR_TO_MEMBER", "_norm_args",
"_LIVE_GROUP_URL", "_LIVE_CLASS_URL", "_LIVE_TYPEDEF_URL",
"_LOCAL_CLASS_URL", "_LOCAL_TYPEDEF_URL", "_CLASS_TEMPLATE_DISPLAY",
"_LOCAL_PAGE_BY_DOXY_FILE", "_PLACEHOLDER_STUBS", "_doxy_page_to_local",
"_func_slug",
"_CITE_NUMBER", "_BIB_ENTRIES_SORTED", "_bib_render_all",
"_REDIRECT_MAP", "_resolve_redirect",
"_ANCHOR_TO_DOC", "_ANCHOR_TO_EXTERNAL", "_ANCHOR_TO_TITLE",
"_REFERENCED_ANCHORS", "_HEAD_RE",
"_scan_internal", "_scan_external",
"_IMAGE_INDEX", "_SNIPPET_INDEX", "_SNIPPET_BASES",
"_ALL_CLASSES", "_ALL_NAMESPACES",
"_TOGGLE_LABELS", "_LANG_ALIASES",
]
File diff suppressed because it is too large Load Diff
+997
View File
@@ -0,0 +1,997 @@
# This file is part of OpenCV project.
# It is subject to the license terms in the LICENSE file found in the top-level directory
# of this distribution and at http://opencv.org/license.html.
# Copyright (C) 2026, BigVision LLC, all rights reserved.
# Third party copyrights are property of their respective owners.
"""Doxygen-flavored .markdown -> MyST translation (the source-read engine)."""
from __future__ import annotations
import pathlib, re, os as _os, shutil as _shutil, textwrap as _textwrap
from .state import *
def _normalize_lang(lang: str) -> str:
lang = (lang or "").strip(".").strip().lower() or "text"
return _LANG_ALIASES.get(lang, lang)
def _read_snippet(rel_path: str, label: str | None) -> tuple[str, str]:
"""Return (code_text, language) for an @include / @snippet directive."""
rel_norm = rel_path.lstrip("/")
p = next((b / rel_norm for b in _SNIPPET_BASES
if (b / rel_norm).is_file()), None)
# Doxygen EXAMPLE_RECURSIVE basename lookup.
if p is None:
hit = _SNIPPET_INDEX.get(pathlib.Path(rel_norm).name)
if hit and hit.is_file():
p = hit
if p is None:
return f"// not found: {rel_path}\n", "text"
text = p.read_text(encoding="utf-8", errors="replace")
ext = p.suffix.lower()
lang = {".cpp": "cpp", ".hpp": "cpp", ".h": "cpp", ".c": "c",
".py": "python", ".java": "java",
".xml": "xml", ".html": "html",
".sh": "bash", ".bash": "bash"}.get(ext, "text")
if label is None:
return text, lang
# Match `[label]` after any comment marker.
pat = re.compile(r"^[^\[\n]*(?://!|//|##|#|<!--)[^\[\n]*\[" + re.escape(label)
+ r"\][^\n]*$", re.MULTILINE)
matches = list(pat.finditer(text))
if len(matches) < 2:
return f"// snippet not found: {rel_path} [{label}]\n", lang
body = text[matches[0].end():matches[1].start()].strip("\n")
lines = body.split("\n")
indents = [len(l) - len(l.lstrip(" ")) for l in lines if l.strip()]
if indents:
dedent = min(indents)
lines = [l[dedent:] if len(l) >= dedent else l for l in lines]
return "\n".join(lines), lang
def _emit_toggles(tabs: list[tuple[str, str]]) -> str:
if HAVE_SPHINX_DESIGN:
out = ["", "``````{tab-set}"]
for lang, body in tabs:
label = _TOGGLE_LABELS.get(lang, lang.title())
out += [f"`````{{tab-item}} {label}", body, "`````"]
out += ["``````", ""]
return "\n".join(out)
# Fallback without sphinx-design.
out = [""]
for lang, body in tabs:
label = _TOGGLE_LABELS.get(lang, lang.title())
out += [f"**{label}**", "", body, ""]
return "\n".join(out)
def _translate(text: str, docname: str | None = None) -> str:
# 0v. @verbatim — stash body; restored at end.
_verbatim_stash: dict[str, str] = {}
def _verbatim_save(body: str, inline: bool) -> str:
key = f"VERBATIM_{len(_verbatim_stash)}"
if inline:
_verbatim_stash[key] = f"`{body.strip()}`"
else:
_verbatim_stash[key] = f"\n```text\n{body.strip()}\n```\n"
return key
# Block form — run first.
text = re.sub(
r"@verbatim[ \t]*\n(?P<body>.*?)\n[ \t]*@endverbatim",
lambda m: _verbatim_save(m.group("body"), inline=False),
text, flags=re.DOTALL)
# Inline form.
text = re.sub(
r"@verbatim[ \t]+(?P<body>[^\n]+?)[ \t]+@endverbatim",
lambda m: _verbatim_save(m.group("body"), inline=True),
text)
if docname == "tutorials/tutorials" and not USE_INDEX_LANDING:
# Lead sidebar with intro; faq/bibliography stay appended.
if "intro" in _ANCHOR_TO_DOC:
text = re.sub(r"^- @subpage", "- @subpage intro\n- @subpage",
text, count=1, flags=re.MULTILINE)
if JS_DOC_MODULES:
text += "\n- @subpage tutorial_js_root\n"
if PY_DOC_MODULES:
text += "\n- @subpage tutorial_py_root\n"
if "faq" in _ANCHOR_TO_DOC:
text += "\n- @subpage faq\n"
if "citelist" in _ANCHOR_TO_DOC:
text += "\n- @subpage citelist\n"
# 0a. py_tutorials root: promote @ref to @subpage (case-by-case avoids cycles).
if docname == "py_tutorials/py_tutorials":
if "py_video" in PY_DOC_MODULES:
text = re.sub(
r"@ref\s+tutorial_table_of_content_video\b",
"@subpage tutorial_py_table_of_contents_video",
text,
)
# Object Detection stays @ref (its C++ page is already in main toctree).
# 0d. py_video/py_objdetect stub trees -> :orphan: (real link via 0a).
if docname and (docname.startswith("py_tutorials/py_video/")
or docname.startswith("py_tutorials/py_objdetect/")):
text = "---\norphan: true\n---\n\n" + text
# 0b. "-# foo" -> "1. foo".
text = re.sub(r"^(?P<indent>[ \t]*)-#[ \t]+",
lambda m: f"{m.group('indent')}1. ", text, flags=re.MULTILINE)
# 0c. Dedent orphan indented bullets under a paragraph.
_orphan_bullets_re = re.compile(
r"^(?P<before>(?![ \t#=*\->]).+\n)"
r"(?P<bullets>(?:[ \t]{2,}-[ \t]+[^\n]+\n)+)",
re.MULTILINE,
)
def _dedent_orphan_bullets(m: re.Match) -> str:
before = m.group("before")
raw = m.group("bullets")
lines = raw.split("\n")
nonempty = [l for l in lines if l.strip()]
if not nonempty:
return m.group(0)
min_indent = min(len(l) - len(l.lstrip(" \t")) for l in nonempty)
dedented = "\n".join(
l[min_indent:] if len(l) >= min_indent else l for l in lines
)
return before + "\n" + dedented
text = _orphan_bullets_re.sub(_dedent_orphan_bullets, text)
# 1. Heading anchors "Title {#name}" -> MyST label + ATX.
def _setext_repl(m: re.Match) -> str:
title = m.group("title").strip()
level = 1 if m.group("bar") == "=" else 2
return f"({m.group('anchor')})=\n{'#' * level} {title}"
text = re.sub(
r"^(?P<title>[^\n]+?)\s*\{#(?P<anchor>[\w-]+)\}\s*\n(?P<bar>[=\-])[=\-]{2,}\s*$",
_setext_repl, text, flags=re.MULTILINE)
text = re.sub(
r"^(?P<hashes>#+)\s+(?P<title>[^\n]+?)\s*\{#(?P<anchor>[\w-]+)\}\s*$",
lambda m: f"({m.group('anchor')})=\n{m.group('hashes')} {m.group('title')}",
text, flags=re.MULTILINE)
# 1b. Trailing setext heading at EOF -> ATX (docutils rejects trailing transition).
text = re.sub(
r"^(?P<title>[^\n#=\-][^\n]*?)[ \t]*\n(?P<bar>[=\-])[=\-]{2,}[ \t]*$\s*\Z",
lambda m: f"{'#' if m.group('bar') == '=' else '##'} {m.group('title').strip()}\n",
text, flags=re.MULTILINE)
# 1c. Remaining mid-doc setext H1s -> ATX (so 1d sees them).
text = re.sub(
r"^(?P<title>[^\n#=\-][^\n]*?)[ \t]*\n=[=]{2,}[ \t]*$",
lambda m: f"# {m.group('title').strip()}",
text, flags=re.MULTILINE)
# 1d. Demote every H1 after the first to H2.
def _demote_extra_h1s(src: str) -> str:
fence_open_re = re.compile(r'^[ \t]*(?:`{3,}|~{3,})')
atx_h1_re = re.compile(r'^#\s')
h1_count = 0
in_fence = False
out = []
for line in src.split('\n'):
if fence_open_re.match(line):
in_fence = not in_fence
out.append(line)
continue
if in_fence:
out.append(line)
continue
if atx_h1_re.match(line):
h1_count += 1
if h1_count > 1:
line = '#' + line
out.append(line)
return '\n'.join(out)
text = _demote_extra_h1s(text)
_ADMON_KIND = {"note": "note", "see": "seealso", "warning": "warning",
"sa": "seealso"}
def _admon_repl(m: re.Match) -> str:
kind = _ADMON_KIND[m.group("dir")]
raw = m.group("body")
lines = raw.split("\n")
min_ind = min(
(len(l) - len(l.lstrip()) for l in lines if l.strip()), default=0)
body = "\n".join(l[min_ind:] for l in lines).strip()
return f"\n:::{{{kind}}}\n{body}\n:::\n"
text = re.sub(
r"^[ \t]*@(?P<dir>note|see|warning|sa)[ \t]*\n?(?P<body>.+?)(?=\n[ \t]*\n|\n[ \t]*@[A-Za-z]|\Z)",
_admon_repl, text, flags=re.DOTALL | re.MULTILINE)
# 2. Doxygen LaTeX math markers; preserve indent so blocks inside list items stay in the list.
def _split_adj_math(m: re.Match) -> str:
indent = m.group("indent")
return m.group(0).replace("\\f]\\f[", f"\\f]\n{indent}\\f[")
text = re.sub(r"^(?P<indent>[ \t]*)[^\n]*\\f\]\\f\[",
_split_adj_math, text, flags=re.MULTILINE)
def _fblock(m: re.Match) -> str:
ind = m.group("indent")
return f"\n{ind}$$\n{m.group('body').strip()}\n{ind}$$\n"
text = re.sub(r"^(?P<indent>[ \t]*)\\f\[(?P<body>.+?)\\f\]",
_fblock, text, flags=re.DOTALL | re.MULTILINE)
text = re.sub(r"\\f\[(.+?)\\f\]",
lambda m: f"\n$$\n{m.group(1).strip()}\n$$\n",
text, flags=re.DOTALL)
text = re.sub(r"\\f\$(.+?)\\f\$", lambda m: f"${m.group(1)}$",
text, flags=re.DOTALL)
# 2b. \bordermatrix is Plain-TeX; convert to a `matrix` env, `\cr` -> `\\`.
text = re.sub(
r"\\bordermatrix\s*\{([^}]*)\}",
lambda m: r"\begin{matrix}" + m.group(1).replace(r"\cr", r"\\")
+ r"\end{matrix}",
text)
# 3. @code{.lang} ... @endcode -> fenced block (indent preserved).
def _code_repl(m: re.Match) -> str:
indent = m.group("indent") or ""
lang = _normalize_lang(m.group("lang") or "")
body = m.group("body")
if indent:
body = _textwrap.dedent(body).strip("\n")
body = "\n".join((indent + line) if line else line
for line in body.split("\n"))
return f"\n{indent}```{lang}\n{body}\n{indent}```\n"
return f"\n```{lang}\n{body.strip()}\n```\n"
text = re.sub(
r"^(?P<indent>[ \t]*)@code(?:\{(?P<lang>[^}]*)\})?\s*\n(?P<body>.*?)\n[ \t]*@endcode",
_code_repl, text, flags=re.DOTALL | re.MULTILINE)
# 3b. Tilde fences `~~~~{.lang}` -> backtick fences (MyST reads `{.lang}` as directive).
def _tilde_repl(m: re.Match) -> str:
lang = (m.group("lang") or "").strip().lstrip(".") or "text"
if lang == "none":
lang = "text"
return f"\n```{lang}\n{m.group('body').strip()}\n```\n"
text = re.sub(
r"^~~~+(?:[ \t]*\{(?P<lang>[^}\n]+)\})?[ \t]*\n"
r"(?P<body>.*?)\n^~~~+[ \t]*$",
_tilde_repl, text, flags=re.DOTALL | re.MULTILINE)
# 3c. Fence lexer `yml` -> `yaml` (Pygments warns on 'yml'); runs after 3/3b.
text = re.sub(
r"^(?P<fence>`{3,}|~{3,})yml\b(?P<rest>[ \t]*)$",
lambda m: f"{m.group('fence')}yaml{m.group('rest')}",
text, flags=re.MULTILINE)
# 3d. \htmlonly ... \endhtmlonly -> `{raw} html`.
text = re.sub(
r"\\htmlonly\s*\n(?P<body>.*?)\n\s*\\endhtmlonly",
lambda m: f"\n```{{raw}} html\n{m.group('body').strip()}\n```\n",
text, flags=re.DOTALL)
# 3e. Plain fences with Doxygen lang spec ("```.sh") -> strip dot, alias-map.
text = re.sub(
r"^(?P<fence>`{3,})(?P<lang>\.?[\w-]+)[ \t]*$",
lambda m: f"{m.group('fence')}{_normalize_lang(m.group('lang'))}",
text, flags=re.MULTILINE)
# Backtick fence with per-line indent (other fence forms break in tab-items).
def _emit_codeblock(indent: str, lang: str, body: str) -> str:
body_lines = body.rstrip().splitlines()
body_indented = "\n".join(indent + line for line in body_lines)
return f"\n{indent}```{lang}\n{body_indented}\n{indent}```\n"
# 4. @include path / @includelineno path.
def _include_repl(m: re.Match) -> str:
code, lang = _read_snippet(m.group("path"), None)
return _emit_codeblock(m.group("indent") or "", lang, code)
text = re.sub(r"^(?P<indent>[ \t]*)@include(?:lineno)?\s+(?P<path>\S+)",
_include_repl, text, flags=re.MULTILINE)
# 4b. Drop a stray @snippet right after @end_toggle (duplicate of tab-set).
text = re.sub(
r"(^([ \t]*)@end_toggle[ \t]*\n)\2@snippet[^\n]*\n",
r"\1",
text, flags=re.MULTILINE)
# 4b. @htmlinclude path -> raw HTML embed; lift just <body> for full docs.
def _htmlinclude_repl(m: re.Match) -> str:
rel = m.group("path")
p = next((b / rel for b in _SNIPPET_BASES
if (b / rel).is_file()), None)
if p is None:
name = pathlib.Path(rel).name
for base in _SNIPPET_BASES:
hit = next(iter(base.rglob(name)), None)
if hit is not None:
p = hit
break
if p is None:
return f"<!-- htmlinclude not found: {rel} -->"
raw = p.read_text(encoding="utf-8", errors="replace")
# Allow quoted attrs so a `=>` inside onload="..." doesn't end the tag.
body = re.search(
r"<body\b(?:[^>\"']|\"[^\"]*\"|'[^']*')*>(.*?)</body>",
raw, re.DOTALL | re.IGNORECASE)
inner = body.group(1).strip() if body else raw
return f"\n```{{raw}} html\n{inner}\n```\n"
text = re.sub(r"^@htmlinclude\s+(?P<path>\S+)\s*$",
_htmlinclude_repl, text, flags=re.MULTILINE)
# 5. @snippet path [Label]
def _snippet_repl(m: re.Match) -> str:
code, lang = _read_snippet(m.group("path"), m.group("label"))
return _emit_codeblock(m.group("indent") or "", lang, code)
text = re.sub(
r"^(?P<indent>[ \t]*)@snippet\s+(?P<path>\S+)\s+(?P<label>[^\n]+?)\s*$",
_snippet_repl, text, flags=re.MULTILINE)
# 6. @add_toggle_LANG ... @end_toggle -> one tab-set
def _toggle_collapse(src: str) -> str:
out, i = [], 0
opener = re.compile(r"^\s*@add_toggle_(\w+)\s*$", re.MULTILINE)
while True:
m = opener.search(src, i)
if not m:
out.append(src[i:]); break
out.append(src[i:m.start()])
tabs, j = [], m.start()
while True:
m2 = re.match(
r"\s*@add_toggle_(\w+)\s*\n(.*?)\n\s*@end_toggle\s*\n?",
src[j:], flags=re.DOTALL)
if not m2:
break
tabs.append((m2.group(1), m2.group(2)))
j += m2.end()
k = re.match(r"\s*", src[j:])
if not k or not re.match(r"@add_toggle_", src[j + k.end():]):
break
j += k.end()
if not tabs:
out.append(src[m.start():m.start() + 1]); i = m.start() + 1; continue
out.append(_emit_toggles(tabs))
i = j
return "".join(out)
text = _toggle_collapse(text)
def _link_repl(m: re.Match) -> str:
target = m.group("target")
disp = (m.group("disp") or "").strip().replace('"', '')
return f'@ref {target} "{disp}"' if disp else f"@ref {target}"
text = re.sub(
r"@link\s+(?P<target>[\w-]+)(?P<disp>.*?)@endlink",
_link_repl, text, flags=re.DOTALL)
if docname and (docname.startswith("main_modules/") or docname.startswith("extra_modules/")):
def _rewrite_class_row(m: re.Match) -> str:
kind = m.group("kind")
name = m.group("name") # 'cv::dnn::BackendNode'
page = m.group("page") # 'classcv_1_1dnn_1_1BackendNode'
desc = m.group("desc").strip()
short = name.split("::")[-1]
tparams = _CLASS_TEMPLATE_DISPLAY.get(short, "")
link_label = f"{name}{tparams}"
more = (f'<a class="opencv-class-more" '
f'href="{page}.html#detailed-description">View details</a>')
desc_out = f"{desc} {more}" if desc else more
return (f"| `{kind}` [`{link_label}`]({page}.md) "
f"| {desc_out} |")
text = re.sub(
r"\| \[`(?P<kind>class|struct) (?P<name>cv::[^`]+)`\]"
r"\((?P<page>(?:class|struct)cv_1_1[A-Za-z0-9_]+)\.md\)"
r" \| (?P<desc>[^\n|]*?) \|",
_rewrite_class_row, text)
if docname == "main_modules/core_basic":
# 8c. `{doxygentypedef} cv::Ptr` -> hand-rolled cpp:type (breathe skips C++11 aliases).
text = re.sub(
r"```\{doxygentypedef\} cv::Ptr\s*\n:project: opencv\s*\n```",
"```{eval-rst}\n"
".. cpp:namespace:: cv\n"
".. cpp:type:: template<typename _Tp> Ptr = std::shared_ptr<_Tp>\n"
"```",
text)
# 8a. Name-column typedef anchors -> slugified detail-block id.
text = re.sub(
r"\[`(?P<name>[A-Za-z_][A-Za-z0-9_]*)`\]"
r"\(#(?P<ref>group__[a-z0-9_]+?_1[a-z0-9]+)\)",
lambda m: (f"[`{m.group('name')}`]"
f"(#{re.sub(r'_+', '-', m.group('ref'))})"),
text)
# 8i. Functions-table rows: split broken-anchor link into name link + params span.
def _rewrite_function_row(m: re.Match) -> str:
ret = m.group("ret").strip()
name = m.group("name")
params = m.group("params")
desc = m.group("desc")
slug = _func_slug(name)
return (f"| {ret} | [`cv::{name}`](#{slug}) "
f"`({params})` | {desc} |")
text = re.sub(
r"\| (?P<ret>[^|\n]*?) \| "
r"\[`(?P<name>[^(`\n]+?)\((?P<params>[^`\n]*)\)`\]"
r"\(#group__[a-z0-9_]+?_1[a-z0-9]+\) \| "
r"(?P<desc>[^\n|]*) \|",
_rewrite_function_row, text)
# 8j. Inject a raw HTML anchor before surviving `{doxygenfunction}` (dedup; first wins).
_seen_anchors: set[str] = set()
def _inject_func_anchor(m: re.Match) -> str:
qname = m.group("qname").strip() # "cv::log" / "cv::operator!="
slug = _func_slug(qname.rsplit("::", 1)[-1])
if slug in _seen_anchors:
return m.group(0)
_seen_anchors.add(slug)
return f'<a id="{slug}"></a>\n\n{m.group(0)}'
text = re.sub(
r"^```\{doxygenfunction\} (?P<qname>[^\n(]+)\([^\n]*\n"
r":project: opencv\n"
r"```",
_inject_func_anchor, text, flags=re.MULTILINE)
# 8b. Type-column class code spans -> inline HTML link to sibling api page.
if _LIVE_CLASS_URL:
def _linkify_class_codespan(m: re.Match) -> str:
cls = m.group("cls")
rest = m.group("rest")
full = _LIVE_CLASS_URL.get(cls)
if not full:
return m.group(0)
href = pathlib.PurePosixPath(full).name # same module directory
rest_esc = (rest.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;"))
return (f'<code class="docutils literal notranslate">'
f'<a class="reference internal" href="{href}">{cls}</a>'
f'{rest_esc}</code>')
text = re.sub(
r"`(?P<cls>[A-Z][A-Za-z0-9_]*)(?P<rest><[^`\n]*>)`",
_linkify_class_codespan, text)
# 8g. Linkify tokens step 8b missed: inner 8b `<code>`, then plain code spans.
if _LOCAL_CLASS_URL or _LOCAL_TYPEDEF_URL:
def _token_url(tok: str) -> str | None:
# Tokens absent from the tagfile stay plain.
return _LOCAL_CLASS_URL.get(tok) or _LOCAL_TYPEDEF_URL.get(tok)
# Match an optional `cv::` prefix so the anchor spans `cv::Name`.
_tok_re = re.compile(r"(?:cv::)?_?[A-Za-z][A-Za-z0-9_]*")
def _bare(tok: str) -> str:
return tok[4:] if tok.startswith("cv::") else tok
def _anchor_text(tok: str) -> str:
# Encode `::` so the later cv-linkifier doesn't nest a second anchor.
return tok.replace("::", "&#58;&#58;")
def _linkify_html_segment(seg: str) -> str:
def _sub(m: re.Match) -> str:
url = _token_url(_bare(m.group(0)))
if not url:
return m.group(0)
return (f'<a class="reference internal" '
f'href="{url}">{_anchor_text(m.group(0))}</a>')
return _tok_re.sub(_sub, seg)
def _linkify_inside_code(m: re.Match) -> str:
inner = m.group("inner")
out, i, n = [], 0, len(m.group("inner"))
while i < n:
if inner.startswith("<a ", i):
j = inner.find("</a>", i)
if j < 0:
out.append(inner[i:]); break
out.append(inner[i:j + 4]); i = j + 4
else:
k = inner.find("<a ", i)
if k < 0:
out.append(_linkify_html_segment(inner[i:])); break
out.append(_linkify_html_segment(inner[i:k])); i = k
return m.group("open") + "".join(out) + m.group("close")
text = re.sub(
r'(?P<open><code class="docutils literal notranslate">)'
r'(?P<inner>.*?)(?P<close></code>)',
_linkify_inside_code, text, flags=re.DOTALL)
def _linkify_markdown_codespan(m: re.Match) -> str:
content = m.group("content")
# Prefix-aware: `cv::Name` is one hit so the anchor covers both.
hits = [(t.start(), t.end(), t.group(0)) for t in
_tok_re.finditer(content)
if _token_url(_bare(t.group(0)))]
if not hits:
return m.group(0)
from html import escape as _esc
parts, last = [], 0
for s, e, tok in hits:
parts.append(_esc(content[last:s]))
parts.append(f'<a class="reference internal" '
f'href="{_token_url(_bare(tok))}">'
f'{_anchor_text(tok)}</a>')
last = e
parts.append(_esc(content[last:]))
return (f'<code class="docutils literal notranslate">'
f'{"".join(parts)}</code>')
_masked: list[str] = []
def _mask(m: re.Match) -> str:
_masked.append(m.group(0))
return f"\x00MDLINK{len(_masked)-1}\x00"
text = re.sub(r"\[(?:`[^`\n]+`|<br>)+\]\([^)\n]+\)", _mask, text)
text = re.sub(r"`(?P<content>[^`\n]+?)`",
_linkify_markdown_codespan, text)
text = re.sub(r"\x00MDLINK(\d+)\x00",
lambda m: _masked[int(m.group(1))], text)
# 6c. Bullet lists of @subpage/@ref -> toctree + visible list. Runs BEFORE step 7.
def _subpage_list_to_toctree(src: str) -> str:
bullet = r"^[ \t]*-\s+[^\n@]*?@(?:subpage|ref)\s+[\w-]+(?:[^\n]*)\n"
desc_re = r"(?:(?:[ \t]*\n)*(?:[ \t]+[^\n]+\n)+)*"
pat = re.compile(rf"((?:{bullet}{desc_re}(?:[ \t]*\n)*)+)", re.MULTILINE)
item_pat = re.compile(
rf"^[ \t]*-\s+(?P<prefix>[^\n@]*?)@(?P<kind>subpage|ref)\s+(?P<anchor>[\w-]+)"
rf'(?:[ \t]+"(?P<disp>[^"\n]+)")?'
rf"(?P<inline>[^\n]*)\n"
rf"(?P<desc>{desc_re})",
re.MULTILINE)
def repl(m: re.Match) -> str:
resolved: list[tuple[str, str, str, str, str, str, str]] = []
for im in item_pat.finditer(m.group(1)):
kind = im.group("kind")
anchor = im.group("anchor")
disp = im.group("disp")
prefix = (im.group("prefix") or "").strip()
inline = im.group("inline") or ""
desc_lines = [l.strip() for l in (im.group("desc") or "").splitlines() if l.strip()]
description = " ".join(desc_lines)
# @ref follows redirects; @subpage uses literal target.
lookup = _resolve_redirect(anchor) if kind == "ref" else anchor
if lookup in _ANCHOR_TO_DOC:
resolved.append((kind, "internal", _ANCHOR_TO_DOC[lookup],
disp or _ANCHOR_TO_TITLE.get(lookup, lookup),
description, prefix, inline))
elif lookup in _ANCHOR_TO_EXTERNAL:
title, url = _ANCHOR_TO_EXTERNAL[lookup]
resolved.append((kind, "external", url, disp or title,
description, prefix, inline))
elif lookup in _TAG_FILENAMES:
title = _TAG_TITLES.get(lookup, lookup)
resolved.append((kind, "external", _doxygen_url(lookup),
disp or title, description, prefix, inline))
if not resolved:
return ""
# toctree gets only @subpage (internal @ref would create cycles).
tt_lines = []
for t in resolved:
kind, doctype, target, title = t[0], t[1], t[2], t[3]
if kind != "subpage":
continue
tt_lines.append("/" + target if doctype == "internal"
else f"{title} <{target}>")
tt_body = "\n".join(tt_lines)
has_descriptions = any(t[4] for t in resolved)
has_prefixes = any(t[5] for t in resolved)
# `inline` alone stays a plain toctree; visible mode needs desc/prefix.
if not (has_descriptions or has_prefixes):
if tt_body:
return f"\n```{{toctree}}\n:maxdepth: 1\n:titlesonly:\n\n{tt_body}\n```\n"
return ""
# Hidden toctree (subpages) + visible list (all items).
list_lines = []
for t in resolved:
doctype, target, title, desc, prefix, inline = t[1], t[2], t[3], t[4], t[5], t[6]
href = f"/{target}" if doctype == "internal" else target
prefix_text = f"{prefix} " if prefix else ""
list_lines.append(f"- {prefix_text}[{title}]({href}){inline}")
if desc:
list_lines.append("")
list_lines.append(f" {desc}")
list_lines.append("")
preamble = (f"\n```{{toctree}}\n:hidden:\n:maxdepth: 1\n:titlesonly:\n\n{tt_body}\n```\n"
if tt_body else "")
return f"{preamble}\n{chr(10).join(list_lines).rstrip()}\n"
return pat.sub(repl, src)
text = _subpage_list_to_toctree(text)
# 7. @ref name -> internal docname / Doxygen tag URL / fragment fallback.
def _ref_repl(m: re.Match) -> str:
name = m.group("name"); disp = m.group("disp")
resolved = _resolve_redirect(name)
target = _ANCHOR_TO_DOC.get(resolved)
if target:
link_text = (disp or _ANCHOR_TO_TITLE.get(resolved)
or _TAG_TITLES.get(resolved) or resolved)
return f"[{link_text}](/{target})"
if resolved in _TAG_FILENAMES:
link_text = disp or _TAG_TITLES.get(resolved, resolved)
return f"[{link_text}]({_doxygen_url(resolved)})"
return f"[{disp or resolved}](#{resolved})"
# `:` allowed for qualified C++ identifiers.
text = re.sub(r'@ref\s+(?P<name>[\w:-]+)(?:\s+"(?P<disp>[^"]+)")?',
_ref_repl, text)
# 8. @cite KEY -> `[N]` HTML anchor to citelist (N from opencv.bib order).
def _cite_repl(m: re.Match) -> str:
key = m.group("key")
num = _CITE_NUMBER.get(key)
label = f"[{num}]" if num is not None else f"[{key}]"
if "citelist" in _ANCHOR_TO_DOC:
depth = docname.count("/") if docname else 0
href = ("../" * depth) + f"citelist.html#CITEREF_{key}"
else:
href = f"{DOXYGEN_BASE_URL}citelist.html#CITEREF_{key}"
return f'<a href="{href}">{label}</a>'
text = _apply_outside_code(text, lambda chunk: re.sub(
r"@cite\s+(?P<key>[\w-]+)", _cite_repl, chunk))
if _CITE_NUMBER and docname != "citelist":
_CITE_KEY_RE = re.compile(
r"(?<![\[\w])(?P<key>"
+ "|".join(re.escape(k) for k in _CITE_NUMBER)
+ r")(?![\w\]])"
)
def _bare_cite_repl(m: re.Match) -> str:
key = m.group("key")
num = _CITE_NUMBER.get(key)
label = f"[{num}]" if num is not None else f"[{key}]"
if "citelist" in _ANCHOR_TO_DOC:
depth = docname.count("/") if docname else 0
href = ("../" * depth) + f"citelist.html#CITEREF_{key}"
else:
href = f"{DOXYGEN_BASE_URL}citelist.html#CITEREF_{key}"
return f'<a href="{href}">{label}</a>'
text = _apply_outside_code(
text, lambda chunk: _CITE_KEY_RE.sub(_bare_cite_repl, chunk))
# 8b. @youtube{ID} -> responsive raw-HTML embed.
text = re.sub(
r"^@youtube\{(?P<id>[\w-]+)\}\s*$",
lambda m: (
'\n<div class="opencv-youtube">'
f'<iframe src="https://www.youtube-nocookie.com/embed/{m.group("id")}" '
'title="YouTube video player" frameborder="0" '
'allow="accelerometer; autoplay; clipboard-write; encrypted-media; '
'gyroscope; picture-in-picture" allowfullscreen></iframe></div>\n'
),
text, flags=re.MULTILINE)
# 8c. @note / @see / @warning handled at step 1b above.
# 8d. Dedent indented descriptions after `- @subpage X`.
def _dedent_subpage_descriptions(src: str) -> str:
# Continuation indent: 4+ spaces/tabs or a single leading tab.
pat = re.compile(
r"^(?P<bullet>[ \t]*-\s+[^\n]*@subpage\s+[\w-]+[^\n]*)\n"
r"(?P<desc>(?:[ \t]*\n|(?:\t|[ \t]{4,})[^\n]+(?:\n|$))+)",
re.MULTILINE)
def repl(m: re.Match) -> str:
desc = _textwrap.dedent(m.group("desc")).strip("\n")
if not desc.strip():
return m.group(0)
return f"{m.group('bullet')}\n\n{desc}\n\n"
return pat.sub(repl, src)
text = _dedent_subpage_descriptions(text)
# 10. @next_tutorial / @prev_tutorial -> drop
text = re.sub(r"^@(?:next|prev)_tutorial\{[^}]*\}\s*$", "",
text, flags=re.MULTILINE)
# 11. @tableofcontents / [TOC] -> drop
text = re.sub(r"^(?:@tableofcontents|\[TOC\])\s*$", "",
text, flags=re.MULTILINE)
# 11b. @cond/@endcond and @parblock/@endparblock -> strip markers.
text = re.sub(r"^@cond\s+\S+\s*$", "", text, flags=re.MULTILINE)
text = re.sub(r"^@endcond\s*$", "", text, flags=re.MULTILINE)
text = re.sub(r"^[ \t]*@parblock\s*$", "", text, flags=re.MULTILINE)
text = re.sub(r"^[ \t]*@endparblock\s*$", "", text, flags=re.MULTILINE)
# 11c. @anchor NAME -> MyST label "(NAME)=".
text = re.sub(r"^@anchor\s+(?P<name>[\w-]+)\s*$",
lambda m: f"({m.group('name')})=",
text, flags=re.MULTILINE)
# 11d. `-#` -> `1.` (indent preserved).
text = re.sub(r"^(?P<indent>[ \t]*)-#(?P<sp>[ \t]+)",
lambda m: f"{m.group('indent')}1.{m.group('sp')}",
text, flags=re.MULTILINE)
# 11e. Normalize 5+ space bullet markers to 3 (avoids code-block reading).
def _normalize_over_indented_markers(src: str) -> str:
lines_in = src.split("\n")
out: list[str] = []
i = 0
while i < len(lines_in):
m = re.match(r"^([ \t]*)([-*+])( {5,})(.*)", lines_in[i])
if m:
outer, marker, spaces, content = (
m.group(1), m.group(2), m.group(3), m.group(4))
old_col = len(outer) + 1 + len(spaces)
new_col = len(outer) + 1 + 3
delta = old_col - new_col
out.append(f"{outer}{marker} {content}")
i += 1
while i < len(lines_in):
line = lines_in[i]
stripped = line.lstrip(" \t")
actual = len(line) - len(stripped)
if not stripped:
out.append(line); i += 1; continue
if actual >= old_col:
out.append(" " * (actual - delta) + stripped); i += 1
else:
break
else:
out.append(lines_in[i]); i += 1
return "\n".join(out)
text = _normalize_over_indented_markers(text)
# 11f. Strip one 4-space level from bullets right after a heading.
text = re.sub(
r"(^#{1,6}[ \t][^\n]+\n(?:[ \t]*\n)*)((?: [ \t]*[-*+][^\n]*\n)+)",
lambda m: m.group(1) + re.sub(r"^ ", "", m.group(2), flags=re.MULTILINE),
text, flags=re.MULTILINE)
# Depth-relative prefix to site root for contrib_modules/ <img src> URLs.
_depth = docname.count("/") if docname else 0
_contrib_url_prefix = ("../" * _depth) + "contrib_modules/"
def _emit_contrib_img(rel_url: str, alt: str) -> str:
"""Raw-HTML <img>/<figure> for a contrib_modules/<rel> path."""
src = _contrib_url_prefix + rel_url
img = f'<img src="{src}" alt="{alt}"/>'
if alt.startswith("Figure "):
return (f'<figure>{img}'
f'<figcaption>{alt}</figcaption></figure>')
return img
# 12. Image paths -> doc-sibling, global basename, or tutorials/others/images/.
def _img_repl(m: re.Match) -> str:
alt = m.group("alt")
rel = m.group("rel")
asset_dir = m.group("dir")
if docname:
parts = pathlib.Path(docname).parent.parts
local = None
if parts and parts[0] in ("tutorials", "js_tutorials", "py_tutorials"):
local = DOC_ROOT / pathlib.Path(docname).parent / asset_dir / rel
elif len(parts) >= 2 and parts[0] == "tutorials_contrib":
rest = pathlib.Path(*parts[2:]) if len(parts) > 2 else pathlib.Path()
local = CONTRIB_ROOT / parts[1] / "tutorials" / rest / asset_dir / rel
if local is not None and local.is_file():
return f'![{alt}]({asset_dir}/{rel})'
hit = _IMAGE_INDEX.get(pathlib.Path(rel).name)
if hit:
if hit.startswith("contrib_modules/"):
return _emit_contrib_img(hit[len("contrib_modules/"):], alt)
return f'![{alt}](/{hit})'
return f'![{alt}](/tutorials/others/images/{rel})'
text = re.sub(
r'!\[(?P<alt>[^\]]*)\]\((?:[^)]*?/)?(?P<dir>images|js_assets)/(?P<rel>[^)]+)\)',
_img_repl, text)
# 12b. Cross-tree contrib image refs -> raw-HTML <img>, depth-relative URL.
def _img_xtree(m: re.Match) -> str:
alt, rel = m.group("alt"), m.group("rel")
if rel.startswith("/") or "://" in rel:
return m.group(0)
if rel.startswith("./"):
rel = rel[2:]
if not docname or not docname.startswith("tutorials_contrib/"):
return m.group(0)
parts = pathlib.Path(docname).parent.parts
if len(parts) < 2:
return m.group(0)
module = parts[1]
for cand in (f"{module}/doc/{rel}",
f"{module}/{rel}",
rel):
if (CONTRIB_ROOT / cand).is_file():
return _emit_contrib_img(cand, alt)
return m.group(0)
text = re.sub(
r'!\[(?P<alt>[^\]]*)\]\((?P<rel>[^)]+)\)',
_img_xtree, text)
# 12d. Blank line between consecutive `Label: ![](image)` lines (skip table rows).
text = re.sub(
r"^(?P<line>(?!\|)[^\n]*!\[[^\]]*\]\([^)]+\)[^\n]*)\n"
r"(?=(?!\|)[^\n]*!\[[^\]]*\]\([^)]+\))",
r"\g<line>\n\n", text, flags=re.MULTILINE)
# 12e. `![Figure N: caption](url)` -> MyST `{figure}`.
text = re.sub(
r"^(?P<indent>[ \t]*)!\[(?P<caption>Figure\s[^\]]+)\]\((?P<url>[^)]+)\)\s*$",
lambda m: (f"{m.group('indent')}:::{{figure}} {m.group('url')}\n"
f"{m.group('indent')}{m.group('caption')}\n"
f"{m.group('indent')}:::"),
text, flags=re.MULTILINE)
# 13. Wrap front-matter table in a `.opencv-meta-table` div.
def _wrap_front_matter(src: str) -> str:
pat = re.compile(
r"(^\|[^\n]*\|[ \t]*\n"
r"\|[ \t]*-:[ \t]*\|[ \t]*:-[ \t]*\|[ \t]*\n"
r"(?:\|[^\n]*\|[ \t]*\n)+)",
re.MULTILINE)
def repl(m: re.Match) -> str:
return f":::{{div}} opencv-meta-table\n\n{m.group(1)}\n:::\n"
return pat.sub(repl, src, count=1)
text = _wrap_front_matter(text)
# 14a. Auto-link `cv.SymbolName` in prose.
text = _linkify_cv_symbols(text)
# 14b. Wrap bare URLs in `<...>` (runs after 14a).
text = _linkify_bare_urls(text)
# 14c. Resolve `opencv_source_code/<path>` Doxygen alias -> GitHub link.
text = _linkify_opencv_source_code(text)
# 14d. Resolve Doxygen `#funcName` cross-references in prose -> link.
text = _linkify_dox_hash_refs(text)
# 15. Restore @verbatim stash (see step 0v).
for _vk, _vv in _verbatim_stash.items():
text = text.replace(_vk, _vv)
return text
_BARE_URL_RE = re.compile(
r"(?<![<\[(\w\"'=])"
r"(?P<url>https?://[^\s<>()`\"']+[^\s<>()`\"'.,;:!?])"
)
# `cv.X`/`cv::X`; lookbehind blocks `frame.cv.X`-style false positives.
_CV_SYMBOL_RE = re.compile(
r"(?<![/\w.:\[])(?P<sep>cv(?:\.|::))(?P<sym>[A-Za-z_]\w*)(?P<parens>\(\))?"
)
# Bare `funcName()` (Doxygen auto-linked these).
_BARE_FN_RE = re.compile(r"(?<![/\w.<>\"])(?P<sym>[A-Za-z_]\w{2,})\(\)")
_FENCED_BLOCK_RE = re.compile(
r"^(?P<fence>[`~]{3,})[^\n]*\n[\s\S]*?\n(?P=fence)[ \t]*$",
re.MULTILINE,
)
_INLINE_CODE_RE = re.compile(r"`+[^`\n]*?`+")
# ATX heading line; exempted from the auto-linkifier.
_ATX_HEADING_RE = re.compile(r"^[ \t]{0,3}#{1,6}[ \t]")
def _apply_outside_code(src: str, transform) -> str:
"""Apply `transform` to regions outside fenced/inline code."""
def _segment(text: str) -> str:
out, last = [], 0
for cm in _INLINE_CODE_RE.finditer(text):
out.append(transform(text[last:cm.start()]))
out.append(cm.group(0))
last = cm.end()
out.append(transform(text[last:]))
return "".join(out)
out, last = [], 0
for fm in _FENCED_BLOCK_RE.finditer(src):
out.append(_segment(src[last:fm.start()]))
out.append(fm.group(0))
last = fm.end()
out.append(_segment(src[last:]))
return "".join(out)
def _linkify_bare_urls(src: str) -> str:
return _apply_outside_code(src,
lambda chunk: _BARE_URL_RE.sub(r"<\g<url>>", chunk))
_OPENCV_SOURCE_CODE_RE = re.compile(
r"\bopencv_source_code/(?P<path>[\w./\-]+)"
)
def _linkify_opencv_source_code(src: str) -> str:
def _repl(m: re.Match) -> str:
path = m.group("path")
url = f"https://github.com/opencv/opencv/blob/5.x/{path}"
return f"[opencv_source_code/{path}]({url})"
return _apply_outside_code(
src, lambda chunk: _OPENCV_SOURCE_CODE_RE.sub(_repl, chunk))
_DOX_HASH_REF_RE = re.compile(
r"(?<![{(#\w])#(?P<name>[A-Za-z_]\w*)\b"
)
def _linkify_dox_hash_refs(src: str) -> str:
if not _CV_SYMBOL_URL:
return src
def _repl(m: re.Match) -> str:
name = m.group("name")
url = _CV_SYMBOL_URL.get(name)
if not url:
return m.group(0)
return f"[{name}]({url})"
return _apply_outside_code(
src, lambda chunk: _DOX_HASH_REF_RE.sub(_repl, chunk))
def _linkify_cv_symbols(src: str) -> str:
if not _CV_SYMBOL_URL:
return src
def _local_url(sym: str) -> str | None:
"""Prefer a Sphinx-local URL when one exists for `sym`.
`_LOCAL_CLASS_URL` and `_LOCAL_TYPEDEF_URL` are populated from
the tagfile but rewritten to point at the local api/ tree
(e.g. `classcv_1_1Mat.html`, `core_basic.html#vec2b`). If no
local entry exists, return None so the caller drops the link —
we never bounce readers off-site for symbols whose Sphinx
version isn't present in this build.
"""
return _LOCAL_CLASS_URL.get(sym) or _LOCAL_TYPEDEF_URL.get(sym)
def repl_cv(m: re.Match) -> str:
sym = m.group("sym")
url = _local_url(sym)
if not url:
return m.group(0)
sep = m.group("sep")
parens = m.group("parens") or ""
return f'<a href="{url}">{sep}{sym}{parens}</a>'
def repl_bare(m: re.Match) -> str:
sym = m.group("sym")
url = _local_url(sym)
if not url:
return m.group(0)
return f'<a href="{url}">{sym}()</a>'
def transform(chunk: str) -> str:
# Headings are definitions — don't linkify.
out_lines = []
for line in chunk.split("\n"):
if _ATX_HEADING_RE.match(line):
out_lines.append(line)
continue
line = _CV_SYMBOL_RE.sub(repl_cv, line)
line = _BARE_FN_RE.sub(repl_bare, line)
out_lines.append(line)
return "\n".join(out_lines)
return _apply_outside_code(src, transform)
_referenced_docs_cache: set[str] | None = None
def _referenced_docs() -> set[str]:
"""Docnames reachable from some @subpage/@ref (computed once)."""
global _referenced_docs_cache
if _referenced_docs_cache is None:
_referenced_docs_cache = {
_ANCHOR_TO_DOC[a] for a in _REFERENCED_ANCHORS
if a in _ANCHOR_TO_DOC
}
return _referenced_docs_cache
_TUTORIAL_PREFIXES = ("tutorials/", "js_tutorials/",
"py_tutorials/", "tutorials_contrib/")
def _source_read(app, docname, source):
# Translate tutorial docs + API stubs; doxygengroup bodies pass through.
if not (docname.startswith("tutorials/")
or docname.startswith("js_tutorials/")
or docname.startswith("py_tutorials/")
or docname.startswith("tutorials_contrib/")
or docname.startswith("main_modules/")
or docname.startswith("extra_modules/")
or docname == "faq"
or docname == "citelist"
or docname == "intro"
or docname == "index"):
return
text = source[0]
# Master doc: append contrib/api roots without editing tutorials.markdown.
if docname == "tutorials/tutorials" and not USE_INDEX_LANDING:
if CONTRIB_MODULES and "tutorial_contrib_root" in _ANCHOR_TO_DOC:
text = text.rstrip() + "\n\n- @subpage tutorial_contrib_root\n"
if API_MODULES and "api_root" in _ANCHOR_TO_DOC:
text = text.rstrip() + "\n\n- @subpage api_root\n"
out = _translate(text, docname)
# Mark unreferenced tutorial pages :orphan: (skip master/front-matter/referenced).
if (docname.startswith(_TUTORIAL_PREFIXES)
and docname != "tutorials/tutorials"
and docname not in _referenced_docs()
and not out.lstrip().startswith("---")):
out = "---\norphan: true\n---\n\n" + out
source[0] = out
File diff suppressed because it is too large Load Diff
+47
View File
@@ -0,0 +1,47 @@
accessible-pygments==0.0.5
alabaster==1.0.0
anyio==4.13.0
babel==2.18.0
beautifulsoup4==4.14.3
breathe==4.36.0
certifi==2026.4.22
charset-normalizer==3.4.7
click==8.4.0
colorama==0.4.6
docutils==0.21.2
exceptiongroup==1.3.1
exhale==0.3.7
h11==0.16.0
idna==3.15
imagesize==2.0.0
Jinja2==3.1.6
lxml==6.1.1
markdown-it-py==3.0.0
MarkupSafe==3.0.3
mdit-py-plugins==0.6.1
mdurl==0.1.2
myst-parser==4.0.1
packaging==26.0
pydata-sphinx-theme==0.17.1
Pygments==2.20.0
PyYAML==6.0.3
requests==2.34.2
six==1.17.0
snowballstemmer==3.0.1
soupsieve==2.8.3
Sphinx==8.1.3
sphinx-autobuild==2024.10.3
sphinx_design==0.6.1
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
starlette==1.0.0
tomli==2.4.1
typing_extensions==4.15.0
urllib3==2.7.0
uvicorn==0.47.0
watchfiles==1.2.0
websockets==16.0
+9 -1
View File
@@ -87,7 +87,15 @@ install(FILES ${OPENCV_JAR_FILE} OPTIONAL DESTINATION ${OPENCV_JAR_INSTALL_PATH}
add_dependencies(${the_module} ${the_module}_jar)
if(BUILD_DOCS)
# Javadoc generation can be disabled independently of BUILD_DOCS so the C++
# Doxygen / Sphinx docs still build when the Java bindings carry Doxygen-style
# tags (e.g. @retval, @remarks) that JDK doclint rejects as fatal errors.
# Default ON for parity with upstream; when OFF the `doxygen_javadoc` target is
# never created and doc/CMakeLists.txt's `if(TARGET doxygen_javadoc)` guard
# drops it from the doxygen_cpp dependency chain automatically.
option(BUILD_JAVADOC "Generate Javadoc as part of the documentation build" ON)
if(BUILD_DOCS AND BUILD_JAVADOC)
if(OPENCV_JAVA_SDK_BUILD_TYPE STREQUAL "ANT")
add_custom_command(OUTPUT "${OPENCV_DEPHELPER}/${the_module}doc"
COMMAND ${ANT_EXECUTABLE} -noinput -k javadoc