vendor: OpenCV 5.0.0 snapshot at 40738fb16ceddb5fb3fea747585f7ce6abb0605b
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
if(NOT BUILD_DOCS)
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Prefer the sphinx-build that lives in the conda env where myst-parser /
|
||||
# pydata-sphinx-theme / breathe / exhale are installed. Override on the
|
||||
# CMake command line: -DSPHINX_BUILD=/path/to/sphinx-build
|
||||
find_program(SPHINX_BUILD
|
||||
NAMES sphinx-build
|
||||
DOC "Path to sphinx-build tool")
|
||||
|
||||
if(NOT SPHINX_BUILD)
|
||||
message(STATUS "docs_sphinx: sphinx-build not found; `sphinx` target disabled")
|
||||
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")
|
||||
|
||||
set(_SPHINX_INPUT_TUTORIALS "${_SPHINX_INPUT_ROOT}/tutorials")
|
||||
set(_SPHINX_INPUT_CONTRIB "${_SPHINX_INPUT_ROOT}/tutorials_contrib")
|
||||
file(REMOVE_RECURSE "${_SPHINX_INPUT_ROOT}")
|
||||
file(MAKE_DIRECTORY "${_SPHINX_INPUT_TUTORIALS}")
|
||||
|
||||
# Main tree: symlink the master file + each main module subtree.
|
||||
file(CREATE_LINK
|
||||
"${CMAKE_SOURCE_DIR}/doc/tutorials/tutorials.markdown"
|
||||
"${_SPHINX_INPUT_TUTORIALS}/tutorials.markdown"
|
||||
SYMBOLIC COPY_ON_ERROR)
|
||||
|
||||
file(CREATE_LINK
|
||||
"${CMAKE_SOURCE_DIR}/doc/faq.markdown"
|
||||
"${_SPHINX_INPUT_ROOT}/faq.markdown"
|
||||
SYMBOLIC COPY_ON_ERROR)
|
||||
file(CREATE_LINK
|
||||
"${CMAKE_SOURCE_DIR}/modules/core/doc/intro.markdown"
|
||||
"${_SPHINX_INPUT_ROOT}/intro.markdown"
|
||||
SYMBOLIC COPY_ON_ERROR)
|
||||
file(GLOB _main_tutorial_children
|
||||
RELATIVE "${CMAKE_SOURCE_DIR}/doc/tutorials"
|
||||
"${CMAKE_SOURCE_DIR}/doc/tutorials/*")
|
||||
foreach(_d ${_main_tutorial_children})
|
||||
if(IS_DIRECTORY "${CMAKE_SOURCE_DIR}/doc/tutorials/${_d}")
|
||||
file(CREATE_LINK
|
||||
"${CMAKE_SOURCE_DIR}/doc/tutorials/${_d}"
|
||||
"${_SPHINX_INPUT_TUTORIALS}/${_d}"
|
||||
SYMBOLIC COPY_ON_ERROR)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# js_tutorials, py_tutorials, images/ sit directly under opencv/doc/.
|
||||
foreach(_root js_tutorials py_tutorials images)
|
||||
if(EXISTS "${CMAKE_SOURCE_DIR}/doc/${_root}")
|
||||
file(CREATE_LINK
|
||||
"${CMAKE_SOURCE_DIR}/doc/${_root}"
|
||||
"${_SPHINX_INPUT_ROOT}/${_root}"
|
||||
SYMBOLIC COPY_ON_ERROR)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
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")
|
||||
file(WRITE "${_contrib_root_md}"
|
||||
"Tutorials for contrib modules {#tutorial_contrib_root}\n"
|
||||
"=============================\n\n")
|
||||
file(GLOB _contrib_subdirs RELATIVE "${OPENCV_EXTRA_MODULES_PATH}"
|
||||
"${OPENCV_EXTRA_MODULES_PATH}/*")
|
||||
foreach(_m ${_contrib_subdirs})
|
||||
set(_tut_dir "${OPENCV_EXTRA_MODULES_PATH}/${_m}/tutorials")
|
||||
if(IS_DIRECTORY "${_tut_dir}")
|
||||
file(CREATE_LINK "${_tut_dir}" "${_SPHINX_INPUT_CONTRIB}/${_m}"
|
||||
SYMBOLIC COPY_ON_ERROR)
|
||||
file(GLOB _tocs RELATIVE "${_tut_dir}" "${_tut_dir}/*.markdown")
|
||||
foreach(_t ${_tocs})
|
||||
file(STRINGS "${_tut_dir}/${_t}" _id LIMIT_COUNT 1 REGEX ".*\\{#[^}]+\\}")
|
||||
string(REGEX REPLACE ".*\\{#([^}]+)\\}.*" "\\1" _id "${_id}")
|
||||
if(_id)
|
||||
file(APPEND "${_contrib_root_md}" "- @subpage ${_id}\n")
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
set(_LEGACY_DOXYFILE "${CMAKE_BINARY_DIR}/doc/Doxyfile")
|
||||
set(_DOXYGEN_XML_DIR "${CMAKE_BINARY_DIR}/doc/doxygen/xml")
|
||||
|
||||
if(NOT DEFINED SPHINX_API_MODULES)
|
||||
# Any module whose include tree declares an @defgroup, keyed on dir name; matlab excluded.
|
||||
set(SPHINX_API_MODULES "")
|
||||
set(_api_module_roots "${CMAKE_SOURCE_DIR}/modules")
|
||||
if(OPENCV_EXTRA_MODULES_PATH AND EXISTS "${OPENCV_EXTRA_MODULES_PATH}")
|
||||
list(APPEND _api_module_roots "${OPENCV_EXTRA_MODULES_PATH}")
|
||||
endif()
|
||||
foreach(_root ${_api_module_roots})
|
||||
file(GLOB _api_module_dirs RELATIVE "${_root}" "${_root}/*")
|
||||
foreach(_m ${_api_module_dirs})
|
||||
if(IS_DIRECTORY "${_root}/${_m}/include/opencv2" AND NOT _m STREQUAL "matlab")
|
||||
file(GLOB_RECURSE _api_hdrs "${_root}/${_m}/include/opencv2/*.hpp")
|
||||
foreach(_hdr ${_api_hdrs})
|
||||
file(READ "${_hdr}" _hdr_contents)
|
||||
if(_hdr_contents MATCHES "@defgroup")
|
||||
list(APPEND SPHINX_API_MODULES "${_m}")
|
||||
break()
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
endforeach()
|
||||
endforeach()
|
||||
list(REMOVE_DUPLICATES SPHINX_API_MODULES)
|
||||
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"
|
||||
"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")
|
||||
# opencv.js (JS Try-it): download once into the build dir, bundled into html output.
|
||||
set(_OPENCV_JS "${CMAKE_CURRENT_BINARY_DIR}/opencv.js")
|
||||
if(NOT EXISTS "${_OPENCV_JS}")
|
||||
message(STATUS "docs_sphinx: downloading opencv.js (one-time)")
|
||||
file(DOWNLOAD "https://docs.opencv.org/5.x/opencv.js" "${_OPENCV_JS}"
|
||||
SHOW_PROGRESS STATUS _DL_STATUS)
|
||||
list(GET _DL_STATUS 0 _DL_OK)
|
||||
if(NOT _DL_OK EQUAL 0)
|
||||
message(WARNING "docs_sphinx: opencv.js download failed — Try-it won't run")
|
||||
file(REMOVE "${_OPENCV_JS}")
|
||||
set(_OPENCV_JS "")
|
||||
endif()
|
||||
endif()
|
||||
if(_OPENCV_JS)
|
||||
list(APPEND _SPHINX_ENV "OPENCV_JS_PATH=${_OPENCV_JS}")
|
||||
endif()
|
||||
|
||||
add_custom_target(sphinx
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${_SPHINX_OUTDIR}
|
||||
COMMAND ${CMAKE_COMMAND} -E env ${_SPHINX_ENV}
|
||||
${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} (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}
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -rf ${CMAKE_CURRENT_BINARY_DIR}/.doctrees
|
||||
COMMENT "Removing ${_SPHINX_OUTDIR} and .doctrees env cache"
|
||||
VERBATIM)
|
||||
|
||||
message(STATUS "docs_sphinx: sphinx target enabled (sphinx-build: ${SPHINX_BUILD})")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> <svg xmlns="http://www.w3.org/2000/svg" width="164" height="153" viewBox="0 0 164 153" fill="none"><path d="M144.618 79.1998C156.154 85.9868 163.907 98.5221 163.932 112.877C163.969 134.484 146.484 152.031 124.877 152.068C103.269 152.106 85.7225 134.62 85.6847 113.013C85.6597 98.6587 93.3683 86.0964 104.881 79.2691L116.123 98.2666C116.405 98.7431 116.245 99.3554 115.787 99.6669C111.536 102.561 108.748 107.443 108.758 112.973C108.773 121.837 115.972 129.011 124.836 128.995C133.701 128.98 140.874 121.781 140.859 112.917C140.849 107.387 138.044 102.515 133.783 99.6355C133.324 99.3256 133.162 98.7139 133.442 98.2364L144.618 79.1998Z" fill="#128DFF"></path><path d="M58.2668 78.9714C52.6177 75.8052 46.1027 74 39.1662 74C17.5588 74 0.0426025 91.5162 0.0426025 113.124C0.0426025 134.731 17.5588 152.247 39.1662 152.247C60.8798 152.247 78.8229 133.813 78.2771 112.12H56.2529C55.6746 112.12 55.2192 112.609 55.2155 113.188C55.1596 121.833 47.9463 129.174 39.1662 129.174C30.3016 129.174 23.1155 121.988 23.1155 113.124C23.1155 104.259 30.3016 97.0729 39.1662 97.0729C41.4876 97.0729 43.694 97.5657 45.6863 98.4525C46.1732 98.6692 46.7542 98.505 47.0247 98.0459L58.2668 78.9714Z" fill="#8BDA67"></path><path d="M61.431 72.834C49.9062 66.0268 42.1757 53.4779 42.1757 39.1235C42.1757 17.5162 59.6919 0 81.2992 0C102.907 0 120.423 17.5162 120.423 39.1235C120.423 53.4779 112.692 66.0268 101.167 72.834L89.9591 53.8169C89.678 53.3399 89.8386 52.7279 90.2968 52.4171C94.5531 49.5307 97.3499 44.6537 97.3499 39.1235C97.3499 30.259 90.1638 23.0729 81.2992 23.0729C72.4347 23.0729 65.2485 30.259 65.2485 39.1235C65.2485 44.6537 68.0453 49.5307 72.3016 52.4171C72.7599 52.7279 72.9204 53.3399 72.6393 53.8169L61.431 72.834Z" fill="#FF2A44"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,135 @@
|
||||
/* Single source of truth for the documentation version dropdown.
|
||||
*
|
||||
* Consumed by BOTH:
|
||||
* - the legacy Doxygen pages (2.x .. 4.x, 5.0.0-pre): the block below
|
||||
* renders this list into the `#projectnumber` span next to the logo
|
||||
* (needs jQuery, which those pages already load); and
|
||||
* - the new Sphinx (PyData) 5.0 docs: the navbar switcher template reads
|
||||
* `window.OPENCV_DOC_VERSIONS` directly and builds its own <select>.
|
||||
*
|
||||
* To publish a new release, add ONE entry here ['<label>', '<site-path>']
|
||||
* and redeploy this file to the bucket root — the option then appears in
|
||||
* the dropdown on every version's pages at once. Do not maintain a second
|
||||
* list anywhere. */
|
||||
window.OPENCV_DOC_VERSIONS = [
|
||||
['4.13.0', '/4.13.0'],
|
||||
['4.12.0', '/4.12.0'],
|
||||
['4.11.0', '/4.11.0'],
|
||||
['5.0', '/5.0'],
|
||||
['5.0.0alpha', '/5.0.0-alpha'],
|
||||
['4.10.0', '/4.10.0'],
|
||||
['4.9.0', '/4.9.0'],
|
||||
['4.8.0', '/4.8.0'],
|
||||
['4.7.0', '/4.7.0'],
|
||||
['4.6.0', '/4.6.0'],
|
||||
['4.5.5', '/4.5.5'],
|
||||
['4.5.4', '/4.5.4'],
|
||||
['4.5.3', '/4.5.3'],
|
||||
['4.5.2', '/4.5.2'],
|
||||
['4.5.1', '/4.5.1'],
|
||||
['4.5.0', '/4.5.0'],
|
||||
['4.4.0', '/4.4.0'],
|
||||
['4.3.0', '/4.3.0'],
|
||||
['4.2.0', '/4.2.0'],
|
||||
['4.1.2', '/4.1.2'],
|
||||
['4.1.1', '/4.1.1'],
|
||||
['4.1.0', '/4.1.0'],
|
||||
['4.0.1', '/4.0.1'],
|
||||
['4.0.0', '/4.0.0'],
|
||||
// no more 3.4 releases: ['3.4.21-pre', '/3.4'],
|
||||
['3.4.20-dev', '/3.4'],
|
||||
['3.4.20', '/3.4.20'],
|
||||
['3.4.19', '/3.4.19'],
|
||||
['3.4.18', '/3.4.18'],
|
||||
['3.4.17', '/3.4.17'],
|
||||
['3.4.16', '/3.4.16'],
|
||||
['3.4.15', '/3.4.15'],
|
||||
['3.4.14', '/3.4.14'],
|
||||
['3.4.13', '/3.4.13'],
|
||||
['3.4.12', '/3.4.12'],
|
||||
['3.4.11', '/3.4.11'],
|
||||
['3.4.10', '/3.4.10'],
|
||||
['3.4.9', '/3.4.9'],
|
||||
['3.4.8', '/3.4.8'],
|
||||
['3.4.7', '/3.4.7'],
|
||||
['3.4.6', '/3.4.6'],
|
||||
['3.4.5', '/3.4.5'],
|
||||
['3.4.4', '/3.4.4'],
|
||||
['3.4.3', '/3.4.3'],
|
||||
['3.4.2', '/3.4.2'],
|
||||
['3.4.1', '/3.4.1'],
|
||||
['3.4.0', '/3.4.0'],
|
||||
['3.3.1', '/3.3.1'],
|
||||
['3.3.0', '/3.3.0'],
|
||||
['3.2.0', '/3.2.0'],
|
||||
['3.1.0', '/3.1.0'],
|
||||
['3.0.0', '/3.0.0'],
|
||||
];
|
||||
|
||||
// Present the dropdown newest-first regardless of the order entries were added
|
||||
// above, so publishing a release stays a one-line append (no manual re-sorting).
|
||||
// Sort by numeric major.minor.patch descending; suffixes like "-dev"/"-pre"/
|
||||
// "alpha" carry no digits and are ignored by the key, so same-base variants
|
||||
// (e.g. "5.0" / "5.0.0-pre" / "5.0.0alpha") tie — Array.sort is stable, so they
|
||||
// keep the order written above.
|
||||
window.OPENCV_DOC_VERSIONS.sort(function (a, b) {
|
||||
var ka = (a[0].match(/\d+/g) || []).map(Number);
|
||||
var kb = (b[0].match(/\d+/g) || []).map(Number);
|
||||
for (var i = 0; i < Math.max(ka.length, kb.length); i++) {
|
||||
var d = (kb[i] || 0) - (ka[i] || 0);
|
||||
if (d) return d;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
function renderDoxygenVersionDropdown() {
|
||||
// Doxygen-only rendering. The Sphinx pages have no `#projectnumber` and
|
||||
// no jQuery, so bail early there — they read OPENCV_DOC_VERSIONS above
|
||||
// and build their own dropdown in the navbar template.
|
||||
if (!document.getElementById("projectnumber") || typeof window.jQuery === "undefined")
|
||||
return;
|
||||
var versions = window.OPENCV_DOC_VERSIONS;
|
||||
var h = '<select>';
|
||||
var current_ver = $("#projectnumber")[0].innerText || versions[0][0];
|
||||
current_ver = current_ver.trim();
|
||||
for (i = 0; i < versions.length; i++) {
|
||||
selected = ''
|
||||
if(current_ver === versions[i][0])
|
||||
selected = ' selected="selected"';
|
||||
h += '<option value="' + versions[i][0] + '"' + selected + '>' + versions[i][0] + '</option>';
|
||||
}
|
||||
h += '</select>';
|
||||
$("#projectnumber")[0].innerHTML = h;
|
||||
$("#projectnumber select")[0].addEventListener('change', function() {
|
||||
var v = $(this).children('option:selected').attr('value');
|
||||
var path = undefined;
|
||||
for (i = 0; i < versions.length; i++) {
|
||||
if(v === versions[i][0]) {
|
||||
path = versions[i][1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!path) return;
|
||||
// Go straight to the chosen version's index via its site-absolute path,
|
||||
// so switching works from ANY page of ANY version — no fragile attempt to
|
||||
// substitute the current version inside the current URL (which fails on
|
||||
// pages whose path isn't "/<version>/...", e.g. the 5.0 C++ API tree).
|
||||
// The S3 *website* endpoint serves "/4.13.0/" as index.html, but the plain
|
||||
// REST endpoint does not, so append index.html explicitly.
|
||||
if (!/\.html?($|[?#])/.test(path))
|
||||
path = path.replace(/\/+$/, '') + '/index.html';
|
||||
window.location.href = path; // navigate
|
||||
});
|
||||
return current_ver;
|
||||
}
|
||||
|
||||
// Run as soon as possible. On a normal page load this fires on DOMContentLoaded;
|
||||
// but on the already-deployed legacy pages this file is pulled in dynamically
|
||||
// (a loader appended to dynsections.js) and may arrive AFTER DOMContentLoaded
|
||||
// has fired — in which case render immediately instead of waiting for an event
|
||||
// that will never come again.
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", renderDoxygenVersionDropdown);
|
||||
} else {
|
||||
renderDoxygenVersionDropdown();
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
{% 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="{{ _doxy }}cookie.js"></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
|
||||
|
||||
var sphinxRoot = "{{ '../' * (pagename or '').count('/') }}";
|
||||
// Doxygen result href -> Sphinx page, or "" if that page is not in our build.
|
||||
function resolveSphinx(href) {
|
||||
if (!href || href.indexOf("javascript:") === 0) return "";
|
||||
var map = (typeof sphinxPageMap !== "undefined") ? sphinxPageMap : {};
|
||||
var stem = href.split("/").pop().replace(/#.*$/, "").replace(/\.html$/, "");
|
||||
var key = stem.replace(/^group__/, "").replace(/^tutorial_/, "").replace(/__/g, "_");
|
||||
var path = map[stem] || map[key];
|
||||
if (!path && stem.indexOf("namespace") === 0) path = map["core_basic"];
|
||||
var rest = stem;
|
||||
while (!path && rest.indexOf("_1_1") >= 0) {
|
||||
rest = rest.substring(0, rest.lastIndexOf("_1_1"));
|
||||
path = map[rest];
|
||||
}
|
||||
return path || "";
|
||||
}
|
||||
|
||||
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") || "";
|
||||
var sphinxPath = resolveSphinx(href);
|
||||
if (sphinxPath) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Doxygen #autotoc_md anchors don't exist in Sphinx; rebuild from heading text.
|
||||
var frag = "";
|
||||
if (/#autotoc_md/.test(href)) {
|
||||
var slug = (a.textContent || "").trim().toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
||||
if (slug) frag = "#" + slug;
|
||||
}
|
||||
window.location.href = sphinxRoot + sphinxPath + frag;
|
||||
} else if (href.indexOf("doc/doxygen/html/") >= 0) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}, 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 %}
|
||||
@@ -0,0 +1,37 @@
|
||||
{# OpenCV navbar logo: the built-in pydata `navbar-logo`, plus an
|
||||
"Open Source Computer Vision" subtitle stacked under the wordmark —
|
||||
mirrors the legacy docs.opencv.org header. The title + subtitle are
|
||||
wrapped in `.logo__textwrap` so they form a column to the right of the
|
||||
SVG mark; the version switcher (a separate navbar_start slot) still sits
|
||||
inline beside the title. Link-resolution logic is copied verbatim from
|
||||
the theme's own navbar-logo.html so the brand href behaves identically. #}
|
||||
{% if theme_logo_link %}
|
||||
{% set href = theme_logo_link %}
|
||||
{% else %}
|
||||
{% if not theme_logo.get("link") %}
|
||||
{% set href = pathto(root_doc) %}
|
||||
{% elif hasdoc(theme_logo.get("link")) %}
|
||||
{% set href = pathto(theme_logo.get("link")) %} {# internal page #}
|
||||
{% else %}
|
||||
{% set href = theme_logo.get("link") %} {# external url #}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<a class="navbar-brand logo" href="{{ href }}">
|
||||
{% set is_logo = "light" in theme_logo["image_relative"] %}
|
||||
{% set alt = theme_logo.get("alt_text", "" if theme_logo.get("text") else "%s - Home" % docstitle) %}
|
||||
{% if is_logo %}
|
||||
{% if default_mode is undefined or default_mode == "auto" %}
|
||||
{% set default_mode = "light" %}
|
||||
{% endif %}
|
||||
{% set js_mode = "light" if default_mode == "dark" else "dark" %}
|
||||
<img src="{{ theme_logo['image_relative'][default_mode] }}" class="logo__image only-{{ default_mode }}" alt="{{ alt }}"/>
|
||||
<img src="{{ theme_logo['image_relative'][js_mode] }}" class="logo__image only-{{ js_mode }} pst-js-only" alt="{{ alt }}"/>
|
||||
{% endif %}
|
||||
{% if not is_logo or theme_logo.get("text") %}
|
||||
<span class="logo__textwrap">
|
||||
<p class="title logo__title">{{ theme_logo.get("text") or docstitle }}</p>
|
||||
<p class="logo__subtitle">Open Source Computer Vision</p>
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
@@ -0,0 +1,36 @@
|
||||
{# 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 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">
|
||||
<a class="nav-link" href="{{ _href }}"
|
||||
{%- if _external %} target="_blank" rel="noopener noreferrer"{% endif %}>{{ link.name }}</a>
|
||||
</li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -0,0 +1,81 @@
|
||||
{# Navbar version switcher: a plain `<select>` rendered right after the
|
||||
`navbar-logo` slot, mirroring the legacy docs.opencv.org header where the
|
||||
version selector sits inline with the wordmark.
|
||||
|
||||
Single source of truth: the options are built client-side from
|
||||
`window.OPENCV_DOC_VERSIONS`, defined in `_static/version.js` — the SAME
|
||||
list the legacy Doxygen pages carry. There is no build-time scrape and no
|
||||
second list: publishing a release is one edit to that file. We load it via
|
||||
`pathto('_static/version.js', 1)` (the theme's own idiom for JS assets) so
|
||||
the script resolves relative to each page, which means the dropdown
|
||||
populates fully in a local `sphinx-build` preview AND on the deployed site,
|
||||
regardless of how deep the page sits or which prefix it lands under.
|
||||
|
||||
The option whose label equals this build's `release` is marked selected;
|
||||
every other option carries that version's site-absolute path and the
|
||||
`onchange` handler navigates the same tab there. #}
|
||||
<form class="opencv-version-switcher" role="search"
|
||||
aria-label="OpenCV documentation version"
|
||||
onsubmit="return false">
|
||||
<select id="opencv-version-select"
|
||||
aria-label="Select OpenCV documentation version"
|
||||
onchange="if(this.value){window.location.href=this.value;}">
|
||||
<option value="" selected>{{ release }}</option>
|
||||
</select>
|
||||
</form>
|
||||
<script src="{{ pathto('_static/version.js', 1) }}"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var current = {{ release | tojson }};
|
||||
|
||||
// Each version.js entry is a bare directory path (e.g. "/4.13.0"). Link
|
||||
// straight to that directory's index document: the S3 *website* endpoint
|
||||
// would resolve "/4.13.0/" to index.html on its own, but the plain REST
|
||||
// endpoint (bucket.s3.amazonaws.com) does NOT — there "/4.13.0" is a
|
||||
// missing object key. Appending "/index.html" works on both endpoints.
|
||||
function toHref(path) {
|
||||
if (/\.html?($|[?#])/.test(path)) return path; // already a file
|
||||
return path.replace(/\/+$/, '') + '/index.html';
|
||||
}
|
||||
|
||||
function populate() {
|
||||
var versions = window.OPENCV_DOC_VERSIONS;
|
||||
if (!Array.isArray(versions)) return; // version.js absent (list stays at current release)
|
||||
document.querySelectorAll('#opencv-version-select').forEach(function (sel) {
|
||||
// Rebuild from the canonical list so the order matches the legacy
|
||||
// docs.opencv.org dropdown exactly.
|
||||
while (sel.firstChild) sel.removeChild(sel.firstChild);
|
||||
versions.forEach(function (v) {
|
||||
var label = v[0], path = v[1];
|
||||
var opt = document.createElement('option');
|
||||
// Selecting the current version is a no-op (empty value -> the
|
||||
// inline `onchange` guard skips navigation); every other option
|
||||
// navigates to that version's index document.
|
||||
opt.value = (label === current) ? '' : toHref(path);
|
||||
opt.textContent = label;
|
||||
if (label === current) opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function resetSelection() {
|
||||
// After picking an older version and hitting Back, the browser can
|
||||
// restore the <select> from bfcache with the OLD choice. Snap it back
|
||||
// to the current release (the option whose value is empty).
|
||||
document.querySelectorAll('#opencv-version-select').forEach(function (sel) {
|
||||
for (var i = 0; i < sel.options.length; i++) {
|
||||
if (sel.options[i].value === '') { sel.selectedIndex = i; break; }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', populate);
|
||||
} else {
|
||||
populate();
|
||||
}
|
||||
// `pageshow` fires on initial load AND on bfcache restore (Back button).
|
||||
window.addEventListener('pageshow', resetSelection);
|
||||
})();
|
||||
</script>
|
||||
@@ -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()"> </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>
|
||||
@@ -0,0 +1,203 @@
|
||||
# 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.
|
||||
|
||||
from __future__ import annotations
|
||||
import os as _os, pathlib, sys as _sys
|
||||
|
||||
# Config dir isn't on sys.path under config/source-dir separation; add it.
|
||||
_sys.path.insert(0, _os.path.dirname(_os.path.abspath(__file__)))
|
||||
|
||||
from conf_helpers.state import (
|
||||
DOC_ROOT, CONTRIB_ROOT, SPHINX_INPUT_ROOT,
|
||||
DOC_MODULES, JS_DOC_MODULES, PY_DOC_MODULES, CONTRIB_MODULES, API_MODULES,
|
||||
DOXYGEN_BASE_URL, _PATCHED_XML_DIR, HAVE_BREATHE,
|
||||
USE_INDEX_LANDING,
|
||||
)
|
||||
import conf_helpers.build # noqa: F401 bib staging, scans, API stubs, indexes.
|
||||
import conf_helpers.patches # noqa: F401 Sphinx C++ xref + warning patches.
|
||||
from conf_helpers.translate import _source_read
|
||||
from conf_helpers.postprocess import _inline_coll_graphs_on_finish
|
||||
|
||||
# -- Project ----------------------------------------------------------------
|
||||
project = "OpenCV"
|
||||
author = "OpenCV Team"
|
||||
release = "5.0"
|
||||
|
||||
# -- Sphinx core ------------------------------------------------------------
|
||||
extensions = ["myst_parser"]
|
||||
for _ext in ("sphinx_design", "sphinx_copybutton"):
|
||||
try:
|
||||
__import__(_ext)
|
||||
extensions.append(_ext)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# -- Breathe (Doxygen XML -> Sphinx C++ domain) -----------------------------
|
||||
if HAVE_BREATHE:
|
||||
extensions.append("breathe")
|
||||
breathe_projects = {"opencv": str(_PATCHED_XML_DIR)}
|
||||
breathe_default_project = "opencv"
|
||||
breathe_default_members = ()
|
||||
|
||||
source_suffix = {".md": "markdown", ".markdown": "markdown"}
|
||||
|
||||
# Swallow OpenCV's compatibility macros during C++ parsing, else signatures
|
||||
# like `... getName() const CV_OVERRIDE` raise "Invalid C++ declaration".
|
||||
cpp_id_attributes = [
|
||||
"CV_OVERRIDE", "CV_FINAL", "CV_NOEXCEPT",
|
||||
"CV_NORETURN", "CV_DEPRECATED", "CV_DEPRECATED_EXTERNAL",
|
||||
"CV_NODISCARD_STD", "CV_NODISCARD",
|
||||
"CV_EXPORTS", "CV_EXPORTS_W",
|
||||
"CV_WRAP",
|
||||
# Python-binding macros prefixing decls like `CV_PROP_RW Point2f pt`.
|
||||
"CV_PROP", "CV_PROP_RW", "CV_PROP_W",
|
||||
"CV_OUT", "CV_IN_OUT",
|
||||
]
|
||||
c_id_attributes = list(cpp_id_attributes)
|
||||
|
||||
master_doc = "index" if USE_INDEX_LANDING else "tutorials/tutorials"
|
||||
|
||||
# Scope: master + enabled main modules + (optionally) enabled contrib modules.
|
||||
include_patterns = (["index.markdown"] if USE_INDEX_LANDING else []) + [
|
||||
"tutorials/tutorials.markdown", "faq.markdown",
|
||||
"citelist.markdown", "intro.markdown",
|
||||
"related_pages.markdown", "namespace_list.markdown",
|
||||
"class_list.markdown"] + [
|
||||
f"tutorials/{m}/**" for m in DOC_MODULES
|
||||
] + (["js_tutorials/js_tutorials.markdown"] if JS_DOC_MODULES else []) + [
|
||||
f"js_tutorials/{m}/**" for m in JS_DOC_MODULES
|
||||
] + (["py_tutorials/py_tutorials.markdown"] if PY_DOC_MODULES else []) + [
|
||||
f"py_tutorials/{m}/**" for m in PY_DOC_MODULES
|
||||
]
|
||||
if CONTRIB_MODULES and (SPHINX_INPUT_ROOT / "tutorials_contrib").is_dir():
|
||||
include_patterns.append("tutorials_contrib/contrib_root.markdown")
|
||||
include_patterns += [f"tutorials_contrib/{m}/**" for m in CONTRIB_MODULES]
|
||||
if API_MODULES:
|
||||
# Glob: the stub file set (generated later) is unknown here.
|
||||
include_patterns.append("main_modules/**")
|
||||
include_patterns.append("extra_modules/**")
|
||||
# Orphan example pages; without this glob the class-page Examples links 404.
|
||||
include_patterns.append("examples/**")
|
||||
|
||||
exclude_patterns = [
|
||||
"**/Thumbs.db", "**/.DS_Store", "**/_old/**",
|
||||
"tutorials/core/how_to_use_OpenCV_parallel_for_/**",
|
||||
"tutorials/introduction/load_save_image/**",
|
||||
"tutorials/app/_old/**",
|
||||
]
|
||||
|
||||
myst_enable_extensions = [
|
||||
"colon_fence", "deflist", "dollarmath", "amsmath",
|
||||
"attrs_inline", "attrs_block", "smartquotes",
|
||||
]
|
||||
myst_heading_anchors = 4
|
||||
|
||||
# OpenCV's custom LaTeX macros (\vecthree, \cameramatrix, …) — ported from
|
||||
# doc/mymath.js so MathJax resolves them the same way the Doxygen site does.
|
||||
mathjax3_config = {
|
||||
"loader": {"load": ["[tex]/ams"]},
|
||||
"tex": {
|
||||
"packages": {"[+]": ["ams"]},
|
||||
"macros": {
|
||||
"matTT": [r"\[ \left|\begin{array}{ccc} #1 & #2 & #3\\ #4 & #5 & #6\\ #7 & #8 & #9 \end{array}\right| \]", 9],
|
||||
"fork": [r"\left\{ \begin{array}{l l} #1 & \mbox{#2}\\ #3 & \mbox{#4}\\ \end{array} \right.", 4],
|
||||
"forkthree": [r"\left\{ \begin{array}{l l} #1 & \mbox{#2}\\ #3 & \mbox{#4}\\ #5 & \mbox{#6}\\ \end{array} \right.", 6],
|
||||
"forkfour": [r"\left\{ \begin{array}{l l} #1 & \mbox{#2}\\ #3 & \mbox{#4}\\ #5 & \mbox{#6}\\ #7 & \mbox{#8}\\ \end{array} \right.", 8],
|
||||
"vecthree": [r"\begin{bmatrix} #1\\ #2\\ #3 \end{bmatrix}", 3],
|
||||
"vecthreethree": [r"\begin{bmatrix} #1 & #2 & #3\\ #4 & #5 & #6\\ #7 & #8 & #9 \end{bmatrix}", 9],
|
||||
"cameramatrix": [r"#1 = \begin{bmatrix} f_x & 0 & c_x\\ 0 & f_y & c_y\\ 0 & 0 & 1 \end{bmatrix}", 1],
|
||||
"distcoeffs": [r"(k_1, k_2, p_1, p_2[, k_3[, k_4, k_5, k_6 [, s_1, s_2, s_3, s_4[, \tau_x, \tau_y]]]]) \text{ of 4, 5, 8, 12 or 14 elements}"],
|
||||
"distcoeffsfisheye": [r"(k_1, k_2, k_3, k_4)"],
|
||||
"hdotsfor": [r"\dots", 1],
|
||||
"mathbbm": [r"\mathbb{#1}", 1],
|
||||
"bordermatrix": [r"\matrix{#1}", 1],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
suppress_warnings = [
|
||||
"myst.header", "myst.xref_missing", "toc.not_included",
|
||||
"misc.highlighting_failure",
|
||||
"image.not_readable",
|
||||
# Same C++ symbol legitimately appears on >1 generated page (group + namespace).
|
||||
"cpp.duplicate_declaration",
|
||||
]
|
||||
|
||||
# -- HTML / PyData theme ----------------------------------------------------
|
||||
try:
|
||||
import pydata_sphinx_theme # noqa: F401
|
||||
html_theme = "pydata_sphinx_theme"
|
||||
except ImportError:
|
||||
html_theme = "alabaster"
|
||||
|
||||
html_title = "OpenCV Tutorials"
|
||||
html_show_sourcelink = False
|
||||
templates_path = ["_templates"]
|
||||
html_static_path = ["_static"]
|
||||
html_css_files = [
|
||||
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700"
|
||||
"&family=JetBrains+Mono:wght@400;500&display=swap",
|
||||
"custom.css",
|
||||
]
|
||||
html_theme_options = {
|
||||
"logo": {
|
||||
"text": f"OpenCV {release}",
|
||||
"image_light": "_static/opencv-logo.svg",
|
||||
"image_dark": "_static/opencv-logo.svg",
|
||||
},
|
||||
# Navbar layout: logo on the left, version switcher right beside it —
|
||||
# mirrors the legacy docs.opencv.org header where the version selector
|
||||
# sits inline with the wordmark. The switcher template reads the shared
|
||||
# /version.js (window.OPENCV_DOC_VERSIONS); no build-time list is generated.
|
||||
"navbar_start": ["navbar-logo", "opencv-version-switcher"],
|
||||
"header_links_before_dropdown": 6,
|
||||
"external_links": [
|
||||
{"docname": master_doc, "name": "Main Page"},
|
||||
{"docname": "related_pages", "name": "Related Pages"},
|
||||
{"docname": "namespace_list", "name": "Namespaces"},
|
||||
{"docname": "class_list", "name": "Classes"},
|
||||
{"docname": "examples/examples_root", "name": "Examples"},
|
||||
{"url": DOXYGEN_BASE_URL + "javadoc/", "name": "Java documentation",
|
||||
"external": True},
|
||||
],
|
||||
"navbar_persistent": [],
|
||||
"navbar_end": ["search-button-field", "theme-switcher", "navbar-icon-links"],
|
||||
"disable_search": True,
|
||||
"show_toc_level": 2,
|
||||
"navigation_with_keys": True,
|
||||
"show_prev_next": True,
|
||||
"show_nav_level": 2,
|
||||
"navigation_depth": 4,
|
||||
"secondary_sidebar_items": {"**": ["page-toc"], "index": []},
|
||||
"back_to_top_button": True,
|
||||
"show_version_warning_banner": False,
|
||||
"icon_links": [{"name": "GitHub",
|
||||
"url": "https://github.com/opencv/opencv",
|
||||
"icon": "fa-brands fa-github"}],
|
||||
}
|
||||
|
||||
html_extra_path: list[str] = []
|
||||
def _in_source_tree(p: pathlib.Path) -> bool:
|
||||
for _root in (DOC_ROOT, CONTRIB_ROOT):
|
||||
try: p.relative_to(_root); return True
|
||||
except ValueError: pass
|
||||
return False
|
||||
if not _in_source_tree(SPHINX_INPUT_ROOT):
|
||||
_extras = SPHINX_INPUT_ROOT.parent / "contrib_extras"
|
||||
_prefix = _extras / "contrib_modules"
|
||||
_prefix.mkdir(parents=True, exist_ok=True)
|
||||
for _m in CONTRIB_MODULES:
|
||||
_src, _link = CONTRIB_ROOT / _m, _prefix / _m
|
||||
if _src.is_dir() and not _link.exists():
|
||||
try: _os.symlink(_src, _link, target_is_directory=True)
|
||||
except (OSError, NotImplementedError): pass
|
||||
html_extra_path = [str(_extras)]
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.connect("source-read", _source_read)
|
||||
app.connect("build-finished", _inline_coll_graphs_on_finish)
|
||||
return {"parallel_read_safe": True, "parallel_write_safe": True}
|
||||
@@ -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
|
||||
"""
|
||||
@@ -0,0 +1,447 @@
|
||||
# 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
|
||||
|
||||
|
||||
def _discover_orphan_groups(xml_dir):
|
||||
if not xml_dir.is_dir():
|
||||
return [], []
|
||||
folders = set()
|
||||
for _root in (OPENCV_ROOT / "modules", CONTRIB_ROOT):
|
||||
if _root.is_dir():
|
||||
folders.update(d.name for d in _root.iterdir()
|
||||
if (d / "include" / "opencv2").is_dir())
|
||||
skip = set(folders) | {f.replace("_", "__") for f in folders}
|
||||
skip |= {_module_group_stem(m) for m in folders}
|
||||
all_groups, child = set(), set()
|
||||
for gx in xml_dir.glob("group__*.xml"):
|
||||
all_groups.add(gx.stem)
|
||||
try:
|
||||
xml = gx.read_text(encoding="utf-8", errors="ignore")
|
||||
except OSError:
|
||||
continue
|
||||
child.update(re.findall(r'<innergroup refid="(group__[^"]+)"', xml))
|
||||
main, extra = [], []
|
||||
for g in sorted(all_groups - child):
|
||||
stem = g[len("group__"):]
|
||||
name = stem.replace("__", "_")
|
||||
if stem in skip or name in skip:
|
||||
continue
|
||||
xml = (xml_dir / f"{g}.xml").read_text(encoding="utf-8", errors="ignore")
|
||||
loc = re.search(r'<location file="([^"]*)"', xml)
|
||||
(extra if loc and "opencv_contrib/" in loc.group(1) else main).append(name)
|
||||
return main, extra
|
||||
|
||||
|
||||
# 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)]
|
||||
_main_orphans, _extra_orphans = _discover_orphan_groups(_API_XML_DIR)
|
||||
_generate_api_stubs(_main_api, _API_XML_DIR, SPHINX_INPUT_ROOT / "main_modules",
|
||||
root_anchor="api_root", root_title="Main modules",
|
||||
extra_groups=_main_orphans)
|
||||
_scan_internal(SPHINX_INPUT_ROOT / "main_modules")
|
||||
if _extra_api or _extra_orphans:
|
||||
_generate_api_stubs(_extra_api, _API_XML_DIR, SPHINX_INPUT_ROOT / "extra_modules",
|
||||
root_anchor="extra_api_root", root_title="Extra modules",
|
||||
extra_groups=_extra_orphans)
|
||||
_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("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
|
||||
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)
|
||||
@@ -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'\\\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",
|
||||
]
|
||||
@@ -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()
|
||||
@@ -0,0 +1,518 @@
|
||||
# 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, DOXYGEN_BASE_URL,
|
||||
_LOCAL_CLASS_URL, _LOCAL_TYPEDEF_URL, _FILE_URL, _API_XML_DIR, DOC_ROOT)
|
||||
|
||||
|
||||
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
|
||||
svg = svg.replace(
|
||||
"<svg ",
|
||||
f'<svg class="{m.group("cls")}" role="img" '
|
||||
f'aria-label="{m.group("alt")}" ', 1)
|
||||
# Wrap in a scroll box so large graphs (e.g. file include graphs)
|
||||
# stay fully reachable instead of being clipped — scrolls when wider
|
||||
# than the content area, fits otherwise.
|
||||
return f'<div class="opencv-graph-scroll">{svg}</div>'
|
||||
|
||||
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 _fix_gapi_images(out_dir: pathlib.Path) -> None:
|
||||
"""Copy gapi doc images to _images/ and fix src paths in gapi.html."""
|
||||
gapi_html = out_dir / "extra_modules" / "gapi.html"
|
||||
if not gapi_html.is_file():
|
||||
return
|
||||
images_dir = out_dir / "_images"
|
||||
images_dir.mkdir(exist_ok=True)
|
||||
|
||||
text = gapi_html.read_text(encoding="utf-8")
|
||||
|
||||
def _fix_src(m):
|
||||
raw_path = m.group(1)
|
||||
if "contrib_modules" not in raw_path:
|
||||
return m.group(0)
|
||||
# raw_path is relative to extra_modules/ in the browser,
|
||||
# but the file lives at out_dir / raw_path (no extra_modules prefix)
|
||||
src_file = out_dir / raw_path
|
||||
if not src_file.is_file():
|
||||
return m.group(0)
|
||||
dest = images_dir / src_file.name
|
||||
if not dest.is_file():
|
||||
import shutil as _shutil
|
||||
_shutil.copy2(src_file, dest)
|
||||
return f'src="../_images/{src_file.name}"'
|
||||
|
||||
new_text = re.sub(r'src="([^"]+)"', _fix_src, text)
|
||||
if new_text != text:
|
||||
gapi_html.write_text(new_text, encoding="utf-8")
|
||||
|
||||
def _copy_js_tryit_files(out_dir: pathlib.Path) -> None:
|
||||
"""Copy js_*.html Try-it pages + assets so iframe src="../../js_*.html" resolves."""
|
||||
import shutil, os
|
||||
js_assets = DOC_ROOT / "js_tutorials" / "js_assets"
|
||||
dest = out_dir / "js_tutorials"
|
||||
if not js_assets.is_dir() or not dest.is_dir():
|
||||
return
|
||||
for src in js_assets.iterdir():
|
||||
if src.is_file():
|
||||
dst = dest / src.name
|
||||
if not dst.exists():
|
||||
shutil.copy2(src, dst)
|
||||
# opencv.js from CMake (OPENCV_JS_PATH); bundle it alongside the Try-it pages.
|
||||
opencv_js = os.environ.get("OPENCV_JS_PATH", "")
|
||||
if opencv_js and pathlib.Path(opencv_js).is_file():
|
||||
dst = dest / "opencv.js"
|
||||
if not dst.exists():
|
||||
shutil.copy2(opencv_js, dst)
|
||||
# Extra assets referenced by Try-it pages but not in js_assets/.
|
||||
_opencv_root = DOC_ROOT.parent
|
||||
for _name, _src in {
|
||||
"box.mp4": _opencv_root / "samples/cpp/tutorial_code/calib3d/real_time_pose_estimation/Data/box.mp4",
|
||||
"space_shuttle.jpg": DOC_ROOT / "tutorials/dnn/images/space_shuttle.jpg",
|
||||
"roi.jpg": DOC_ROOT / "py_tutorials/py_core/py_basic_ops/images/roi.jpg",
|
||||
}.items():
|
||||
if _src.is_file() and not (dest / _name).exists():
|
||||
shutil.copy2(_src, dest / _name)
|
||||
|
||||
|
||||
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")
|
||||
|
||||
_SYM = r"(?:group__|classcv|structcv|unioncv|namespacecv)\w*\.html"
|
||||
|
||||
def _localize_doxygen_links(out_dir: pathlib.Path) -> None:
|
||||
"""Point symbol-page links at the local Sphinx page when we built it:
|
||||
docs.opencv.org URLs, and bare relative names that 404 off the API dir."""
|
||||
import os
|
||||
skip = {"_static", "_sources", "_images", "_sphinx_design_static"}
|
||||
page_paths: dict[str, str] = {}
|
||||
for f in out_dir.rglob("*.html"):
|
||||
rel = f.relative_to(out_dir)
|
||||
if rel.parts and rel.parts[0] in skip:
|
||||
continue
|
||||
page_paths.setdefault(f.name, rel.as_posix())
|
||||
ext_re = re.compile(
|
||||
r'(?P<a><a class="reference external"\s+)?'
|
||||
r'href="' + re.escape(DOXYGEN_BASE_URL) + r'(?:[\w-]+/)*?'
|
||||
r'(?P<page>' + _SYM + r')(?:#(?P<frag>\w+))?"')
|
||||
bare_re = re.compile(r'href="(?P<page>' + _SYM + r')(?:#(?P<frag>[\w:.-]+))?"')
|
||||
# `#include` links -> local file-reference page (same Doxygen file stem).
|
||||
inc_re = re.compile(
|
||||
r'<a class="reference external opencv-include-link" '
|
||||
r'href="[^"]*doc/doxygen/html/[^"]*?(?P<file>[^/"]+\.html)">(?P<path>[^<]+)</a>')
|
||||
strip_re = re.compile(
|
||||
r'<a\b[^>]*\bhref="(?:' + re.escape(DOXYGEN_BASE_URL) + r'|[^"]*doc/doxygen/html/)'
|
||||
r'(?![^"]*javadoc)[^"]*"[^>]*>(?P<txt>[^<]*)</a>')
|
||||
|
||||
for html in out_dir.rglob("*.html"):
|
||||
rel = html.relative_to(out_dir)
|
||||
if rel.parts and rel.parts[0] in skip:
|
||||
continue
|
||||
text = html.read_text(encoding="utf-8")
|
||||
cur = rel.parent.as_posix()
|
||||
|
||||
def _href(local: str, anchor: str) -> str | None:
|
||||
target = page_paths.get(local)
|
||||
if not target:
|
||||
return None
|
||||
href = target if cur == "." else os.path.relpath(target, cur)
|
||||
return f'href="{href}{anchor}"'
|
||||
|
||||
def _ext(m: "re.Match") -> str:
|
||||
member = _DOXY_ANCHOR_TO_MEMBER.get(m.group("frag") or "")
|
||||
h = _href(_doxy_page_to_local(m.group("page")),
|
||||
f"#{member}" if member else "")
|
||||
if not h:
|
||||
return m.group(0)
|
||||
# Flip the now-local link from external to internal styling.
|
||||
return f'<a class="reference internal" {h}' if m.group("a") else h
|
||||
|
||||
def _bare(m: "re.Match") -> str:
|
||||
frag = m.group("frag")
|
||||
return _href(m.group("page"), f"#{frag}" if frag else "") or m.group(0)
|
||||
|
||||
def _inc(m: "re.Match") -> str:
|
||||
h = _href(m.group("file"), "")
|
||||
if not h:
|
||||
return m.group(0)
|
||||
return f'<a class="reference internal opencv-include-link" {h}>{m.group("path")}</a>'
|
||||
|
||||
new = inc_re.sub(_inc, bare_re.sub(_bare, ext_re.sub(_ext, text)))
|
||||
new = strip_re.sub(lambda m: m.group("txt"), new)
|
||||
if new != text:
|
||||
html.write_text(new, encoding="utf-8")
|
||||
|
||||
|
||||
def _drop_moved_stub_search_entries() -> None:
|
||||
"""Drop moved-tutorial stub pages from the Doxygen search index."""
|
||||
doxy_html = _API_XML_DIR.parent / "html"
|
||||
search_dir = doxy_html / "search"
|
||||
if not search_dir.is_dir():
|
||||
return
|
||||
stubs = set()
|
||||
for h in doxy_html.rglob("*table_of_content*.html"):
|
||||
try:
|
||||
if "has been moved to this page" in h.read_text(encoding="utf-8", errors="ignore"):
|
||||
stubs.add(h.name)
|
||||
except OSError:
|
||||
pass
|
||||
if not stubs:
|
||||
return
|
||||
for js in search_dir.glob("*.js"):
|
||||
lines = js.read_text(encoding="utf-8", errors="ignore").splitlines(keepends=True)
|
||||
# Drop only single-target stub entries; keep multi-target terms.
|
||||
kept = [ln for ln in lines if not (
|
||||
ln.count("['../") == 1 and any(f"/{s}'" in ln for s in stubs))]
|
||||
if len(kept) != len(lines):
|
||||
js.write_text("".join(kept), encoding="utf-8")
|
||||
|
||||
|
||||
# Pygments emits `<span class="n">NAME</span>` (and `class="nc"`/`"nf"`
|
||||
# for class/function tokens) inside its rendered `<pre>` for every C++
|
||||
# identifier in a code block. The example pages, snippet pages, tutorial
|
||||
# samples, and any `:::{code-block} cpp` fence all go through Pygments,
|
||||
# so by the time the HTML is written the code blocks have full syntax
|
||||
# colouring but ZERO clickable tokens — `Mat`, `InputArray`,
|
||||
# `getOptimalDFTSize`, etc. are inert text.
|
||||
#
|
||||
# This pass wraps each such span in an `<a class="reference internal"
|
||||
# href="…">` when the token resolves via `_LOCAL_CLASS_URL` /
|
||||
# `_LOCAL_TYPEDEF_URL`, mirroring what the API-stub renderer already
|
||||
# does for inline `<programlisting>`. Idempotent: skips spans already
|
||||
# inside an `<a>`.
|
||||
_PYG_IDENT_SPAN_RE = re.compile(
|
||||
r'(?P<prefix><span class="(?:n|nc|nf|nb|nv|na)">)'
|
||||
r'(?P<name>[A-Za-z_][A-Za-z0-9_]*)'
|
||||
r'(?P<suffix></span>)'
|
||||
)
|
||||
|
||||
# Pygments preprocessor-file-name span:
|
||||
# `<span class="cpf">"opencv2/core.hpp"</span>` (or <…>)
|
||||
# The quote characters are HTML-escaped (`"` / `<` / `>`).
|
||||
# Capture the inner path so we can wrap it in an `<a>` linking to the
|
||||
# local Doxygen file page (`_FILE_URL` map). The quote chars stay
|
||||
# outside the anchor — mirrors how the enum-detail `#include` line is
|
||||
# rendered (`#include <a href="…">opencv2/core.hpp</a>`).
|
||||
_PYG_CPF_SPAN_RE = re.compile(
|
||||
r'(?P<prefix><span class="cpf">)'
|
||||
r'(?P<openq>"|<)'
|
||||
r'(?P<path>[A-Za-z0-9_./+\-]+\.[A-Za-z0-9]+)'
|
||||
r'(?P<closeq>"|>)'
|
||||
r'(?P<suffix></span>)'
|
||||
)
|
||||
|
||||
|
||||
def _linkify_code_blocks(html_dir: pathlib.Path) -> None:
|
||||
"""Walk every `.html` under `html_dir` and turn known identifier
|
||||
tokens inside Pygments-rendered `<pre>` blocks into clickable
|
||||
anchors. The substitution is scoped to spans inside `<pre>` so we
|
||||
don't accidentally repaint inline `<code class="n">` chips in
|
||||
prose; the rule above already targets only Pygments span classes
|
||||
that Pygments uses inside its `<pre>` output."""
|
||||
if not (_LOCAL_CLASS_URL or _LOCAL_TYPEDEF_URL or _FILE_URL):
|
||||
return
|
||||
if not html_dir.is_dir():
|
||||
return
|
||||
import os
|
||||
|
||||
def _resolve(name: str) -> str | None:
|
||||
return _LOCAL_CLASS_URL.get(name) or _LOCAL_TYPEDEF_URL.get(name)
|
||||
|
||||
# `<pre>…</pre>` blocks only — keeps the substitution from touching
|
||||
# inline `<span class="n">` runs that may appear in other contexts.
|
||||
_PRE_BLOCK_RE = re.compile(r"<pre>(.*?)</pre>", re.DOTALL)
|
||||
|
||||
# Relative path from each rendered `.html` file's directory to the
|
||||
# Doxygen html tree (which sits alongside `docs_sphinx/html/` at
|
||||
# `doc/doxygen/html/`). Reused per file so paths render with the
|
||||
# right number of `../` segments regardless of subdir depth.
|
||||
_DOXY_ROOT = html_dir.parent.parent / "doc" / "doxygen" / "html"
|
||||
|
||||
def _doxy_rel(html_path: pathlib.Path, file_url: str) -> str:
|
||||
target = _DOXY_ROOT / file_url
|
||||
try:
|
||||
return os.path.relpath(target, start=html_path.parent)
|
||||
except ValueError:
|
||||
return f"../../../doc/doxygen/html/{file_url}"
|
||||
|
||||
def _wrap_span(m: re.Match) -> str:
|
||||
name = m.group("name")
|
||||
url = _resolve(name)
|
||||
if not url:
|
||||
return m.group(0)
|
||||
return (f'<a class="reference internal" href="{url}">'
|
||||
f'{m.group("prefix")}{name}{m.group("suffix")}</a>')
|
||||
|
||||
def _wrap_cpf(m: re.Match, current_html: pathlib.Path) -> str:
|
||||
path = m.group("path")
|
||||
# Only opencv headers — `<iostream>`, `<stdio.h>` are not in
|
||||
# `_FILE_URL` and stay plain.
|
||||
file_url = _FILE_URL.get(path)
|
||||
if not file_url:
|
||||
return m.group(0)
|
||||
href = _doxy_rel(current_html, file_url)
|
||||
# Keep the opening/closing quotes outside the `<a>` (so they
|
||||
# render as plain `"` / `<`/`>`), and put the link on just
|
||||
# the path text — same shape the enum-detail `#include` line
|
||||
# already uses elsewhere.
|
||||
return (f'{m.group("prefix")}{m.group("openq")}'
|
||||
f'<a class="reference external opencv-include-link" '
|
||||
f'href="{href}">{path}</a>'
|
||||
f'{m.group("closeq")}{m.group("suffix")}')
|
||||
|
||||
def _rewrite_pre(m: re.Match, current_html: pathlib.Path) -> str:
|
||||
inner = m.group(1)
|
||||
# Skip spans already wrapped: if `<a …>` immediately precedes
|
||||
# the `<span class="n">…</span>`, leave it. The Pygments
|
||||
# output doesn't generate `<a>` itself, so the only place
|
||||
# `<a>` appears in the inner text is from a prior pass — we
|
||||
# detect by scanning for `<a `/`</a>` pairs and only rewrite
|
||||
# text outside them.
|
||||
out: list[str] = []
|
||||
i, n = 0, len(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:
|
||||
seg = inner[i:]
|
||||
seg = _PYG_IDENT_SPAN_RE.sub(_wrap_span, seg)
|
||||
seg = _PYG_CPF_SPAN_RE.sub(
|
||||
lambda mm: _wrap_cpf(mm, current_html), seg)
|
||||
out.append(seg)
|
||||
break
|
||||
seg = inner[i:k]
|
||||
seg = _PYG_IDENT_SPAN_RE.sub(_wrap_span, seg)
|
||||
seg = _PYG_CPF_SPAN_RE.sub(
|
||||
lambda mm: _wrap_cpf(mm, current_html), seg)
|
||||
out.append(seg)
|
||||
i = k
|
||||
return "<pre>" + "".join(out) + "</pre>"
|
||||
|
||||
for html in html_dir.rglob("*.html"):
|
||||
try:
|
||||
text = html.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
continue
|
||||
if "<pre>" not in text:
|
||||
continue
|
||||
new_text = _PRE_BLOCK_RE.sub(
|
||||
lambda m: _rewrite_pre(m, html), text)
|
||||
if new_text != text:
|
||||
try:
|
||||
html.write_text(new_text, encoding="utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
_INLINE_CODE_RE = re.compile(
|
||||
r'<code class="docutils literal notranslate">(?P<body>[^<]*?)</code>'
|
||||
)
|
||||
_A_BLOCK_RE = re.compile(r'<a\b[^>]*>.*?</a>', re.DOTALL)
|
||||
_INLINE_TOK_RE = re.compile(r'(?:cv::)?_?[A-Za-z][A-Za-z0-9_]*')
|
||||
|
||||
|
||||
def _linkify_inline_code(html_dir: pathlib.Path) -> None:
|
||||
if not (_LOCAL_CLASS_URL or _LOCAL_TYPEDEF_URL):
|
||||
return
|
||||
if not html_dir.is_dir():
|
||||
return
|
||||
|
||||
def _resolve(name: str) -> str | None:
|
||||
bare = name[4:] if name.startswith("cv::") else name
|
||||
return _LOCAL_CLASS_URL.get(bare) or _LOCAL_TYPEDEF_URL.get(bare)
|
||||
|
||||
def _anchor_text(tok: str) -> str:
|
||||
return tok.replace("::", "::")
|
||||
|
||||
def _linkify_body(body: str) -> str:
|
||||
def _sub(m: re.Match) -> str:
|
||||
tok = m.group(0)
|
||||
url = _resolve(tok)
|
||||
if not url:
|
||||
return tok
|
||||
return (f'<a class="reference internal" '
|
||||
f'href="{url}">{_anchor_text(tok)}</a>')
|
||||
return _INLINE_TOK_RE.sub(_sub, body)
|
||||
|
||||
def _rewrite_code(m: re.Match) -> str:
|
||||
body = m.group("body")
|
||||
new_body = _linkify_body(body)
|
||||
if new_body == body:
|
||||
return m.group(0)
|
||||
return (f'<code class="docutils literal notranslate">'
|
||||
f'{new_body}</code>')
|
||||
|
||||
for html in html_dir.rglob("*.html"):
|
||||
try:
|
||||
text = html.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
continue
|
||||
if 'class="docutils literal notranslate"' not in text:
|
||||
continue
|
||||
masked: list[str] = []
|
||||
def _mask(m: re.Match) -> str:
|
||||
masked.append(m.group(0))
|
||||
return f"\x00A{len(masked) - 1}\x00"
|
||||
masked_text = _A_BLOCK_RE.sub(_mask, text)
|
||||
new_masked = _INLINE_CODE_RE.sub(_rewrite_code, masked_text)
|
||||
if new_masked == masked_text:
|
||||
continue
|
||||
new_text = re.sub(
|
||||
r"\x00A(\d+)\x00",
|
||||
lambda mm: masked[int(mm.group(1))],
|
||||
new_masked,
|
||||
)
|
||||
try:
|
||||
html.write_text(new_text, encoding="utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
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)
|
||||
_linkify_code_blocks(out)
|
||||
_linkify_inline_code(out)
|
||||
_localize_doxygen_links(out)
|
||||
_drop_moved_stub_search_entries()
|
||||
_copy_js_tryit_files(out)
|
||||
_fix_gapi_images(out)
|
||||
_generate_search_map(out)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
Reference in New Issue
Block a user