From 8756e68bff26ca9b802bc286488eeed37a761e43 Mon Sep 17 00:00:00 2001 From: kirtijindal14 Date: Tue, 2 Jun 2026 00:09:16 +0530 Subject: [PATCH] Merge pull request #29206 from kirtijindal14:doc_optimizations_v3 OpenCV New Documentation #29206 This PR introduces end to end working new Documentation in OpenCV. Co-authored by: @abhishek-gola @omrope79 @Akansha-977 @Prasadayus @varun-jaiswal17 ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [x] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [x] The feature is well documented and sample code can be built with the project CMake --- docs_sphinx/CMakeLists.txt | 136 +- docs_sphinx/_static/custom.css | 992 ++++++++- docs_sphinx/_templates/layout.html | 127 ++ docs_sphinx/_templates/navbar-nav.html | 38 +- .../_templates/search-button-field.html | 40 + docs_sphinx/conf.py | 1965 +---------------- docs_sphinx/conf_helpers/__init__.py | 19 + docs_sphinx/conf_helpers/build.py | 413 ++++ docs_sphinx/conf_helpers/examples.py | 435 ++++ docs_sphinx/conf_helpers/patches.py | 171 ++ docs_sphinx/conf_helpers/postprocess.py | 154 ++ docs_sphinx/conf_helpers/state.py | 888 ++++++++ docs_sphinx/conf_helpers/stubs.py | 1241 +++++++++++ docs_sphinx/conf_helpers/translate.py | 997 +++++++++ docs_sphinx/conf_helpers/xml_render.py | 1156 ++++++++++ docs_sphinx/requirements.txt | 47 + modules/java/jar/CMakeLists.txt | 10 +- 17 files changed, 6886 insertions(+), 1943 deletions(-) create mode 100644 docs_sphinx/_templates/layout.html create mode 100644 docs_sphinx/_templates/search-button-field.html create mode 100644 docs_sphinx/conf_helpers/__init__.py create mode 100644 docs_sphinx/conf_helpers/build.py create mode 100644 docs_sphinx/conf_helpers/examples.py create mode 100644 docs_sphinx/conf_helpers/patches.py create mode 100644 docs_sphinx/conf_helpers/postprocess.py create mode 100644 docs_sphinx/conf_helpers/state.py create mode 100644 docs_sphinx/conf_helpers/stubs.py create mode 100644 docs_sphinx/conf_helpers/translate.py create mode 100644 docs_sphinx/conf_helpers/xml_render.py create mode 100644 docs_sphinx/requirements.txt diff --git a/docs_sphinx/CMakeLists.txt b/docs_sphinx/CMakeLists.txt index 6121756c0d..243b5a5751 100644 --- a/docs_sphinx/CMakeLists.txt +++ b/docs_sphinx/CMakeLists.txt @@ -1,9 +1,3 @@ -# Sphinx wrapper for the OpenCV doc/ tree. -# Adds a `sphinx` custom target so `cmake --build --target sphinx` -# works the same way `--target doxygen` does. -# -# Gated on BUILD_DOCS so it follows the existing toggle. - if(NOT BUILD_DOCS) return() endif() @@ -20,11 +14,23 @@ if(NOT SPHINX_BUILD) return() endif() +find_program(DOT_EXECUTABLE NAMES dot + DOC "Path to graphviz `dot` (required for API collaboration diagrams)") +if(NOT DOT_EXECUTABLE) + message(WARNING + "docs_sphinx: graphviz `dot` not found — API class-page collaboration " + "diagrams will be skipped. Install graphviz (e.g. `sudo apt-get install " + "graphviz`) and re-run cmake so the Doxygen HTML build enables HAVE_DOT.") +else() + message(STATUS + "docs_sphinx: graphviz dot found (${DOT_EXECUTABLE}) — " + "collaboration diagrams enabled") +endif() + set(_SPHINX_CONFDIR "${CMAKE_CURRENT_SOURCE_DIR}") set(_SPHINX_OUTDIR "${CMAKE_CURRENT_BINARY_DIR}/html") set(_SPHINX_INPUT_ROOT "${CMAKE_CURRENT_BINARY_DIR}/docs_sphinx_input") -# Stage merged input tree: symlink main + contrib tutorial subtrees into one srcdir. set(_SPHINX_INPUT_TUTORIALS "${_SPHINX_INPUT_ROOT}/tutorials") set(_SPHINX_INPUT_CONTRIB "${_SPHINX_INPUT_ROOT}/tutorials_contrib") file(REMOVE_RECURSE "${_SPHINX_INPUT_ROOT}") @@ -36,11 +42,6 @@ file(CREATE_LINK "${_SPHINX_INPUT_TUTORIALS}/tutorials.markdown" SYMBOLIC COPY_ON_ERROR) -# Top-level standalone pages staged at srcdir root (siblings of tutorials/). -# Add further `file(CREATE_LINK …)` lines here if more top-level pages get -# onboarded. intro.markdown lives inside modules/core/doc/ (it's the core -# module's @page; the legacy Doxygen build pulls it in via the same module -# scan it uses for bib files) — symlink it under a flat name at srcdir root. file(CREATE_LINK "${CMAKE_SOURCE_DIR}/doc/faq.markdown" "${_SPHINX_INPUT_ROOT}/faq.markdown" @@ -71,7 +72,6 @@ foreach(_root js_tutorials py_tutorials images) endif() endforeach() -# Contrib tree: stage when OPENCV_EXTRA_MODULES_PATH is set; unselected modules dropped by conf.py. if(OPENCV_EXTRA_MODULES_PATH AND EXISTS "${OPENCV_EXTRA_MODULES_PATH}") file(MAKE_DIRECTORY "${_SPHINX_INPUT_CONTRIB}") set(_contrib_root_md "${_SPHINX_INPUT_CONTRIB}/contrib_root.markdown") @@ -97,22 +97,126 @@ if(OPENCV_EXTRA_MODULES_PATH AND EXISTS "${OPENCV_EXTRA_MODULES_PATH}") endforeach() endif() -# Forward staging paths via env. OPENCV_CONTRIB_MODULES is not forwarded; set it in shell to override. +set(_LEGACY_DOXYFILE "${CMAKE_BINARY_DIR}/doc/Doxyfile") +set(_DOXYGEN_XML_DIR "${CMAKE_BINARY_DIR}/doc/doxygen/xml") + +if(NOT DEFINED SPHINX_API_MODULES) + set(SPHINX_API_MODULES "") + file(GLOB _umbrella_headers "${CMAKE_SOURCE_DIR}/modules/*/include/opencv2/*.hpp") + # Also scan opencv_contrib modules when present. + if(OPENCV_EXTRA_MODULES_PATH AND EXISTS "${OPENCV_EXTRA_MODULES_PATH}") + file(GLOB _contrib_umbrellas "${OPENCV_EXTRA_MODULES_PATH}/*/include/opencv2/*.hpp") + list(APPEND _umbrella_headers ${_contrib_umbrellas}) + endif() + foreach(_hdr ${_umbrella_headers}) + get_filename_component(_stem "${_hdr}" NAME_WE) + # Only the umbrella header modules//include/opencv2/.hpp. + if(_hdr MATCHES "/modules/${_stem}/include/opencv2/${_stem}\\.hpp$") + file(READ "${_hdr}" _hdr_contents) + if(_hdr_contents MATCHES "@defgroup") + list(APPEND SPHINX_API_MODULES "${_stem}") + endif() + endif() + endforeach() + list(SORT SPHINX_API_MODULES) + message(STATUS "docs_sphinx: discovered API modules: ${SPHINX_API_MODULES}") +endif() +set(_doxy_input "") +foreach(_m ${SPHINX_API_MODULES}) + foreach(_base "${CMAKE_SOURCE_DIR}/modules" "${OPENCV_EXTRA_MODULES_PATH}") + if(EXISTS "${_base}/${_m}/include") + string(APPEND _doxy_input " ${_base}/${_m}/include") + break() + endif() + endforeach() +endforeach() + +if(EXISTS "${_LEGACY_DOXYFILE}") + set(_SPHINX_DOXYFILE "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile-xml") + file(WRITE "${_SPHINX_DOXYFILE}" + "@INCLUDE = ${_LEGACY_DOXYFILE}\n" + "GENERATE_HTML = NO\n" + "GENERATE_LATEX = NO\n" + "GENERATE_XML = YES\n" + "XML_OUTPUT = xml\n" + "XML_PROGRAMLISTING = NO\n" + "CREATE_SUBDIRS = NO\n" + "EXCLUDE_PATTERNS += cap_ios.h\n" + "CLASS_GRAPH = NO\n" + "COLLABORATION_GRAPH = NO\n" + "GROUP_GRAPHS = NO\n" + "INCLUDE_GRAPH = NO\n" + "INCLUDED_BY_GRAPH = NO\n" + "DIRECTORY_GRAPH = NO\n" + "INPUT =${_doxy_input}\n" + "RECURSIVE = YES\n" + "EXCLUDE_SYMBOLS = cv::DataType<*> int void CV__* T __CV* cv::gapi::detail*\n" + "MACRO_EXPANSION = YES\n" + "EXPAND_ONLY_PREDEF = YES\n" + "PREDEFINED += __device__= __host__= __forceinline__= __global__= __constant__= __shared__= __restrict__=\n" + ) + find_program(DOXYGEN_EXE NAMES doxygen) + if(DOXYGEN_EXE) + # Stamp-based: Doxygen only re-runs when Doxyfile changes, not on every build. + set(_SPHINX_XML_STAMP "${_DOXYGEN_XML_DIR}/.sphinx_xml.stamp") + add_custom_command( + OUTPUT "${_SPHINX_XML_STAMP}" + COMMAND ${CMAKE_COMMAND} -E make_directory ${_DOXYGEN_XML_DIR} + COMMAND ${DOXYGEN_EXE} ${_SPHINX_DOXYFILE} + COMMAND ${CMAKE_COMMAND} -E touch "${_SPHINX_XML_STAMP}" + DEPENDS "${_SPHINX_DOXYFILE}" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/doc + COMMENT "Doxygen XML (for Sphinx breathe) -> ${_DOXYGEN_XML_DIR}" + VERBATIM) + add_custom_target(sphinx-xml DEPENDS "${_SPHINX_XML_STAMP}") + message(STATUS "docs_sphinx: sphinx-xml target enabled (doxygen: ${DOXYGEN_EXE})") + else() + message(STATUS "docs_sphinx: doxygen not found; `sphinx-xml` disabled") + endif() +else() + message(STATUS "docs_sphinx: legacy Doxyfile not present at ${_LEGACY_DOXYFILE}; " + "`sphinx-xml` disabled (re-run cmake after doc/ subdir configures)") +endif() + set(_SPHINX_ENV "OPENCV_SPHINX_INPUT_ROOT=${_SPHINX_INPUT_ROOT}" "OPENCV_CONTRIB_ROOT=${OPENCV_EXTRA_MODULES_PATH}" + "OPENCV_DOXYGEN_XML_DIR=${_DOXYGEN_XML_DIR}" ) +if(OPENCV_PYTHON_SIGNATURES_FILE) + list(APPEND _SPHINX_ENV "OPENCV_PYTHON_SIGNATURES_FILE=${OPENCV_PYTHON_SIGNATURES_FILE}") +endif() +if(NOT SPHINX_JOBS) + set(SPHINX_JOBS "auto") +endif() +set(_SPHINX_WARNINGS "${CMAKE_CURRENT_BINARY_DIR}/sphinx-warnings.log") add_custom_target(sphinx COMMAND ${CMAKE_COMMAND} -E make_directory ${_SPHINX_OUTDIR} COMMAND ${CMAKE_COMMAND} -E env ${_SPHINX_ENV} - ${SPHINX_BUILD} -c ${_SPHINX_CONFDIR} + ${SPHINX_BUILD} -j ${SPHINX_JOBS} --keep-going + -c ${_SPHINX_CONFDIR} ${_SPHINX_INPUT_ROOT} ${_SPHINX_OUTDIR} WORKING_DIRECTORY ${_SPHINX_CONFDIR} - COMMENT "Building Sphinx HTML site -> ${_SPHINX_OUTDIR}" + COMMENT "Building Sphinx HTML site -> ${_SPHINX_OUTDIR} (jobs=${SPHINX_JOBS})" VERBATIM) +# Ensure breathe sees current XML on every `sphinx` build. +if(TARGET sphinx-xml) + add_dependencies(sphinx sphinx-xml) +endif() +# Auto-generate Python binding signatures (needed for Python names in enum tables). +if(TARGET gen_opencv_python_source) + add_dependencies(sphinx gen_opencv_python_source) +endif() + +option(SPHINX_BUILD_DIAGRAMS + "Rebuild the Doxygen HTML (collaboration diagrams) as part of the sphinx target" ON) +if(SPHINX_BUILD_DIAGRAMS AND TARGET doxygen) + add_dependencies(sphinx doxygen) +endif() + # Clean rebuild: `cmake --build --target sphinx-clean` add_custom_target(sphinx-clean COMMAND ${CMAKE_COMMAND} -E rm -rf ${_SPHINX_OUTDIR} diff --git a/docs_sphinx/_static/custom.css b/docs_sphinx/_static/custom.css index 8928368bd8..dc2ca32b2a 100644 --- a/docs_sphinx/_static/custom.css +++ b/docs_sphinx/_static/custom.css @@ -1,12 +1,10 @@ -/* - * docs_sphinx/_static/custom.css - * Ported visual layer from the Prasadayus/opencv#27 Sphinx port, - * trimmed to the rules that apply to our minimal wrapper (PyData - * pydata-sphinx-theme + MyST, no doxysnippet/opencv_code_links/tabs - * extensions). Rules that target HTML markup we don't emit are - * dropped; rules that are pure pydata overrides are kept verbatim. - */ - +/* +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. +*/ :root, html[data-theme="light"], html[data-theme="dark"] { @@ -32,6 +30,14 @@ h3 { font-size: 1.125rem; font-weight: 600; line-height: 1.4; } .bd-content ol ol ol { list-style-type: lower-roman; } .bd-content ol > li > p:first-child > strong:first-child::after { content: ":"; } +.bd-content .toctree-wrapper > ul { list-style: disc; padding-left: 1.5rem; } +.bd-content .toctree-wrapper li { list-style: disc; margin: 0.4rem 0; } +.bd-content .toctree-wrapper { margin: 0.5rem 0 1.5rem; } + +.ocv-landing { max-width: 720px; } +.ocv-landing h2:first-child { margin-top: 1rem; } +.ocv-landing p { margin: 0.25rem 0 0.5rem; } + html[data-theme="light"] { --pst-color-link: #0969da; --pst-color-link-hover: #0550ae; @@ -52,7 +58,44 @@ html[data-theme="dark"] .bd-article-container a { color: #539bf5 !important; } html[data-theme="dark"] .bd-article-container a:hover { color: #6cb6ff !important; } .bd-content a > code, .bd-content code > a { color: inherit; } -/* --- Front-matter table (.opencv-meta-table — applied in source-read) -- */ +.bd-content a.opencv-enum-link, +.bd-content code.opencv-enum-sig > a, +.bd-content .opencv-enum-clickable a, +.bd-content .opencv-enum-clickable a span, +.bd-content code > a.opencv-include-link { + color: var(--pst-color-link, #0969da) !important; + text-decoration: none; + border-radius: 2px; +} +.bd-content a.opencv-enum-link:hover, +.bd-content code.opencv-enum-sig > a:hover, +.bd-content .opencv-enum-clickable a:hover, +.bd-content code > a.opencv-include-link:hover { + color: var(--pst-color-link-hover, #0550ae) !important; + text-decoration: none; /* blue only — no underline on hover */ +} +html[data-theme="dark"] .bd-content a.opencv-enum-link, +html[data-theme="dark"] .bd-content code.opencv-enum-sig > a, +html[data-theme="dark"] .bd-content .opencv-enum-clickable a, +html[data-theme="dark"] .bd-content .opencv-enum-clickable a span, +html[data-theme="dark"] .bd-content code > a.opencv-include-link { + color: #539bf5 !important; +} +html[data-theme="dark"] .bd-content a.opencv-enum-link:hover, +html[data-theme="dark"] .bd-content code.opencv-enum-sig > a:hover, +html[data-theme="dark"] .bd-content .opencv-enum-clickable a:hover, +html[data-theme="dark"] .bd-content code > a.opencv-include-link:hover { + color: #6cb6ff !important; +} + +html:not([data-theme="dark"]) .bd-content #typedef-documentation a { + color: var(--pst-color-link, #0969da) !important; +} + +html:not([data-theme="dark"]) .bd-content #function-documentation a { + color: var(--pst-color-link, #0969da) !important; +} + .opencv-meta-table, .opencv-meta-table .pst-scrollable-table-container { margin: 0 !important; padding: 0 !important; } .opencv-meta-table table { margin-bottom: 0 !important; } @@ -134,7 +177,6 @@ html[data-theme="dark"] table.opencv-meta-table tr:last-child td { border-bottom html[data-theme="dark"] .opencv-meta-table table td:last-child, html[data-theme="dark"] table.opencv-meta-table td:last-child { border-right: none !important; } -/* --- Images & figures -------------------------------------------------- */ .bd-content figure figcaption, .bd-content figure figcaption p, .bd-content figure figcaption .caption-text { @@ -148,7 +190,6 @@ html[data-theme="dark"] table.opencv-meta-table td:last-child { border-right: no .bd-content p:has(> img:only-child) { text-align: center; } .bd-content p > img { display: inline-block; } -/* --- YouTube embeds (.opencv-youtube — emitted by @youtube{ID} rule) --- */ .opencv-youtube { position: relative; width: 100%; @@ -167,7 +208,6 @@ html[data-theme="dark"] table.opencv-meta-table td:last-child { border-right: no border: 0; } -/* --- Generic tables ---------------------------------------------------- */ .bd-content table { border-collapse: collapse !important; width: 100%; @@ -191,7 +231,6 @@ html[data-theme="dark"] table.opencv-meta-table td:last-child { border-right: no } .bd-content table tbody tr:hover td { background: var(--pst-color-surface) !important; } -/* --- Header & navbar --------------------------------------------------- */ .bd-header { border-bottom: 1px solid var(--pst-color-border); backdrop-filter: blur(8px); @@ -255,7 +294,6 @@ html[data-theme="dark"] table.opencv-meta-table td:last-child { border-right: no .bd-main .bd-article { padding-left: 0; padding-right: 0; } } -/* --- Left sidebar nav -------------------------------------------------- */ .bd-sidebar-primary nav.bd-links { margin-right: 0 !important; } .bd-sidebar-primary nav.bd-docs-nav p.bd-links__title { font-size: 0.9rem !important; @@ -322,7 +360,6 @@ html[data-theme="dark"] { color-scheme: dark; } html[data-theme="dark"] div.highlight span, html[data-theme="dark"] div.highlight * { color: unset !important; } -/* Dark-mode Pygments tokens (matches the PR's mapping) */ html[data-theme="dark"] .highlight .c, html[data-theme="dark"] .highlight .c1, html[data-theme="dark"] .highlight .c2, html[data-theme="dark"] .highlight .cm, html[data-theme="dark"] .highlight .cp, html[data-theme="dark"] .highlight .cs, @@ -359,13 +396,11 @@ html[data-theme="dark"] .sig { color: #cdd9e5 !important; } -/* --- Heading anchors fade-in ------------------------------------------ */ a.headerlink { opacity: 0; transition: opacity 120ms; } h1:hover a.headerlink, h2:hover a.headerlink, h3:hover a.headerlink, h4:hover a.headerlink, h5:hover a.headerlink, h6:hover a.headerlink, dt:hover a.headerlink { opacity: 1; } -/* --- Prev/next strip --------------------------------------------------- */ .prev-next-area { overflow: hidden; clear: both; } .prev-next-area .prev-next-title { color: var(--pst-color-link) !important; @@ -383,7 +418,6 @@ dt:hover a.headerlink { opacity: 1; } color: var(--pst-color-muted) !important; } -/* --- Back-to-top pill (PyData's built-in #pst-back-to-top) ------------ */ #pst-back-to-top { font-family: var(--pst-font-family-base) !important; font-size: 0.82rem !important; @@ -407,35 +441,192 @@ dt:hover a.headerlink { opacity: 1; } opacity: 0.92 !important; } -/* --- C++ symbol signatures (Breathe-rendered API pages, when enabled) - */ -dl.cpp.function > dt.sig, -dl.cpp.class > dt.sig, -dl.cpp.struct > dt.sig, -dl.cpp.type > dt.sig { - background: var(--pst-color-surface); - border: 1px solid var(--pst-color-border); - border-left: 3px solid var(--opencv-accent); - border-radius: 0.4rem; - padding: 0.75rem 1rem; +dl.cpp.function, +dl.cpp.class, +dl.cpp.struct, +dl.cpp.type, +dl.cpp.var, +dl.cpp.enum, +dl.cpp.member, +dl.cpp.macro { + border: 1px solid var(--pst-color-border, #d0d7de); + border-radius: 8px; + background: #ffffff; + margin: 0.85rem 0 1.35rem; + padding: 0; + overflow: hidden; + transition: border-color 120ms ease, box-shadow 120ms ease; +} +dl.cpp.function:hover, +dl.cpp.class:hover, +dl.cpp.struct:hover, +dl.cpp.type:hover, +dl.cpp.var:hover, +dl.cpp.enum:hover, +dl.cpp.member:hover, +dl.cpp.macro:hover { + border-color: var(--pst-color-link, #0969da); + box-shadow: 0 1px 6px rgba(9, 105, 218, 0.12); +} +html[data-theme="dark"] dl.cpp.function, +html[data-theme="dark"] dl.cpp.class, +html[data-theme="dark"] dl.cpp.struct, +html[data-theme="dark"] dl.cpp.type, +html[data-theme="dark"] dl.cpp.var, +html[data-theme="dark"] dl.cpp.enum, +html[data-theme="dark"] dl.cpp.member, +html[data-theme="dark"] dl.cpp.macro { + background: #1a2234; + border-color: #2d3748; +} +html[data-theme="dark"] dl.cpp.function:hover, +html[data-theme="dark"] dl.cpp.class:hover, +html[data-theme="dark"] dl.cpp.struct:hover, +html[data-theme="dark"] dl.cpp.type:hover, +html[data-theme="dark"] dl.cpp.var:hover, +html[data-theme="dark"] dl.cpp.enum:hover, +html[data-theme="dark"] dl.cpp.member:hover, +html[data-theme="dark"] dl.cpp.macro:hover { + border-color: #58a6ff; + box-shadow: none; +} + +dl.cpp > dt.sig { + background: var(--pst-color-surface, #f6f8fa); + border: none; + border-radius: 0; + padding: 0.7rem 1rem; + margin: 0; + font-family: var(--pst-font-family-monospace); font-size: 0.9rem; line-height: 1.8; overflow-x: auto; - margin-bottom: 0.5rem; } -dl.cpp.function .sig-name.descname { color: var(--opencv-accent); font-weight: 700; } -dl.cpp.function .sig-prename.descclassname { color: var(--pst-color-text-muted); } +dl.cpp > dt.sig:not(:last-child) { + border-bottom: 1px solid var(--pst-color-border, #d0d7de); +} +html[data-theme="dark"] dl.cpp > dt.sig { + background: #111622; +} +html[data-theme="dark"] dl.cpp > dt.sig:not(:last-child) { + border-bottom-color: #2d3748; +} +dl.cpp .sig-name.descname { color: var(--opencv-accent); font-weight: 700; } +dl.cpp .sig-prename.descclassname { color: var(--pst-color-text-muted); } + +dl.cpp > dd { + margin: 0; + padding: 0.85rem 1rem 0.6rem; +} +dl.cpp > dd > :last-child { margin-bottom: 0; } + +section[id$="-documentation"] > section { + border: 1px solid var(--pst-color-border, #d0d7de); + border-radius: 8px; + background: #ffffff; + margin: 1rem 0 1.4rem; + overflow: hidden; + transition: border-color 120ms ease, box-shadow 120ms ease; +} +section[id$="-documentation"] > section:hover { + border-color: var(--pst-color-link, #0969da); + box-shadow: 0 1px 6px rgba(9, 105, 218, 0.12); +} +html[data-theme="dark"] section[id$="-documentation"] > section { + background: #1a2234; + border-color: #2d3748; +} +html[data-theme="dark"] section[id$="-documentation"] > section:hover { + border-color: #58a6ff; + box-shadow: none; +} + +section[id$="-documentation"] > section > h3 { + display: flex; + width: fit-content; + max-width: 100%; + align-items: baseline; + margin: 0.7rem 0.9rem 0.35rem; + padding: 0.3rem 0.7rem; + background: var(--pst-color-surface, #f6f8fa); + border: 1px solid var(--pst-color-border, #d0d7de); + border-radius: 6px; + font-size: 1.0rem; + font-weight: 600; +} +section[id$="-documentation"] > section > h3::before { + content: "\25C6"; /* ◆ */ + color: var(--pst-color-link, #0969da); + margin-right: 0.5rem; + font-size: 0.8em; + align-self: center; +} +section[id$="-documentation"] > section > h3 > a.headerlink { margin-left: 0.4rem; } +html[data-theme="dark"] section[id$="-documentation"] > section > h3 { + background: #111622; + border-color: #2d3748; +} +html[data-theme="dark"] section[id$="-documentation"] > section > h3::before { color: #58a6ff; } + +section[id$="-documentation"] > section > p, +section[id$="-documentation"] > section > ul, +section[id$="-documentation"] > section > dl, +section[id$="-documentation"] > section > div[class*="highlight"] { + margin: 0; + padding: 0.35rem 1rem; +} + +section[id$="-documentation"] > section > ul, +section[id$="-documentation"] > section > ol { + padding-left: 2.5rem; +} + +section[id$="-documentation"] > section > p:has(> code) { line-height: 1.7; } +section[id$="-documentation"] > section > p > code { + background: transparent; + border: none; + padding: 0; + font-size: 0.875rem; +} + +section[id$="-documentation"] > section > p.opencv-api-sig > code { + white-space: pre-wrap; +} + +html[data-theme="dark"] section[id$="-documentation"] > section code, +html[data-theme="dark"] section[id$="-documentation"] > section code span { + background: transparent !important; + background-color: transparent !important; +} + +section[id$="-documentation"] > section > p.opencv-api-include { + margin: 0.65rem 1rem 0.85rem; + padding: 0.45rem 0.75rem; + background: var(--pst-color-surface, #f6f8fa); + border: 1px solid var(--pst-color-border, #d0d7de); + border-radius: 6px; +} +html[data-theme="dark"] section[id$="-documentation"] > section > p.opencv-api-include { + background: #111622; + border-color: #2d3748; +} + +section[id$="-documentation"] > section, +section[id$="-documentation"] > section > span[id], +section[id$="-documentation"] > section > h3 { + scroll-margin-top: 5rem; +} -/* --- MathJax inline alignment tweak ----------------------------------- */ mjx-container.MathJax:not([display="true"]) { vertical-align: -0.15em !important; } -/* --- Copy-to-clipboard button ----------------------------------------- - * Provided by the `sphinx-copybutton` extension, which injects - * ` +{# Centered modal overlay (layout.html moves it to and toggles the open + class). Holds the Doxygen search box; results render via Doxygen's search.js. #} + diff --git a/docs_sphinx/conf.py b/docs_sphinx/conf.py index 731b0ab372..4f0dfbf2a7 100644 --- a/docs_sphinx/conf.py +++ b/docs_sphinx/conf.py @@ -1,76 +1,25 @@ -"""Sphinx wrapper for opencv/doc/. - -The wrapper lives in opencv/docs_sphinx/ as a single conf.py. Sphinx is -invoked with config-dir / source-dir separation so the wrapper never -duplicates the legacy tree. Build via the CMake `sphinx` target: - - cmake --build --target sphinx - # output -> /docs_sphinx/html/ - -opencv/doc/ stays untouched: Doxygen-flavored directives in the .markdown -sources are translated to MyST in a `source-read` hook below. - -To enable additional tutorial modules, append their directory names (the -folder under opencv/doc/tutorials/) to DOC_MODULES below. The root index -(tutorials/tutorials.markdown) lists every module, but only modules in -DOC_MODULES are actually compiled — entries for the rest are dropped -from toctrees automatically. -""" +# 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 pathlib, re, textwrap as _textwrap +import os as _os, pathlib, sys as _sys -HERE = pathlib.Path(__file__).parent.resolve() -DOC_ROOT = (HERE.parent / "doc").resolve() -OPENCV_ROOT = HERE.parent.resolve() +# 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__))) -# --------------------------------------------------------------------------- -# SCOPE — add module folder names from opencv/doc/tutorials/ here. -# Override via env var to avoid editing this file: -# OPENCV_DOC_MODULES=photo,imgproc cmake --build --target sphinx -# --------------------------------------------------------------------------- -import os as _os -DOC_MODULES = [ - m.strip() - for m in (_os.environ.get("OPENCV_DOC_MODULES") or "photo,objdetect,core,calib3d,features,3d,app,introduction,imgproc,ios,dnn,gpu,others").split(",") - if m.strip() -] -DOC_JS_MODULES = [ - m.strip() - for m in (_os.environ.get("OPENCV_DOC_JS_MODULES") or "js_setup,js_gui,js_core,js_imgproc,js_video,js_dnn").split(",") - if m.strip() -] - -DOC_PY_MODULES = [ - m.strip() - for m in (_os.environ.get("OPENCV_DOC_PY_MODULES") or "py_setup,py_gui,py_core,py_imgproc,py_features,py_video,py_calib3d,py_ml,py_photo,py_objdetect,py_bindings").split(",") - if m.strip() -] - -# --------------------------------------------------------------------------- -# SCOPE — contrib tree. Folder names under opencv_contrib/modules/. -# Override via env var to avoid editing this file: -# OPENCV_CONTRIB_MODULES=ml,bgsegm cmake --build --target sphinx -# Empty list = main-only build (legacy behavior, no contrib site). -# --------------------------------------------------------------------------- -CONTRIB_MODULES = [ - m.strip() - for m in (_os.environ.get("OPENCV_CONTRIB_MODULES") or "ml,bgsegm,bioinspired,cannops,ccalib,cnn_3dobj,cvv,dnn_objdetect,dnn_superres,gapi,hdf,julia,line_descriptor,phase_unwrapping,structured_light,sfm,viz,tracking").split(",") - if m.strip() -] -CONTRIB_ROOT = pathlib.Path( - _os.environ.get("OPENCV_CONTRIB_ROOT") - or str(HERE.parent.parent / "opencv_contrib" / "modules") -).resolve() - -# Sphinx srcdir as seen by conf.py. CMake stages a merged tree at -# ${CMAKE_BINARY_DIR}/docs_sphinx_input/ and forwards this env var. -# Default = DOC_ROOT so ad-hoc sphinx-build runs keep working. The `or` -# idiom (rather than dict.get's default) treats an empty-string env var -# the same as unset — CMake forwards "" when contrib is disabled. -SPHINX_INPUT_ROOT = pathlib.Path( - _os.environ.get("OPENCV_SPHINX_INPUT_ROOT") or str(DOC_ROOT) -).resolve() +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" @@ -78,39 +27,63 @@ author = "OpenCV Team" release = "5.x" # -- Sphinx core ------------------------------------------------------------ -extensions = ["myst_parser", "sphinx.ext.graphviz"] -# Render Doxygen \dot ... \enddot blocks as inline SVG (matches Doxygen's -# DOT_IMAGE_FORMAT=svg default — keeps text crisp and selectable). -graphviz_output_format = "svg" +extensions = ["myst_parser"] for _ext in ("sphinx_design", "sphinx_copybutton"): try: __import__(_ext) extensions.append(_ext) except ImportError: pass -HAVE_SPHINX_DESIGN = "sphinx_design" in extensions + +# -- 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"} -# Root tutorial index (lists all modules via @subpage). Stays the master -# regardless of how many modules are in DOC_MODULES. -master_doc = "tutorials/tutorials" +# 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" -# Source dir is the staged tree (or DOC_ROOT for legacy ad-hoc runs). # Scope: master + enabled main modules + (optionally) enabled contrib modules. -include_patterns = ["tutorials/tutorials.markdown", "faq.markdown", - "citelist.markdown", "intro.markdown"] + [ +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"] + [ - f"js_tutorials/{m}/**" for m in DOC_JS_MODULES -] if DOC_JS_MODULES else []) + (["py_tutorials/py_tutorials.markdown"] + [ - f"py_tutorials/{m}/**" for m in DOC_PY_MODULES -] if DOC_PY_MODULES else []) +] + (["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", + "**/Thumbs.db", "**/.DS_Store", "**/_old/**", "tutorials/core/how_to_use_OpenCV_parallel_for_/**", "tutorials/introduction/load_save_image/**", "tutorials/app/_old/**", @@ -121,411 +94,38 @@ myst_enable_extensions = [ "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", ] -# -- Doxygen integration ----------------------------------------------------- -# External links in the navbar and unbuilt-module sidebar entries point at -# the existing Doxygen build. Override the base URL or tagfile via env vars. -DOXYGEN_BASE_URL = ( - _os.environ.get("OPENCV_DOXYGEN_BASE_URL", "https://docs.opencv.org/5.x/") - .rstrip("/") + "/") -_TAG_FILE = pathlib.Path(_os.environ.get("OPENCV_DOXYGEN_TAGFILE", "")) -if not (_TAG_FILE.name and _TAG_FILE.is_file()): - _TAG_FILE_CANDIDATES = ( - [HERE.parent.parent / "build" / "doc" / "doxygen" / "html" / "opencv.tag", - HERE.parent.parent / "build" / "doc" / "opencv.tag"] - + sorted((HERE.parent.parent).glob("build*/doc/opencv.tag")) - + sorted((HERE.parent.parent).glob("build*/doc/doxygen/html/opencv.tag")) - # Also support an in-tree build/ inside opencv/ (the usual - # `cmake -B build` from the repo root). Tried after the out-of-tree - # candidates so existing setups resolve unchanged. - + [HERE.parent / "build" / "doc" / "doxygen" / "html" / "opencv.tag", - HERE.parent / "build" / "doc" / "opencv.tag"] - + sorted((HERE.parent).glob("build*/doc/opencv.tag")) - + sorted((HERE.parent).glob("build*/doc/doxygen/html/opencv.tag")) - ) - _TAG_FILE = next((p for p in _TAG_FILE_CANDIDATES if p.is_file()), pathlib.Path("")) - -# anchor -> doxygen URL filename (from opencv.tag if available). -_TAG_FILENAMES: dict[str, str] = {} -# anchor -> human-readable page title (from opencv.tag). -_TAG_PAGE_TITLES: dict[str, str] = {} -# cv API name -> full doxygen URL (for cv.Name auto-linking). -_CV_API: dict[str, str] = {} -if _TAG_FILE.is_file(): - try: - import xml.etree.ElementTree as _ET - for _c in _ET.parse(str(_TAG_FILE)).getroot().iter("compound"): - if _c.get("kind") == "page": - _n, _f = _c.findtext("name"), _c.findtext("filename") - _t = _c.findtext("title", "") - if _n and _f: - _TAG_FILENAMES[_n] = _f if _f.endswith(".html") else _f + ".html" - if _t: - _TAG_PAGE_TITLES[_n] = _t - if _c.get("kind") in ("class", "namespace"): - _cn = (_c.findtext("name") or "").split("::")[-1] - _cf = _c.findtext("filename", "") - if _cn and _cf: - _CV_API.setdefault(_cn, DOXYGEN_BASE_URL + (_cf if _cf.endswith(".html") else _cf + ".html")) - if _c.get("kind") == "group": - # Doxygen module pages (core, imgproc, dnn, …) live as - # `kind="group"` compounds in the tagfile. Without capturing - # them here, inline `@ref core` and bullet-list refs to module - # roots in intro.markdown don't resolve to anything. - _gn = _c.findtext("name") - _gf = _c.findtext("filename") - _gt = _c.findtext("title") - if _gn and _gf: - _TAG_FILENAMES[_gn] = _gf if _gf.endswith(".html") else _gf + ".html" - if _gn and _gt: - _TAG_PAGE_TITLES[_gn] = _gt - for _m in _c.findall("member"): - _n = _m.findtext("name", "") - _af = _m.findtext("anchorfile", "") - _an = _m.findtext("anchor", "") - if _n and _af and _an: - _CV_API.setdefault(_n, DOXYGEN_BASE_URL + _af + "#" + _an) - except Exception: - pass - -def _doxygen_url(page: str) -> str: - return DOXYGEN_BASE_URL + _TAG_FILENAMES.get(page, page) - - -# ---- Citation numbering -------------------------------------------------- -# `@cite KEY` resolves to `[N]` where N is the entry's position in -# `doc/opencv.bib` sorted case-insensitively by key (Doxygen's default -# ordering). Doxygen's live citelist.html numbers map keys to integers the -# same way; reading from the bib means our build is self-contained and -# doesn't need a network fetch. The same parsed entries also feed the -# Sphinx-side bibliography page generated further down, so the `[N]` -# emitted at @cite-resolution time always matches the `[N]` rendered on -# the citelist page. -def _bib_parse(text: str) -> list[dict]: - """Walk a BibTeX file into a list of {_type, _key, field: value, ...}. - Brace-balanced; handles `{...}` and `"..."` value forms. Concatenation - (`val # "more"`) is not supported — opencv.bib doesn't use it.""" - out: list[dict] = [] - n, i = len(text), 0 - while i < n: - m = re.search(r"@(\w+)\s*\{\s*([^\s,]+)\s*,", text[i:]) - if not m: - break - kind, key = m.group(1), m.group(2) - i += m.end() - depth, body_start = 1, i - while i < n and depth > 0: - c = text[i] - if c == "{": - depth += 1 - elif c == "}": - depth -= 1 - i += 1 - if depth != 0: - break # malformed entry; stop rather than misparse the rest - out.append({"_type": kind.lower(), "_key": key, - **_bib_fields(text[body_start:i - 1])}) - return out - -def _bib_fields(body: str) -> dict[str, str]: - fields: dict[str, str] = {} - n, j = len(body), 0 - while j < n: - while j < n and body[j] in " \t\n\r,": - j += 1 - if j >= n: - break - ns = j - while j < n and (body[j].isalnum() or body[j] == "_"): - j += 1 - name = body[ns:j].lower() - if not name: - break - while j < n and body[j] in " \t\n\r": - j += 1 - if j >= n or body[j] != "=": - break - j += 1 - while j < n and body[j] in " \t\n\r": - j += 1 - if j >= n: - break - if body[j] == "{": - j += 1 - depth, vs = 1, j - while j < n and depth > 0: - if body[j] == "{": - depth += 1 - elif body[j] == "}": - depth -= 1 - j += 1 - value = body[vs:j - 1] - elif body[j] == '"': - j += 1 - vs = j - while j < n and body[j] != '"': - if body[j] == "\\" and j + 1 < n: - j += 1 - j += 1 - value = body[vs:j] - if j < n: - j += 1 - else: - vs = j - while j < n and body[j] not in ",\n": - j += 1 - value = body[vs:j].strip() - fields[name] = value - return fields - -# LaTeX accent + special-char cleanup. Just enough to render opencv.bib -# readably; not a full parser. -_LATEX_ACCENT_RE = re.compile(r"\\([\"'`^~.])\s*\{?\s*([A-Za-z])\s*\}?") -_LATEX_ACCENT_MAP = { - ('"', 'a'): 'ä', ('"', 'e'): 'ë', ('"', 'i'): 'ï', ('"', 'o'): 'ö', - ('"', 'u'): 'ü', ('"', 'A'): 'Ä', ('"', 'O'): 'Ö', ('"', 'U'): 'Ü', - ("'", 'a'): 'á', ("'", 'e'): 'é', ("'", 'i'): 'í', ("'", 'o'): 'ó', - ("'", 'u'): 'ú', ("'", 'c'): 'ć', ("'", 'n'): 'ń', ("'", 'A'): 'Á', - ("'", 'E'): 'É', ("'", 'I'): 'Í', ("'", 'O'): 'Ó', ("'", 'U'): 'Ú', - ("`", 'a'): 'à', ("`", 'e'): 'è', ("`", 'i'): 'ì', ("`", 'o'): 'ò', - ("`", 'u'): 'ù', - ('^', 'a'): 'â', ('^', 'e'): 'ê', ('^', 'i'): 'î', ('^', 'o'): 'ô', - ('^', 'u'): 'û', - ('~', 'a'): 'ã', ('~', 'n'): 'ñ', ('~', 'o'): 'õ', - ('.', 'c'): 'ċ', ('.', 'e'): 'ė', -} -_LATEX_SPECIAL = { - r"\&": "&", r"\%": "%", r"\#": "#", r"\$": "$", - r"\_": "_", r"\{": "{", r"\}": "}", - r"\textendash": "–", r"\textemdash": "—", - r"\ldots": "…", r"\dots": "…", - r"\o": "ø", r"\O": "Ø", r"\ss": "ß", - r"\aa": "å", r"\AA": "Å", r"\ae": "æ", r"\AE": "Æ", - "---": "—", "--": "–", -} - -def _bib_clean(s: str) -> str: - s = re.sub(r"\s+", " ", s or "").strip() - s = _LATEX_ACCENT_RE.sub( - lambda m: _LATEX_ACCENT_MAP.get((m.group(1), m.group(2)), m.group(2)), s) - for k, v in _LATEX_SPECIAL.items(): - s = s.replace(k, v) - return s.replace("{", "").replace("}", "").strip() - -def _bib_join_authors(field: str) -> str: - """Render an author/editor list bibtex-plain style: - 1 author -> "A" - 2 authors -> "A and B" - 3+ -> "A, B, and C" (Oxford comma + 'and')""" - if not field: - return "" - parts = re.split(r"\s+and\s+", field) - out: list[str] = [] - for p in parts: - p = _bib_clean(p) - if "," in p: # "Last, First" → "First Last" - last, first = p.split(",", 1) - p = f"{first.strip()} {last.strip()}" - out.append(p) - if len(out) <= 1: - return out[0] if out else "" - if len(out) == 2: - return f"{out[0]} and {out[1]}" - return ", ".join(out[:-1]) + f", and {out[-1]}" - -def _bib_render_entry(e: dict, num: int | None) -> str: - key = e["_key"] - bracket = f"[{num}]" if num is not None else f"[{key}]" - authors = _bib_join_authors(e.get("author") or e.get("editor") or "") - title = _bib_clean(e.get("title", "")) - year = _bib_clean(e.get("year", "")) - month = _bib_clean(e.get("month", "")) - pages = _bib_clean(e.get("pages", "")) - volume = _bib_clean(e.get("volume", "")) - number = _bib_clean(e.get("number", "")) - doi = _bib_clean(e.get("doi", "")) - url = _bib_clean(e.get("url", "")) - journal = _bib_clean(e.get("journal", "")) - booktitle = _bib_clean(e.get("booktitle", "")) - publisher = _bib_clean(e.get("publisher") or e.get("howpublished") - or e.get("institution") or "") - - # bibtex `plain` style wraps the title in the URL hyperlink (or DOI URL - # when no `url` field is set). DOI without a URL field is rendered as a - # https://doi.org/... link on the title. - title_url = url or (f"https://doi.org/{doi}" if doi else "") - title_md = f"[{title}]({title_url})" if (title and title_url) else title - - # date = month + year ("nov 2012") — bibtex's plain `byear` formatter. - date = (f"{month} {year}".strip()) if (month or year) else "" - - bits: list[str] = [] - if authors: - bits.append(authors) - if title_md: - bits.append(title_md) - - # Venue formatting differs by entry kind (Doxygen/bibtex plain style): - # @article -> "*Journal*, V(N):pages, date." - # @inproceedings -> "In *Booktitle*, pages X-Y. Publisher, date." - # @incollection -> "In *Booktitle*, pages X-Y. Publisher, date." - # @book/@misc -> "Publisher, date." (or just date) - kind = e.get("_type", "") - if kind == "article" and journal: - seg = f"*{journal}*" - if volume: - seg += f", {volume}" + (f"({number})" if number else "") - if pages: - seg += f":{pages}" - elif pages: - seg += f", {pages}" - if date: - seg += f", {date}" - bits.append(seg) - elif kind in ("inproceedings", "incollection") and booktitle: - seg = f"In *{booktitle}*" - if pages: - seg += f", pages {pages}" - if publisher: - seg += f". {publisher}" - if date: - seg += f", {date}" - bits.append(seg) - else: - # @book / @misc / @techreport / fallback - tail = [] - if publisher: - tail.append(publisher) - if booktitle and not publisher: - tail.append(f"*{booktitle}*") - if date: - tail.append(date) - if tail: - bits.append(", ".join(tail)) - - body = ". ".join(bits) - if body and not body.endswith("."): - body += "." - - # Raw HTML anchor preserves the original CITEREF_ case (matches - # Doxygen's URL convention exactly, so cached links to - # `citelist.html#CITEREF_` keep resolving on the new site too). - return f'\n\n**{bracket}** {body}' - -def _bib_render_all(entries: list[dict], numbering: dict[str, int]) -> str: - out = ["Bibliography {#citelist}", "============", ""] - for e in entries: - out.append(_bib_render_entry(e, numbering.get(e["_key"]))) - out.append("") - return "\n".join(out) - - -def _bib_sort_key(e: dict) -> tuple: - """bibtex `plain` style: sort by first author's last name, then year, - then title. Without this, our citelist numbering doesn't match - docs.opencv.org/5.x — Doxygen runs bibtex with LATEX_BIB_STYLE=plain - (set in doc/Doxyfile.in), which sorts by author, NOT by bib key.""" - authors = e.get("author") or e.get("editor") or "zzz" - first = re.split(r"\s+and\s+", authors)[0] - first = _bib_clean(first) - if "," in first: # "Last, First" - last = first.split(",", 1)[0].strip() - else: # "First Middle Last" - toks = first.split() - last = toks[-1] if toks else "zzz" - return (last.lower(), - _bib_clean(e.get("year", "")), - _bib_clean(e.get("title", "")).lower()) - -# Discover every .bib file Doxygen would feed bibtex (see opencv/doc/ -# CMakeLists.txt: paths_bib accumulates `${m}.bib` for each module in -# OPENCV_DOC_LIST plus the main opencv.bib). Reading them all here is -# what makes `[1] Achanta…` appear first — that entry lives in -# opencv_contrib/modules/ximgproc/doc/ximgproc.bib, not in doc/opencv.bib. -_BIB_FILES: list[pathlib.Path] = [] -if (DOC_ROOT / "opencv.bib").is_file(): - _BIB_FILES.append(DOC_ROOT / "opencv.bib") -_BIB_FILES += sorted((OPENCV_ROOT / "modules").glob("*/doc/*.bib")) -if CONTRIB_ROOT.is_dir(): - _BIB_FILES += sorted(CONTRIB_ROOT.glob("*/doc/*.bib")) - -_CITE_NUMBER: dict[str, int] = {} -# Parsed entries kept in module scope so the citelist generator (below) reuses -# the same sort order that fed `_CITE_NUMBER`. Numbering stays consistent -# without re-parsing or re-sorting in two places. -_BIB_ENTRIES_SORTED: list[dict] = [] -_seen_keys: set[str] = set() # dedupe across bibs; first occurrence wins -_all_entries: list[dict] = [] -for _bf in _BIB_FILES: - try: - _txt = _bf.read_text(encoding="utf-8", errors="replace") - except OSError: - continue - for _e in _bib_parse(_txt): - if _e["_key"] in _seen_keys: - continue - _seen_keys.add(_e["_key"]) - _all_entries.append(_e) -_BIB_ENTRIES_SORTED = sorted(_all_entries, key=_bib_sort_key) -for _i, _e in enumerate(_BIB_ENTRIES_SORTED, 1): - _CITE_NUMBER[_e["_key"]] = _i - -# Stage the bibliography into the Sphinx srcdir so `@subpage citelist` -# below resolves to an internal docname. Skipped when SPHINX_INPUT_ROOT -# is DOC_ROOT (ad-hoc sphinx-build) — writing into opencv/doc/ is -# forbidden, and `@cite` falls back to the external Doxygen URL in that -# case (see _cite_repl). -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 - - -# ---- Redirect-page map --------------------------------------------------- -# OpenCV's docs include many "stub" pages whose entire body is -# `Content has been moved: @ref destination`. Following these chains at -# render time means inline `@ref X` ends up pointing at the actual content -# instead of an intermediate redirect (which would itself need clicking -# through). Built from anything under `doc/{tutorials,py_tutorials, -# js_tutorials}/**/*.markdown`; `_old/` subtrees are intentionally included -# because that's where many of the canonical redirect stubs live. -_REDIRECT_MAP: dict[str, str] = {} -_REDIRECT_RE = re.compile( - r"\{#(?P[\w-]+)\}\s*\n[=\-]+\s*\n+" - r"\s*(?:Content|Tutorial\s+content)\s+has\s+been\s+moved\b" - r"[^@]{0,300}@ref\s+(?P[\w-]+)", - re.IGNORECASE, -) -for _scan_dir in ("tutorials", "py_tutorials", "js_tutorials"): - _root = DOC_ROOT / _scan_dir - if not _root.is_dir(): - continue - for _md in _root.rglob("*.markdown"): - try: - _t = _md.read_text(encoding="utf-8", errors="replace")[:2000] - except OSError: - continue - _m = _REDIRECT_RE.search(_t) - if _m: - _REDIRECT_MAP[_m.group("src")] = _m.group("dst") - -def _resolve_redirect(anchor: str) -> str: - """Follow `_REDIRECT_MAP` transitively. Cycles bail safely.""" - seen: set[str] = set() - while anchor in _REDIRECT_MAP and anchor not in seen: - seen.add(anchor) - anchor = _REDIRECT_MAP[anchor] - return anchor - # -- HTML / PyData theme ---------------------------------------------------- try: import pydata_sphinx_theme # noqa: F401 @@ -544,26 +144,25 @@ html_css_files = [ ] html_theme_options = { "logo": {"text": f"OpenCV {release}"}, - # Show all 7 Doxygen-style external links inline (no "More" dropdown). - "header_links_before_dropdown": 7, - # Doxygen-style top-level nav (the legacy site's MAIN PAGE / RELATED - # PAGES / NAMESPACES / CLASSES / FILES / EXAMPLES / JAVA DOCUMENTATION). - # All external — they target the existing Doxygen build. + "header_links_before_dropdown": 6, "external_links": [ - {"url": _doxygen_url("index.html"), "name": "Main Page"}, - {"url": _doxygen_url("pages.html"), "name": "Related Pages"}, - {"url": _doxygen_url("namespaces.html"), "name": "Namespaces"}, - {"url": _doxygen_url("annotated.html"), "name": "Classes"}, - {"url": _doxygen_url("files.html"), "name": "Files"}, - {"url": _doxygen_url("examples.html"), "name": "Examples"}, - {"url": DOXYGEN_BASE_URL + "javadoc/", "name": "Java Documentation"}, + {"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"], + "secondary_sidebar_items": {"**": ["page-toc"], "index": []}, "back_to_top_button": True, "show_version_warning_banner": False, "icon_links": [{"name": "GitHub", @@ -571,178 +170,6 @@ html_theme_options = { "icon": "fa-brands fa-github"}], } -# Doxygen defines \fork{a}{b}{c}{d} as a piecewise-function shorthand. -# Define it as a MathJax macro so threshold.markdown renders correctly. -mathjax3_config = { - "tex": { - "macros": { - "fork": [r"\left\{ \begin{array}{ll} #1 & \mbox{#2}\\ #3 & \mbox{#4}\end{array} \right.", 4], - } - } -} - -# =========================================================================== -# Doxygen-flavored .markdown -> MyST translation via source-read. -# Nothing on disk under opencv/doc/ is modified. -# =========================================================================== - -# Build anchor maps. Two kinds: -# _ANCHOR_TO_DOC anchor -> docname (internal — for enabled modules) -# _ANCHOR_TO_EXTERNAL anchor -> (title, url) (external — for the rest) -# Disabled modules still appear in the master toctree as external links to -# the Doxygen build, so the left sidebar shows the full module list. -_ANCHOR_TO_DOC: dict[str, str] = {} -_ANCHOR_TO_EXTERNAL: dict[str, tuple[str, str]] = {} - -_HEAD_RE = re.compile( - r"^(?P[^\n]+?)\s*\{#(?P[\w-]+)\}\s*\n[=\-]{3,}\s*$" - r"|" - r"^#+\s+(?P[^\n]+?)\s*\{#(?P[\w-]+)\}\s*$", - re.MULTILINE) - -def _scan_internal(path: pathlib.Path, base: pathlib.Path | None = None) -> None: - """Add every {#anchor} and standalone `@anchor NAME` in `path` (file or - dir) to _ANCHOR_TO_DOC. Docname is computed relative to `base` (default - SPHINX_INPUT_ROOT) so the same scanner serves both main and contrib. - Picks up both `.markdown` (the bulk of the tree) and `.md` (the form - used by ports like dnn/dnn_pytorch_tf_*).""" - base = base or SPHINX_INPUT_ROOT - _SUFFIXES = (".markdown", ".md") - if path.is_file(): - files = [path] if path.suffix in _SUFFIXES else [] - elif path.is_dir(): - files = [p for s in _SUFFIXES for p in path.rglob(f"*{s}")] - else: - files = [] - for md in files: - try: - body = md.read_text(encoding="utf-8", errors="replace") - except OSError: - continue - # Use the unresolved path so symlinks in the staged input tree - # produce docnames relative to the staging root, not to their - # real source location (opencv/doc/ or opencv_contrib/modules/). - rel = md.relative_to(base).with_suffix("").as_posix() - for m in re.finditer(r"\{#([\w-]+)\}", body): - _ANCHOR_TO_DOC[m.group(1)] = rel - for m in re.finditer(r"^@anchor\s+([\w-]+)\s*$", body, re.MULTILINE): - _ANCHOR_TO_DOC[m.group(1)] = rel - -def _scan_external(toc_file: pathlib.Path) -> None: - """Pull the top heading's (title, anchor) from a module's table_of_content - file and add it to _ANCHOR_TO_EXTERNAL with a URL into the Doxygen build.""" - try: - head = toc_file.read_text(encoding="utf-8", errors="replace")[:4000] - except OSError: - return - m = _HEAD_RE.search(head) - if not m: - return - anchor = m.group("anchor1") or m.group("anchor2") - title = (m.group("title1") or m.group("title2") or "").strip() - if not anchor: - return - url = DOXYGEN_BASE_URL + _TAG_FILENAMES.get(anchor, "index.html") - _ANCHOR_TO_EXTERNAL[anchor] = (title, url) - -# Internal scan: master + every enabled main and contrib module subtree. -# Walk the staged tree so docnames stay relative to SPHINX_INPUT_ROOT (Sphinx -# srcdir), regardless of where the actual source files live on disk. -_scan_internal(SPHINX_INPUT_ROOT / "tutorials" / "tutorials.markdown") -for _m in DOC_MODULES: - _scan_internal(SPHINX_INPUT_ROOT / "tutorials" / _m) -_contrib_root_md = SPHINX_INPUT_ROOT / "tutorials_contrib" / "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 (siblings of tutorials/ in the staged tree). -# Registers their {#anchor} in _ANCHOR_TO_DOC so the master-doc @subpage -# injection below resolves to an internal docname instead of being dropped. -_scan_internal(SPHINX_INPUT_ROOT / "faq.markdown") -_scan_internal(SPHINX_INPUT_ROOT / "citelist.markdown") -_scan_internal(SPHINX_INPUT_ROOT / "intro.markdown") - -# External scan: every OTHER main module's top-level table_of_content_*.markdown. -# Sources live under DOC_ROOT (the staged tree only contains *enabled* main -# modules, not the rest), so scan DOC_ROOT directly here. -for _toc in (DOC_ROOT / "tutorials").glob("*/table_of_content_*.markdown"): - if _toc.parent.name not in DOC_MODULES: - _scan_external(_toc) - -_scan_internal(SPHINX_INPUT_ROOT / "js_tutorials" / "js_tutorials.markdown") -for _m in DOC_JS_MODULES: - _scan_internal(SPHINX_INPUT_ROOT / "js_tutorials" / _m) -for _toc in (DOC_ROOT / "js_tutorials").glob("*/js_table_of_contents_*.markdown"): - if _toc.parent.name not in DOC_JS_MODULES: - _scan_external(_toc) - -_scan_internal(SPHINX_INPUT_ROOT / "py_tutorials" / "py_tutorials.markdown") -for _m in DOC_PY_MODULES: - _scan_internal(SPHINX_INPUT_ROOT / "py_tutorials" / _m) -for _toc in (DOC_ROOT / "py_tutorials").glob("*/py_table_of_contents_*.markdown"): - if _toc.parent.name not in DOC_PY_MODULES: - _scan_external(_toc) - -for _m in CONTRIB_MODULES: - _tut_dir = CONTRIB_ROOT / _m / "tutorials" - if not _tut_dir.is_dir(): - continue - for _md in list(_tut_dir.rglob("*.markdown")) + list(_tut_dir.rglob("*.md")): - try: - _head = _md.read_text(encoding="utf-8", errors="replace")[:4000] - except OSError: - continue - _rel = "tutorials_contrib/" + _m + "/" + _md.relative_to(_tut_dir).with_suffix("").as_posix() - for _mm in re.finditer(r"\{#([\w-]+)\}", _head): - _ANCHOR_TO_DOC[_mm.group(1)] = _rel - -# Basename -> srcdir-relative URL index for image lookup, mirroring -# Doxygen's flat IMAGE_PATH. Walks source trees directly (not the staged -# tree) because pathlib.rglob in Python <3.13 doesn't follow symlinks. -_IMAGE_INDEX: dict[str, str] = {} -_IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".svg", ".bmp", ".webp"} -for _img_tree in (DOC_ROOT / "tutorials", DOC_ROOT / "js_tutorials", DOC_ROOT / "py_tutorials"): - for _img in _img_tree.rglob("images/*"): - if _img.is_file(): - _IMAGE_INDEX.setdefault(_img.name, _img.relative_to(DOC_ROOT).as_posix()) -for _img in (DOC_ROOT / "js_tutorials" / "js_assets").glob("*"): - if _img.is_file(): - _IMAGE_INDEX.setdefault(_img.name, _img.relative_to(DOC_ROOT).as_posix()) -for _img in (DOC_ROOT / "images").glob("*"): - if _img.is_file(): - _IMAGE_INDEX.setdefault(_img.name, - _img.relative_to(DOC_ROOT).as_posix()) -for _m in CONTRIB_MODULES: - # /tutorials/**/images/* — same shape as main, reachable through - # the existing tutorials_contrib/ symlink CMake stages. - _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 /tutorials/ (/doc/pics, /samples). - # URL is /contrib_modules//. Files are served from there via - # html_extra_path set below — no copies in srcdir. - 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}") - - -# Expose each enabled contrib module as a symlink under a build-dir subdir -# and let Sphinx's html_extra_path publish the tree to the output. Output -# URLs are /contrib_modules//... — no files duplicated in srcdir. -# Skipped when SPHINX_INPUT_ROOT lives inside a source tree (i.e. ad-hoc -# sphinx-build without CMake's OPENCV_SPHINX_INPUT_ROOT) — matches the -# documented "always build through CMake" expectation. html_extra_path: list[str] = [] def _in_source_tree(p: pathlib.Path) -> bool: for _root in (DOC_ROOT, CONTRIB_ROOT): @@ -760,1204 +187,8 @@ if not _in_source_tree(SPHINX_INPUT_ROOT): except (OSError, NotImplementedError): pass html_extra_path = [str(_extras)] -_TOGGLE_LABELS = {"cpp": "C++", "java": "Java", "python": "Python"} - - -# Mirror of Doxygen's EXAMPLE_PATH (see opencv/doc/Doxyfile.in) — the bases a -# bare `@snippet some/path.cpp` is resolved against. OPENCV_ROOT comes first so -# fully-qualified paths like `samples/cpp/...` keep working. Contrib module -# samples are appended so `@snippet introduction_to_svm.cpp ...` in a contrib -# tutorial resolves to opencv_contrib/modules//samples/... -_SNIPPET_BASES = [ - OPENCV_ROOT, - OPENCV_ROOT / "samples", - OPENCV_ROOT / "apps", -] + [CONTRIB_ROOT / _m / "samples" for _m in CONTRIB_MODULES] - -# Doxygen's Doxyfile has EXAMPLE_RECURSIVE = YES, so a bare basename like -# `@snippet linux_quick_install.sh body` resolves to -# `samples/install/linux_quick_install.sh` even though the directive omits -# the `install/` qualifier. Mirror that with a basename -> path index built -# once at import time. Restricted to common source-file extensions to keep -# the scan fast. -_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_INDEX: dict[str, pathlib.Path] = {} -_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) - - -# Doxygen accepts language names that Pygments doesn't recognize (or wraps -# them with a leading `.` in the `@code{.lang}` and ```.lang fenced forms). -# Strip the dot and remap a few aliases so Pygments stays warning-free. -_LANG_ALIASES = { - "none": "text", - "unparsed": "text", - "guess": "text", - "gradle": "groovy", - # `run` is a custom convention some contrib tutorials use to mean - # "this is a shell command you run" (e.g. dnn_superres/upscale_image_*). - # Pygments has no `run` lexer — map to bash so it highlights as shell. - "run": "bash", -} - -def _normalize_lang(lang: str) -> str: - lang = (lang or "").strip(".").strip().lower() or "text" - return _LANG_ALIASES.get(lang, lang) - - -def _read_snippet(rel_path: str, label: str | None) -> tuple[str, str]: - """Return (code_text, language) for an @include / @snippet directive.""" - # Some sources write the path with a leading slash (e.g. `@include - # /samples/android/.../tutorial1_surface_view.xml`). pathlib's `/` would - # treat that as absolute and lose the snippet base, so strip it. - rel_norm = rel_path.lstrip("/") - p = next((b / rel_norm for b in _SNIPPET_BASES - if (b / rel_norm).is_file()), None) - # Doxygen does a recursive basename lookup across EXAMPLE_PATH (see - # opencv/doc/Doxyfile.in: EXAMPLE_RECURSIVE = YES). If the direct join - # doesn't find the file, fall back to the prebuilt basename index. - if p is None: - hit = _SNIPPET_INDEX.get(pathlib.Path(rel_norm).name) - if hit and hit.is_file(): - p = hit - if p is None: - return f"// not found: {rel_path}\n", "text" - text = p.read_text(encoding="utf-8", errors="replace") - ext = p.suffix.lower() - lang = {".cpp": "cpp", ".hpp": "cpp", ".h": "cpp", ".c": "c", - ".py": "python", ".java": "java", - ".xml": "xml", ".html": "html", - ".sh": "bash", ".bash": "bash"}.get(ext, "text") - if label is None: - return text, lang - # Doxygen matches `[label]` after any comment-style marker anywhere on a - # line: //, //! and // for C/C++/Java/Kotlin, # and ## for Python/shell, - # " - raw = p.read_text(encoding="utf-8", errors="replace") - # `` opener: allow quoted attribute values so a `=>` - # arrow function inside `onload="..."` doesn't terminate the tag. - body = re.search( - r"\"']|\"[^\"]*\"|'[^']*')*>(.*?)", - raw, re.DOTALL | re.IGNORECASE) - inner = body.group(1).strip() if body else raw - return f"\n```{{raw}} html\n{inner}\n```\n" - text = re.sub(r"^@htmlinclude\s+(?P\S+)\s*$", - _htmlinclude_repl, text, flags=re.MULTILINE) - - # 5. @snippet path [Label] - # Indent preserved so code blocks inside list items don't break the list. - def _snippet_repl(m: re.Match) -> str: - indent = m.group("indent") - code, lang = _read_snippet(m.group("path"), m.group("label")) - return _emit_codeblock(m.group("indent") or "", lang, code) - text = re.sub( - r"^(?P[ \t]*)@snippet\s+(?P\S+)\s+(?P