├── .github └── workflows │ ├── ci.yml │ ├── publish.yml │ └── test-publish.yml ├── .gitignore ├── .gitmodules ├── .readthedocs.yml ├── CHANGELOG.md ├── LICENSE.txt ├── README.rst ├── RELEASING.rst ├── docs ├── Makefile ├── _static │ └── custom.css ├── conf.py ├── img.jpg ├── index.rst ├── make.bat └── rtd-requirements.txt ├── pyproject.toml ├── sphinxcontrib └── images.py ├── sphinxcontrib_images_lightbox2 ├── __init__.py ├── lightbox2-customize │ ├── jquery-noconflict.js │ └── pointer.css └── lightbox2.py └── tox.ini /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Test Python ${{ matrix.python-version }} 8 | 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | python-version: 15 | - '3.9' 16 | - '3.10' 17 | - '3.11' 18 | - '3.12' 19 | - '3.13' 20 | - pypy-3.10 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | submodules: true 26 | 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | 32 | - name: Cache 33 | uses: actions/cache@v4 34 | with: 35 | path: ~/.cache/pip 36 | key: pip-test-python-${{ matrix.python-version }}-${{ hashFiles('**/tox.ini', '**/pyproject.toml') }} 37 | restore-keys: | 38 | pip-test-python-${{ matrix.python-version }}-${{ hashFiles('**/tox.ini', '**/pyproject.toml') }} 39 | pip-test-python-${{ matrix.python-version }} 40 | pip-test- 41 | 42 | - name: Install dependencies 43 | run: | 44 | python -m pip install --upgrade pip setuptools wheel 45 | python -m pip install --upgrade tox tox-gh-actions 46 | 47 | - name: Tox tests 48 | run: | 49 | tox 50 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | push: 5 | tags: * 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | environment: 12 | name: pypi 13 | url: https://pypi.org/p/sphinxcontrib-images 14 | 15 | permissions: 16 | # Needed for trusted publishing 17 | id-token: write 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | persist-credentials: false 23 | submodules: true 24 | 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: '3.x' 28 | cache: pip 29 | 30 | - name: Install pypa/build 31 | run: python -m pip install build 32 | 33 | - name: Build a binary wheel and a source tarball 34 | run: python -m build 35 | 36 | - uses: actions/upload-artifact@v4 37 | with: 38 | name: sphinxcontrib-images 39 | path: dist/ 40 | 41 | - uses: pypa/gh-action-pypi-publish@release/v1 42 | -------------------------------------------------------------------------------- /.github/workflows/test-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to TestPyPI 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | environment: 10 | name: testpypi 11 | url: https://test.pypi.org/p/sphinxcontrib-images 12 | 13 | permissions: 14 | # Needed for trusted publishing 15 | id-token: write 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | persist-credentials: false 21 | submodules: true 22 | 23 | - uses: actions/setup-python@v5 24 | with: 25 | python-version: '3.x' 26 | cache: pip 27 | 28 | - name: Install pypa/build 29 | run: python -m pip install build 30 | 31 | - name: Build a binary wheel and a source tarball 32 | run: python -m build 33 | 34 | - uses: actions/upload-artifact@v4 35 | with: 36 | name: sphinxcontrib-images 37 | path: dist/ 38 | 39 | - uses: pypa/gh-action-pypi-publish@release/v1 40 | with: 41 | repository-url: https://test.pypi.org/legacy/ 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/_images 2 | /docs/_build 3 | /.tox 4 | *~ 5 | *pyc 6 | *egg* 7 | build 8 | dist 9 | venv*/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "sphinxcontrib_images_lightbox2/lightbox2"] 2 | path = sphinxcontrib_images_lightbox2/lightbox2 3 | url = https://github.com/lokesh/lightbox2.git 4 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the OS, Python version and other tools you might need 9 | build: 10 | os: ubuntu-lts-latest 11 | tools: 12 | python: "3.12" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # Optionally build your docs in additional formats such as PDF and ePub 19 | formats: 20 | - htmlzip 21 | 22 | # Optionally set the version of Python and requirements required to build your docs 23 | python: 24 | install: 25 | - requirements: docs/rtd-requirements.txt -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.1] - 2025-06-08 9 | 10 | ### Changed 11 | 12 | - Updated the included Lightbox2 backend to version 2.11.4 13 | and with it jQuery to version 3.6.3. 14 | 15 | 16 | 17 | ## [1.0.0] - 2025-06-07 18 | 19 | ### Added 20 | 21 | - Support for Sphinx 6, 7, 8. 22 | - Support for Python 3.10, 3.11, 3.12, 3.13 and PyPy 3.10. 23 | - Note about using this extension with MyST Markdown. 24 | 25 | ### Removed 26 | 27 | - Support for Sphinx 1, 2, 3. 28 | - Support for Python 2.7, 3.6, 3.7, 3.8 and PyPy 3.7. 29 | 30 | 31 | 32 | 33 | ## [0.9.4] - 2021-08-09 34 | 35 | ### Changed 36 | 37 | - Updated the included Lightbox2 backend to version 2.11.3 and with it jQuery to 38 | version 3.4.1. 39 | 40 | ### Fixed 41 | 42 | - Issue where explicit targets for thumbnails were ignored. 43 | `.. _my target:` before a thumbnail directive would not make the thumbnail 44 | referenceable by `:ref:Title `. 45 | 46 | 47 | 48 | ## [0.9.3] - 2021-04-27 49 | 50 | ### Fixed 51 | 52 | - Issue where adding package configuration options to conf.py 53 | (e.g. `images_config={'show_caption'=True}`) would cause incremental builds 54 | to stop working. 55 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | sphinxcontrib-images 2 | ==================== 3 | 4 | sphinxcontrib-images (formerly `sphinxcontrib-fancybox 5 | `_). 6 | 7 | Easy thumbnails in Sphinx documentation (focused on HTML). 8 | 9 | * `Documentation `_ 10 | * `Repository (GitHub) `_ 11 | * `PyPI `_ 12 | * `GitHub actions `_ 13 | 14 | .. image:: https://github.com/sphinx-contrib/images/actions/workflows/ci.yml/badge.svg 15 | :target: https://github.com/sphinx-contrib/images/actions/workflows/ci.yml 16 | :alt: GitHub Actions 17 | 18 | Features 19 | -------- 20 | 21 | * Show thumbnails instead of full size images inside documentation (HTML). 22 | * Ability to zoom/enlarge picture using LightBox2 (HTML). 23 | * Ability to group pictures 24 | * Download remote pictures and keep it in cache (if requested) 25 | * Support for other formats (latex, epub, ... - fallback to image directive) 26 | * Easy to extend (add own backend in only few lines of code) 27 | 28 | * Add other HTML "preview" solution than LightBox2 29 | * Add better support to non-HTML outputs 30 | * Preprocess images 31 | 32 | TODO 33 | ^^^^ 34 | 35 | * Make proper thumbnails (scale down images) 36 | 37 | How to install? 38 | --------------- 39 | 40 | Instalation through pip: :: 41 | 42 | pip install sphinxcontrib-images 43 | 44 | or through the GitHub: :: 45 | 46 | pip install git+https://github.com/sphinx-contrib/images 47 | 48 | Next, you have to add extension to ``conf.py`` in your Sphinx project. :: 49 | 50 | extensions = [ 51 | … 52 | 'sphinxcontrib.images', 53 | … 54 | ] 55 | 56 | 57 | How to use it? 58 | -------------- 59 | 60 | Example: :: 61 | 62 | .. thumbnail:: picture.png 63 | 64 | 65 | You can also override the default ``image`` directive provided by Sphinx. 66 | Check the documentation for all configuration options. 67 | 68 | 69 | Questions and suggestions 70 | ------------------------- 71 | 72 | If you have any suggstions, patches, problems - please use 73 | `GitHub Issues `_. 74 | -------------------------------------------------------------------------------- /RELEASING.rst: -------------------------------------------------------------------------------- 1 | Releasing sphinxcontrib-images 2 | ============================== 3 | 4 | 1. Test releasing to TestPyPI 5 | 6 | - Bump the package version by editing ``sphinxcontrib/images.py`` 7 | (i.e. from 0.9.2 to 0.9.3rc1 -- use rcN for subsequent releases), 8 | create a release branch with this change in the 9 | https://github.com/sphinx-contrib/images repository, and finally create a 10 | pull request. 11 | 12 | - Trigger `publishing to TestPyPI via the GitHub UI`_ and select the release 13 | branch as where to run the workflow. 14 | 15 | 2. Test the TestPyPI release 16 | 17 | - Make a new virtual environment:: 18 | 19 | read -p 'Version: ' VERSION \ 20 | && python3 -m venv venv-$VERSION \ 21 | && . venv-$VERSION/bin/activate 22 | 23 | - Install the release (candidate) (see [1]_) and verify that the correct 24 | version is installed:: 25 | 26 | pip install -U \ 27 | --index-url https://test.pypi.org/simple/ \ 28 | --extra-index-url https://pypi.org/simple \ 29 | "sphinxcontrib-images >= $VERSION.dev0" \ 30 | && pip freeze | grep sphinxcontrib-images 31 | 32 | - Verify the release candidate can build the package docs in docs/:: 33 | 34 | cd docs/ && make html 35 | 36 | 3. Finally test releasing the actual release to TestPyPI: 37 | 38 | Repeat the previous steps but without rcN in the version number, e.g. 39 | change 0.9.3rc1 to 0.9.3 40 | 41 | 4. Merge the release PR with GitHub's "squash merge", tag the release commit, 42 | and push the tag to GitHub to trigger a release to PyPI 43 | 44 | .. [1] `Alternate "rc" spellings`_ ("c", "pre", "preview") are normalized to 45 | "rc", so version ``0.9.3pre1`` is converted to ``0.9.3rc1``. To reduce 46 | confusion when installing the RC package use "rcN" for the version number 47 | suffix. This allows using the same version when installing with ``pip`` 48 | (i.e. ``pip install sphinxcontrib-images==0.9.3rc1``). 49 | 50 | If you release to TestPyPI in this order: 0.9.3rc1 - 0.9.3rc2 - 0.9.3 51 | then you should be able to test all three versions by upgrading over the 52 | previous versions with the provided command. 53 | 54 | .. _Alternate "rc" spellings: https://peps.python.org/pep-0440/#pre-release-spelling 55 | .. _publishing to TestPyPI via the GitHub UI: https://github.com/sphinx-contrib/images/actions/workflows/test-publish.yml 56 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | .framed img { border: 2px solid black;} 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'sphinxcontrib-images' 21 | copyright = u'2014, Tomasz Czyż' 22 | author = u'Tomasz Czyż' 23 | 24 | from sphinxcontrib import images 25 | version = images.__version__ 26 | 27 | # The full version, including alpha/beta/rc tags 28 | release = '1' 29 | 30 | 31 | # -- General configuration --------------------------------------------------- 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | 37 | source_suffix = '.rst' 38 | master_doc = 'index' 39 | 40 | extensions = [ 41 | 'sphinxcontrib.images', 42 | 'sphinx.ext.viewcode', 43 | ] 44 | 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ['_templates'] 48 | 49 | # List of patterns, relative to source directory, that match files and 50 | # directories to ignore when looking for source files. 51 | # This pattern also affects html_static_path and html_extra_path. 52 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 53 | 54 | 55 | # -- Options for HTML output ------------------------------------------------- 56 | 57 | # The theme to use for HTML and HTML Help pages. See the documentation for 58 | # a list of builtin themes. 59 | # 60 | html_theme = 'sphinxdoc' 61 | 62 | #html_theme_options = { } 63 | # Add any paths that contain custom static files (such as style sheets) here, 64 | # relative to this directory. They are copied after the builtin static files, 65 | # so a file named "default.css" will overwrite the builtin "default.css". 66 | html_static_path = ['_static'] 67 | html_css_files = ['custom.css'] 68 | 69 | images_config = dict(backend='LightBox2', 70 | default_image_width='200px' 71 | ) 72 | -------------------------------------------------------------------------------- /docs/img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sphinx-contrib/images/fce459ff975a74266f3fbcc3d0850f33d7b09202/docs/img.jpg -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | sphinxcontrib-images |version| 2 | ============================== 3 | 4 | Project home: ``_ 5 | 6 | PyPI: ``_ 7 | 8 | 9 | How to install? 10 | --------------- 11 | 12 | :: 13 | 14 | pip install sphinxcontrib-images 15 | 16 | Add extension to ``conf.py`` in your sphinx project. :: 17 | 18 | extensions = [ 19 | … 20 | 'sphinxcontrib.images', 21 | … 22 | ] 23 | 24 | How to configure? 25 | ----------------- 26 | 27 | You can configure behaviour using a dictionary with options placed in ``conf.py``:: 28 | 29 | images_config = { 30 | … 31 | } 32 | 33 | All available options with comments: 34 | 35 | :backend: (default: 'LightBox2') 36 | 37 | ``string`` or ``callable`` 38 | 39 | If ``string`` it has to be the name of the 40 | ``entrypoint`` registered in the 41 | ``sphinxcontrib.images.backend`` group (look at the source 42 | of ``setup.py`` in this project). 43 | 44 | Callable can be a function or a class which 45 | returns an instance of a backend to render images. 46 | The first argument is Sphinx's app instance. Go to 47 | LightBox2 backend to see how to implement that. 48 | 49 | Each backend should implement rendering ``image_node`` on specific outputs. 50 | If methods are not implemented, default ``image`` directive is a fallback. 51 | 52 | By default ``LightBox2`` backends natively support only HTML (other 53 | outputs are supported by fallback-image-directive). 54 | 55 | :override_image_directive: (default: ``False``) 56 | 57 | ``True`` or ``False``. It overrides the default Sphinx ``image`` directive 58 | with the ``thumbnail`` directive provided by this extension. 59 | 60 | :cache_path: (default :``_images``) 61 | 62 | Path, where to keep downloaded images. Relative to 63 | source (actually relative to ``conf.py``) directory or absolute path. 64 | 65 | :requests_kwargs: (default: ``{}``) 66 | 67 | Remote images are downloaded by `requests 68 | `_. This 69 | ``dict`` keeps the keyword arguments that will be 70 | passed to the ``get`` method. 71 | 72 | :default_image_width: (default: ``100%``) 73 | 74 | Default width of an image. Backend can use this 75 | setting to set width properly. 76 | 77 | :default_image_height: (default: ``auto``) 78 | 79 | Default height of an image. Backend can use this 80 | setting to set width properly. 81 | 82 | :default_group: (default: ``None``) 83 | 84 | This setting sets the default group for images without 85 | a defined group. If ``None`` each image is placed in a 86 | separate group, which means that images are not 87 | grouped by default. Otherwise you can set a group 88 | name like ``default`` to group all ungrouped images 89 | together. 90 | 91 | :default_show_title: (default: ``False``) 92 | 93 | Defines whether a caption below the picture should be visible or not. 94 | 95 | .. warning:: 96 | 97 | Currently this options does not work, I have no idea how to 98 | enable this feature in lightbox2. If you have any idea please do 99 | a pull request. 100 | 101 | 102 | :download: (default: ``True``) 103 | 104 | Download remote images. 105 | 106 | 107 | 108 | Thumbnail directive 109 | ------------------- 110 | 111 | You can use it with local images:: 112 | 113 | .. thumbnail:: path/to/image.jpg 114 | 115 | remote images:: 116 | 117 | .. thumbnail:: http://remote/image.jpg 118 | 119 | MyST Markdown:: 120 | 121 | ```{thumbnail} path/to/image.png 122 | :width: 20 % 123 | :alt: Firefox logo 124 | ``` 125 | 126 | You can pass options like regular Sphinxs' directives:: 127 | 128 | .. thumbnail:: http://remote/image.jpg 129 | :download: true 130 | 131 | All available arguments: 132 | 133 | :group: 134 | 135 | If you set the same group for different images the backend 136 | can *group* them. 137 | 138 | :class: 139 | 140 | This can be used by the backend to apply some style. 141 | 142 | The straightforward use case is to define HTML classes here (LightBox2 143 | backend puts these classes on outer ``a`` element, not inner ``img``). 144 | 145 | :width: 146 | 147 | Backend can use this option to set the width of the 148 | image. This overrides ``default_image_width`` from configuration. 149 | 150 | Values like: 151 | 152 | * percentage ``100%`` 153 | * length with unit ``100px`` 154 | * ``auto`` 155 | 156 | are accepted. 157 | 158 | :height: 159 | 160 | Backend can use this option to set the height of the 161 | image. This overrides ``default_image_height`` from configuration. 162 | 163 | Values like: 164 | 165 | * length with unit: ``100px`` 166 | * ``auto`` 167 | 168 | are accepted. 169 | 170 | :alt: 171 | 172 | If image cannot be displayed, this text will be shown. 173 | 174 | :download: 175 | 176 | This overrides ``download`` from configuration. You can set 177 | for particular image to download it or not. Works only for remote images. 178 | 179 | :title: 180 | 181 | * If you do not define it, ``default_show_title`` configuration option will 182 | be used (it will define whether to show title or not). 183 | 184 | * If you define this option but leave it empty, the content of the 185 | directive will be used as the title:: 186 | 187 | .. thumbnail:: image.jpg 188 | :title: 189 | 190 | This will be a title 191 | 192 | * If you define this option as text, it will be used as title:: 193 | 194 | .. thumbnail:: image.jpg 195 | :title: This is title 196 | 197 | This is description 198 | 199 | It's up to the backend, how this will be displayed. 200 | 201 | Currently I have a problem with LightBox2 to make captions below thumbnails. 202 | If you have any idea how to solve it please write. 203 | 204 | :align: (default: '') 205 | 206 | Align the picture. 207 | 208 | LightBox2 backend uses ``align-`` Sphinx html classes. 209 | By default alignment is not used at all. 210 | 211 | Values like: 212 | 213 | * ``left`` 214 | * ``center`` 215 | * ``right`` 216 | 217 | are accepted. 218 | 219 | .. note:: 220 | 221 | You may want to wrap aligned element with:: 222 | 223 | .. container:: clearfix 224 | 225 | to fix document flow. 226 | 227 | :show_caption: (default: ``False``) 228 | 229 | Show the title as a caption below the image. 230 | 231 | .. warning:: 232 | 233 | Enabling the caption nests the clickable image inside an HTML ``figure`` 234 | which gets the class if defined. 235 | 236 | This mays break existing styles. 237 | 238 | To solve styles compatibility issues, you may use the *legacy_class* argument. 239 | 240 | :legacy_class: 241 | 242 | Only applicable when *show_caption* is ``True``. 243 | 244 | The classese specified are added to the clickable image. 245 | 246 | The ``figure`` HTML element still gets the classes specified by the *class* argument. 247 | 248 | Examples 249 | -------- 250 | 251 | Thumbnail 252 | ^^^^^^^^^ 253 | 254 | .. sourcecode:: rst 255 | 256 | .. thumbnail:: img.jpg 257 | 258 | .. thumbnail:: img.jpg 259 | 260 | Remote image 261 | ^^^^^^^^^^^^ 262 | 263 | .. sourcecode:: rst 264 | 265 | .. thumbnail:: https://www.sphinx-doc.org/en/master/_images/linux-logo.png 266 | :download: false 267 | 268 | .. thumbnail:: https://www.sphinx-doc.org/en/master/_images/linux-logo.png 269 | :download: false 270 | 271 | Remote image (download) 272 | ^^^^^^^^^^^^^^^^^^^^^^^ 273 | 274 | The image is downloaded and placed in `_build/html/_images` 275 | (for html build) making it availble locally. 276 | 277 | .. sourcecode:: rst 278 | 279 | .. thumbnail:: https://www.sphinx-doc.org/en/master/_images/linux-logo.png 280 | :download: true 281 | 282 | .. thumbnail:: https://www.sphinx-doc.org/en/master/_images/linux-logo.png 283 | :download: true 284 | 285 | Image with forced dimensions 286 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 287 | 288 | .. sourcecode:: rst 289 | 290 | .. thumbnail:: img.jpg 291 | :width: 300px 292 | :height: 100px 293 | 294 | .. thumbnail:: img.jpg 295 | :width: 300px 296 | :height: 100px 297 | 298 | Image with additional class 299 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 300 | 301 | .. sourcecode:: rst 302 | 303 | .. thumbnail:: img.jpg 304 | :class: framed 305 | 306 | .. thumbnail:: img.jpg 307 | :class: framed 308 | 309 | .. note:: 310 | Requires a custom `custom css file`_ with a rule like: 311 | 312 | .. sourcecode:: css 313 | 314 | .framed img { border: 2px solid black;} 315 | 316 | .. _custom css file: https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_css_files 317 | 318 | Image with description 319 | ^^^^^^^^^^^^^^^^^^^^^^ 320 | 321 | .. sourcecode:: rst 322 | 323 | .. thumbnail:: img.jpg 324 | 325 | Descriptive description. 326 | 327 | .. thumbnail:: img.jpg 328 | 329 | Descriptive description. 330 | 331 | .. seealso:: :ref:`sec-caption-title` 332 | 333 | Image alternative text 334 | ^^^^^^^^^^^^^^^^^^^^^^ 335 | 336 | .. sourcecode:: rst 337 | 338 | .. thumbnail:: http://a.b/non_existing_image.png 339 | :alt: Cannot load this photo, but believe me it is nice. 340 | 341 | .. thumbnail:: http://a.b/non_existing_image.png 342 | :alt: Cannot load this photo, but believe me it's nice. 343 | 344 | Group images 345 | ^^^^^^^^^^^^ 346 | 347 | .. sourcecode:: rst 348 | 349 | .. thumbnail:: img.jpg 350 | :group: group1 351 | 352 | .. thumbnail:: https://www.sphinx-doc.org/en/master/_images/linux-logo.png 353 | :group: group1 354 | 355 | .. thumbnail:: img.jpg 356 | :group: group1 357 | 358 | .. thumbnail:: https://www.sphinx-doc.org/en/master/_images/linux-logo.png 359 | :group: group1 360 | 361 | Alignment 362 | ^^^^^^^^^ 363 | 364 | .. sourcecode:: rst 365 | 366 | .. thumbnail:: img.jpg 367 | :align: center 368 | 369 | .. thumbnail:: img.jpg 370 | :align: center 371 | 372 | 373 | .. _sec-caption-title: 374 | 375 | Caption / title 376 | ^^^^^^^^^^^^^^^ 377 | .. sourcecode:: rst 378 | 379 | .. thumbnail:: img.jpg 380 | :title: Some title / caption. 381 | 382 | .. thumbnail:: img.jpg 383 | :title: Some nice title to the picture. 384 | :show_caption: True 385 | 386 | .. thumbnail:: img.jpg 387 | :title: Some title / caption. 388 | 389 | .. thumbnail:: img.jpg 390 | :title: Some nice title to the picture. 391 | :show_caption: True 392 | 393 | 394 | Reference via explicit target 395 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 396 | 397 | Thumbnails can not, at present, be numbered like figures, 398 | hence they can not be referenced by `:numref:`, but they can be referenced 399 | by `:ref:`. 400 | 401 | .. sourcecode:: rst 402 | 403 | This is a reference to a thumbnail: :ref:`A thumbnail`. 404 | Scroll until the image is not visible before clicking the link 405 | to see the effect. 406 | 407 | .. _my target: 408 | 409 | .. thumbnail:: img.jpg 410 | 411 | 412 | 413 | This is a reference to a thumbnail: :ref:`A thumbnail`. 414 | Scroll until the image is not visible before clicking the link 415 | to see the effect. 416 | 417 | .. _my target: 418 | 419 | .. thumbnail:: img.jpg 420 | 421 | 422 | 423 | 424 | Indices and tables 425 | ================== 426 | 427 | * :ref:`genindex` 428 | * :ref:`modindex` 429 | * :ref:`search` 430 | 431 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/rtd-requirements.txt: -------------------------------------------------------------------------------- 1 | sphinxcontrib-images 2 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 77.0.3"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "sphinxcontrib-images" 7 | requires-python = ">= 3.9" 8 | 9 | authors = [ 10 | {name = "Tomasz Czyż", email = "tomasz.czyz@gmail.com"}, 11 | ] 12 | classifiers = [ 13 | "Development Status :: 4 - Beta", 14 | "Environment :: Console", 15 | "Environment :: Web Environment", 16 | "Intended Audience :: Developers", 17 | "Operating System :: OS Independent", 18 | "Programming Language :: Python", 19 | "Programming Language :: Python :: 3 :: Only", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | "Programming Language :: Python :: 3.12", 24 | "Programming Language :: Python :: 3.13", 25 | "Topic :: Documentation", 26 | ] 27 | dependencies = [ 28 | "requests>2.2,<3", 29 | "sphinx>=5.0", 30 | ] 31 | description = "Sphinx extension for thumbnails" 32 | license = "Apache-2.0" 33 | license-files = ["LICENSE.txt"] 34 | readme = "README.rst" 35 | dynamic = ["version"] 36 | 37 | [project.entry-points."sphinxcontrib.images.backend"] 38 | FakeBackend = "sphinxcontrib_images_lightbox2:LightBox2" 39 | LightBox2 = "sphinxcontrib_images_lightbox2:LightBox2" 40 | 41 | [project.scripts] 42 | sphinxcontrib-images = "sphinxcontrib.images:main" 43 | 44 | [project.urls] 45 | Documentation = "https://sphinxcontrib-images.readthedocs.io/" 46 | Download = "https://pypi.python.org/pypi/sphinxcontrib-images" 47 | Issues = "https://github.com/sphinx-contrib/images/issues" 48 | Repository = "https://github.com/sphinx-contrib/images" 49 | 50 | [tool.mypy] 51 | check_untyped_defs = true 52 | modules = ["sphinxcontrib_images_lightbox2"] 53 | packages = ["sphinxcontrib.images"] 54 | pretty = true 55 | show_error_context = true 56 | warn_redundant_casts = true 57 | warn_unused_ignores = true 58 | warn_unreachable = true 59 | 60 | [tool.setuptools.dynamic] 61 | version = {attr = "sphinxcontrib.images.__version__"} 62 | 63 | [tool.setuptools.packages.find] 64 | include = ["sphinxcontrib*"] 65 | 66 | [tool.setuptools.package-data] 67 | sphinxcontrib_images_lightbox2 = [ 68 | "lightbox2/dist/css/lightbox.min.css", 69 | "lightbox2/dist/images/*", 70 | "lightbox2/dist/js/lightbox-plus-jquery.min.*", 71 | "lightbox2-customize/*", 72 | ] 73 | -------------------------------------------------------------------------------- /sphinxcontrib/images.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | import copy 5 | import functools 6 | import hashlib 7 | import importlib.metadata 8 | import os 9 | import requests 10 | import sphinx 11 | import sys 12 | import uuid 13 | 14 | from collections.abc import Iterable 15 | from docutils import nodes 16 | from docutils.parsers.rst import Directive, directives 17 | from sphinx.util import logging, osutil 18 | from typing import Any, Literal, Protocol, TYPE_CHECKING 19 | 20 | try: 21 | from sphinx.util.display import status_iterator 22 | except ImportError: 23 | # remove when Sphinx < 6.1 is not supported 24 | from sphinx.util import status_iterator # type: ignore[no-redef] 25 | 26 | if TYPE_CHECKING: 27 | from sphinx.application import Sphinx as _Sphinx 28 | from sphinx.config import Config 29 | from sphinx.environment import BuildEnvironment as _BuildEnvironment 30 | from sphinx.util.typing import ExtensionMetadata 31 | 32 | 33 | class BuildEnvironment(_BuildEnvironment): 34 | remote_images: dict[str, str] 35 | 36 | class Sphinx(_Sphinx): 37 | env: BuildEnvironment 38 | sphinxcontrib_images_backend: type[Backend] 39 | 40 | 41 | __version__ = '1.0.1' 42 | __author__ = 'Tomasz Czyż ' 43 | __license__ = "Apache 2" 44 | 45 | logger = logging.getLogger(__name__) 46 | 47 | STATICS_DIR_NAME = '_static' 48 | 49 | DEFAULT_CONFIG = dict[str, Any]( 50 | backend='LightBox2', 51 | default_image_width='100%', 52 | default_image_height='auto', 53 | default_group=None, 54 | default_show_title=False, 55 | download=True, 56 | requests_kwargs={}, 57 | cache_path='_images', 58 | override_image_directive=False, 59 | show_caption=False, 60 | ) 61 | 62 | def get_entry_points() -> Iterable[importlib.metadata.EntryPoint]: 63 | group = 'sphinxcontrib.images.backend' 64 | return ( 65 | importlib.metadata.entry_points(group=group) # type: ignore[return-value] 66 | if sys.version_info > (3, 10) 67 | else importlib.metadata.entry_points()[group] 68 | ) 69 | 70 | 71 | class Writer(Protocol): 72 | body: list[str] 73 | 74 | def visit_image(self, node: image_node) -> None: 75 | ... 76 | 77 | def depart_image(self, node: image_node) -> None: 78 | ... 79 | 80 | 81 | class Backend: 82 | STATIC_FILES: tuple[str, ...] = () 83 | 84 | def __init__(self, app: Sphinx) -> None: 85 | self._app = app 86 | 87 | def visit_image_node_fallback( 88 | self, writer: Writer, node: image_node 89 | ) -> None: 90 | writer.visit_image(node) 91 | 92 | def depart_image_node_fallback( 93 | self, writer: Writer, node: image_node 94 | ) -> None: 95 | writer.depart_image(node) 96 | 97 | 98 | class image_node(nodes.image, nodes.General, nodes.Element): 99 | pass 100 | 101 | 102 | def directive_boolean(value: str) -> bool: 103 | if not value.strip(): 104 | raise ValueError("No argument provided but required") 105 | if value.lower().strip() in ["yes", "1", 1, "true", "ok"]: 106 | return True 107 | elif value.lower().strip() in ['no', '0', 0, 'false', 'none']: 108 | return False 109 | else: 110 | raise ValueError("Please use on of: yes, true, no, false. " 111 | f"Do not use `{value}` as boolean.") 112 | 113 | 114 | def align_option(value: str) -> Literal['left', 'center', 'right']: 115 | return directives.choice(value, ('left', 'center', 'right')) # type: ignore[return-value] 116 | 117 | 118 | class ImageDirective(Directive): 119 | ''' 120 | Directive which overrides default sphinx directive. 121 | It's backward compatibile and it's adding more cool stuff. 122 | ''' 123 | 124 | has_content = True 125 | required_arguments = True 126 | 127 | option_spec = { 128 | 'width': directives.length_or_percentage_or_unitless, 129 | 'height': directives.length_or_unitless, 130 | 131 | 'group': directives.unchanged, 132 | 'class': directives.class_option, # or str? 133 | 'alt': directives.unchanged, 134 | 'download': directive_boolean, 135 | 'title': directives.unchanged, 136 | 'align': align_option, 137 | 'show_caption': directive_boolean, 138 | 'legacy_class': directives.class_option, 139 | } 140 | 141 | def run(self) -> list[image_node]: 142 | env = self.state.document.settings.env 143 | conf = env.app.config.images_config 144 | 145 | #TODO get defaults from config 146 | group = self.options.get('group', 147 | conf['default_group'] if conf['default_group'] else uuid.uuid4()) 148 | classes = self.options.get('class', '') 149 | width = self.options.get('width', conf['default_image_width']) 150 | height = self.options.get('height', conf['default_image_height']) 151 | alt = self.options.get('alt', '') 152 | title = self.options.get('title', '' if conf['default_show_title'] else None) 153 | align = self.options.get('align', '') 154 | show_caption = self.options.get('show_caption', False) 155 | legacy_classes = self.options.get('legacy_class', '') 156 | 157 | #TODO get default from config 158 | download = self.options.get('download', conf['download']) 159 | 160 | # parse nested content 161 | #TODO: something is broken here, not parsed as expected 162 | description = nodes.paragraph() 163 | content = nodes.paragraph() 164 | content += [nodes.Text(str(x)) for x in self.content] 165 | self.state.nested_parse(content, 0, description) 166 | 167 | img = image_node() 168 | uri = self.arguments[0] 169 | 170 | if self.is_remote(uri): 171 | img['remote'] = True 172 | if download: 173 | local_uri = img['uri'] = os.path.join('_images', hashlib.sha1(uri.encode()).hexdigest()) 174 | img['remote_uri'] = uri 175 | env.remote_images[uri] = local_uri 176 | env.images.add_file('', local_uri) 177 | else: 178 | img['uri'] = img['remote_uri'] = uri 179 | else: 180 | img['uri'] = uri 181 | img['remote'] = False 182 | env.images.add_file('', uri) 183 | 184 | img['content'] = description.astext() 185 | 186 | if title is None: 187 | img['title'] = '' 188 | elif title: 189 | img['title'] = title 190 | else: 191 | img['title'] = img['content'] 192 | img['content'] = '' 193 | 194 | img['show_caption'] = show_caption 195 | img['legacy_classes'] = legacy_classes 196 | img['group'] = group 197 | img['size'] = (width, height) 198 | img['classes'] += classes 199 | img['alt'] = alt 200 | img['align'] = align 201 | return [img] 202 | 203 | def is_remote(self, uri: str) -> bool: 204 | uri = uri.strip() 205 | env = self.state.document.settings.env 206 | app_directory = os.path.dirname(os.path.abspath(self.state.document.settings._source)) 207 | if sphinx.__version__.startswith('1.1'): 208 | app_directory = app_directory.decode('utf-8') 209 | 210 | if uri[0] == '/': 211 | return False 212 | if uri[0:7] == 'file://': 213 | return False 214 | if os.path.isfile(os.path.join(env.srcdir, uri)): 215 | return False 216 | if os.path.isfile(os.path.join(app_directory, uri)): 217 | return False 218 | if '://' in uri: 219 | return True 220 | raise ValueError(f'Image URI `{uri}` have to be local relative or ' 221 | 'absolute path to image, or remote address.') 222 | 223 | 224 | def install_backend_static_files(app: Sphinx, env: BuildEnvironment) -> None: 225 | STATICS_DIR_PATH = os.path.join(app.builder.outdir, STATICS_DIR_NAME) 226 | dest_path = os.path.join(STATICS_DIR_PATH, 'sphinxcontrib-images', 227 | app.sphinxcontrib_images_backend.__class__.__name__) 228 | files_to_copy = app.sphinxcontrib_images_backend.STATIC_FILES 229 | 230 | for source_file_path in status_iterator( 231 | files_to_copy, 232 | 'Copying static files for sphinxcontrib-images...', 233 | 'brown', len(files_to_copy)): 234 | 235 | dest_file_path = os.path.join(dest_path, source_file_path) 236 | 237 | if not os.path.exists(os.path.dirname(dest_file_path)): 238 | osutil.ensuredir(os.path.dirname(dest_file_path)) 239 | 240 | assert ( 241 | backend_path := sys.modules[ 242 | app.sphinxcontrib_images_backend.__class__.__module__ 243 | ].__file__ 244 | ) 245 | source_file_path = os.path.join(os.path.dirname(backend_path), 246 | source_file_path) 247 | 248 | osutil.copyfile(source_file_path, dest_file_path) 249 | 250 | if dest_file_path.endswith('.js'): 251 | app.add_js_file(os.path.relpath(dest_file_path, STATICS_DIR_PATH)) 252 | elif dest_file_path.endswith('.css'): 253 | app.add_css_file(os.path.relpath(dest_file_path, STATICS_DIR_PATH)) 254 | 255 | 256 | def download_images(app: Sphinx, env: BuildEnvironment) -> None: 257 | conf = app.config.images_config 258 | 259 | for src in status_iterator( 260 | env.remote_images, 261 | 'Downloading remote images...', 262 | 'brown', 263 | len(env.remote_images)): 264 | 265 | dst = os.path.join(env.srcdir, env.remote_images[src]) 266 | if not os.path.isfile(dst): 267 | logger.info(f'{src!r} -> {dst!r} (downloading)') 268 | with open(dst, 'wb') as f: 269 | # TODO: apply reuqests_kwargs 270 | try: 271 | f.write(requests.get(src, 272 | **conf['requests_kwargs']).content) 273 | except requests.ConnectionError: 274 | logger.info(f"Cannot download `{src!r}`") 275 | else: 276 | logger.info(f'{src!r} -> {dst!r} (already in cache)') 277 | 278 | 279 | def update_config(app: Sphinx, config: Config) -> None: 280 | '''Ensure all config values are defined''' 281 | 282 | merged = copy.deepcopy(DEFAULT_CONFIG) 283 | merged.update(config.images_config) 284 | config.images_config = merged 285 | 286 | def configure_backend(app: Sphinx) -> None: 287 | config = app.config.images_config 288 | osutil.ensuredir(os.path.join(app.env.srcdir, config['cache_path'])) 289 | 290 | # html builder 291 | # self.relfn2path(imguri, docname) 292 | 293 | backend_name_or_callable = config['backend'] 294 | if isinstance(backend_name_or_callable, str): 295 | try: 296 | backend = next( 297 | i for i in get_entry_points() if i.name == backend_name_or_callable 298 | ).load() 299 | except StopIteration: 300 | raise IndexError("Cannot find sphinxcontrib-images backend " 301 | f"with name `{backend_name_or_callable!r}`.") 302 | elif callable(backend_name_or_callable): 303 | backend = backend_name_or_callable 304 | else: 305 | raise TypeError("sphinxcontrib-images backend is configured " 306 | "improperly. It has to be a string (name of " 307 | "installed backend) or callable which returns " 308 | "backend instance but is " 309 | f"`{backend_name_or_callable!r}`. Please read " 310 | "sphinxcontrib-images documentation for " 311 | "more information.") 312 | 313 | try: 314 | backend = backend(app) 315 | except TypeError as error: 316 | logger.info('Cannot instantiate sphinxcontrib-images backend ' 317 | f'`{config["backend"]}`. Please, select correct backend. ' 318 | f'Available backends: {", ".join(ep.name for ep in get_entry_points())}.') 319 | raise SystemExit(1) 320 | 321 | # remember the chosen backend for processing. Env and config cannot be used 322 | # because sphinx try to make a pickle from it. 323 | app.sphinxcontrib_images_backend = backend 324 | 325 | logger.info('Initiated sphinxcontrib-images backend: ', nonl=True) 326 | logger.info(f'`{backend.__class__.__module__}:{backend.__class__.__name__}`') 327 | 328 | def backend_methods(node, output_type): 329 | def backend_method(f): 330 | @functools.wraps(f) 331 | def inner_wrapper(writer, node): 332 | return f(writer, node) 333 | return inner_wrapper 334 | 335 | return tuple( 336 | backend_method( 337 | getattr( 338 | backend, 339 | f'{name}_{node.__name__}_{output_type}', 340 | getattr(backend, f'{name}_{node.__name__}_fallback'), 341 | ) 342 | ) for name in ('visit', 'depart') 343 | ) 344 | 345 | # add new node to the stack 346 | # connect backend processing methods to this node 347 | app.add_node(image_node, 348 | **{output_type: backend_methods(image_node, output_type) 349 | for output_type in ('html', 'latex', 'man', 'texinfo', 350 | 'text', 'epub')}) 351 | 352 | app.add_directive('thumbnail', ImageDirective) 353 | if config['override_image_directive']: 354 | app.add_directive('image', ImageDirective) 355 | app.env.remote_images = {} 356 | 357 | def setup(app: Sphinx) -> ExtensionMetadata: 358 | app.require_sphinx('1.0') 359 | app.add_config_value('images_config', {}, 'env') 360 | app.connect('config-inited', update_config) 361 | app.connect('builder-inited', configure_backend) 362 | app.connect('env-updated', download_images) 363 | app.connect('env-updated', install_backend_static_files) 364 | 365 | return {'version': sphinx.__version__, 'parallel_read_safe': True} 366 | 367 | 368 | def main(args=sys.argv[1:]): 369 | parser = argparse.ArgumentParser() 370 | parser.add_argument("command", 371 | choices=['show-backends']) 372 | args = parser.parse_args(args) 373 | if args.command == 'show-backends': 374 | if backends := get_entry_points(): 375 | for backend in backends: 376 | assert backend.dist 377 | print(f'- {backend.name} (from package `{backend.dist.name}`)') 378 | else: 379 | print('No backends installed') 380 | 381 | 382 | if __name__ == '__main__': 383 | main() 384 | -------------------------------------------------------------------------------- /sphinxcontrib_images_lightbox2/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .lightbox2 import LightBox2 3 | -------------------------------------------------------------------------------- /sphinxcontrib_images_lightbox2/lightbox2-customize/jquery-noconflict.js: -------------------------------------------------------------------------------- 1 | jQuery.noConflict(true); 2 | -------------------------------------------------------------------------------- /sphinxcontrib_images_lightbox2/lightbox2-customize/pointer.css: -------------------------------------------------------------------------------- 1 | /* change the lightbox cursor */ 2 | a[data-lightbox] {cursor: zoom-in;} -------------------------------------------------------------------------------- /sphinxcontrib_images_lightbox2/lightbox2.py: -------------------------------------------------------------------------------- 1 | import html 2 | 3 | from sphinxcontrib import images 4 | 5 | 6 | def _a(value: str) -> str: 7 | return html.escape(value, quote=True) 8 | 9 | 10 | def _h(value: str) -> str: 11 | return html.escape(value) 12 | 13 | 14 | class LightBox2(images.Backend): 15 | STATIC_FILES = ( 16 | 'lightbox2/dist/images/close.png', 17 | 'lightbox2/dist/images/next.png', 18 | 'lightbox2/dist/images/prev.png', 19 | 'lightbox2/dist/images/loading.gif', 20 | 'lightbox2/dist/js/lightbox-plus-jquery.min.js', 21 | 'lightbox2/dist/js/lightbox-plus-jquery.min.map', 22 | 'lightbox2/dist/css/lightbox.min.css', 23 | 'lightbox2-customize/jquery-noconflict.js', 24 | 'lightbox2-customize/pointer.css' 25 | ) 26 | 27 | def visit_image_node_html( 28 | self, writer: images.Writer, node: images.image_node 29 | ) -> None: 30 | # make links local (for local images only) 31 | builder = self._app.builder 32 | if node['uri'] in builder.images: 33 | node['uri'] = '/'.join([builder.imgpath, builder.images[node['uri']]]) 34 | 35 | # add wrapping optional figure tag and then anchor tag 36 | if node['show_caption']: 37 | writer.body.append(f'
') 38 | writer.body.append('') 71 | 72 | 73 | def depart_image_node_html( 74 | self, writer: images.Writer, node: images.image_node 75 | ) -> None: 76 | writer.body.append('') 77 | if node['show_caption']: 78 | writer.body.append(f'
{_h(node["title"])}
') 79 | writer.body.append('
') 80 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | {py39}-sphinx{621,747}, 4 | {py310,pypy3}-sphinx{621,747,813}, 5 | {py311,py312,py313}-sphinx{621,747,813,823,latest}, 6 | mypy, 7 | wheel 8 | 9 | [testenv] 10 | minversion = 3.15.0 11 | allowlist_externals = make 12 | commands = 13 | make -C docs html 14 | deps = 15 | sphinx-rtd-theme==0.1.6 16 | sphinxlatest: sphinx 17 | # status_iterator changed between 8.1.3 and 8.2.3 18 | sphinx823: sphinx==8.2.3 19 | sphinx813: sphinx==8.1.3 20 | # last release for old versions 21 | sphinx747: sphinx==7.4.7 22 | sphinx621: sphinx==6.2.1 23 | 24 | [testenv:mypy] 25 | minversion = 3.15.0 26 | commands = 27 | mypy 28 | skip_install = true 29 | deps = 30 | mypy 31 | sphinx 32 | types-docutils 33 | types-requests 34 | 35 | [testenv:wheel] 36 | minversion = 3.15.0 37 | commands = 38 | python -m build --wheel . 39 | skip_install = true 40 | deps = 41 | build 42 | 43 | [gh-actions] 44 | python = 45 | 3.9: py39 46 | 3.10: py310 47 | 3.11: py311 48 | 3.12: py312 49 | 3.13: py313, mypy, wheel 50 | pypy-3.10: pypy3 51 | --------------------------------------------------------------------------------