├── site ├── GitHub-Mark-32px.png └── index.html ├── test ├── images │ └── sample_rgba.gif ├── conftest.py └── test_image.py ├── .readthedocs.yml ├── docs ├── index.rst ├── conf.py └── ril.rst ├── src ├── utils.rs ├── lib.rs ├── error.rs ├── types.rs ├── sequence.rs ├── pixels.rs ├── draw.rs ├── text.rs ├── image.rs └── workaround.rs ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── scripts │ └── site_template.py │ └── py-binding.yml ├── pyproject.toml ├── Cargo.toml ├── LICENSE ├── README.md └── ril.pyi /site/GitHub-Mark-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cryptex-github/ril-py/HEAD/site/GitHub-Mark-32px.png -------------------------------------------------------------------------------- /test/images/sample_rgba.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cryptex-github/ril-py/HEAD/test/images/sample_rgba.gif -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests 3 | 4 | BASE_URL: str = 'https://raw.githubusercontent.com/Cryptex-github/ril-py/main/test/images/' 5 | 6 | @pytest.fixture 7 | def fetch_file(): 8 | def inner(filename: str) -> bytes: 9 | return requests.get(BASE_URL + filename).content 10 | 11 | return inner 12 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # https://docs.readthedocs.io/en/stable/config-file/v2.html#supported-settings 2 | 3 | version: 2 4 | 5 | sphinx: 6 | builder: html 7 | 8 | build: 9 | os: "ubuntu-22.04" 10 | tools: 11 | python: "3" 12 | rust: "1.61" 13 | 14 | formats: all 15 | 16 | python: 17 | install: 18 | - method: pip 19 | path: . 20 | extra_requirements: 21 | - docs 22 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Ril's documentation! 2 | ==================================== 3 | 4 | Rust Imaging Library's Python binding: A performant and high-level image processing library for Python written in Rust 5 | 6 | Ril 7 | === 8 | 9 | Contents 10 | ======== 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | 15 | API Reference 16 | 17 | Index 18 | ===== 19 | * :ref:`genindex` 20 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::pixels::{BitPixel, Rgb, Rgba, L}; 2 | use pyo3::prelude::*; 3 | use ril::Dynamic; 4 | 5 | pub fn cast_pixel_to_pyobject(py: Python<'_>, pixel: Dynamic) -> PyObject { 6 | match pixel { 7 | Dynamic::BitPixel(v) => BitPixel::from(v).into_py(py), 8 | Dynamic::L(v) => L::from(v).into_py(py), 9 | Dynamic::Rgb(v) => Rgb::from(v).into_py(py), 10 | Dynamic::Rgba(v) => Rgba::from(v).into_py(py), 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # These are generated by python binding 13 | **/*.pyd 14 | **/ril.egg-info 15 | venv/ 16 | 17 | **.pyc 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "ril" 3 | version = "0.7.0" 4 | license = { text = "MIT" } 5 | authors = [ { name = "Cryptex" } ] 6 | requires-python = ">=3.7" 7 | readme = "README.md" 8 | description = "Rust Imaging Library's Python binding: A performant and high-level image processing library for Python written in Rust" 9 | 10 | [project.urls] 11 | homepage = "https://github.com/Cryptex-github/ril-py" 12 | 13 | [project.optional-dependencies] 14 | docs = [ 15 | "sphinx", 16 | "furo", 17 | "sphinxext-opengraph", 18 | "sphinx-copybutton" 19 | ] 20 | 21 | [build-system] 22 | requires = ["maturin>=0.13,<0.14"] 23 | build-backend = "maturin" 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ril-py" 3 | authors = ["Cryptex"] 4 | version = "0.7.0" 5 | license = "MIT" 6 | edition = "2021" 7 | description = "Rust Imaging Library's Python binding: A performant and high-level image processing library for Python written in Rust" 8 | repository = "https://github.com/Cryptex-github/ril-py" 9 | homepage = "https://github.com/Cryptex-github/ril-py" 10 | readme = "README.md" 11 | keywords = ["ril", "imaging", "image", "processing", "editing"] 12 | categories = ["encoding", "graphics", "multimedia", "visualization"] 13 | 14 | [lib] 15 | name = "ril" 16 | crate-type = ["cdylib"] 17 | 18 | [dependencies] 19 | ril = { git = "https://github.com/jay3332/ril", features = ["all-pure"] } 20 | pyo3 = { version = "0.17", features = ["extension-module", "abi3-py37"] } 21 | fontdue = { version = "0.7" } 22 | -------------------------------------------------------------------------------- /.github/workflows/scripts/site_template.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | 5 | TEMPLATE: str = ' {0}' 6 | 7 | def main() -> None: 8 | with open('site/index.html', 'r') as f: 9 | current = f.read() 10 | 11 | with open('site/index.html', 'w') as f: 12 | wheels = [TEMPLATE.format(wheel) for wheel in os.listdir('site/wheels')] 13 | long_hash = subprocess.run(["git", "rev-parse", "HEAD"], capture_output=True).stdout.decode('utf-8') 14 | short_hash = subprocess.run(["git", "rev-parse", "--short", "HEAD"], capture_output=True).stdout.decode('utf-8') 15 | 16 | f.write(current.replace('', '\n'.join(wheels)).replace('short_hash', short_hash).replace('long_hash', long_hash)) 17 | 18 | if __name__ == '__main__': 19 | main() 20 | -------------------------------------------------------------------------------- /test/test_image.py: -------------------------------------------------------------------------------- 1 | from ril import Image, ImageSequence, Pixel, Rgba 2 | 3 | PIXELS = [ 4 | Rgba(255, 0, 0, 255), 5 | Rgba(255, 128, 0, 255), 6 | Rgba(255, 255, 0, 255), 7 | Rgba(128, 255, 0, 255), 8 | Rgba(0, 255, 0, 255), 9 | Rgba(0, 255, 128, 255), 10 | Rgba(0, 255, 255, 255), 11 | Rgba(0, 128, 255, 255), 12 | Rgba(0, 0, 255, 255), 13 | Rgba(128, 0, 255, 255), 14 | Rgba(255, 0, 255, 255), 15 | Rgba(255, 0, 128, 255), 16 | ] 17 | 18 | def test_create_image() -> None: 19 | image = Image.new(1, 1, Pixel.from_rgb(255, 255, 255)) 20 | 21 | assert image.height == 1 22 | assert image.width == 1 23 | assert image.dimensions == (1, 1) 24 | 25 | def test_image_pixels() -> None: 26 | image = Image.new(1, 1, Pixel.from_rgb(255, 255, 255)) 27 | 28 | image.pixels() 29 | 30 | def test_gif_decode(fetch_file) -> None: 31 | for i, frame in enumerate(ImageSequence.from_bytes(fetch_file('sample_rgba.gif'))): 32 | assert frame.dimensions == (256, 256) 33 | assert frame.image.get_pixel(0, 0) == PIXELS[i] 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Cryptex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::missing_const_for_fn)] 2 | // Warnings created by pyo3 3 | #![allow(clippy::used_underscore_binding)] 4 | #![allow(clippy::borrow_deref_ref)] 5 | #![allow(clippy::use_self)] 6 | 7 | mod draw; 8 | mod error; 9 | mod image; 10 | mod pixels; 11 | mod sequence; 12 | mod types; 13 | mod utils; 14 | mod text; 15 | mod workaround; 16 | 17 | use draw::{Border, Ellipse, Rectangle}; 18 | use image::Image; 19 | use pixels::{BitPixel, Pixel, Rgb, Rgba, L}; 20 | use pyo3::prelude::*; 21 | use sequence::{Frame, ImageSequence}; 22 | use types::{DisposalMethod, ResizeAlgorithm}; 23 | 24 | use text::{TextLayout, TextSegment, Font}; 25 | 26 | type Xy = (u32, u32); 27 | 28 | macro_rules! add_classes { 29 | ($m:expr, $($class:ty),*) => {{ 30 | $( 31 | $m.add_class::<$class>()?; 32 | )* 33 | }}; 34 | } 35 | 36 | #[pymodule] 37 | fn ril(_py: Python<'_>, m: &PyModule) -> PyResult<()> { 38 | add_classes!( 39 | m, 40 | BitPixel, 41 | Image, 42 | L, 43 | Pixel, 44 | Rgb, 45 | Rgba, 46 | Border, 47 | Rectangle, 48 | DisposalMethod, 49 | ResizeAlgorithm, 50 | Frame, 51 | Ellipse, 52 | ImageSequence, 53 | TextSegment, 54 | TextLayout, 55 | Font 56 | ); 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Ril-py Wheels 8 | 14 | 15 | 16 |

17 | Ril-py Wheels 18 |

19 | 20 |
21 |

Commit: short_hash

22 |
23 | 
24 |         
25 |
26 | 27 |
28 | Copyright © 2022 Cryptex 29 |
30 |
31 | GitHub logo 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{PoisonError, RwLockReadGuard, RwLockWriteGuard}; 2 | 3 | use pyo3::{ 4 | exceptions::{PyIOError, PyRuntimeError, PyTypeError, PyValueError}, 5 | prelude::*, 6 | }; 7 | use ril::{Error as RilError, Dynamic}; 8 | 9 | use crate::workaround::OwnedTextLayout; 10 | 11 | pub enum Error { 12 | Ril(RilError), 13 | UnexpectedFormat(String, String), // (Expected, Got) 14 | PoisionError 15 | } 16 | 17 | impl From for PyErr { 18 | fn from(err: Error) -> Self { 19 | match err { 20 | Error::Ril(err) => match err { 21 | RilError::InvalidHexCode(_) 22 | | RilError::InvalidExtension(_) 23 | | RilError::UnsupportedColorType => PyValueError::new_err(format!("{}", err)), 24 | RilError::EncodingError(_) 25 | | RilError::DecodingError(_) 26 | | RilError::UnknownEncodingFormat 27 | | RilError::FontError(_) 28 | | RilError::IncompatibleImageData { .. } 29 | | RilError::InvalidPaletteIndex 30 | | RilError::QuantizationOverflow { .. } => { 31 | PyRuntimeError::new_err(format!("{}", err)) 32 | } 33 | RilError::IOError(_) => PyIOError::new_err(format!("{}", err)), 34 | RilError::EmptyImageError => PyRuntimeError::new_err( 35 | "Cannot encode an empty image, or an image without data.", 36 | ), 37 | }, 38 | Error::UnexpectedFormat(expected, got) => PyTypeError::new_err(format!( 39 | "Invalid Image format, expected `{}`, got `{}`", 40 | expected, got 41 | )), 42 | Error::PoisionError => PyRuntimeError::new_err("The internal RwLock was poisoned."), 43 | } 44 | } 45 | } 46 | 47 | impl From for Error { 48 | fn from(err: RilError) -> Self { 49 | Self::Ril(err) 50 | } 51 | } 52 | 53 | type ReadPoisionError<'a> = PoisonError>>; 54 | type WritePoisionError<'a> = PoisonError>>; 55 | 56 | impl<'a> From> for Error { 57 | fn from(_: ReadPoisionError) -> Self { 58 | Self::PoisionError 59 | } 60 | } 61 | 62 | impl<'a> From> for Error { 63 | fn from(_: WritePoisionError) -> Self { 64 | Self::PoisionError 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ril-py 2 | **R**ust **I**maging **L**ibrary for Python: Python bindings for [ril](https://github.com/jay3332/ril), a performant and high-level image processing library written in Rust. 3 | 4 | ## What's this? 5 | This is a python binding around [ril](https://github.com/jay3332/ril) designed to provide an easy-to-use, high-level interface 6 | around image processing in Rust. Image and animation processing has never been 7 | this easy and fast before. 8 | 9 | ## Support 10 | ⚠ This package is a work in progress and it heavily depends on the progress of [ril](https://github.com/jay3332/ril) 11 | 12 | By the first stable release, we plan to support the following image encodings: 13 | 14 | | Encoding Format | Current Status | 15 | |-----------------|--------------------| 16 | | PNG / APNG | Supported | 17 | | JPEG | Supported | 18 | | GIF | Supported | 19 | | WebP | Can't support [(#8)](https://github.com/Cryptex-github/ril-py/issues/8) | 20 | | BMP | Not yet supported | 21 | | TIFF | Not yet supported | 22 | 23 | ## Installation 24 | 25 | ### Prebuilt wheels 26 | 27 | There will be prebuilt wheels for these platforms: 28 | 29 | * Linux x86-64: Cpython 3.7+, PyPy 3.7, 3.8, 3.9 30 | * MacOS x86-64: Cpython 3.7+, PyPy 3.7, 3.8, 3.9 31 | * Windows x86-64: Cpython 3.7+, PyPy 3.7, 3.8, 3.9 32 | * Linux i686: Cpython 3.7+, PyPy 3.7, 3.8, 3.9 33 | * MacOS aarch64: Cpython 3.8+ 34 | 35 | If you want another platform to have prebuilt wheels, please open an issue. 36 | 37 | CPython 3.11 support will be available once its ABI has been stabilized. 38 | 39 | If your platform has prebuilt wheels, installing is as simple as 40 | 41 | ``` 42 | pip install ril 43 | ``` 44 | 45 | ### Building from Source 46 | In order to build from source, you will need to have the Rust compiler available in your PATH. See documentation on [https://rust-lang.org](https://rust-lang.org) to learn how to install Rust on your platform. 47 | 48 | Then building is as simple as 49 | 50 | ``` 51 | pip install ril 52 | ``` 53 | 54 | or from Github 55 | 56 | ``` 57 | pip install git+https://github.com/Cryptex-github/ril-py 58 | ``` 59 | 60 | Pip will handle the building process. 61 | 62 | 63 | ## Examples 64 | 65 | #### Open an image, invert it, and then save it: 66 | ```py 67 | from ril import Image 68 | 69 | image = Image.open("example.png") 70 | image.invert() 71 | 72 | image.save("example.png") 73 | ``` 74 | 75 | #### Create a new black image, open the sample image, and paste it on top of the black image: 76 | ```py 77 | from ril import Image, Pixel 78 | 79 | image = Image.new(600, 600, Pixel.from_rgb(0, 0, 0)) 80 | image.paste(100, 100, Image.open("sample.png")) 81 | 82 | image.save("sample_on_black.png", "PNG") # You can also specify format if you like 83 | ``` 84 | -------------------------------------------------------------------------------- /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 re 14 | 15 | # -- Project information ----------------------------------------------------- 16 | 17 | project = 'ril' 18 | copyright = '2022, Cryptex' 19 | author = 'Cryptex' 20 | 21 | # The full version, including alpha/beta/rc tags 22 | 23 | # with open('../pyproject.toml') as f: 24 | # matches = re.search(r'^version\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE) 25 | 26 | # if matches: 27 | # release = matches.group(0) 28 | # else: 29 | # raise RuntimeError('Unable to find version string in pyproject.toml') 30 | 31 | release = '0.4.0' 32 | 33 | 34 | # -- General configuration --------------------------------------------------- 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = [ 40 | 'sphinx.ext.autodoc', 41 | 'sphinx.ext.extlinks', 42 | 'sphinx.ext.intersphinx', 43 | 'sphinx.ext.napoleon', 44 | 'sphinxext.opengraph', 45 | 'sphinx_copybutton', 46 | ] 47 | 48 | autodoc_typehints = 'both' 49 | napoleon_google_docstring = False 50 | napoleon_numpy_docstring = True 51 | autodoc_member_order = 'bysource' 52 | 53 | # Add any paths that contain templates here, relative to this directory. 54 | templates_path = [] 55 | 56 | rst_prolog = """ 57 | .. |enum| replace:: This is an |enum_link|_. 58 | .. |enum_link| replace:: *enum* 59 | .. _enum_link: https://docs.python.org/3/library/enum.html#enum.Enum 60 | """ 61 | 62 | intersphinx_mapping = { 63 | 'py': ('https://docs.python.org/3', None), 64 | } 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | # This pattern also affects html_static_path and html_extra_path. 69 | exclude_patterns = [] 70 | 71 | pygments_style = "friendly" 72 | 73 | 74 | # -- Options for HTML output ------------------------------------------------- 75 | 76 | # The theme to use for HTML and HTML Help pages. See the documentation for 77 | # a list of builtin themes. 78 | # 79 | html_theme = 'furo' 80 | 81 | html_theme_options = {} 82 | 83 | 84 | 85 | # Add any paths that contain custom static files (such as style sheets) here, 86 | # relative to this directory. They are copied after the builtin static files, 87 | # so a file named "default.css" will overwrite the builtin "default.css". 88 | # html_static_path = ["./_static"] 89 | -------------------------------------------------------------------------------- /docs/ril.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: ril 2 | 3 | Ril API Reference 4 | ================= 5 | 6 | 7 | Ril provide a performant and high-level image processing library for Python written in Rust. 8 | 9 | 10 | Image 11 | ----- 12 | 13 | .. autoclass:: Image 14 | :members: 15 | 16 | 17 | Pixel 18 | ----- 19 | 20 | There are two pixel types. 21 | 22 | :class:`Pixel` and other pixel classes. 23 | 24 | :class:`Pixel` is what the user creates, to represent the pixel type they desire. 25 | 26 | Other pixel types are usually returned from the library. 27 | 28 | This is done due to some limitation between converting types. 29 | 30 | .. autoclass:: BitPixel 31 | :members: 32 | 33 | .. autoclass:: L 34 | :members: 35 | 36 | .. autoclass:: Rgb 37 | :members: 38 | 39 | .. autoclass:: Rgba 40 | :members: 41 | 42 | .. autoclass:: Pixel 43 | :members: 44 | 45 | 46 | Draw 47 | ---- 48 | 49 | .. autoclass:: Border 50 | :members: 51 | 52 | .. autoclass:: Rectangle 53 | :members: 54 | 55 | .. autoclass:: Ellipse 56 | :members: 57 | 58 | 59 | Sequence 60 | -------- 61 | 62 | .. autoclass:: ImageSequence 63 | :members: 64 | 65 | .. autoclass:: Frame 66 | :members: 67 | 68 | 69 | Text 70 | ---- 71 | 72 | .. autoclass:: Font 73 | :members: 74 | 75 | .. autoclass:: TextSegment 76 | :members: 77 | 78 | .. autoclass:: TextLayout 79 | :members: 80 | 81 | 82 | Enums 83 | ----- 84 | 85 | .. class:: DisposalMethod 86 | 87 | The method used to dispose a frame before transitioning to the next frame in an image sequence. 88 | 89 | .. attribute:: Keep 90 | 91 | Do not dispose the current frame. Usually not desired for transparent images. 92 | 93 | .. attribute:: Background 94 | 95 | Dispose the current frame completely and replace it with the image's background color. 96 | 97 | .. attribute:: Previous 98 | 99 | Dispose and replace the current frame with the previous frame. 100 | 101 | .. class:: ResizeAlgorithm 102 | 103 | A filtering algorithm that is used to resize an image. 104 | 105 | .. attribute:: Nearest 106 | 107 | A simple nearest neighbor algorithm. Although the fastest, this gives the lowest quality resizings. 108 | 109 | When upscaling this is good if you want a "pixelated" effect with no aliasing. 110 | 111 | .. attribute:: Box 112 | 113 | A box filter algorithm. Equivalent to the :attr:`Nearest` filter if you are upscaling. 114 | 115 | .. attribute:: Bilinear 116 | 117 | A bilinear filter. Calculates output pixel value using linear interpolation on all pixels. 118 | 119 | .. attribute:: Hamming 120 | 121 | While having similar performance as the :attr:`Bilinear` filter, this produces a sharper and usually considered better quality image than the :attr:`Bilinear` filter, but only when downscaling. This may give worse results than bilinear when upscaling. 122 | 123 | .. attribute:: Bicubic 124 | 125 | A Catmull-Rom bicubic filter, which is the most common bicubic filtering algorithm. Just like all cubic filters, it uses cubic interpolation on all pixels to calculate output pixels. 126 | 127 | .. attribute:: Mitchell 128 | 129 | A Mitchell-Netravali bicubic filter. Just like all cubic filters, it uses cubic interpolation on all pixels to calculate output pixels. 130 | 131 | .. attribute:: Lanczos3 132 | 133 | A Lanczos filter with a window of 3. Calculates output pixel value using a high-quality Lanczos filter on all pixels. 134 | 135 | .. class:: WrapStyle 136 | 137 | The wrapping style of text. 138 | 139 | .. attribute:: NoWrap 140 | 141 | Do not wrap text. 142 | 143 | .. attribute:: Word 144 | 145 | Wrap text on word boundaries. 146 | 147 | .. attribute:: Character 148 | 149 | Wrap text on character boundaries. 150 | 151 | .. class:: OverlayMode 152 | 153 | The mode to use when overlaying an image onto another image. 154 | 155 | .. attribute:: Overwrite 156 | 157 | Overwrite the pixels of the image with the pixels of the overlay image. 158 | 159 | .. attribute:: Blend 160 | 161 | Blend the pixels of the image with the pixels of the overlay image. 162 | 163 | .. class:: HorizontalAnchor 164 | 165 | The horizontal anchor of text. 166 | 167 | .. attribute:: Left 168 | 169 | Anchor text to the left. 170 | 171 | .. attribute:: Center 172 | 173 | Anchor text to the center. 174 | 175 | .. attribute:: Right 176 | 177 | Anchor text to the right. 178 | 179 | .. class:: VerticalAnchor 180 | 181 | The vertical anchor of text. 182 | 183 | .. attribute:: Top 184 | 185 | Anchor text to the top. 186 | 187 | .. attribute:: Center 188 | 189 | Anchor text to the center. 190 | 191 | .. attribute:: Bottom 192 | 193 | Anchor text to the bottom. 194 | -------------------------------------------------------------------------------- /.github/workflows/py-binding.yml: -------------------------------------------------------------------------------- 1 | name: Python Binding 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | build: # Workflow credit to https://github.com/samuelcolvin/rtoml/blob/main/.github/workflows/ci.yml 10 | name: > 11 | build ${{ matrix.python-version }} on ${{ matrix.platform || matrix.os }} 12 | (${{ matrix.alt_arch_name || matrix.arch }}) 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ubuntu, macos, windows] 17 | python-version: ['cp310', 'pp37', 'pp38', 'pp39'] 18 | arch: [main, alt] 19 | include: 20 | - os: ubuntu 21 | platform: linux 22 | - os: windows 23 | ls: dir 24 | - os: macos 25 | arch: alt 26 | alt_arch_name: 'arm64 universal2' 27 | exclude: 28 | - os: windows 29 | arch: alt 30 | - os: macos 31 | python-version: 'pp37' 32 | arch: alt 33 | - os: macos 34 | python-version: 'pp38' 35 | arch: alt 36 | - os: macos 37 | python-version: 'pp39' 38 | arch: alt 39 | - os: ubuntu 40 | arch: alt 41 | 42 | runs-on: ${{ format('{0}-latest', matrix.os) }} 43 | steps: 44 | - uses: actions/checkout@v3 45 | 46 | - name: set up python 47 | uses: actions/setup-python@v4 48 | with: 49 | python-version: '3.9' 50 | 51 | - name: set up rust 52 | uses: dtolnay/rust-toolchain@stable 53 | with: 54 | toolchain: stable 55 | 56 | - name: Setup Rust cache 57 | uses: Swatinem/rust-cache@v2 58 | with: 59 | key: ${{ matrix.alt_arch_name }} 60 | 61 | - run: rustup target add aarch64-apple-darwin 62 | if: matrix.os == 'macos' 63 | 64 | - run: rustup toolchain install stable-i686-pc-windows-msvc 65 | if: matrix.os == 'windows' 66 | 67 | - run: rustup target add i686-pc-windows-msvc 68 | if: matrix.os == 'windows' 69 | 70 | - name: Get pip cache dir 71 | id: pip-cache 72 | if: matrix.os != 'windows' 73 | run: | 74 | echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT 75 | 76 | - name: Get pip cache dir 77 | id: pip-cache-win 78 | if: matrix.os == 'windows' 79 | run: | 80 | "dir=$(pip cache dir)" >> $env:GITHUB_OUTPUT 81 | 82 | - name: Cache python dependencies 83 | uses: actions/cache@v3 84 | with: 85 | path: ${{ steps.pip-cache.outputs.dir || steps.pip-cache-win.outputs.dir }} 86 | key: ${{ runner.os }}-pip-${{ matrix.python-version }} 87 | 88 | - name: install python dependencies 89 | run: pip install -U setuptools wheel twine cibuildwheel platformdirs 90 | 91 | - name: Display cibuildwheel cache dir 92 | id: cibuildwheel-cache 93 | run: | 94 | from platformdirs import user_cache_path 95 | import os 96 | 97 | with open(os.getenv('GITHUB_OUTPUT'), 'w') as f: 98 | f.write(f"dir={str(user_cache_path(appname='cibuildwheel', appauthor='pypa'))}") 99 | shell: python 100 | 101 | - name: Cache cibuildwheel tools 102 | uses: actions/cache@v3 103 | with: 104 | path: ${{ steps.cibuildwheel-cache.outputs.dir }} 105 | key: ${{ runner.os }}-cibuildwheel-${{ matrix.python-version }} 106 | 107 | - name: build sdist 108 | if: matrix.os == 'ubuntu' && matrix.python-version == 'cp310' 109 | run: | 110 | pip install maturin build 111 | python -m build --sdist -o wheelhouse 112 | 113 | - name: build ${{ matrix.platform || matrix.os }} binaries 114 | run: cibuildwheel --output-dir wheelhouse 115 | env: 116 | CIBW_BUILD: '${{ matrix.python-version }}-*' 117 | # rust doesn't seem to be available for musl linux on i686 118 | CIBW_SKIP: '*-musllinux_i686' 119 | # we build for "alt_arch_name" if it exists, else 'auto' 120 | CIBW_ARCHS: ${{ matrix.alt_arch_name || 'auto' }} 121 | CIBW_ENVIRONMENT: 'PATH="$HOME/.cargo/bin:$PATH" CARGO_TERM_COLOR="always"' 122 | CIBW_ENVIRONMENT_WINDOWS: 'PATH="$UserProfile\.cargo\bin;$PATH"' 123 | CIBW_BEFORE_BUILD: rustup show 124 | CIBW_BEFORE_BUILD_LINUX: > 125 | curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain=stable --profile=minimal -y && 126 | rustup show 127 | CIBW_TEST_COMMAND: 'pytest {project}/test' 128 | CIBW_TEST_REQUIRES: pytest requests 129 | CIBW_TEST_SKIP: '*-macosx_arm64 *-macosx_universal2:arm64' 130 | CIBW_BUILD_VERBOSITY: 1 131 | 132 | - run: ${{ matrix.ls || 'ls -lh' }} wheelhouse/ 133 | 134 | - run: twine check wheelhouse/* 135 | 136 | - uses: actions/upload-artifact@v3 137 | with: 138 | name: wheels 139 | path: wheelhouse 140 | 141 | release: 142 | needs: [build] 143 | if: "success() && startsWith(github.ref, 'refs/tags/')" 144 | runs-on: ubuntu-latest 145 | 146 | steps: 147 | - uses: actions/checkout@v3 148 | 149 | - name: set up python 150 | uses: actions/setup-python@v4 151 | with: 152 | python-version: '3.10' 153 | 154 | - run: pip install -U twine 155 | 156 | - name: get wheelhouse artifacts 157 | uses: actions/download-artifact@v3 158 | with: 159 | name: wheels 160 | path: wheelhouse 161 | 162 | - run: twine check wheelhouse/* 163 | 164 | - name: upload to pypi 165 | run: twine upload wheelhouse/* 166 | env: 167 | TWINE_USERNAME: __token__ 168 | TWINE_PASSWORD: ${{ secrets.pypi_token }} 169 | 170 | - name: Upload artifacts to release 171 | uses: softprops/action-gh-release@v1 172 | with: 173 | files: wheelhouse/* 174 | 175 | website: 176 | needs: [build] 177 | if: "success() && github.ref == 'refs/heads/main'" 178 | runs-on: ubuntu-latest 179 | 180 | steps: 181 | - uses: actions/checkout@v3 182 | 183 | - name: get wheelhouse artifacts 184 | uses: actions/download-artifact@v3 185 | with: 186 | name: wheels 187 | path: wheels 188 | 189 | - run: mv wheels site/ 190 | 191 | - name: Run template 192 | run: python .github/workflows/scripts/site_template.py 193 | 194 | - name: Setup Pages 195 | uses: actions/configure-pages@v2 196 | 197 | - name: Upload artifact 198 | uses: actions/upload-pages-artifact@v1 199 | with: 200 | path: ./site 201 | 202 | deploy_website: 203 | needs: [website] 204 | 205 | permissions: 206 | pages: write 207 | id-token: write 208 | 209 | environment: 210 | name: github-pages 211 | url: ${{ steps.deployment.outputs.page_url }} 212 | 213 | runs-on: ubuntu-latest 214 | steps: 215 | - name: Deploy to GitHub Pages 216 | id: deployment 217 | uses: actions/deploy-pages@v1 -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use pyo3::prelude::*; 4 | 5 | macro_rules! cast_enum { 6 | ($from:ty, $to:ty, $item:expr, $($var:tt),*) => {{ 7 | match $item { 8 | $( 9 | <$from>::$var => <$to>::$var, 10 | )* 11 | } 12 | }}; 13 | } 14 | 15 | /// A filtering algorithm that is used to resize an image. 16 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 17 | #[pyclass] 18 | pub enum ResizeAlgorithm { 19 | /// A simple nearest neighbor algorithm. Although the fastest, this gives the lowest quality 20 | /// resizings. 21 | /// 22 | /// When upscaling this is good if you want a "pixelated" effect with no aliasing. 23 | Nearest, 24 | /// A box filter algorithm. Equivalent to the [`Nearest`] filter if you are upscaling. 25 | Box, 26 | /// A bilinear filter. Calculates output pixel value using linear interpolation on all pixels. 27 | Bilinear, 28 | /// While having similar performance as the [`Bilinear`] filter, this produces a sharper and 29 | /// usually considered better quality image than the [`Bilinear`] filter, but **only** when 30 | /// downscaling. This may give worse results than bilinear when upscaling. 31 | Hamming, 32 | /// A Catmull-Rom bicubic filter, which is the most common bicubic filtering algorithm. Just 33 | /// like all cubic filters, it uses cubic interpolation on all pixels to calculate output 34 | /// pixels. 35 | Bicubic, 36 | /// A Mitchell-Netravali bicubic filter. Just like all cubic filters, it uses cubic 37 | /// interpolation on all pixels to calculate output pixels. 38 | Mitchell, 39 | /// A Lanczos filter with a window of 3. Calculates output pixel value using a high-quality 40 | /// Lanczos filter on all pixels. 41 | Lanczos3, 42 | } 43 | 44 | impl From for ril::ResizeAlgorithm { 45 | fn from(algo: ResizeAlgorithm) -> ril::ResizeAlgorithm { 46 | cast_enum!( 47 | ResizeAlgorithm, 48 | Self, 49 | algo, 50 | Nearest, 51 | Box, 52 | Bilinear, 53 | Hamming, 54 | Bicubic, 55 | Mitchell, 56 | Lanczos3 57 | ) 58 | } 59 | } 60 | 61 | impl From for ResizeAlgorithm { 62 | fn from(algo: ril::ResizeAlgorithm) -> ResizeAlgorithm { 63 | cast_enum!( 64 | ril::ResizeAlgorithm, 65 | Self, 66 | algo, 67 | Nearest, 68 | Box, 69 | Bilinear, 70 | Hamming, 71 | Bicubic, 72 | Mitchell, 73 | Lanczos3 74 | ) 75 | } 76 | } 77 | 78 | /// The method used to dispose a frame before transitioning to the next frame in an image sequence. 79 | #[pyclass] 80 | #[derive(Clone)] 81 | pub enum DisposalMethod { 82 | /// Do not dispose the current frame. Usually not desired for transparent images. 83 | Keep, 84 | /// Dispose the current frame completely and replace it with the image’s background color. 85 | Background, 86 | /// Dispose and replace the current frame with the previous frame. 87 | Previous, 88 | } 89 | 90 | impl Display for DisposalMethod { 91 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 92 | match self { 93 | Self::Keep => f.write_str("Keep"), 94 | Self::Background => f.write_str("Background"), 95 | Self::Previous => f.write_str("Previous"), 96 | } 97 | } 98 | } 99 | 100 | impl From for ril::DisposalMethod { 101 | fn from(method: DisposalMethod) -> ril::DisposalMethod { 102 | match method { 103 | DisposalMethod::Keep => ril::DisposalMethod::None, 104 | DisposalMethod::Background => ril::DisposalMethod::Background, 105 | DisposalMethod::Previous => ril::DisposalMethod::Previous, 106 | } 107 | } 108 | } 109 | 110 | impl From for DisposalMethod { 111 | fn from(method: ril::DisposalMethod) -> Self { 112 | match method { 113 | ril::DisposalMethod::None => DisposalMethod::Keep, 114 | ril::DisposalMethod::Background => DisposalMethod::Background, 115 | ril::DisposalMethod::Previous => DisposalMethod::Previous, 116 | } 117 | } 118 | } 119 | 120 | #[pyclass] 121 | #[derive(Clone, Debug)] 122 | pub enum WrapStyle { 123 | NoWrap, 124 | Word, 125 | Character, 126 | } 127 | 128 | impl From for ril::WrapStyle { 129 | fn from(style: WrapStyle) -> Self { 130 | match style { 131 | WrapStyle::NoWrap => ril::WrapStyle::None, 132 | WrapStyle::Word => ril::WrapStyle::Word, 133 | WrapStyle::Character => ril::WrapStyle::Character, 134 | } 135 | } 136 | } 137 | 138 | impl From for WrapStyle { 139 | fn from(style: ril::WrapStyle) -> Self { 140 | match style { 141 | ril::WrapStyle::None => WrapStyle::NoWrap, 142 | ril::WrapStyle::Word => WrapStyle::Word, 143 | ril::WrapStyle::Character => WrapStyle::Character, 144 | } 145 | } 146 | } 147 | 148 | #[derive(Clone, Debug)] 149 | #[pyclass] 150 | pub enum OverlayMode { 151 | Replace, 152 | Merge, 153 | } 154 | 155 | impl From for ril::OverlayMode { 156 | fn from(mode: OverlayMode) -> Self { 157 | match mode { 158 | OverlayMode::Replace => ril::OverlayMode::Replace, 159 | OverlayMode::Merge => ril::OverlayMode::Merge, 160 | } 161 | } 162 | } 163 | 164 | impl From for OverlayMode { 165 | fn from(mode: ril::OverlayMode) -> Self { 166 | match mode { 167 | ril::OverlayMode::Replace => OverlayMode::Replace, 168 | ril::OverlayMode::Merge => OverlayMode::Merge, 169 | } 170 | } 171 | } 172 | 173 | #[pyclass] 174 | #[derive(Clone)] 175 | pub enum HorizontalAnchor { 176 | Left, 177 | Center, 178 | Right, 179 | } 180 | 181 | impl From for ril::HorizontalAnchor { 182 | fn from(anchor: HorizontalAnchor) -> Self { 183 | cast_enum!(HorizontalAnchor, Self, anchor, Left, Center, Right) 184 | } 185 | } 186 | 187 | impl From for HorizontalAnchor { 188 | fn from(anchor: ril::HorizontalAnchor) -> Self { 189 | cast_enum!(ril::HorizontalAnchor, Self, anchor, Left, Center, Right) 190 | } 191 | } 192 | 193 | #[pyclass] 194 | #[derive(Clone)] 195 | pub enum VerticalAnchor { 196 | Top, 197 | Center, 198 | Bottom, 199 | } 200 | 201 | impl From for ril::VerticalAnchor { 202 | fn from(anchor: VerticalAnchor) -> Self { 203 | cast_enum!(VerticalAnchor, Self, anchor, Top, Center, Bottom) 204 | } 205 | } 206 | 207 | impl From for VerticalAnchor { 208 | fn from(anchor: ril::VerticalAnchor) -> Self { 209 | cast_enum!(ril::VerticalAnchor, Self, anchor, Top, Center, Bottom) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/sequence.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, time::Duration}; 2 | 3 | use pyo3::{ 4 | prelude::*, 5 | types::{PyBytes, PyType}, 6 | }; 7 | use ril::{ 8 | Dynamic, Frame as RilFrame, FrameIterator, ImageFormat, ImageSequence as RilImageSequence, 9 | }; 10 | 11 | use crate::{error::Error, image::Image, types::DisposalMethod, Xy}; 12 | 13 | /// Represents a frame in an image sequence. It encloses :class:`.Image` and extra metadata about the frame. 14 | /// 15 | /// Parameters 16 | /// ---------- 17 | /// image: :class:`.Image` 18 | /// The image used for this frame. 19 | #[derive(Clone)] 20 | #[pyclass] 21 | #[pyo3(text_signature = "(image)")] 22 | pub struct Frame { 23 | inner: RilFrame, 24 | } 25 | 26 | #[pymethods] 27 | impl Frame { 28 | #[new] 29 | fn new(image: Image) -> Self { 30 | Self { 31 | inner: RilFrame::from_image(image.inner), 32 | } 33 | } 34 | 35 | /// int: Returns the delay duration for this frame. 36 | #[getter] 37 | fn get_delay(&self) -> u128 { 38 | self.inner.delay().as_millis() 39 | } 40 | 41 | /// Tuple[int, int]: Returns the dimensions of this frame. 42 | #[getter] 43 | fn get_dimensions(&self) -> Xy { 44 | self.inner.dimensions() 45 | } 46 | 47 | /// :class:`.DisposalMethod`: Returns the disposal method for this frame. 48 | #[getter] 49 | fn get_disposal(&self) -> DisposalMethod { 50 | self.inner.disposal().into() 51 | } 52 | 53 | /// :class:`.Image`: Returns the image this frame contains. 54 | #[getter] 55 | fn get_image(&self) -> Image { 56 | Image { 57 | inner: self.inner.image().clone(), 58 | } 59 | } 60 | 61 | #[setter] 62 | fn set_delay(&mut self, delay: u64) { 63 | self.inner.set_delay(Duration::from_millis(delay)); 64 | } 65 | 66 | #[setter] 67 | fn set_disposal(&mut self, disposal: DisposalMethod) { 68 | self.inner.set_disposal(disposal.into()) 69 | } 70 | 71 | fn __repr__(&self) -> String { 72 | format!( 73 | "", 74 | self.get_delay(), 75 | self.get_dimensions().0, 76 | self.get_dimensions().1, 77 | self.get_disposal() 78 | ) 79 | } 80 | } 81 | 82 | /// Represents a sequence of image frames such as an animated image. 83 | /// 84 | /// See :class:`.Image` for the static image counterpart, and see :class:`.Frame` to see how each frame is represented in an image sequence. 85 | /// 86 | /// The iterator is exhausive, so when you iterate through :class:`.ImageSequence` like 87 | /// 88 | /// .. code-block:: python3 89 | /// 90 | /// seq = ImageSequence.from_bytes(bytes) 91 | /// list(seq) # [...] 92 | /// # But if you do it again 93 | /// list(seq) # [] 94 | /// # It will return a empty list 95 | /// 96 | /// .. note:: 97 | /// Any change made to the :class:`.Frame` will not be reflected to the :class:`.ImageSequence`, so you must create a new :class:`.ImageSequence` after you make changes to the frames. 98 | #[pyclass] 99 | pub struct ImageSequence { 100 | inner: RilImageSequence, 101 | iter: Box> + Send>, 102 | } 103 | 104 | #[pymethods] 105 | impl ImageSequence { 106 | /// Decodes a sequence with the explicitly given image encoding from the raw bytes. 107 | /// 108 | /// if `format` is not provided then it will try to infer its encoding. 109 | /// 110 | /// Parameters 111 | /// ---------- 112 | /// bytes: bytes 113 | /// The bytes of the image. 114 | /// format: Optional[str], default: None 115 | /// The format of the image. 116 | /// 117 | /// Raises 118 | /// ------ 119 | /// ValueError 120 | /// The format provided is invalid. 121 | /// RuntimeError 122 | /// Failed to decode the image or Failed to infer the image's format. 123 | #[classmethod] 124 | #[pyo3(text_signature = "(cls, bytes, format)")] 125 | fn from_bytes(_: &PyType, bytes: &[u8], format: Option<&str>) -> Result { 126 | Ok(if let Some(format) = format { 127 | let inner = RilImageSequence::from_bytes(ImageFormat::from_extension(format)?, bytes)? 128 | .into_sequence()?; 129 | let iter = Box::new(inner.clone().into_iter()); 130 | 131 | Self { inner, iter } 132 | } else { 133 | let inner = RilImageSequence::from_bytes(ImageFormat::infer_encoding(bytes), bytes)? 134 | .into_sequence()?; 135 | let iter = Box::new(inner.clone().into_iter()); 136 | 137 | Self { inner, iter } 138 | }) 139 | } 140 | 141 | /// Creates a new image sequence from the given frames 142 | /// 143 | /// Parameters 144 | /// ---------- 145 | /// frames: List[:class:`Frame`] 146 | /// The list of frames to create the sequence from 147 | #[classmethod] 148 | fn from_frames(_: &PyType, frames: Vec) -> Self { 149 | let inner = 150 | RilImageSequence::from_frames(frames.into_iter().map(|x| x.inner).collect::>()); 151 | let iter = Box::new(inner.clone().into_iter()); 152 | 153 | Self { inner, iter } 154 | } 155 | 156 | /// Opens a file from the given path and decodes it into an :class:`.ImageSequence`. 157 | /// 158 | /// The encoding of the image is automatically inferred. 159 | /// You can explicitly pass in an encoding by using the :meth:`from_bytes` method. 160 | /// 161 | /// Parameters 162 | /// ---------- 163 | /// path: str 164 | /// The path to the image. 165 | /// 166 | /// Raises 167 | /// ------ 168 | /// ValueError 169 | /// The file extension is invalid. 170 | /// RuntimeError 171 | /// Failed to infer file format or Failed to decode image. 172 | #[classmethod] 173 | #[pyo3(text_signature = "(cls, path)")] 174 | fn open(_: &PyType, path: PathBuf) -> Result { 175 | let inner = RilImageSequence::open(path)?.into_sequence()?; 176 | let iter = Box::new(inner.clone().into_iter()); 177 | Ok(Self { inner, iter }) 178 | } 179 | 180 | /// Encodes the image with the given encoding and returns `bytes`. 181 | /// 182 | /// Parameters 183 | /// ---------- 184 | /// encoding: str 185 | /// The encoding to encode to. 186 | /// 187 | /// Returns 188 | /// ------- 189 | /// bytes 190 | /// The encoded bytes. 191 | fn encode(&self, encoding: &str) -> Result<&PyBytes, Error> { 192 | let encoding = ImageFormat::from_extension(encoding)?; 193 | 194 | let mut buf = Vec::new(); 195 | self.inner.encode(encoding, &mut buf)?; 196 | 197 | // SAFETY: We acquired the GIL before calling `assume_gil_acquired`. 198 | // `assume_gil_acquired` is only used to ensure that PyBytes don't outlive the current function 199 | unsafe { 200 | Python::with_gil(|_| { 201 | let buf = buf.as_slice(); 202 | let pyacq = Python::assume_gil_acquired(); 203 | Ok(PyBytes::new(pyacq, buf)) 204 | }) 205 | } 206 | } 207 | 208 | /// Saves the image to the given path. 209 | /// If encoding is not provided, it will attempt to infer it by the path/filename's extension 210 | /// You can try saving to a memory buffer by using the :meth:`encode` method. 211 | /// 212 | /// Parameters 213 | /// ---------- 214 | /// path: str 215 | /// The path to the image. 216 | /// 217 | /// Raises 218 | /// ------ 219 | /// ValueError 220 | /// The file extension is invalid. 221 | /// RuntimeError 222 | /// Failed to infer file format or Failed to decode image. 223 | fn save(&self, path: PathBuf, encoding: Option<&str>) -> Result<(), Error> { 224 | if let Some(encoding) = encoding { 225 | let encoding = ImageFormat::from_extension(encoding)?; 226 | self.inner.save(encoding, path)?; 227 | } else { 228 | self.inner.save_inferred(path)?; 229 | } 230 | 231 | Ok(()) 232 | } 233 | 234 | fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { 235 | slf 236 | } 237 | 238 | fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { 239 | slf.iter.next().map(|x| Frame { inner: x }) 240 | } 241 | 242 | fn __len__(&self) -> usize { 243 | self.inner.len() 244 | } 245 | 246 | fn __repr__(&self) -> String { 247 | format!("", self.__len__()) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/pixels.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use pyo3::{prelude::*, pyclass::CompareOp, types::PyType}; 4 | use ril::Dynamic; 5 | 6 | /// Represents a single-bit pixel that represents either a pixel that is on or off. 7 | #[pyclass] 8 | #[derive(Clone, Eq, PartialEq)] 9 | pub struct BitPixel { 10 | /// bool: Whether the pixel is on. 11 | #[pyo3(get, set)] 12 | value: bool, 13 | } 14 | 15 | /// Represents an L, or luminance pixel that is stored as only one single number representing how bright, or intense, the pixel is. 16 | /// 17 | /// This can be thought of as the “unit channel” as this represents only a single channel in which other pixel types can be composed of. 18 | #[pyclass] 19 | #[derive(Clone, Eq, PartialEq)] 20 | pub struct L { 21 | /// int: The luminance value of the pixel, between 0 and 255. 22 | #[pyo3(get, set)] 23 | value: u8, 24 | } 25 | 26 | /// Represents an RGB pixel. 27 | #[pyclass] 28 | #[derive(Clone, Eq, PartialEq)] 29 | pub struct Rgb { 30 | /// int: The red component of the pixel. 31 | #[pyo3(get, set)] 32 | r: u8, 33 | /// int: The green component of the pixel. 34 | #[pyo3(get, set)] 35 | g: u8, 36 | /// int: The blue component of the pixel. 37 | #[pyo3(get, set)] 38 | b: u8, 39 | } 40 | 41 | /// Represents an RGBA pixel. 42 | #[pyclass] 43 | #[derive(Clone, Eq, PartialEq)] 44 | pub struct Rgba { 45 | /// int: The red component of the pixel. 46 | #[pyo3(get, set)] 47 | r: u8, 48 | /// int: The green component of the pixel. 49 | #[pyo3(get, set)] 50 | g: u8, 51 | /// int: The blue component of the pixel. 52 | #[pyo3(get, set)] 53 | b: u8, 54 | /// int: The alpha component of the pixel. 55 | #[pyo3(get, set)] 56 | a: u8, 57 | } 58 | 59 | /// The user created Pixel type. 60 | #[pyclass] 61 | #[derive(Clone, Eq, PartialEq)] 62 | pub struct Pixel { 63 | pub inner: Dynamic, 64 | } 65 | 66 | impl From for Pixel { 67 | fn from(inner: Dynamic) -> Self { 68 | Self { inner } 69 | } 70 | } 71 | 72 | #[pymethods] 73 | impl Pixel { 74 | /// Create a bitpixel. 75 | /// 76 | /// Parameters 77 | /// ---------- 78 | /// value: bool 79 | /// Whether the pixel is on. 80 | #[classmethod] 81 | #[pyo3(text_signature = "(cls, value)")] 82 | fn from_bitpixel(_: &PyType, value: bool) -> Self { 83 | Self { 84 | inner: Dynamic::BitPixel(ril::BitPixel(value)), 85 | } 86 | } 87 | 88 | /// Create a L Pixel. 89 | /// 90 | /// Parameters 91 | /// ---------- 92 | /// value: int 93 | /// The luminance value of the pixel, between 0 and 255. 94 | #[classmethod] 95 | #[pyo3(text_signature = "(cls, value)")] 96 | fn from_l(_: &PyType, value: u8) -> Self { 97 | Self { 98 | inner: Dynamic::L(ril::L(value)), 99 | } 100 | } 101 | 102 | /// Creates a Rgb Pixel 103 | /// 104 | /// Parameters 105 | /// ---------- 106 | /// r: int 107 | /// The red component of the pixel. 108 | /// g: int 109 | /// The green component of the pixel. 110 | /// b: int 111 | /// The blue component of the pixel. 112 | #[classmethod] 113 | #[pyo3(text_signature = "(cls, r, g, b)")] 114 | fn from_rgb(_: &PyType, r: u8, g: u8, b: u8) -> Self { 115 | Self { 116 | inner: Dynamic::Rgb(ril::Rgb { r, g, b }), 117 | } 118 | } 119 | 120 | /// Creates a Rgba Pixel 121 | /// 122 | /// Parameters 123 | /// ---------- 124 | /// r: int 125 | /// The red component of the pixel. 126 | /// g: int 127 | /// The green component of the pixel. 128 | /// b: int 129 | /// The blue component of the pixel. 130 | /// a: int 131 | /// The alpha component of the pixel. 132 | #[classmethod] 133 | #[pyo3(text_signature = "(cls, r, g, b, a)")] 134 | fn from_rgba(_: &PyType, r: u8, g: u8, b: u8, a: u8) -> Self { 135 | Self { 136 | inner: Dynamic::Rgba(ril::Rgba { r, g, b, a }), 137 | } 138 | } 139 | 140 | fn __richcmp__(&self, py: Python<'_>, other: PyObject, op: CompareOp) -> PyObject { 141 | match op { 142 | CompareOp::Eq => { 143 | let other = other.extract::(py); 144 | if let Ok(other) = other { 145 | let val = self == &other; 146 | val.into_py(py) 147 | } else { 148 | false.into_py(py) 149 | } 150 | } 151 | CompareOp::Ne => { 152 | if let Ok(other) = other.extract::(py) { 153 | let val = self != &other; 154 | val.into_py(py) 155 | } else { 156 | true.into_py(py) 157 | } 158 | } 159 | _ => py.NotImplemented(), 160 | } 161 | } 162 | 163 | fn __repr__(&self) -> String { 164 | let out = match self.inner { 165 | Dynamic::BitPixel(v) => format!("BitPixel({})", v.value()), 166 | Dynamic::L(v) => format!("L({})", v.value()), 167 | Dynamic::Rgb(v) => format!("Rgb({}, {}, {})", v.r, v.g, v.b), 168 | Dynamic::Rgba(v) => format!("Rgba({}, {}, {}, {})", v.r, v.g, v.b, v.a), 169 | }; 170 | 171 | format!("", out) 172 | } 173 | } 174 | 175 | impl Display for Pixel { 176 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 177 | f.write_str(&self.__repr__()) 178 | } 179 | } 180 | 181 | #[pymethods] 182 | impl BitPixel { 183 | #[new] 184 | fn new(value: bool) -> Self { 185 | Self { value } 186 | } 187 | 188 | fn __richcmp__(&self, py: Python<'_>, other: PyObject, op: CompareOp) -> PyObject { 189 | match op { 190 | CompareOp::Eq => { 191 | let other = other.extract::(py); 192 | if let Ok(other) = other { 193 | let val = self == &other; 194 | val.into_py(py) 195 | } else { 196 | false.into_py(py) 197 | } 198 | } 199 | CompareOp::Ne => { 200 | if let Ok(other) = other.extract::(py) { 201 | let val = self != &other; 202 | val.into_py(py) 203 | } else { 204 | true.into_py(py) 205 | } 206 | } 207 | _ => py.NotImplemented(), 208 | } 209 | } 210 | 211 | fn __repr__(&self) -> String { 212 | format!("", self.value) 213 | } 214 | } 215 | 216 | #[pymethods] 217 | impl L { 218 | #[new] 219 | fn new(value: u8) -> Self { 220 | Self { value } 221 | } 222 | 223 | fn __richcmp__(&self, py: Python<'_>, other: PyObject, op: CompareOp) -> PyObject { 224 | match op { 225 | CompareOp::Eq => { 226 | let other = other.extract::(py); 227 | if let Ok(other) = other { 228 | let val = self == &other; 229 | val.into_py(py) 230 | } else { 231 | false.into_py(py) 232 | } 233 | } 234 | CompareOp::Ne => { 235 | if let Ok(other) = other.extract::(py) { 236 | let val = self != &other; 237 | val.into_py(py) 238 | } else { 239 | true.into_py(py) 240 | } 241 | } 242 | _ => py.NotImplemented(), 243 | } 244 | } 245 | 246 | fn __repr__(&self) -> String { 247 | format!("", self.value) 248 | } 249 | } 250 | 251 | #[pymethods] 252 | impl Rgb { 253 | #[new] 254 | fn new(r: u8, g: u8, b: u8) -> Self { 255 | Self { r, g, b } 256 | } 257 | 258 | fn __richcmp__(&self, py: Python<'_>, other: PyObject, op: CompareOp) -> PyObject { 259 | match op { 260 | CompareOp::Eq => { 261 | let other = other.extract::(py); 262 | if let Ok(other) = other { 263 | let val = self == &other; 264 | val.into_py(py) 265 | } else { 266 | false.into_py(py) 267 | } 268 | } 269 | CompareOp::Ne => { 270 | if let Ok(other) = other.extract::(py) { 271 | let val = self != &other; 272 | val.into_py(py) 273 | } else { 274 | true.into_py(py) 275 | } 276 | } 277 | _ => py.NotImplemented(), 278 | } 279 | } 280 | 281 | fn __repr__(&self) -> String { 282 | format!("", self.r, self.g, self.b) 283 | } 284 | } 285 | 286 | #[pymethods] 287 | impl Rgba { 288 | #[new] 289 | fn new(r: u8, g: u8, b: u8, a: u8) -> Self { 290 | Self { r, g, b, a } 291 | } 292 | 293 | fn __richcmp__(&self, py: Python<'_>, other: PyObject, op: CompareOp) -> PyObject { 294 | match op { 295 | CompareOp::Eq => { 296 | let other = other.extract::(py); 297 | if let Ok(other) = other { 298 | let val = self == &other; 299 | val.into_py(py) 300 | } else { 301 | false.into_py(py) 302 | } 303 | } 304 | CompareOp::Ne => { 305 | if let Ok(other) = other.extract::(py) { 306 | let val = self != &other; 307 | val.into_py(py) 308 | } else { 309 | true.into_py(py) 310 | } 311 | } 312 | _ => py.NotImplemented(), 313 | } 314 | } 315 | 316 | fn __repr__(&self) -> String { 317 | format!("", self.r, self.g, self.b, self.a) 318 | } 319 | } 320 | 321 | impl From for BitPixel { 322 | fn from(pixel: ril::BitPixel) -> Self { 323 | Self { 324 | value: pixel.value(), 325 | } 326 | } 327 | } 328 | 329 | impl From for L { 330 | fn from(pixel: ril::L) -> Self { 331 | Self { 332 | value: pixel.value(), 333 | } 334 | } 335 | } 336 | 337 | impl From for Rgb { 338 | fn from(pixel: ril::Rgb) -> Self { 339 | Self { 340 | r: pixel.r, 341 | g: pixel.g, 342 | b: pixel.b, 343 | } 344 | } 345 | } 346 | 347 | impl From for Rgba { 348 | fn from(pixel: ril::Rgba) -> Self { 349 | Self { 350 | r: pixel.r, 351 | g: pixel.g, 352 | b: pixel.b, 353 | a: pixel.a, 354 | } 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/draw.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, marker::PhantomData}; 2 | 3 | use pyo3::{ 4 | exceptions::{PyRuntimeError, PyValueError}, 5 | prelude::*, 6 | types::PyType, 7 | }; 8 | use ril::{ 9 | draw::{ 10 | Border as RilBorder, BorderPosition as RilBorderPosition, Ellipse as RilEllipse, 11 | Rectangle as RilRectangle, 12 | }, 13 | Dynamic, 14 | }; 15 | 16 | use crate::{ 17 | pixels::Pixel, 18 | utils::{cast_pixel_to_pyobject}, 19 | Xy, text::{TextSegment, TextLayout}, types::OverlayMode, 20 | }; 21 | 22 | fn get_border_position(position: &str) -> PyResult { 23 | match position { 24 | "inset" => Ok(RilBorderPosition::Inset), 25 | "center" => Ok(RilBorderPosition::Center), 26 | "outset" => Ok(RilBorderPosition::Outset), 27 | _ => Err(PyValueError::new_err( 28 | "position provided is not valid, it must be one of `inset`, `center`, or `outset`" 29 | .to_string(), 30 | )), 31 | } 32 | } 33 | 34 | fn from_border_position(position: RilBorderPosition) -> String { 35 | match position { 36 | RilBorderPosition::Inset => "inset".to_string(), 37 | RilBorderPosition::Center => "center".to_string(), 38 | RilBorderPosition::Outset => "outset".to_string(), 39 | } 40 | } 41 | 42 | /// Represents a shape border. 43 | /// 44 | /// Parameters 45 | /// ---------- 46 | /// color: :class:`.Pixel` 47 | /// The color of the border 48 | /// thickness: int 49 | /// The thickness of the border 50 | /// position: str 51 | /// The position of the border 52 | /// 53 | /// Raises 54 | /// ------ 55 | /// ValueError 56 | /// The position is not one of `inset`, `center`, or `outset` 57 | #[pyclass] 58 | #[derive(Clone)] 59 | #[pyo3(text_signature = "(color, thickness, position)")] 60 | pub struct Border { 61 | pub inner: RilBorder, 62 | } 63 | 64 | #[pymethods] 65 | impl Border { 66 | #[new] 67 | #[args("*", color, thickness, position)] 68 | fn new(color: Pixel, thickness: u32, position: &str) -> PyResult { 69 | let position = get_border_position(position)?; 70 | 71 | Ok(Self { 72 | inner: RilBorder { 73 | color: color.inner, 74 | thickness, 75 | position, 76 | }, 77 | }) 78 | } 79 | 80 | /// :class:`.Pixel`: The color of the border. 81 | #[getter] 82 | fn get_color(&self) -> Pixel { 83 | self.inner.color.into() 84 | } 85 | 86 | /// int: The thickness of the border, in pixels. 87 | #[getter] 88 | fn get_thickness(&self) -> u32 { 89 | self.inner.thickness 90 | } 91 | 92 | /// str: The position of the border. 93 | #[getter] 94 | fn get_position(&self) -> String { 95 | from_border_position(self.inner.position) 96 | } 97 | 98 | #[setter] 99 | fn set_color(&mut self, pixel: Pixel) { 100 | self.inner.color = pixel.inner; 101 | } 102 | 103 | #[setter] 104 | fn set_thickness(&mut self, thickness: u32) { 105 | self.inner.thickness = thickness; 106 | } 107 | 108 | #[setter] 109 | fn set_position(&mut self, position: &str) -> PyResult<()> { 110 | self.inner.position = get_border_position(position)?; 111 | 112 | Ok(()) 113 | } 114 | 115 | fn __repr__(&self) -> String { 116 | format!( 117 | "", 118 | self.get_color(), 119 | self.get_thickness(), 120 | self.get_position() 121 | ) 122 | } 123 | } 124 | 125 | impl Display for Border { 126 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 127 | f.write_str(&self.__repr__()) 128 | } 129 | } 130 | 131 | /// An ellipse, which could be a circle. 132 | /// 133 | /// .. warning:: 134 | /// Using any of the predefined constructors will automatically set the position to (0, 0) and you must explicitly set the size of the ellipse with `.size` in order to set a size for the ellipse. 135 | /// A size must be set before drawing. 136 | /// 137 | /// This also does not set any border or fill for the ellipse, you must explicitly set either one of them. 138 | /// 139 | /// Parameters 140 | /// --------- 141 | /// position: Tuple[int, int] 142 | /// The position of the ellipse 143 | /// radii: Tuple[int, int] 144 | /// The radii of the ellipse 145 | /// border: Optional[:class:`.Border`] 146 | /// The border of the ellipse. 147 | /// fill: Optional[:class:`.Pixel`] 148 | /// The color to use for filling the ellipse 149 | /// overlay: Optional[str] 150 | /// The overlay mode of the ellipse. 151 | #[pyclass] 152 | #[derive(Clone)] 153 | #[pyo3(text_signature = "(*, position, radii, border, fill, overlay)")] 154 | pub struct Ellipse { 155 | pub inner: RilEllipse, 156 | } 157 | 158 | #[pymethods] 159 | impl Ellipse { 160 | #[new] 161 | #[args("*", position, radii, border, fill, overlay)] 162 | fn new( 163 | position: Xy, 164 | radii: Xy, 165 | border: Option, 166 | fill: Option, 167 | overlay: Option, 168 | ) -> PyResult { 169 | let mut inner = RilEllipse:: { 170 | position, 171 | radii, 172 | border: None, 173 | fill: None, 174 | overlay: None, 175 | }; 176 | 177 | inner.border = border.map(|i| i.inner); 178 | 179 | inner.fill = fill.map(|i| i.inner); 180 | 181 | inner.overlay = overlay.map(|i| i.into()); 182 | 183 | Ok(Self { inner }) 184 | } 185 | 186 | /// Creates a new ellipse from the given bounding box. 187 | /// 188 | /// Parameters 189 | /// ---------- 190 | /// x1: int 191 | /// The x axis of the upper-left corner 192 | /// y1: int 193 | /// The y axis of the upper-left corner 194 | /// x2: int 195 | /// The x axis of the lower-right corner 196 | /// y2: int 197 | /// The y axis of the lower-right corner 198 | /// 199 | /// Returns 200 | /// ------- 201 | /// :class:`.Ellipse` 202 | #[classmethod] 203 | #[pyo3(text_signature = "(cls, x1, y1, x2, y2)")] 204 | fn from_bounding_box(_: &PyType, x1: u32, y1: u32, x2: u32, y2: u32) -> Self { 205 | Self { 206 | inner: RilEllipse::from_bounding_box(x1, y1, x2, y2), 207 | } 208 | } 209 | 210 | /// Creates a new circle with the given center position and radius. 211 | /// 212 | /// Parameters 213 | /// ---------- 214 | /// x: int 215 | /// The x axis 216 | /// y: int 217 | /// The y axis 218 | /// radius: int 219 | /// The radius 220 | #[classmethod] 221 | #[pyo3(text_signature = "(cls, x, y, radius)")] 222 | fn circle(_: &PyType, x: u32, y: u32, radius: u32) -> Self { 223 | Self { 224 | inner: RilEllipse::circle(x, y, radius), 225 | } 226 | } 227 | 228 | /// Tuple[int, int]: The center position of the ellipse. The center of this ellipse will be rendered at this position. 229 | #[getter] 230 | fn get_position(&self) -> Xy { 231 | self.inner.position 232 | } 233 | 234 | /// Tuple[int, int]: The radii of the ellipse, in pixels; (horizontal, vertical). 235 | #[getter] 236 | fn get_radii(&self) -> Xy { 237 | self.inner.radii 238 | } 239 | 240 | /// Optional[:class:`.Border`]: The border of the ellipse. 241 | #[getter] 242 | fn get_border(&self) -> Option { 243 | self.inner 244 | .border 245 | .as_ref() 246 | .map(|b| Border { inner: b.clone() }) 247 | } 248 | 249 | /// Optional[Union[:class:`.BitPixel`, :class:`.L`, :class:`.Rgb`, :class:`.Rgba`]]: The color used to fill the ellipse. 250 | #[getter] 251 | fn get_fill(&self, py: Python<'_>) -> Option { 252 | self.inner 253 | .fill 254 | .map_or(None, |fill| Some(cast_pixel_to_pyobject(py, fill))) 255 | } 256 | 257 | /// Optional[:class:`.OverlayMode`]: The overlay mode of the ellipse. 258 | #[getter] 259 | fn get_overlay(&self) -> Option { 260 | self.inner.overlay.map(|i| i.into()) 261 | } 262 | 263 | #[setter] 264 | fn set_position(&mut self, position: Xy) { 265 | self.inner.position = position; 266 | } 267 | 268 | #[setter] 269 | fn set_radii(&mut self, radii: Xy) { 270 | self.inner.radii = radii; 271 | } 272 | 273 | #[setter] 274 | fn set_border(&mut self, border: Border) { 275 | self.inner.border = Some(border.inner); 276 | } 277 | 278 | #[setter] 279 | fn set_fill(&mut self, fill: Pixel) { 280 | self.inner.fill = Some(fill.inner); 281 | } 282 | 283 | #[setter] 284 | fn set_overlay(&mut self, overlay: OverlayMode) -> PyResult<()> { 285 | self.inner.overlay = Some(overlay.into()); 286 | 287 | Ok(()) 288 | } 289 | 290 | fn __repr__(&self, py: Python<'_>) -> String { 291 | format!( 292 | "", 293 | self.get_position().0, 294 | self.get_position().1, 295 | self.get_radii().0, 296 | self.get_radii().1, 297 | self.get_border() 298 | .map_or("None".to_string(), |f| f.to_string()), 299 | self.get_fill(py) 300 | .map_or("None".to_string(), |f| f.to_string()), 301 | self.get_overlay() 302 | .map_or("None".to_string(), |f| format!("{:?}", f)), 303 | ) 304 | } 305 | } 306 | 307 | /// A rectangle. 308 | /// 309 | /// .. warning:: 310 | /// Using any of the predefined construction methods will automatically set the position to (0, 0). 311 | /// If you want to specify a different position, you must set the position with `.position` 312 | /// 313 | /// You must specify a width and height for the rectangle with something such as with_size. 314 | /// If you don't, a panic will be raised during drawing. 315 | /// You can also try using from_bounding_box to create a rectangle from a bounding box, which automatically fills in the size. 316 | /// 317 | /// Additionally, a panic will be raised during drawing if you do not specify either a fill color or a border. 318 | /// these can be set with `.fill` and `.border` respectively. 319 | /// 320 | /// Parameters 321 | /// ---------- 322 | /// position: Tuple[int, int] 323 | /// The position of the rectangle 324 | /// size: Tuple[int, int] 325 | /// The size of the rectangle 326 | /// border: Optional[:class:`.Border`] 327 | /// The border of the ellipse. 328 | /// fill: Optional[:class:`.Pixel`] 329 | /// The color to use for filling the rectangle 330 | /// overlay: Optional[:class:`.OverlayMode`] 331 | /// The overlay mode of the rectangle. 332 | /// 333 | /// Raises 334 | /// ------ 335 | /// ValueError 336 | /// The overlay mode provided is not one of `replace`, or `merge` 337 | #[pyclass] 338 | #[derive(Clone)] 339 | #[pyo3(text_signature = "(*, position, size, border, fill, overlay)")] 340 | pub struct Rectangle { 341 | pub inner: RilRectangle, 342 | } 343 | 344 | #[pymethods] 345 | impl Rectangle { 346 | #[new] 347 | #[args("*", position, size, border, fill, overlay)] 348 | fn new( 349 | position: Xy, 350 | size: Xy, 351 | border: Option, 352 | fill: Option, 353 | overlay: Option, 354 | ) -> PyResult { 355 | Ok(Self { 356 | inner: RilRectangle { 357 | position, 358 | size, 359 | border: border.map(|b| b.inner), 360 | fill: fill.map(|f| f.inner), 361 | overlay: overlay.map(|o| o.into()), 362 | }, 363 | }) 364 | } 365 | 366 | /// Creates a new rectangle from two coordinates specified as 4 parameters. 367 | /// The first coordinate is the top-left corner of the rectangle, and the second coordinate is the bottom-right corner of the rectangle. 368 | /// 369 | /// Parameters 370 | /// ---------- 371 | /// x1: int 372 | /// The x axis of the upper-left corner 373 | /// y1: int 374 | /// The y axis of the upper-left corner 375 | /// x2: int 376 | /// The x axis of the lower-right corner 377 | /// y2: int 378 | /// The y axis of the lower-right corner 379 | #[classmethod] 380 | #[pyo3(text_signature = "(cls, x1, y1, x2, y2)")] 381 | fn from_bounding_box(_: &PyType, x1: u32, y1: u32, x2: u32, y2: u32) -> Self { 382 | Self { 383 | inner: RilRectangle::from_bounding_box(x1, y1, x2, y2), 384 | } 385 | } 386 | 387 | /// Tuple[int, int]: The position of the rectangle. The top-left corner of the rectangle will be rendered at this position. 388 | #[getter] 389 | fn get_position(&self) -> Xy { 390 | self.inner.position 391 | } 392 | 393 | /// Tuple[int, int]: The dimensions of the rectangle, in pixels. 394 | #[getter] 395 | fn get_size(&self) -> Xy { 396 | self.inner.size 397 | } 398 | 399 | /// :class:`.Border`: The border of the rectangle, or None if there is no border. 400 | #[getter] 401 | fn get_border(&self) -> Option { 402 | self.inner 403 | .border 404 | .as_ref() 405 | .map(|b| Border { inner: b.clone() }) 406 | } 407 | 408 | /// Optional[Union[:class:`.BitPixel`, :class:`.L`, :class:`.Rgb`, :class:`.Rgba`]]: The color used to fill the rectangle. 409 | #[getter] 410 | fn get_fill(&self, py: Python<'_>) -> Option { 411 | self.inner 412 | .fill 413 | .map_or(None, |fill| Some(cast_pixel_to_pyobject(py, fill))) 414 | } 415 | 416 | /// Optional[:class:`.OverlayMode`]: The overlay mode of the rectangle. 417 | #[getter] 418 | fn get_overlay(&self) -> Option { 419 | self.inner.overlay.map(|i| i.into()) 420 | } 421 | 422 | #[setter] 423 | fn set_position(&mut self, position: Xy) { 424 | self.inner.position = position; 425 | } 426 | 427 | #[setter] 428 | fn set_size(&mut self, size: Xy) { 429 | self.inner.size = size; 430 | } 431 | 432 | #[setter] 433 | fn set_border(&mut self, border: Option) { 434 | self.inner.border = border.map(|b| b.inner); 435 | } 436 | 437 | #[setter] 438 | fn set_fill(&mut self, fill: Option) { 439 | self.inner.fill = fill.map(|f| f.inner); 440 | } 441 | 442 | #[setter] 443 | fn set_overlay(&mut self, overlay: OverlayMode) -> PyResult<()> { 444 | self.inner.overlay = Some(overlay.into()); 445 | 446 | Ok(()) 447 | } 448 | 449 | fn __repr__(&self, py: Python<'_>) -> String { 450 | format!( 451 | "", 452 | self.get_position().0, 453 | self.get_position().1, 454 | self.get_size().0, 455 | self.get_size().1, 456 | self.get_border() 457 | .map_or("None".to_string(), |f| f.to_string()), 458 | self.get_fill(py) 459 | .map_or("None".to_string(), |f| f.to_string()), 460 | self.get_overlay() 461 | .map_or("None".to_string(), |f| format!("{:?}", f)), 462 | ) 463 | } 464 | } 465 | 466 | macro_rules! impl_draw_entities { 467 | ($obj:expr, $( $class:ident ),*) => {{ 468 | $( 469 | match $obj.extract::<$class>() { 470 | Ok(r) => return Ok(DrawEntity::$class(r)), 471 | Err(_) => () 472 | } 473 | )* 474 | 475 | Err(PyRuntimeError::new_err( 476 | "Invalid argument for draw".to_string(), 477 | )) 478 | }}; 479 | } 480 | 481 | #[allow(dead_code)] 482 | pub enum DrawEntity<'a> { 483 | Rectangle(Rectangle), 484 | Ellipse(Ellipse), 485 | TextSegment(TextSegment), 486 | TextLayout(TextLayout), 487 | PhantomData(PhantomData<&'a ()>) 488 | } 489 | 490 | impl<'a> FromPyObject<'a> for DrawEntity<'a> { 491 | fn extract(obj: &'a PyAny) -> PyResult { 492 | impl_draw_entities!(obj, Rectangle, Ellipse, TextSegment, TextLayout) 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /src/text.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{prelude::*, types::PyType}; 2 | use ril::{Dynamic, Font as RilFont}; 3 | 4 | use std::{path::PathBuf, sync::{Arc, RwLock}}; 5 | 6 | use crate::{ 7 | error::Error, 8 | pixels::Pixel, 9 | workaround::{OwnedTextSegment as RilTextSegment, OwnedTextLayout as RilTextLayout}, 10 | types::{HorizontalAnchor, OverlayMode, VerticalAnchor, WrapStyle}, 11 | utils::cast_pixel_to_pyobject, 12 | Xy, 13 | }; 14 | 15 | /// Represents a text segment that can be drawn. 16 | /// 17 | /// See :class:`TextLayout` for a more robust implementation that supports rendering text with multiple styles. 18 | /// This type is for more simple and lightweight usages. 19 | /// 20 | /// Additionally, accessing metrics such as the width and height of the text cannot be done here, 21 | /// but can be done in TextLayout since it keeps a running copy of the layout. 22 | /// Use TextLayout if you will be needing to calculate the width and height of the text. 23 | /// Additionally, TextLayout supports text anchoring, which can be used to align text. 24 | /// 25 | /// If you need none of these features, :class:`TextSegment` should be used in favor of being much more lightweight. 26 | /// 27 | /// Parameters 28 | /// ---------- 29 | /// font: :class:`Font` 30 | /// The font to use to render the text. 31 | /// text: str 32 | /// The text to render. 33 | /// fill: :class:`Pixel` 34 | /// The fill color the text will be in. 35 | /// position: Optional[Tuple[int, int]] 36 | /// The position the text will be rendered at. 37 | /// 38 | /// **This must be set before adding any text segments!** 39 | /// 40 | /// Either with :attr:`position` or by passing it to the constructor. 41 | /// size: Optional[float] 42 | /// The size of the text in pixels. 43 | /// overlay: Optional[:class:`OverlayMode`] 44 | /// The overlay mode to use when rendering the text. 45 | /// width: Optional[int] 46 | /// The width of the text layout. 47 | /// wrap: Optional[:class:`WrapStyle`] 48 | /// The wrapping style of the text. Note that text will only wrap if `width` is set. 49 | /// If this is used in a :class:`TextLayout`, this is ignored and :attr:`.WrapStyle.Wrap` is used instead. 50 | /// 51 | /// 52 | /// .. warning:: 53 | /// As this class contains the data of an entire font, copying this class is expensive. 54 | #[pyclass] 55 | #[derive(Clone)] 56 | pub struct TextSegment { 57 | pub(crate) inner: RilTextSegment, 58 | } 59 | 60 | #[pymethods] 61 | impl TextSegment { 62 | #[new] 63 | fn new( 64 | font: Font, 65 | text: &str, 66 | fill: Pixel, 67 | position: Option, 68 | size: Option, 69 | overlay: Option, 70 | width: Option, 71 | wrap: Option, 72 | ) -> Self { 73 | let font_size = font.optimal_size(); 74 | 75 | let mut inner = RilTextSegment::new(font.inner, text, fill.inner); 76 | 77 | inner.position = position.unwrap_or((0, 0)); 78 | inner.size = size.unwrap_or(font_size); 79 | inner.overlay = overlay.unwrap_or(OverlayMode::Merge).into(); 80 | inner.width = width; 81 | inner.wrap = wrap.unwrap_or(WrapStyle::Word).into(); 82 | 83 | Self { inner } 84 | } 85 | 86 | /// Tuple[int, int]: The position of the text segment. 87 | #[getter] 88 | fn position(&self) -> Xy { 89 | self.inner.position 90 | } 91 | 92 | /// float: The width of the text box. 93 | /// 94 | /// .. warning:: 95 | /// If this is used in a :class:`TextLayout`, this is ignored and :meth:`TextLayout.width` is used instead. 96 | #[getter] 97 | fn width(&self) -> Option { 98 | self.inner.width 99 | } 100 | 101 | /// str: The content of the text segment. 102 | #[getter] 103 | fn text(&self) -> String { 104 | self.inner.text.clone() 105 | } 106 | 107 | /// :class:`Font`: The font of the text segment. 108 | /// 109 | /// .. warning:: 110 | /// Due to design limitation, accessing font requires a deep clone each time, which is expensive. 111 | #[getter] 112 | fn font(&self) -> Font { 113 | Font { 114 | inner: self.inner.font.clone(), 115 | } 116 | } 117 | 118 | /// List[List[Union[:class:`.BitPixel`, :class:`.L`, :class:`.Rgb`, :class:`.Rgba`]]]: The fill color of the text segment. 119 | #[getter] 120 | fn fill(&self, py: Python<'_>) -> PyObject { 121 | cast_pixel_to_pyobject(py, self.inner.fill) 122 | } 123 | 124 | /// :class:`OverlayMode`: The overlay mode of the text segment. 125 | #[getter] 126 | fn overlay(&self) -> OverlayMode { 127 | self.inner.overlay.into() 128 | } 129 | 130 | /// float: The size of the text segment in pixels. 131 | #[getter] 132 | fn size(&self) -> f32 { 133 | self.inner.size 134 | } 135 | 136 | /// :class:`WrapStyle`: The wrapping style of the text segment. 137 | #[getter] 138 | fn wrap(&self) -> WrapStyle { 139 | self.inner.wrap.into() 140 | } 141 | 142 | #[setter] 143 | fn set_position(&mut self, position: Xy) { 144 | self.inner.position = position; 145 | } 146 | 147 | #[setter] 148 | fn set_width(&mut self, width: Option) { 149 | self.inner.width = width; 150 | } 151 | 152 | #[setter] 153 | fn set_text(&mut self, text: &str) { 154 | self.inner.text = text.to_string(); 155 | } 156 | 157 | #[setter] 158 | fn set_font(&mut self, font: Font) { 159 | self.inner.font = font.inner; 160 | } 161 | 162 | #[setter] 163 | fn set_fill(&mut self, fill: Pixel) { 164 | self.inner.fill = fill.inner; 165 | } 166 | 167 | #[setter] 168 | fn set_overlay(&mut self, overlay: OverlayMode) { 169 | self.inner.overlay = overlay.into(); 170 | } 171 | 172 | #[setter] 173 | fn set_size(&mut self, size: f32) { 174 | self.inner.size = size; 175 | } 176 | 177 | #[setter] 178 | fn set_wrap(&mut self, wrap: WrapStyle) { 179 | self.inner.wrap = wrap.into(); 180 | } 181 | 182 | fn __repr__(&self, py: Python<'_>) -> String { 183 | format!( 184 | "", 185 | self.fill(py), 186 | self.position().0, 187 | self.position().1, 188 | self.size(), 189 | self.overlay(), 190 | self.width().map_or("None".to_string(), |f| f.to_string()), 191 | self.wrap() 192 | ) 193 | } 194 | } 195 | 196 | /// Represents a high-level text layout that can layout text segments, maybe with different fonts. 197 | /// 198 | /// This is a high-level layout that can be used to layout text segments. 199 | /// It can be used to layout text segments with different fonts and styles, and has many features over :class:`TextSegment` such as text anchoring, 200 | /// which can be useful for text alignment. 201 | /// This also keeps track of font metrics, meaning that unlike :class:`TextSegment`, 202 | /// this can be used to determine the width and height of text before rendering it. 203 | /// 204 | /// This is less efficient than :class:`TextSegment` and you should use :class:`TextSegment` if you don't need any of the features TextLayout provides. 205 | /// 206 | /// Parameters 207 | /// ---------- 208 | /// position: Optional[Tuple[int, int]] 209 | /// The position the text will be rendered at. 210 | /// 211 | /// **This must be set before adding any text segments!** 212 | /// 213 | /// Either with :attr:`position` or by passing it to the constructor. 214 | 215 | /// horizontal_anchor: Optional[:class:`.HorizontalAnchor`] 216 | /// The horizontal anchor of the text. 217 | /// 218 | /// vertical_anchor: Optional[:class:`.VerticalAnchor`] 219 | /// The vertical anchor of the text. 220 | 221 | /// wrap: Optional[:class:`.WrapStyle`] 222 | /// Sets the wrapping style of the text. Make sure to also set the wrapping width using :attr:`width` for wrapping to work. 223 | /// 224 | /// **This must be set before adding any text segments!** 225 | /// 226 | /// 227 | /// .. warning:: 228 | /// As this class contains the data of one or more font(s), copying this class can be extremely expensive. 229 | #[pyclass] 230 | #[derive(Clone)] 231 | #[pyo3( 232 | text_signature = "(font, text, fill, position = None, size = None, overlay = None, width = None, wrap = None)" 233 | )] 234 | pub struct TextLayout { 235 | pub(crate) inner: Arc>>, 236 | } 237 | 238 | #[pymethods] 239 | impl TextLayout { 240 | #[new] 241 | fn new( 242 | position: Option, 243 | width: Option, 244 | horizontal_anchor: Option, 245 | vertical_anchor: Option, 246 | wrap: Option, 247 | ) -> Self { 248 | let mut inner = RilTextLayout::new(); 249 | 250 | if let Some(position) = position { 251 | inner.set_position(position.0, position.1); 252 | } 253 | 254 | if let Some(width) = width { 255 | inner.set_width(width); 256 | } 257 | 258 | if let Some(horizontal_anchor) = horizontal_anchor { 259 | inner.x_anchor = horizontal_anchor.into(); 260 | } 261 | 262 | if let Some(vertical_anchor) = vertical_anchor { 263 | inner.y_anchor = vertical_anchor.into(); 264 | } 265 | 266 | if let Some(wrap) = wrap { 267 | inner.set_wrap(wrap.into()); 268 | } 269 | 270 | Self { 271 | inner: Arc::new(RwLock::new(inner)), 272 | } 273 | } 274 | 275 | /// Sets the horizontal anchor and vertial anchor of the text to be centered. 276 | /// This makes the position of the text be the center as opposed to the top-left corner. 277 | fn centered(&mut self) -> Result<(), Error>{ 278 | self.inner.write()?.centered(); 279 | 280 | Ok(()) 281 | } 282 | 283 | /// Tuple[int, int, int, int]: Returns the bounding box of the text. 284 | /// Left and top bounds are inclusive; right and bottom bounds are exclusive. 285 | #[getter] 286 | fn bounding_box(&self) -> Result<(u32, u32, u32, u32), Error> { 287 | Ok(self.inner.read()?.bounding_box()) 288 | } 289 | 290 | /// Tuple[int, int]: Returns the width and height of the text. 291 | /// 292 | /// .. warning:: 293 | /// This is a slightly expensive operation and is not a simple getter. 294 | /// 295 | /// .. note:: 296 | /// If you want both width and height, use :attr:`dimensions`. 297 | #[getter] 298 | fn dimensions(&self) -> Result { 299 | Ok(self.inner.read()?.dimensions()) 300 | } 301 | 302 | /// int: Returns the height of the text. 303 | /// 304 | /// .. warning:: 305 | /// This is a slightly expensive operation and is not a simple getter. 306 | /// 307 | /// .. note:: 308 | /// If you want both width and height, use :attr:`dimensions`. 309 | #[getter] 310 | fn height(&self) -> Result { 311 | Ok(self.inner.read()?.height()) 312 | } 313 | 314 | /// int: Returns the width of the text. 315 | /// 316 | /// .. warning:: 317 | /// This is a slightly expensive operation and is not a simple getter. 318 | /// 319 | /// .. note:: 320 | /// If you want both width and height, use :attr:`dimensions`. 321 | #[getter] 322 | fn width(&self) -> Result { 323 | Ok(self.inner.read()?.width()) 324 | } 325 | 326 | /// Sets the position of the text layout. 327 | /// 328 | /// **This must be set before adding any text segments!** 329 | #[setter] 330 | fn set_position(&mut self, position: Xy) -> Result<(), Error> { 331 | self.inner.write()?.set_position(position.0, position.1); 332 | 333 | Ok(()) 334 | } 335 | 336 | /// Sets the horizontal anchor of the text layout. 337 | #[setter] 338 | fn set_horizontal_anchor(&mut self, anchor: HorizontalAnchor) -> Result<(), Error> { 339 | self.inner.write()?.x_anchor = anchor.into(); 340 | 341 | Ok(()) 342 | } 343 | 344 | /// Sets the vertical anchor of the text layout. 345 | #[setter] 346 | fn set_vertical_anchor(&mut self, anchor: VerticalAnchor) -> Result<(), Error> { 347 | self.inner.write()?.y_anchor = anchor.into(); 348 | 349 | Ok(()) 350 | } 351 | 352 | /// Sets the width of the text layout. 353 | /// This does not impact :attr:`dimensions`. 354 | /// 355 | /// **This must be set before adding any text segments!** 356 | #[setter] 357 | fn set_width(&mut self, width: u32) -> Result<(), Error> { 358 | self.inner.write()?.set_width(width); 359 | 360 | Ok(()) 361 | } 362 | 363 | /// Sets the wrapping style of the text layout. 364 | /// Make sure to also set the wrapping width using :attr:`width` for wrapping to work. 365 | /// 366 | /// **This must be set before adding any text segments!** 367 | #[setter] 368 | fn set_wrap(&mut self, wrap: WrapStyle) -> Result<(), Error> { 369 | self.inner.write()?.set_wrap(wrap.into()); 370 | 371 | Ok(()) 372 | } 373 | 374 | /// Pushes a basic text to the text layout. 375 | /// Adds basic text to the text layout. This is a convenience method that creates a :class:`TextSegment` with the given font, text, and fill and adds it to the text layout. 376 | /// The size of the text is determined by the font’s optimal size. 377 | /// 378 | /// Parameters 379 | /// ---------- 380 | /// font: :class:`Font` 381 | /// The font to use for the text. 382 | /// text: str 383 | /// The text to add. 384 | /// fill: :class:`Pixel` 385 | /// The color of the text. 386 | #[pyo3(text_signature = "(self, font, text, fill)")] 387 | fn push_basic_text(&mut self, font: Font, text: &str, fill: Pixel) -> Result<(), Error> { 388 | self.inner.write()?.push_basic_text(font.inner, text, fill.inner); 389 | 390 | Ok(()) 391 | } 392 | 393 | /// Pushes a text segment to the text layout. 394 | /// 395 | /// Parameters 396 | /// ---------- 397 | /// segment: :class:`TextSegment` 398 | /// The text segment to add. 399 | #[pyo3(text_signature = "(self, segment)")] 400 | fn push_segment(&mut self, segment: TextSegment) -> Result<(), Error> { 401 | self.inner.write()?.push_segment(segment.inner); 402 | 403 | Ok(()) 404 | } 405 | 406 | fn __repr__(&self) -> Result { 407 | let inner = self.inner.read()?; 408 | let bound = inner.bounding_box(); 409 | let dimensions = inner.dimensions(); 410 | 411 | Ok(format!( 412 | "", 413 | bound.0, 414 | bound.1, 415 | bound.2, 416 | bound.3, 417 | dimensions.0, 418 | dimensions.1, 419 | inner.x_anchor, 420 | inner.y_anchor, 421 | )) 422 | } 423 | } 424 | 425 | /// Represents a single font along with its alternatives used to render text. Currently, this supports TrueType and OpenType fonts. 426 | #[pyclass] 427 | #[derive(Clone)] 428 | pub struct Font { 429 | inner: RilFont, 430 | } 431 | 432 | #[pymethods] 433 | impl Font { 434 | /// Opens the font from the given path. 435 | /// 436 | /// .. note:: 437 | /// 438 | /// The optimal size is not the fixed size of the font - rather it is the size to optimize rasterizing the font for. 439 | /// 440 | /// Lower sizes will look worse but perform faster, while higher sizes will look better but perform slower. 441 | /// It is best to set this to the size that will likely be the most use 442 | /// 443 | /// Parameters 444 | /// ---------- 445 | /// path: str 446 | /// The path of the font. 447 | /// optimal_size: float 448 | /// The optimal size of the font. 449 | /// 450 | /// Raises 451 | /// ------ 452 | /// IOError 453 | /// Fails to read the font file. 454 | /// RuntimeError 455 | /// Fails to load the font. 456 | /// 457 | /// 458 | /// .. seealso:: 459 | /// :meth:`from_bytes` 460 | #[classmethod] 461 | #[pyo3(text_signature = "(cls, path, optimal_size)")] 462 | fn open(_: &PyType, path: PathBuf, optimal_size: f32) -> Result { 463 | Ok(Self { 464 | inner: RilFont::open(path, optimal_size)?, 465 | }) 466 | } 467 | 468 | /// Loads the font from the given bytes. 469 | /// 470 | /// .. note:: 471 | /// The optimal size is not the fixed size of the font - rather it is the size to optimize rasterizing the font for. 472 | /// 473 | /// Lower sizes will look worse but perform faster, while higher sizes will look better but perform slower. 474 | /// It is best to set this to the size that will likely be the most use 475 | /// 476 | /// Parameters 477 | /// ---------- 478 | /// path: str 479 | /// The path of the font. 480 | /// optimal_size: float 481 | /// The optimal size of the font. 482 | /// 483 | /// Raises 484 | /// ------ 485 | /// IOError 486 | /// Fails to read the font file. 487 | /// RuntimeError 488 | /// Fails to load the font. 489 | #[classmethod] 490 | #[pyo3(text_signature = "(cls, bytes, optimal_size)")] 491 | fn from_bytes(_: &PyType, bytes: &[u8], optimal_size: f32) -> Result { 492 | Ok(Self { 493 | inner: RilFont::from_bytes(bytes, optimal_size)?, 494 | }) 495 | } 496 | 497 | /// float: Returns the optimal size, in pixels, of this font. 498 | /// 499 | /// .. note:: 500 | /// The optimal size is not the fixed size of the font - rather it is the size to optimize rasterizing the font for. 501 | /// 502 | /// Lower sizes will look worse but perform faster, while higher sizes will look better but perform slower. 503 | /// It is best to set this to the size that will likely be the most used. 504 | #[getter] 505 | fn optimal_size(&self) -> f32 { 506 | self.inner.optimal_size() 507 | } 508 | 509 | fn __repr__(&self) -> String { 510 | format!( 511 | "", 512 | self.inner.optimal_size() 513 | ) 514 | } 515 | } 516 | 517 | // macro_rules! impl_shared_draw_entities { 518 | // ($obj:expr, $( $class:ty ),*) => {{ 519 | // $( 520 | // match $obj.extract::<$class>() { 521 | // Ok(r) => return Ok(Self(r.inner, PhantomData)), 522 | // Err(_) => () 523 | // } 524 | // )* 525 | 526 | // Err(PyRuntimeError::new_err( 527 | // "Invalid argument for draw".to_string(), 528 | // )) 529 | // }}; 530 | // } 531 | 532 | // pub struct SharedDrawEntity<'a>(pub Arc>>, PhantomData<&'a ()>); 533 | 534 | // impl<'a> FromPyObject<'a> for SharedDrawEntity<'a> { 535 | // fn extract(obj: &'a PyAny) -> PyResult { 536 | // impl_shared_draw_entities!(obj, TextLayout) 537 | // } 538 | // } 539 | -------------------------------------------------------------------------------- /src/image.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use crate::draw::DrawEntity; 4 | use crate::error::Error; 5 | use crate::pixels::{BitPixel, Pixel, Rgb, Rgba, L}; 6 | use crate::types::{ResizeAlgorithm, OverlayMode}; 7 | use crate::utils::cast_pixel_to_pyobject; 8 | use pyo3::types::PyBytes; 9 | use pyo3::{ 10 | exceptions::{PyTypeError, PyValueError}, 11 | prelude::*, 12 | types::{PyTuple, PyType}, 13 | }; 14 | use ril::{Banded, Dynamic, Image as RilImage, ImageFormat, Draw as _}; 15 | 16 | /// A high-level image representation. 17 | /// 18 | /// This represents a static, single-frame image. See :class:`.ImageSequence` for information on opening animated or multi-frame images. 19 | #[pyclass] 20 | #[derive(Clone)] 21 | pub struct Image { 22 | pub inner: RilImage, 23 | } 24 | 25 | macro_rules! cast_bands_to_pyobjects { 26 | ($py:expr, $($band:expr),*) => {{ 27 | Ok(( 28 | $( 29 | Self::from_inner($band.convert::()), 30 | )* 31 | ).into_py($py)) 32 | }}; 33 | } 34 | 35 | macro_rules! to_inner_bands { 36 | ($bands:expr, $($band:tt),*) => {{ 37 | ( 38 | $( 39 | $bands.$band.inner.convert::(), 40 | )* 41 | ) 42 | }}; 43 | } 44 | 45 | macro_rules! ensure_mode { 46 | ($bands:expr, $($band:tt),*) => {{ 47 | $( 48 | if $bands.$band.mode() != "L" { 49 | return Err(PyTypeError::new_err(format!("Expected mode `L`, got `{}`", $bands.$band.mode()))); 50 | } 51 | )* 52 | 53 | Ok::<(), PyErr>(()) 54 | }}; 55 | } 56 | 57 | #[pymethods] 58 | impl Image { 59 | /// Creates a new image with the given width and height, with all pixels being set intially to `fill`. 60 | /// 61 | /// Parameters 62 | /// ---------- 63 | /// width: int 64 | /// The width of the Image. 65 | /// height: int 66 | /// The height of the Image. 67 | /// fill: :class:`.Pixel` 68 | /// The pixel used to fill the image. 69 | /// 70 | /// Examples 71 | /// -------- 72 | /// 73 | /// .. code-block:: python3 74 | /// 75 | /// Image.new(100, 100, Pixel.from_rgb(255, 255, 255)) 76 | #[classmethod] 77 | #[pyo3(text_signature = "(cls, width, height, fill)")] 78 | fn new(_: &PyType, width: u32, height: u32, fill: Pixel) -> Self { 79 | Self { 80 | inner: RilImage::new(width, height, fill.inner), 81 | } 82 | } 83 | 84 | /// Decodes an image with the explicitly given image encoding from the raw bytes. 85 | /// 86 | /// if `format` is not provided then it will try to infer its encoding. 87 | /// 88 | /// Parameters 89 | /// ---------- 90 | /// bytes: bytes 91 | /// The bytes of the Image. 92 | /// format: Optional[str], default: None 93 | /// The format of the image, defaults to `None`. 94 | /// 95 | /// Raises 96 | /// ------ 97 | /// ValueError 98 | /// Raised if the format provided is invalid. 99 | /// RuntimeError 100 | /// Raised if the image can't be decoded or the format is unknown. 101 | #[classmethod] 102 | #[pyo3(text_signature = "(cls, bytes, format = None)")] 103 | fn from_bytes(_: &PyType, bytes: &[u8], format: Option<&str>) -> Result { 104 | Ok(if let Some(format) = format { 105 | Self { 106 | inner: RilImage::from_bytes(ImageFormat::from_extension(format)?, bytes)?, 107 | } 108 | } else { 109 | Self { 110 | inner: RilImage::from_bytes_inferred(bytes)?, 111 | } 112 | }) 113 | } 114 | 115 | /// Creates a new image shaped with the given width 116 | /// and a 1-dimensional sequence of pixels which will be shaped according to the width. 117 | /// 118 | /// Parameters 119 | /// ---------- 120 | /// width: int 121 | /// The width of the image. 122 | /// pixels: List[:class:`.Pixel`] 123 | /// A List of pixels. 124 | #[classmethod] 125 | #[pyo3(text_signature = "(cls, width, pixels)")] 126 | fn from_pixels(_: &PyType, width: u32, pixels: Vec) -> Self { 127 | Self { 128 | inner: RilImage::from_pixels( 129 | width, 130 | pixels 131 | .into_iter() 132 | .map(|p| p.inner) 133 | .collect::>(), 134 | ), 135 | } 136 | } 137 | 138 | /// Opens a file from the given path and decodes it into an image. 139 | /// 140 | /// The encoding of the image is automatically inferred. 141 | /// You can explicitly pass in an encoding by using the :meth:`from_bytes` method. 142 | /// 143 | /// Parameters 144 | /// ---------- 145 | /// path: str 146 | /// The path to the image. 147 | /// 148 | /// Raises 149 | /// ------ 150 | /// ValueError 151 | /// The file extension is invalid. 152 | /// RuntimeError 153 | /// Failed to infer file format or Failed to decode image. 154 | #[classmethod] 155 | #[pyo3(text_signature = "(cls, path)")] 156 | fn open(_: &PyType, path: PathBuf) -> Result { 157 | Ok(Self { 158 | inner: RilImage::open(path)?, 159 | }) 160 | } 161 | 162 | /// :class:`.OverlayMode`: Returns the overlay mode of the image. 163 | #[getter] 164 | fn overlay_mode(&self) -> OverlayMode { 165 | self.inner.overlay_mode().into() 166 | } 167 | 168 | /// str: Returns the mode of the image. 169 | #[getter] 170 | fn mode(&self) -> &str { 171 | match self.inner.pixel(0, 0) { 172 | Dynamic::BitPixel(_) => "bitpixel", 173 | Dynamic::L(_) => "L", 174 | Dynamic::Rgb(_) => "RGB", 175 | Dynamic::Rgba(_) => "RGBA", 176 | } 177 | } 178 | 179 | /// int: Returns the width of the image. 180 | #[getter] 181 | fn width(&self) -> u32 { 182 | self.inner.width() 183 | } 184 | 185 | /// int: Returns the height of the image. 186 | #[getter] 187 | fn height(&self) -> u32 { 188 | self.inner.height() 189 | } 190 | 191 | /// Return the bands of the image. 192 | /// 193 | /// Returns 194 | /// ------- 195 | /// Tuple[:class:`.L`, ...] 196 | /// 197 | /// Raises 198 | /// ------ 199 | /// TypeError 200 | /// The image is not of mode `RGB` or `RGBA`. 201 | fn bands(&self, py: Python<'_>) -> Result { 202 | match self.mode() { 203 | "RGB" => { 204 | let (r, g, b) = self.inner.clone().convert::().bands(); 205 | 206 | cast_bands_to_pyobjects!(py, r, g, b) 207 | } 208 | "RGBA" => { 209 | let (r, g, b, a) = self.inner.clone().convert::().bands(); 210 | 211 | cast_bands_to_pyobjects!(py, r, g, b, a) 212 | } 213 | _ => Err(Error::UnexpectedFormat( 214 | self.mode().to_string(), 215 | "Rgb or Rgba".to_string(), 216 | )), 217 | } 218 | } 219 | 220 | /// Creates a new image from the given bands. 221 | /// 222 | /// Parameters 223 | /// ---------- 224 | /// bands: \* :class:`.L` 225 | /// The bands of the image. 226 | #[classmethod] 227 | #[args(bands = "*")] 228 | #[pyo3(text_signature = "(self, *bands)")] 229 | fn from_bands(_: &PyType, bands: &PyTuple) -> PyResult { 230 | match bands.len() { 231 | 3 => { 232 | let bands: (Self, Self, Self) = bands.extract()?; 233 | 234 | ensure_mode!(bands, 0, 1, 2)?; 235 | 236 | Ok(Self::from_inner( 237 | RilImage::from_bands(to_inner_bands!(bands, 0, 1, 2)).convert::(), 238 | )) 239 | } 240 | 4 => { 241 | let bands: (Self, Self, Self, Self) = bands.extract()?; 242 | 243 | ensure_mode!(bands, 0, 1, 2, 3)?; 244 | 245 | Ok(Self::from_inner( 246 | RilImage::from_bands(to_inner_bands!(bands, 0, 1, 2, 3)) 247 | .convert::(), 248 | )) 249 | } 250 | _ => Err(PyValueError::new_err(format!( 251 | "Expected 3 or 4 arguments, got `{}`", 252 | bands.len() 253 | ))), 254 | } 255 | } 256 | 257 | /// Crops this image in place to the given bounding box. 258 | /// 259 | /// Parameters 260 | /// ---------- 261 | /// x1: int 262 | /// The x axis of the upper-left corner 263 | /// y1: int 264 | /// The y axis of the upper-left corner 265 | /// x2: int 266 | /// The x axis of the lower-right corner 267 | /// y2: int 268 | /// The y axis of the lower-right corner 269 | #[pyo3(text_signature = "(self, x1, y1, x2, y2)")] 270 | fn crop(&mut self, x1: u32, y1: u32, x2: u32, y2: u32) { 271 | self.inner.crop(x1, y1, x2, y2); 272 | } 273 | 274 | /// Draws an object or shape onto this image. 275 | /// 276 | /// Parameters 277 | /// ---------- 278 | /// entity: Union[:class:`.Rectangle`, :class:`.Ellipse`] 279 | /// The entity to draw on the image. 280 | #[pyo3(text_signature = "(self, entity)")] 281 | fn draw(&mut self, entity: DrawEntity) -> Result<(), Error>{ 282 | match entity { 283 | DrawEntity::Rectangle(e) => e.inner.draw(&mut self.inner), 284 | DrawEntity::Ellipse(e) => e.inner.draw(&mut self.inner), 285 | DrawEntity::TextSegment(e) => e.inner.draw(&mut self.inner), 286 | DrawEntity::TextLayout(e) => e.inner.write()?.draw(&mut self.inner), 287 | DrawEntity::PhantomData(_) => {}, 288 | }; 289 | 290 | Ok(()) 291 | } 292 | 293 | /// Resizes this image in place to the given dimensions using the given resizing algorithm in place. 294 | /// 295 | /// Parameters 296 | /// ---------- 297 | /// width: int 298 | /// The target width to resize to 299 | /// height: int 300 | /// The target height to resize to 301 | /// algorithm: :class:`.ResizeAlgorithm` 302 | /// The resize algorithm to use 303 | #[pyo3(text_signature = "(self, width, height, algorithm)")] 304 | fn resize(&mut self, width: u32, height: u32, algorithm: ResizeAlgorithm) { 305 | self.inner.resize(width, height, algorithm.into()); 306 | } 307 | 308 | /// Encodes the image with the given encoding and returns `bytes`. 309 | /// 310 | /// Parameters 311 | /// ---------- 312 | /// encoding: str 313 | /// The encoding of the image. 314 | /// 315 | /// Returns 316 | /// ------- 317 | /// bytes 318 | /// The encoded bytes of the image. 319 | /// 320 | /// Raises 321 | /// ------ 322 | /// ValueError 323 | /// The encoding is invalid. 324 | /// RuntimeError 325 | /// Failed to encode the image. 326 | #[pyo3(text_signature = "(self, encoding)")] 327 | fn encode(&self, encoding: &str) -> Result<&PyBytes, Error> { 328 | let encoding = ImageFormat::from_extension(encoding)?; 329 | 330 | let mut buf = Vec::new(); 331 | self.inner.encode(encoding, &mut buf)?; 332 | 333 | // SAFETY: We acquired the GIL before calling `assume_gil_acquired`. 334 | // `assume_gil_acquired` is only used to ensure that PyBytes don't outlive the current function 335 | unsafe { 336 | Python::with_gil(|_| { 337 | let buf = buf.as_slice(); 338 | let pyacq = Python::assume_gil_acquired(); 339 | Ok(PyBytes::new(pyacq, buf)) 340 | }) 341 | } 342 | } 343 | 344 | /// Saves the image to the given path. 345 | /// If encoding is not provided, it will attempt to infer it by the path/filename's extension 346 | /// You can try saving to a memory buffer by using the :meth:`encode` method. 347 | /// 348 | /// Parameters 349 | /// ---------- 350 | /// path: str 351 | /// The path to save the image to. 352 | /// encoding: Optional[str], default: None 353 | /// The encoding of the image, defaults to `None`. 354 | /// 355 | /// Raises 356 | /// ------ 357 | /// ValueError 358 | /// The encoding provided is invalid. 359 | /// RuntimeError 360 | /// Failed to encode the image or Failed to infer the image format. 361 | #[pyo3(text_signature = "(self, path, encoding = None)")] 362 | fn save(&self, path: PathBuf, encoding: Option<&str>) -> Result<(), Error> { 363 | if let Some(encoding) = encoding { 364 | let encoding = ImageFormat::from_extension(encoding)?; 365 | self.inner.save(encoding, path)?; 366 | } else { 367 | self.inner.save_inferred(path)?; 368 | } 369 | 370 | Ok(()) 371 | } 372 | 373 | /// Returns a 2D list representing the pixels of the image. Each list in the list is a row. 374 | /// 375 | /// For example: 376 | /// 377 | /// [[Pixel, Pixel, Pixel], [Pixel, Pixel, Pixel]] 378 | /// 379 | /// where the width of the inner list is determined by the width of the image. 380 | /// 381 | /// .. warning:: **This function involves heavy operation** 382 | /// 383 | /// This function requires multiple iterations, so it is a heavy operation for larger image. 384 | /// 385 | /// Returns 386 | /// ------- 387 | /// List[List[Union[:class:`.BitPixel`, :class:`.L`, :class:`.Rgb`, :class:`.Rgba`]]] 388 | /// The pixels of the image. 389 | fn pixels(&self, py: Python<'_>) -> Vec> { 390 | self.inner 391 | .pixels() 392 | .into_iter() 393 | .map(|p| { 394 | p.into_iter() 395 | .map(|p| cast_pixel_to_pyobject(py, p.clone())) 396 | .collect::>() 397 | }) 398 | .collect::>>() 399 | } 400 | 401 | /// Pastes the given image onto this image at the given x and y axis. 402 | /// 403 | /// If `mask` is provided it will be masked with the given masking image. 404 | /// 405 | /// Currently, only BitPixel images are supported for the masking image. 406 | /// 407 | /// Parameters 408 | /// ---------- 409 | /// x: int 410 | /// The x axis 411 | /// y: int 412 | /// The y axis 413 | /// image: :class:`Image` 414 | /// The image to paste. 415 | /// mask: Optional[:class:`Image`], default: None 416 | /// The mask to use, defaults to `None` 417 | /// 418 | /// Raises 419 | /// ------ 420 | /// ValueError 421 | /// The mask provided is not of mode `BitPixel` 422 | #[pyo3(text_signature = "(self, x, y, image, mask = None)")] 423 | fn paste(&mut self, x: u32, y: u32, image: Self, mask: Option) -> Result<(), Error> { 424 | if let Some(mask) = mask { 425 | if mask.mode() != "bitpixel" { 426 | return Err(Error::UnexpectedFormat( 427 | "bitpixel".to_string(), 428 | mask.mode().to_string(), 429 | )); 430 | } 431 | 432 | self.inner 433 | .paste_with_mask(x, y, image.inner, mask.inner.convert::()); 434 | } else { 435 | self.inner.paste(x, y, image.inner); 436 | } 437 | 438 | Ok(()) 439 | } 440 | 441 | /// Masks the alpha values of this image with the luminance values of the given single-channel L image. 442 | /// 443 | /// If you want to mask using the alpha values of the image instead of providing an L image, you can split the bands of the image and extract the alpha band. 444 | /// 445 | /// This masking image must have the same dimensions as this image. 446 | /// 447 | /// Parameters 448 | /// ---------- 449 | /// mask: :class:`Image` 450 | /// The mask to use 451 | /// 452 | /// Raises 453 | /// ------ 454 | /// ValueError 455 | /// The mask provided is not of mode `L` 456 | #[pyo3(text_signature = "(self, mask)")] 457 | fn mask_alpha(&mut self, mask: Self) -> Result<(), Error> { 458 | if mask.mode() != "L" { 459 | return Err(Error::UnexpectedFormat( 460 | "L".to_string(), 461 | mask.mode().to_string(), 462 | )); 463 | } 464 | 465 | self.inner.mask_alpha(&mask.inner.convert::()); 466 | 467 | Ok(()) 468 | } 469 | 470 | /// Mirrors, or flips this image horizontally (about the y-axis) in place. 471 | fn mirror(&mut self) { 472 | self.inner.mirror(); 473 | } 474 | 475 | /// Flips this image vertically (about the x-axis) in place. 476 | fn flip(&mut self) { 477 | self.inner.flip(); 478 | } 479 | 480 | /// str: Returns the encoding format of the image. 481 | /// 482 | /// .. note:: 483 | /// This is nothing more but metadata about the image. 484 | /// When saving the image, you will still have to explicitly specify the encoding format. 485 | #[getter] 486 | fn format(&self) -> String { 487 | format!("{}", self.inner.format()) 488 | } 489 | 490 | /// Tuple[int, int]: Returns the dimensions of the image. 491 | #[getter] 492 | fn dimensions(&self) -> (u32, u32) { 493 | self.inner.dimensions() 494 | } 495 | 496 | /// Returns the pixel at the given coordinates. 497 | /// 498 | /// Parameters 499 | /// ---------- 500 | /// x: int 501 | /// The x axis 502 | /// y: int 503 | /// The y axis 504 | /// 505 | /// Returns 506 | /// ------- 507 | /// Union[:class:`.BitPixel`, :class:`.L`, :class:`.Rgb`, :class:`.Rgba`] 508 | /// The pixel of that specific coordinate. 509 | #[pyo3(text_signature = "(self, x, y)")] 510 | fn get_pixel(&self, py: Python<'_>, x: u32, y: u32) -> PyObject { 511 | match *self.inner.pixel(x, y) { 512 | Dynamic::BitPixel(v) => BitPixel::from(v).into_py(py), 513 | Dynamic::L(v) => L::from(v).into_py(py), 514 | Dynamic::Rgb(v) => Rgb::from(v).into_py(py), 515 | Dynamic::Rgba(v) => Rgba::from(v).into_py(py), 516 | } 517 | } 518 | 519 | /// Sets the pixel at the given coordinates to the given pixel. 520 | /// 521 | /// Parameters 522 | /// --------- 523 | /// x: int 524 | /// The x axis 525 | /// y: int 526 | /// The y axis 527 | /// pixel: :class:`.Pixel` 528 | /// The pixel to set it to 529 | #[pyo3(text_signature = "(self, x, y, pixel)")] 530 | fn set_pixel(&mut self, x: u32, y: u32, pixel: Pixel) { 531 | self.inner.set_pixel(x, y, pixel.inner) 532 | } 533 | 534 | /// Inverts the image in-place. 535 | fn invert(&mut self) { 536 | self.inner.invert(); 537 | } 538 | 539 | fn __len__(&self) -> usize { 540 | self.inner.len() as usize 541 | } 542 | 543 | fn __repr__(&self) -> String { 544 | format!( 545 | "", 546 | self.mode(), 547 | self.width(), 548 | self.height(), 549 | self.format(), 550 | self.dimensions().0, 551 | self.dimensions().1 552 | ) 553 | } 554 | 555 | fn __bool__(&self) -> bool { 556 | !self.inner.is_empty() 557 | } 558 | } 559 | 560 | impl Image { 561 | fn from_inner(image: RilImage) -> Self { 562 | Self { inner: image } 563 | } 564 | } 565 | -------------------------------------------------------------------------------- /src/workaround.rs: -------------------------------------------------------------------------------- 1 | use std::ops::DerefMut; 2 | 3 | use ril::{Font, Draw, Pixel, OverlayMode, WrapStyle, Image, HorizontalAnchor, VerticalAnchor}; 4 | use fontdue::layout::{CoordinateSystem, TextStyle, Layout, LayoutSettings}; 5 | 6 | /// Represents a text segment that can be drawn. 7 | /// 8 | /// See [`TextLayout`] for a more robust implementation that supports rendering text with multiple 9 | /// styles. This type is for more simple and lightweight usages. 10 | /// 11 | /// Additionally, accessing metrics such as the width and height of the text cannot be done here, 12 | /// but can be done in [`TextLayout`] since it keeps a running copy of the layout. 13 | /// Use [`TextLayout`] if you will be needing to calculate the width and height of the text. 14 | /// Additionally, [`TextLayout`] supports text anchoring, which can be used to align text. 15 | /// 16 | /// If you need none of these features, text segments should be used in favor of being much more 17 | /// lightweight. 18 | /// 19 | /// Note that [`TextLayout`] is not cloneable while text segments are, which is one advantage 20 | /// of using this over [`TextLayout`]. 21 | #[derive(Clone)] 22 | pub struct OwnedTextSegment { 23 | /// The position the text will be rendered at. Ignored if this is used in a [`TextLayout`]. 24 | pub position: (u32, u32), 25 | /// The width of the text box. If this is used in a [`TextLayout`], this is ignored and 26 | /// [`TextLayout::with_width`] is used instead. This is used for text wrapping and wrapping only. 27 | pub width: Option, 28 | /// The content of the text segment. 29 | pub text: String, 30 | /// The font to use to render the text. 31 | pub font: Font, 32 | /// The fill color the text will be in. 33 | pub fill: P, 34 | /// The overlay mode of the text. Note that anti-aliasing is still a bit funky with 35 | /// [`OverlayMode::Replace`], so it is best to use [`OverlayMode::Merge`] for this, which is 36 | /// the default. 37 | pub overlay: OverlayMode, 38 | /// The size of the text in pixels. 39 | pub size: f32, 40 | /// The wrapping style of the text. Note that text will only wrap if [`width`] is set. 41 | /// If this is used in a [`TextLayout`], this is ignored and [`TextLayout::with_wrap`] is 42 | /// used instead. 43 | pub wrap: WrapStyle, 44 | } 45 | 46 | impl OwnedTextSegment

{ 47 | /// Creates a new text segment with the given text, font, and fill color. 48 | /// The text can be anything that implements [`AsRef`]. 49 | /// 50 | /// If this is used to be directly drawn (as opposed to in a [`TextLayout`]), the position 51 | /// is set to ``(0, 0)`` by default. Use [`with_position`][TextSegment::with_position] to set 52 | /// the position. 53 | /// 54 | /// The size defaults to the font's optimal size. 55 | /// You can override this by using the [`with_size`][Self::with_size] method. 56 | #[must_use] 57 | pub fn new(font: Font, text: impl AsRef, fill: P) -> Self { 58 | let size = font.optimal_size(); 59 | 60 | Self { 61 | position: (0, 0), 62 | width: None, 63 | text: text.as_ref().to_string(), 64 | font, 65 | fill, 66 | overlay: OverlayMode::Merge, 67 | size, 68 | wrap: WrapStyle::Word, 69 | } 70 | } 71 | 72 | /// Sets the position of the text segment. Ignored if this segment is used in a [`TextLayout`]. 73 | #[must_use] 74 | pub const fn with_position(mut self, x: u32, y: u32) -> Self { 75 | self.position = (x, y); 76 | self 77 | } 78 | 79 | /// Sets the size of the text segment. 80 | #[must_use] 81 | pub const fn with_size(mut self, size: f32) -> Self { 82 | self.size = size; 83 | self 84 | } 85 | 86 | /// Sets the overlay mode of the text segment. 87 | #[must_use] 88 | pub const fn with_overlay_mode(mut self, mode: OverlayMode) -> Self { 89 | self.overlay = mode; 90 | self 91 | } 92 | 93 | /// Sets the width of the text segment, used for text wrapping. 94 | /// If this is used in a [`TextLayout`], this is ignored and [`TextLayout::width`] is used instead. 95 | #[must_use] 96 | pub const fn with_width(mut self, width: u32) -> Self { 97 | self.width = Some(width); 98 | self 99 | } 100 | 101 | /// Sets the wrapping style of the text segment. 102 | /// If this is used in a [`TextLayout`], this is ignored and [`TextLayout::wrap`] is used instead. 103 | #[must_use] 104 | pub const fn with_wrap(mut self, wrap: WrapStyle) -> Self { 105 | self.wrap = wrap; 106 | self 107 | } 108 | 109 | fn layout(&self) -> Layout<(P, OverlayMode)> { 110 | let mut layout = Layout::new(CoordinateSystem::PositiveYDown); 111 | layout.reset(&LayoutSettings { 112 | x: self.position.0 as f32, 113 | y: self.position.1 as f32, 114 | max_width: if self.wrap == WrapStyle::None { 115 | None 116 | } else { 117 | self.width.map(|w| w as f32) 118 | }, 119 | wrap_style: match self.wrap { 120 | WrapStyle::None | WrapStyle::Word => fontdue::layout::WrapStyle::Word, 121 | WrapStyle::Character => fontdue::layout::WrapStyle::Letter, 122 | }, 123 | ..LayoutSettings::default() 124 | }); 125 | layout.append( 126 | &[self.font.inner()], 127 | &TextStyle::with_user_data(&self.text, self.size, 0, (self.fill, self.overlay)), 128 | ); 129 | layout 130 | } 131 | } 132 | 133 | fn render_layout_as_ref( 134 | image: &mut Image

, 135 | font: &fontdue::Font, 136 | layout: &Layout<(P, OverlayMode)>, 137 | ) { 138 | let glyphs = layout.glyphs(); 139 | if glyphs.is_empty() { 140 | return; 141 | } 142 | 143 | // SAFETY: already checked before calling 144 | let lines = unsafe { layout.lines().unwrap_unchecked() }; 145 | for line in lines { 146 | for glyph in &glyphs[line.glyph_start..=line.glyph_end] { 147 | let (fill, overlay) = glyph.user_data; 148 | let (metrics, bitmap) = font.rasterize_config(glyph.key); 149 | 150 | if metrics.width == 0 || glyph.char_data.is_whitespace() || metrics.height == 0 { 151 | continue; 152 | } 153 | 154 | for (row, y) in bitmap.chunks_exact(metrics.width).zip(glyph.y as i32..) { 155 | for (value, x) in row.iter().zip(glyph.x as i32..) { 156 | let (x, y) = if x < 0 || y < 0 { 157 | continue; 158 | } else { 159 | (x as u32, y as u32) 160 | }; 161 | 162 | let value = *value; 163 | if value == 0 { 164 | continue; 165 | } 166 | 167 | if let Some(pixel) = image.get_pixel(x, y) { 168 | *image.pixel_mut(x, y) = pixel.overlay_with_alpha(fill, overlay, value); 169 | } 170 | } 171 | } 172 | } 173 | } 174 | } 175 | 176 | fn render_layout( 177 | image: &mut Image

, 178 | fonts: &[&fontdue::Font], 179 | layout: &Layout<(P, OverlayMode)>, 180 | ) { 181 | let glyphs = layout.glyphs(); 182 | if glyphs.is_empty() { 183 | return; 184 | } 185 | 186 | // SAFETY: already checked before calling 187 | let lines = unsafe { layout.lines().unwrap_unchecked() }; 188 | for line in lines { 189 | for glyph in &glyphs[line.glyph_start..=line.glyph_end] { 190 | let (fill, overlay) = glyph.user_data; 191 | let font = &fonts[glyph.font_index]; 192 | let (metrics, bitmap) = font.rasterize_config(glyph.key); 193 | 194 | if metrics.width == 0 || glyph.char_data.is_whitespace() || metrics.height == 0 { 195 | continue; 196 | } 197 | 198 | for (row, y) in bitmap.chunks_exact(metrics.width).zip(glyph.y as i32..) { 199 | for (value, x) in row.iter().zip(glyph.x as i32..) { 200 | let (x, y) = if x < 0 || y < 0 { 201 | continue; 202 | } else { 203 | (x as u32, y as u32) 204 | }; 205 | 206 | let value = *value; 207 | if value == 0 { 208 | continue; 209 | } 210 | 211 | if let Some(pixel) = image.get_pixel(x, y) { 212 | *image.pixel_mut(x, y) = pixel.overlay_with_alpha(fill, overlay, value); 213 | } 214 | } 215 | } 216 | } 217 | } 218 | } 219 | 220 | fn render_layout_with_alignment( 221 | image: &mut Image

, 222 | fonts: &Vec, 223 | layout: &Layout<(P, OverlayMode)>, 224 | widths: Vec, 225 | max_width: u32, 226 | fx: f32, 227 | ox: f32, 228 | oy: f32, 229 | ) { 230 | let glyphs = layout.glyphs(); 231 | if glyphs.is_empty() { 232 | return; 233 | } 234 | 235 | // SAFETY: this was checked before calling 236 | let lines = unsafe { layout.lines().unwrap_unchecked() }; 237 | for (line, width) in lines.iter().zip(widths) { 238 | let ox = ((max_width - width) as f32).mul_add(fx, ox); 239 | 240 | for glyph in &glyphs[line.glyph_start..=line.glyph_end] { 241 | let (fill, overlay) = glyph.user_data; 242 | let font = &fonts[glyph.font_index]; 243 | let (metrics, bitmap) = font.rasterize_config(glyph.key); 244 | 245 | if metrics.width == 0 || glyph.char_data.is_whitespace() || metrics.height == 0 { 246 | continue; 247 | } 248 | 249 | let x = (glyph.x + ox) as i32; 250 | let y = (glyph.y + oy) as i32; 251 | 252 | for (row, y) in bitmap.chunks_exact(metrics.width).zip(y..) { 253 | for (value, x) in row.iter().zip(x..) { 254 | let (x, y) = if x < 0 || y < 0 { 255 | continue; 256 | } else { 257 | (x as u32, y as u32) 258 | }; 259 | 260 | let value = *value; 261 | if value == 0 { 262 | continue; 263 | } 264 | 265 | if let Some(pixel) = image.get_pixel(x, y) { 266 | *image.pixel_mut(x, y) = pixel.overlay_with_alpha(fill, overlay, value); 267 | } 268 | } 269 | } 270 | } 271 | } 272 | } 273 | 274 | impl Draw

for OwnedTextSegment

{ 275 | fn draw>>(&self, mut image: I) { 276 | render_layout_as_ref(&mut *image, self.font.inner(), &self.layout()); 277 | } 278 | } 279 | 280 | /// Represents a high-level text layout that can layout text segments, maybe with different fonts. 281 | /// 282 | /// This is a high-level layout that can be used to layout text segments. It can be used to layout 283 | /// text segments with different fonts and styles, and has many features over [`TextSegment`] such 284 | /// as text anchoring, which can be useful for text alignment. This also keeps track of font 285 | /// metrics, meaning that unlike [`TextSegment`], this can be used to determine the width and height 286 | /// of text before rendering it. 287 | /// 288 | /// This is less efficient than [`TextSegment`] and you should use [`TextSegment`] if you don't need 289 | /// any of the features [`TextLayout`] provides. 290 | /// 291 | /// # Note 292 | /// This is does not implement [`Clone`] and therefore it is not cloneable! Consider using 293 | /// [`TextSegment`] if you require cloning functionality. 294 | 295 | pub struct OwnedTextLayout { 296 | inner: Layout<(P, OverlayMode)>, 297 | fonts: Vec, 298 | pub(crate) settings: LayoutSettings, 299 | pub(crate) x_anchor: HorizontalAnchor, 300 | pub(crate) y_anchor: VerticalAnchor, 301 | } 302 | 303 | impl OwnedTextLayout

{ 304 | /// Creates a new text layout with default settings. 305 | #[must_use] 306 | pub fn new() -> Self { 307 | Self { 308 | inner: Layout::new(CoordinateSystem::PositiveYDown), 309 | fonts: Vec::new(), 310 | settings: LayoutSettings::default(), 311 | x_anchor: HorizontalAnchor::default(), 312 | y_anchor: VerticalAnchor::default(), 313 | } 314 | } 315 | 316 | pub fn set_settings(&mut self, settings: LayoutSettings) { 317 | self.inner.reset(&settings); 318 | self.settings = settings; 319 | } 320 | 321 | /// Sets the position of the text layout. 322 | /// 323 | /// **This must be set before adding any text segments!** 324 | pub fn set_position(&mut self, x: u32, y: u32) { 325 | self.set_settings(LayoutSettings { 326 | x: x as f32, 327 | y: y as f32, 328 | ..self.settings 329 | }); 330 | } 331 | 332 | /// Sets the wrapping style of the text. Make sure to also set the wrapping width using 333 | /// [`with_width`] for wrapping to work. 334 | /// 335 | /// **This must be set before adding any text segments!** 336 | pub fn set_wrap(&mut self, wrap: WrapStyle) { 337 | self.set_settings(LayoutSettings { 338 | wrap_style: match wrap { 339 | WrapStyle::None | WrapStyle::Word => fontdue::layout::WrapStyle::Word, 340 | WrapStyle::Character => fontdue::layout::WrapStyle::Letter, 341 | }, 342 | max_width: Some(self.settings.max_width.unwrap_or(f32::MAX)), 343 | ..self.settings 344 | }); 345 | } 346 | 347 | /// Sets the wrapping width of the text. This does not impact [`Self::dimensions`]. 348 | /// 349 | /// **This must be set before adding any text segments!** 350 | pub fn set_width(&mut self, width: u32) { 351 | self.set_settings(LayoutSettings { 352 | max_width: Some(width as f32), 353 | ..self.settings 354 | }); 355 | } 356 | 357 | /// Adds a text segment to the text layout. 358 | pub fn push_segment(&mut self, segment: OwnedTextSegment

) { 359 | self.fonts.push(segment.font.into_inner()); 360 | self.inner.append( 361 | &self.fonts, 362 | &TextStyle::with_user_data( 363 | &segment.text, 364 | segment.size, 365 | 0, 366 | (segment.fill, segment.overlay), 367 | ), 368 | ); 369 | } 370 | 371 | /// Takes this text layout and returns it with the given text segment added to the text layout. 372 | /// Useful for method chaining. 373 | #[must_use] 374 | pub fn with_segment(mut self, segment: OwnedTextSegment

) -> Self { 375 | self.push_segment(segment); 376 | self 377 | } 378 | 379 | /// Adds basic text to the text layout. This is a convenience method that creates a [`TextSegment`] 380 | /// with the given font, text, and fill and adds it to the text layout. 381 | /// 382 | /// The size of the text is determined by the font's optimal size. 383 | /// 384 | /// # Note 385 | /// The overlay mode is set to [`OverlayMode::Merge`] and not the image's overlay mode, since 386 | /// anti-aliasing is funky with the replace overlay mode. 387 | pub fn push_basic_text(&mut self, font: Font, text: impl AsRef, fill: P) { 388 | self.push_segment(OwnedTextSegment::new(font, text, fill)); 389 | } 390 | 391 | /// Takes this text layout and returns it with the given basic text added to the text layout. 392 | /// Useful for method chaining. 393 | /// 394 | /// # Note 395 | /// The overlay mode is set to [`OverlayMode::Merge`] and not the image's overlay mode, since 396 | /// anti-aliasing is funky with the replace overlay mode. 397 | /// 398 | /// # See Also 399 | /// * [`push_basic_text`][TextLayout::push_basic_text] 400 | #[must_use] 401 | pub fn with_basic_text(mut self, font: Font, text: impl AsRef, fill: P) -> Self { 402 | self.push_basic_text(font, text, fill); 403 | self 404 | } 405 | 406 | /// Sets the horizontal anchor of the text. The horizontal anchor determines where the x 407 | /// position of the text is anchored. 408 | #[must_use] 409 | pub const fn with_horizontal_anchor(mut self, anchor: HorizontalAnchor) -> Self { 410 | self.x_anchor = anchor; 411 | self 412 | } 413 | 414 | /// Sets the vertical anchor of the text. The vertical anchor determines where the y position of 415 | /// the text is anchored. 416 | #[must_use] 417 | pub const fn with_vertical_anchor(mut self, anchor: VerticalAnchor) -> Self { 418 | self.y_anchor = anchor; 419 | self 420 | } 421 | 422 | /// Sets the horizontal anchor and vertial anchor of the text to be centered. This makes the 423 | /// position of the text be the center as opposed to the top-left corner. 424 | pub fn centered(&mut self) { 425 | self.x_anchor = HorizontalAnchor::Center; 426 | self.y_anchor = VerticalAnchor::Center; 427 | } 428 | 429 | fn line_widths(&self) -> (Vec, u32, u32) { 430 | let glyphs = self.inner.glyphs(); 431 | if glyphs.is_empty() { 432 | return (Vec::new(), 0, 0); 433 | } 434 | 435 | let mut widths = Vec::new(); 436 | let mut max_width = 0; 437 | 438 | // SAFETY: checking glyphs.is_empty() above means that glyphs is not empty. 439 | for line in unsafe { self.inner.lines().unwrap_unchecked() } { 440 | let x = self.settings.x as u32; 441 | 442 | let glyph = &glyphs[line.glyph_end]; 443 | let right = glyph.x + glyph.width as f32; 444 | let line_width = (right - x as f32).ceil() as u32; 445 | widths.push(line_width); 446 | max_width = Ord::max(max_width, line_width); 447 | } 448 | 449 | (widths, max_width, self.inner.height() as u32) 450 | } 451 | 452 | /// Returns the width and height of the text. This is a slightly expensive operation and should 453 | /// be used sparingly - it is not a simple getter. 454 | #[must_use] 455 | pub fn dimensions(&self) -> (u32, u32) { 456 | let glyphs = self.inner.glyphs(); 457 | if glyphs.is_empty() { 458 | return (0, 0); 459 | } 460 | 461 | let mut width = 0; 462 | 463 | // SAFETY: checking glyphs.is_empty() above means that glyphs is not empty. 464 | for line in unsafe { self.inner.lines().unwrap_unchecked() } { 465 | let x = self.settings.x as u32; 466 | 467 | for glyph in glyphs[line.glyph_start..=line.glyph_end].iter().rev() { 468 | if glyph.char_data.is_whitespace() { 469 | continue; 470 | } 471 | 472 | let right = glyph.x + glyph.width as f32; 473 | let line_width = (right - x as f32).ceil() as u32; 474 | width = Ord::max(width, line_width); 475 | 476 | break; 477 | } 478 | } 479 | 480 | (width, self.inner.height() as u32) 481 | } 482 | 483 | /// Returns the width of the text. This is a slightly expensive operation and is not a simple 484 | /// getter. 485 | /// 486 | /// If you want both width and height, use [`dimensions`][TextLayout::dimensions]. 487 | #[must_use] 488 | pub fn width(&self) -> u32 { 489 | self.dimensions().0 490 | } 491 | 492 | /// Returns the height of the text. This is a slightly expensive operation and is not a simple 493 | /// getter. 494 | /// 495 | /// If you want both width and height, use [`dimensions`][TextLayout::dimensions]. 496 | #[must_use] 497 | pub fn height(&self) -> u32 { 498 | self.dimensions().1 499 | } 500 | 501 | /// Returns the bounding box of the text. Left and top bounds are inclusive; right and bottom 502 | /// bounds are exclusive. 503 | #[must_use] 504 | pub fn bounding_box(&self) -> (u32, u32, u32, u32) { 505 | let (width, height) = self.dimensions(); 506 | 507 | let ox = match self.x_anchor { 508 | HorizontalAnchor::Left => 0.0, 509 | HorizontalAnchor::Center => width as f32 / -2.0, 510 | HorizontalAnchor::Right => -(width as f32), 511 | }; 512 | let oy = match self.y_anchor { 513 | VerticalAnchor::Top => 0.0, 514 | VerticalAnchor::Center => height as f32 / -2.0, 515 | VerticalAnchor::Bottom => -(height as f32), 516 | }; 517 | 518 | let x = (self.settings.x + ox) as u32; 519 | let y = (self.settings.y + oy) as u32; 520 | 521 | (x, y, x + width, y + height) 522 | } 523 | 524 | fn calculate_offsets(&self) -> (Vec, u32, f32, f32, f32) { 525 | let (widths, width, height) = self.line_widths(); 526 | 527 | let (ox, fx) = match self.x_anchor { 528 | HorizontalAnchor::Left => (0.0, 0.0), 529 | HorizontalAnchor::Center => (width as f32 / -2.0, 0.5), 530 | HorizontalAnchor::Right => (-(width as f32), 1.0), 531 | }; 532 | let oy = match self.y_anchor { 533 | VerticalAnchor::Top => 0.0, 534 | VerticalAnchor::Center => height as f32 / -2.0, 535 | VerticalAnchor::Bottom => -(height as f32), 536 | }; 537 | 538 | (widths, width, fx, ox, oy) 539 | } 540 | } 541 | 542 | impl Draw

for OwnedTextLayout

{ 543 | fn draw>>(&self, mut image: I) { 544 | let image = &mut *image; 545 | 546 | // Skips the calculation of offsets 547 | if self.x_anchor == HorizontalAnchor::Left && self.y_anchor == VerticalAnchor::Top { 548 | render_layout(image, &self.fonts.iter().collect::>()[..], &self.inner); 549 | } 550 | 551 | let (widths, max_width, fx, ox, oy) = self.calculate_offsets(); 552 | render_layout_with_alignment( 553 | image, 554 | &self.fonts, 555 | &self.inner, 556 | widths, 557 | max_width, 558 | fx, 559 | ox, 560 | oy, 561 | ); 562 | } 563 | } 564 | 565 | impl Default for OwnedTextLayout { 566 | fn default() -> Self { 567 | Self::new() 568 | } 569 | } 570 | -------------------------------------------------------------------------------- /ril.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Iterator, List, Optional, Tuple, Type, TypeAlias, Union 4 | 5 | Pixels: TypeAlias = Union[BitPixel, L, Rgb, Rgba] 6 | Xy: TypeAlias = Tuple[int, int] 7 | 8 | 9 | class Image: 10 | """ 11 | A high-level image representation. 12 | 13 | This represents a static, single-frame image. See :class:`.ImageSequence` for information on opening animated or multi-frame images. 14 | """ 15 | @classmethod 16 | def new(cls: Type[Image], width: int, height: int, fill: Pixel) -> Image: 17 | """ 18 | Creates a new image with the given width and height, with all pixels being set intially to `fill`. 19 | 20 | Parameters 21 | ---------- 22 | width: int 23 | The width of the Image. 24 | height: int 25 | The height of the Image. 26 | fill: :class:`.Pixel` 27 | The pixel used to fill the image. 28 | 29 | Examples 30 | -------- 31 | 32 | .. code-block:: python3 33 | 34 | Image.new(100, 100, Pixel.from_rgb(255, 255, 255)) 35 | """ 36 | 37 | @classmethod 38 | def from_bytes(cls: Type[Image], bytes: bytes, format: str | None = None) -> Image: 39 | """ 40 | Decodes an image with the explicitly given image encoding from the raw bytes. 41 | 42 | if `format` is not provided then it will try to infer its encoding. 43 | 44 | Parameters 45 | ---------- 46 | bytes: bytes 47 | The bytes of the Image. 48 | format: Optional[str], default: None 49 | The format of the image, defaults to `None`. 50 | 51 | Raises 52 | ------ 53 | ValueError 54 | Raised if the format provided is invalid. 55 | RuntimeError 56 | Raised if the image can't be decoded or the format is unknown. 57 | """ 58 | 59 | @classmethod 60 | def from_pixels(cls: Type[Image], width: int, pixels: List[Pixel]) -> Image: 61 | """ 62 | Creates a new image shaped with the given width 63 | and a 1-dimensional sequence of pixels which will be shaped according to the width. 64 | 65 | Parameters 66 | ---------- 67 | width: int 68 | The width of the image. 69 | pixels: List[:class:`.Pixel`] 70 | A List of pixels. 71 | """ 72 | 73 | @classmethod 74 | def open(cls: Type[Image], path: str) -> Image: 75 | """ 76 | Opens a file from the given path and decodes it into an image. 77 | 78 | The encoding of the image is automatically inferred. 79 | You can explicitly pass in an encoding by using the :meth:`from_bytes` method. 80 | 81 | Parameters 82 | ---------- 83 | path: str 84 | The path to the image. 85 | 86 | Raises 87 | ------ 88 | ValueError 89 | The file extension is invalid. 90 | RuntimeError 91 | Failed to infer file format or Failed to decode image. 92 | """ 93 | 94 | @property 95 | def overlay_mode(self) -> OverlayMode: 96 | """:class:`.OverlayMode`: Returns the overlay mode of the image.""" 97 | 98 | @property 99 | def mode(self) -> str: 100 | """str: Returns the mode of the image.""" 101 | 102 | @property 103 | def width(self) -> int: 104 | """int: Returns the width of the image.""" 105 | 106 | @property 107 | def height(self) -> int: 108 | """int: Returns the height of the image.""" 109 | 110 | def bands(self) -> Pixels: 111 | """ 112 | Return the bands of the image. 113 | 114 | Returns 115 | ------- 116 | Tuple[:class:`.L`, ...] 117 | 118 | Raises 119 | ------ 120 | TypeError 121 | The image is not of mode `RGB` or `RGBA`. 122 | """ 123 | 124 | @classmethod 125 | def from_bands(cls: Type[Image], *bands: Union[Tuple[Rgb, ...], Tuple[Rgba, ...]]) -> Image: 126 | """ 127 | Creates a new image from the given bands. 128 | 129 | Parameters 130 | ---------- 131 | bands: \\* :class:`.L` 132 | The bands of the image. 133 | """ 134 | 135 | def crop(self, x1: int, y1: int, x2: int, y2: int) -> None: 136 | """ 137 | Crops this image in place to the given bounding box. 138 | 139 | Parameters 140 | ---------- 141 | x1: int 142 | The x axis of the upper-left corner 143 | y1: int 144 | The y axis of the upper-left corner 145 | x2: int 146 | The x axis of the lower-right corner 147 | y2: int 148 | The y axis of the lower-right corner 149 | """ 150 | 151 | def draw(self, entity: Union[Rectangle, Ellipse]) -> None: 152 | """ 153 | Draws an object or shape onto this image. 154 | 155 | Parameters 156 | ---------- 157 | entity: Union[:class:`.Rectangle`, :class:`.Ellipse`] 158 | The entity to draw on the image. 159 | """ 160 | 161 | def resize(self, width: int, height: int, algorithm: ResizeAlgorithm) -> None: 162 | """ 163 | Resizes this image in place to the given dimensions using the given resizing algorithm in place. 164 | 165 | Parameters 166 | ---------- 167 | width: int 168 | The target width to resize to 169 | height: int 170 | The target height to resize to 171 | algorithm: :class:`.ResizeAlgorithm` 172 | The resize algorithm to use 173 | """ 174 | 175 | def encode(self, encoding: str) -> bytes: 176 | """ 177 | Encodes the image with the given encoding and returns `bytes`. 178 | 179 | Parameters 180 | ---------- 181 | encoding: str 182 | The encoding of the image. 183 | 184 | Returns 185 | ------- 186 | bytes 187 | The encoded bytes of the image. 188 | 189 | Raises 190 | ------ 191 | ValueError 192 | The encoding is invalid. 193 | RuntimeError 194 | Failed to encode the image. 195 | """ 196 | 197 | def save(self, path: str, encoding: Optional[str] = None) -> None: 198 | """ 199 | Saves the image to the given path. 200 | If encoding is not provided, it will attempt to infer it by the path/filename's extension 201 | You can try saving to a memory buffer by using the :meth:`encode` method. 202 | 203 | Parameters 204 | ---------- 205 | path: str 206 | The path to save the image to. 207 | encoding: Optional[str], default: None 208 | The encoding of the image, defaults to `None`. 209 | 210 | Raises 211 | ------ 212 | ValueError 213 | The encoding provided is invalid. 214 | RuntimeError 215 | Failed to encode the image or Failed to infer the image format. 216 | """ 217 | 218 | def pixels(self) -> List[List[Pixels]]: 219 | """ 220 | Returns a 2D list representing the pixels of the image. Each list in the list is a row. 221 | 222 | For example: 223 | 224 | [[Pixel, Pixel, Pixel], [Pixel, Pixel, Pixel]] 225 | 226 | where the width of the inner list is determined by the width of the image. 227 | 228 | .. warning:: **This function involves heavy operation** 229 | 230 | This function requires multiple iterations, so it is a heavy operation for larger image. 231 | 232 | Returns 233 | ------- 234 | List[List[Union[:class:`.BitPixel`, :class:`.L`, :class:`.Rgb`, :class:`.Rgba`]]] 235 | The pixels of the image. 236 | """ 237 | 238 | def paste(self, x: int, y: int, image: Image, mask: Optional[Image]) -> None: 239 | """ 240 | Pastes the given image onto this image at the given x and y axiss. 241 | 242 | If `maske` is provided it will be masked with the given masking image. 243 | 244 | Currently, only BitPixel images are supported for the masking image. 245 | 246 | Parameters 247 | ---------- 248 | x: int 249 | The x axis 250 | y: int 251 | The y axis 252 | image: :class:`Image` 253 | The image to paste. 254 | mask: Optional[:class:`Image`], default: None 255 | The mask to use, defaults to `None` 256 | 257 | Raises 258 | ------ 259 | ValueError 260 | The mask provided is not of mode `BitPixel` 261 | """ 262 | 263 | def mask_alpha(self, mask: Image) -> None: 264 | """ 265 | Masks the alpha values of this image with the luminance values of the given single-channel L image. 266 | 267 | If you want to mask using the alpha values of the image instead of providing an L image, you can split the bands of the image and extract the alpha band. 268 | 269 | This masking image must have the same dimensions as this image. 270 | 271 | Parameters 272 | ---------- 273 | mask: :class:`Image` 274 | The mask to use 275 | 276 | Raises 277 | ------ 278 | ValueError 279 | The mask provided is not of mode `L` 280 | """ 281 | 282 | def mirror(self) -> None: 283 | """Mirrors, or flips this image horizontally (about the y-axis) in place.""" 284 | 285 | def flip(self) -> None: 286 | """Flips this image vertically (about the x-axis) in place.""" 287 | 288 | @property 289 | def format(self) -> str: 290 | """ 291 | str: Returns the encoding format of the image. 292 | 293 | .. note:: 294 | This is nothing more but metadata about the image. 295 | When saving the image, you will still have to explicitly specify the encoding format. 296 | """ 297 | 298 | @property 299 | def dimensions(self) -> Tuple[int, int]: 300 | """Tuple[int, int]: Returns the dimensions of the image.""" 301 | 302 | def get_pixel(self, x: int, y: int) -> Pixels: 303 | """ 304 | Returns the pixel at the given coordinates. 305 | 306 | Parameters 307 | ---------- 308 | x: int 309 | The x axis 310 | y: int 311 | The y axis 312 | 313 | Returns 314 | ------- 315 | Union[:class:`.BitPixel`, :class:`.L`, :class:`.Rgb`, :class:`.Rgba`] 316 | The pixel of that specific coordinate. 317 | """ 318 | 319 | def set_pixel(self, x: int, y: int, pixel: Pixel) -> None: 320 | """ 321 | Sets the pixel at the given coordinates to the given pixel. 322 | 323 | Parameters 324 | --------- 325 | x: int 326 | The x axis 327 | y: int 328 | The y axis 329 | pixel: :class:`.Pixel` 330 | The pixel to set it to 331 | """ 332 | 333 | def invert(self) -> None: 334 | """Inverts the image in-place.""" 335 | 336 | 337 | class Border: 338 | """ 339 | Represents a shape border. 340 | """ 341 | color: Pixel 342 | thickness: int 343 | position: str 344 | 345 | def __init__(self, color: Pixel, thickness: int, position: str) -> None: 346 | """ 347 | Parameters 348 | ---------- 349 | color: :class:`.Pixel` 350 | The color of the border 351 | thickness: int 352 | The thickness of the border 353 | position: str 354 | The position of the border 355 | 356 | Raises 357 | ------ 358 | ValueError 359 | The position is not one of `inset`, `center`, or `outset` 360 | """ 361 | 362 | 363 | class Ellipse: 364 | """ 365 | An ellipse, which could be a circle. 366 | 367 | .. warning:: 368 | Using any of the predefined constructors will automatically set the position to (0, 0) and you must explicitly set the size of the ellipse with `.size` in order to set a size for the ellipse. 369 | A size must be set before drawing. 370 | 371 | This also does not set any border or fill for the ellipse, you must explicitly set either one of them. 372 | """ 373 | position: Xy 374 | radii: Xy 375 | border: Optional[Border] 376 | fill: Optional[Pixel] 377 | overlay: Optional[OverlayMode] 378 | 379 | def __init__( 380 | self, 381 | position: Xy, 382 | radii: Xy, 383 | border: Optional[Border] = None, 384 | fill: Optional[Pixel] = None, 385 | overlay: Optional[str] = None 386 | ) -> None: 387 | """ 388 | Parameters 389 | --------- 390 | position: Tuple[int, int] 391 | The position of the ellipse 392 | radii: Tuple[int, int] 393 | The radii of the ellipse 394 | border: Optional[:class:`.Border`] 395 | The border of the ellipse. 396 | fill: Optional[:class:`.Pixel`] 397 | The color to use for filling the ellipse 398 | overlay: Optional[:class:`.OverlayMode`] 399 | The overlay mode of the ellipse. 400 | """ 401 | 402 | @classmethod 403 | def from_bounding_box(cls, x1: int, y1: int, x2: int, y2: int) -> Ellipse: 404 | """ 405 | Creates a new ellipse from the given bounding box. 406 | 407 | Parameters 408 | ---------- 409 | x1: int 410 | The x axis of the upper left corner 411 | y1: int 412 | The y axis of the upper left corner 413 | x2: int 414 | The x axis of the lower right corner 415 | y2: int 416 | The y axis of the lower right corner 417 | 418 | Returns 419 | ------- 420 | :class:`.Ellipse` 421 | The newly created ellipse 422 | """ 423 | 424 | @classmethod 425 | def circle(cls, x: int, y: int, radius: int) -> Ellipse: 426 | """ 427 | Creates a new circle with the given center position and radius. 428 | 429 | Parameters 430 | ---------- 431 | x: int 432 | The x axis 433 | y: int 434 | The y axis 435 | radius: int 436 | The radius 437 | """ 438 | 439 | 440 | class Rectangle: 441 | """ 442 | A rectangle. 443 | 444 | .. warning:: 445 | Using any of the predefined construction methods will automatically set the position to (0, 0). 446 | If you want to specify a different position, you must set the position with `.position` 447 | 448 | You must specify a width and height for the rectangle with something such as with_size. 449 | If you don't, a panic will be raised during drawing. 450 | You can also try using from_bounding_box to create a rectangle from a bounding box, which automatically fills in the size. 451 | 452 | Additionally, a panic will be raised during drawing if you do not specify either a fill color or a border. 453 | these can be set with `.fill` and `.border` respectively. 454 | """ 455 | position: Xy 456 | size: Xy 457 | border: Optional[Border] 458 | fill: Optional[Pixel] 459 | overlay: Optional[OverlayMode] 460 | 461 | def __init__( 462 | self, 463 | position: Xy, 464 | size: Xy, 465 | border: Optional[Border] = None, 466 | fill: Optional[Pixel] = None, 467 | overlay: Optional[OverlayMode] = None 468 | ) -> None: 469 | """ 470 | Parameters 471 | ---------- 472 | position: Tuple[int, int] 473 | The position of the rectangle 474 | size: Tuple[int, int] 475 | The size of the rectangle 476 | border: Optional[:class:`.Border`] 477 | The border of the ellipse. 478 | fill: Optional[:class:`.Pixel`] 479 | The color to use for filling the rectangle 480 | overlay: Optional[:class:`.OverlayMode`] 481 | The overlay mode of the rectangle. 482 | """ 483 | 484 | @classmethod 485 | def from_bounding_box(cls, x1: int, y1: int, x2: int, y2: int) -> Rectangle: 486 | """ 487 | Creates a new ellipse from the given bounding box. 488 | 489 | Parameters 490 | ---------- 491 | x1: int 492 | The x axis of the upper left corner 493 | y1: int 494 | The y axis of the upper left corner 495 | x2: int 496 | The x axis of the lower right corner 497 | y2: int 498 | The y axis of the lower right corner 499 | 500 | Returns 501 | ------- 502 | :class:`.Rectangle` 503 | The newly created rectangle 504 | """ 505 | 506 | 507 | class BitPixel: 508 | """Represents a single-bit pixel that represents either a pixel that is on or off.""" 509 | value: bool 510 | 511 | def __init__(self, value: bool) -> None: ... 512 | 513 | 514 | class L: 515 | """ 516 | Represents an L, or luminance pixel that is stored as only one single number representing how bright, or intense, the pixel is. 517 | 518 | This can be thought of as the “unit channel” as this represents only a single channel in which other pixel types can be composed of. 519 | """ 520 | value: int 521 | 522 | def __init__(self, value: int) -> None: ... 523 | 524 | 525 | class Rgb: 526 | """Represents an RGB pixel.""" 527 | r: int 528 | g: int 529 | b: int 530 | 531 | def __init__(self, r: int, g: int, b: int) -> None: ... 532 | 533 | 534 | class Rgba: 535 | """Represents an RGBA pixel.""" 536 | r: int 537 | g: int 538 | b: int 539 | a: int 540 | 541 | def __init__(self, r: int, g: int, b: int, a: int) -> None: ... 542 | 543 | 544 | 545 | class Pixel: 546 | """The user created Pixel type.""" 547 | @classmethod 548 | def from_bitpixel(cls, value: bool) -> Pixel: 549 | """ 550 | Create a bitpixel. 551 | 552 | Parameters 553 | ---------- 554 | value: bool 555 | Whether the pixel is on. 556 | """ 557 | 558 | @classmethod 559 | def from_l(cls, value: int) -> Pixel: 560 | """ 561 | Create a L Pixel. 562 | 563 | Parameters 564 | ---------- 565 | value: int 566 | The luminance value of the pixel, between 0 and 255. 567 | """ 568 | 569 | @classmethod 570 | def from_rgb(cls, r: int, g: int, b: int) -> Pixel: 571 | """ 572 | Creates a Rgb Pixel 573 | 574 | Parameters 575 | ---------- 576 | r: int 577 | The red component of the pixel. 578 | g: int 579 | The green component of the pixel. 580 | b: int 581 | The blue component of the pixel. 582 | """ 583 | 584 | @classmethod 585 | def from_rgba(cls, r: int, g: int, b: int, a: int) -> Pixel: 586 | """ 587 | Creates a Rgba Pixel 588 | 589 | Parameters 590 | ---------- 591 | r: int 592 | The red component of the pixel. 593 | g: int 594 | The green component of the pixel. 595 | b: int 596 | The blue component of the pixel. 597 | a: int 598 | The alpha component of the pixel. 599 | """ 600 | 601 | 602 | class Frame: 603 | """Represents a frame in an image sequence. It encloses :class:`.Image` and extra metadata about the frame.""" 604 | def __init__(self, image: Image) -> None: 605 | """ 606 | Parameters 607 | ---------- 608 | image: :class:`.Image` 609 | The image used for this frame. 610 | """ 611 | 612 | @property 613 | def delay(self) -> int: 614 | """int: Returns the delay duration for this frame.""" 615 | 616 | @property 617 | def dimensions(self) -> Xy: 618 | """Tuple[int, int]: Returns the dimensions of this frame.""" 619 | 620 | @property 621 | def disposal(self) -> DisposalMethod: 622 | """:class:`.DisposalMethod`: Returns the disposal method for this frame.""" 623 | 624 | @property 625 | def image(self) -> Image: 626 | """:class:`.Image`: Returns the image this frame contains.""" 627 | 628 | @delay.setter 629 | def set_delay(self, delay: int) -> None: ... 630 | 631 | 632 | class ImageSequence(Iterator[Frame]): 633 | """ 634 | Represents a sequence of image frames such as an animated image. 635 | 636 | See :class:`.Image` for the static image counterpart, and see :class:`.Frame` to see how each frame is represented in an image sequence. 637 | 638 | The iterator is exhausive, so when you iterate through :class:`.ImageSequence` like 639 | 640 | .. code-block:: python3 641 | 642 | seq = ImageSequence.from_bytes(bytes) 643 | list(seq) # [...] 644 | # But if you do it again 645 | list(seq) # [] 646 | # It will return a empty list 647 | 648 | .. note:: 649 | Any change made to the :class:`.Frame` will not be reflected to the :class:`.ImageSequence`, so you must create a new :class:`.ImageSequence` after you make changes to the frames. 650 | """ 651 | @classmethod 652 | def from_bytes(cls, bytes: bytes, format: Optional[str] = None) -> ImageSequence: 653 | """ 654 | Decodes a sequence with the explicitly given image encoding from the raw bytes. 655 | 656 | if `format` is not provided then it will try to infer its encoding. 657 | 658 | Parameters 659 | ---------- 660 | bytes: bytes 661 | The bytes of the image. 662 | format: Optional[str], default: None 663 | The format of the image. 664 | 665 | Raises 666 | ------ 667 | ValueError 668 | The format provided is invalid. 669 | RuntimeError 670 | Failed to decode the image or Failed to infer the image's format. 671 | """ 672 | 673 | @classmethod 674 | def from_frames(cls, frames: List[Frame]) -> ImageSequence: 675 | """ 676 | Creates a new image sequence from the given frames 677 | 678 | Parameters 679 | ---------- 680 | frames: List[:class:`Frame`] 681 | The list of frames to create the sequence from 682 | """ 683 | 684 | @classmethod 685 | def open(cls, path: str) -> ImageSequence: 686 | """ 687 | Opens a file from the given path and decodes it into an :class:`.ImageSequence`. 688 | 689 | The encoding of the image is automatically inferred. 690 | You can explicitly pass in an encoding by using the :meth:`from_bytes` method. 691 | 692 | Parameters 693 | ---------- 694 | path: str 695 | The path to the image. 696 | 697 | Raises 698 | ------ 699 | ValueError 700 | The file extension is invalid. 701 | RuntimeError 702 | Failed to infer file format or Failed to decode image. 703 | """ 704 | 705 | def encode(self, encoding: str) -> bytes: 706 | """ 707 | Encodes the image with the given encoding and returns `bytes`. 708 | 709 | Parameters 710 | ---------- 711 | encoding: str 712 | The encoding to encode to. 713 | 714 | Returns 715 | ------- 716 | bytes 717 | The encoded bytes. 718 | """ 719 | 720 | def save(self, path: str, encoding: Optional[str] = None) -> None: 721 | """ 722 | Saves the image to the given path. 723 | If encoding is not provided, it will attempt to infer it by the path/filename's extension 724 | You can try saving to a memory buffer by using the :meth:`encode` method. 725 | 726 | Parameters 727 | ---------- 728 | path: str 729 | The path to the image. 730 | 731 | Raises 732 | ------ 733 | ValueError 734 | The file extension is invalid. 735 | RuntimeError 736 | Failed to infer file format or Failed to decode image. 737 | """ 738 | 739 | def __iter__(self) -> ImageSequence: ... 740 | 741 | def __next__(self) -> Frame: ... 742 | 743 | 744 | class TextSegment: 745 | """ 746 | Represents a text segment that can be drawn. 747 | 748 | See :class:`TextLayout` for a more robust implementation that supports rendering text with multiple styles. 749 | This type is for more simple and lightweight usages. 750 | 751 | Additionally, accessing metrics such as the width and height of the text cannot be done here, 752 | but can be done in TextLayout since it keeps a running copy of the layout. 753 | Use TextLayout if you will be needing to calculate the width and height of the text. 754 | Additionally, TextLayout supports text anchoring, which can be used to align text. 755 | 756 | If you need none of these features, :class:`TextSegment` should be used in favor of being much more lightweight. 757 | """ 758 | def __init__( 759 | self, 760 | font: Font, 761 | text: str, 762 | fill: Pixel, 763 | position: Optional[Tuple[int, int]] = None, 764 | size: Optional[float] = None, 765 | overlay: Optional[OverlayMode] = None, 766 | width: Optional[int] = None, 767 | wrap: Optional[WrapStyle] = None, 768 | ) -> None: 769 | """ 770 | Parameters 771 | ---------- 772 | font: :class:`Font` 773 | The font to use to render the text. 774 | text: str 775 | The text to render. 776 | fill: :class:`Pixel` 777 | The fill color the text will be in. 778 | position: Optional[Tuple[int, int]] 779 | The position the text will be rendered at. 780 | 781 | **This must be set before adding any text segments!** 782 | 783 | Either with :attr:`position` or by passing it to the constructor. 784 | size: Optional[float] 785 | The size of the text in pixels. 786 | overlay: Optional[:class:`OverlayMode`] 787 | The overlay mode to use when rendering the text. 788 | width: Optional[int] 789 | The width of the text layout. 790 | wrap: Optional[:class:`WrapStyle`] 791 | The wrapping style of the text. Note that text will only wrap if `width` is set. 792 | If this is used in a :class:`TextLayout`, this is ignored and :attr:`.WrapStyle.Wrap` is used instead. 793 | 794 | .. warning:: 795 | As this class contains the data of an entire font, copying this class is expensive. 796 | """ 797 | 798 | @property 799 | def position(self) -> Tuple[int, int]: 800 | """Tuple[int, int]: The position of the text segment.""" 801 | 802 | @property 803 | def width(self) -> Optional[int]: 804 | """ 805 | float: The width of the text box. 806 | 807 | .. warning:: 808 | If this is used in a :class:`TextLayout`, this is ignored and :meth:`TextLayout.width` is used instead. 809 | """ 810 | 811 | @property 812 | def text(self) -> str: 813 | """str: The content of the text segment.""" 814 | 815 | @property 816 | def font(self) -> Font: 817 | """ 818 | :class:`Font`: The font of the text segment. 819 | 820 | .. warning:: 821 | Due to design limitation, accessing font requires a deep clone each time, which is expensive. 822 | """ 823 | 824 | @property 825 | def fill(self) -> Pixels: 826 | """List[List[Union[:class:`.BitPixel`, :class:`.L`, :class:`.Rgb`, :class:`.Rgba`]]]: The fill color of the text segment.""" 827 | 828 | @property 829 | def overlay(self) -> OverlayMode: 830 | """Optional[:class:`OverlayMode`]: The overlay mode of the text segment.""" 831 | 832 | @property 833 | def size(self) -> float: 834 | """float: The size of the text segment in pixels.""" 835 | 836 | @property 837 | def wrap(self) -> WrapStyle: 838 | """:class:`WrapStyle`: The wrapping style of the text segment.""" 839 | 840 | @position.setter 841 | def set_position(self, position: Tuple[int, int]) -> None: 842 | ... 843 | 844 | @width.setter 845 | def set_width(self, width: int) -> None: 846 | ... 847 | 848 | @text.setter 849 | def set_text(self, text: str) -> None: 850 | ... 851 | 852 | @font.setter 853 | def set_font(self, font: Font) -> None: 854 | ... 855 | 856 | @fill.setter 857 | def set_fill(self, fill: Pixel) -> None: 858 | ... 859 | 860 | @overlay.setter 861 | def set_overlay(self, overlay: OverlayMode) -> None: 862 | ... 863 | 864 | @size.setter 865 | def set_size(self, size: float) -> None: 866 | ... 867 | 868 | @wrap.setter 869 | def set_wrap(self, wrap: WrapStyle) -> None: 870 | ... 871 | 872 | 873 | class TextLayout: 874 | """ 875 | Represents a high-level text layout that can layout text segments, maybe with different fonts. 876 | 877 | This is a high-level layout that can be used to layout text segments. 878 | It can be used to layout text segments with different fonts and styles, and has many features over :class:`TextSegment` such as text anchoring, 879 | which can be useful for text alignment. 880 | This also keeps track of font metrics, meaning that unlike :class:`TextSegment`, 881 | this can be used to determine the width and height of text before rendering it. 882 | 883 | This is less efficient than :class:`TextSegment` and you should use :class:`TextSegment` if you don't need any of the features TextLayout provides. 884 | """ 885 | def __init__( 886 | self, 887 | position: Optional[Tuple[int, int]] = None, 888 | width: Optional[int] = None, 889 | height: Optional[int] = None, 890 | horizontal_anchor: Optional[HorizontalAnchor] = None, 891 | vertical_anchor: Optional[VerticalAnchor] = None, 892 | wrap: Optional[WrapStyle] = None, 893 | ) -> None: 894 | """ 895 | Parameters 896 | ---------- 897 | position: Optional[Tuple[int, int]] 898 | The position the text will be rendered at. 899 | 900 | **This must be set before adding any text segments!** 901 | 902 | Either with :attr:`position` or by passing it to the constructor. 903 | 904 | horizontal_anchor: Optional[:class:`.HorizontalAnchor`] 905 | The horizontal anchor of the text. 906 | 907 | vertical_anchor: Optional[:class:`.VerticalAnchor`] 908 | The vertical anchor of the text. 909 | 910 | wrap: Optional[:class:`.WrapStyle`] 911 | Sets the wrapping style of the text. Make sure to also set the wrapping width using :attr:`width` for wrapping to work. 912 | 913 | **This must be set before adding any text segments!** 914 | 915 | .. warning:: 916 | As this class contains the data of one or more font(s), copying this class can be extremely expensive. 917 | """ 918 | 919 | def center(self) -> None: 920 | """ 921 | Sets the horizontal anchor and vertial anchor of the text to be centered. 922 | This makes the position of the text be the center as opposed to the top-left corner. 923 | """ 924 | 925 | @property 926 | def bounding_box(self) -> Tuple[int, int, int, int]: 927 | """ 928 | Tuple[int, int, int, int]: Returns the bounding box of the text. 929 | Left and top bounds are inclusive; right and bottom bounds are exclusive. 930 | """ 931 | 932 | @property 933 | def dimensions(self) -> Tuple[int, int]: 934 | """ 935 | Tuple[int, int]: Returns the width and height of the text. 936 | 937 | .. warning:: 938 | This is a slightly expensive operation and is not a simple getter. 939 | 940 | .. note:: 941 | If you want both width and height, use :attr:`dimensions`. 942 | """ 943 | 944 | @property 945 | def height(self) -> int: 946 | """ 947 | int: Returns the height of the text. 948 | 949 | .. warning:: 950 | This is a slightly expensive operation and is not a simple getter. 951 | 952 | .. note:: 953 | If you want both width and height, use :attr:`dimensions`. 954 | """ 955 | 956 | @property 957 | def width(self) -> int: 958 | """ 959 | int: Returns the width of the text. 960 | 961 | .. warning:: 962 | This is a slightly expensive operation and is not a simple getter. 963 | 964 | .. note:: 965 | If you want both width and height, use :attr:`dimensions`. 966 | """ 967 | 968 | 969 | class Font: 970 | """ 971 | Represents a single font along with its alternatives used to render text. Currently, this supports TrueType and OpenType fonts. 972 | """ 973 | @classmethod 974 | def open(cls, path: str, optimal_size: float) -> Font: 975 | """ 976 | Opens the font from the given path. 977 | 978 | .. note:: 979 | The optimal size is not the fixed size of the font - rather it is the size to optimize rasterizing the font for. 980 | 981 | Lower sizes will look worse but perform faster, while higher sizes will look better but perform slower. 982 | It is best to set this to the size that will likely be the most use 983 | 984 | Parameters 985 | ---------- 986 | path: str 987 | The path of the font. 988 | optimal_size: float 989 | The optimal size of the font. 990 | 991 | Raises 992 | ------ 993 | IOError 994 | Fails to read the font file. 995 | RuntimeError 996 | Fails to load the font. 997 | 998 | .. seealso:: 999 | :meth:`from_bytes` 1000 | """ 1001 | 1002 | @classmethod 1003 | def from_bytes(cls, bytes: bytes, optimal_size: float) -> Font: 1004 | """ 1005 | Loads the font from the given bytes. 1006 | 1007 | .. note:: 1008 | The optimal size is not the fixed size of the font - rather it is the size to optimize rasterizing the font for. 1009 | 1010 | Lower sizes will look worse but perform faster, while higher sizes will look better but perform slower. 1011 | It is best to set this to the size that will likely be the most use 1012 | 1013 | Parameters 1014 | ---------- 1015 | path: str 1016 | The path of the font. 1017 | optimal_size: float 1018 | The optimal size of the font. 1019 | 1020 | Raises 1021 | ------ 1022 | IOError 1023 | Fails to read the font file. 1024 | RuntimeError 1025 | Fails to load the font. 1026 | """ 1027 | 1028 | @property 1029 | def optimal_size(self) -> float: 1030 | """ 1031 | float: Returns the optimal size, in pixels, of this font. 1032 | 1033 | ..note:: 1034 | The optimal size is not the fixed size of the font - rather it is the size to optimize rasterizing the font for. 1035 | 1036 | Lower sizes will look worse but perform faster, while higher sizes will look better but perform slower. 1037 | It is best to set this to the size that will likely be the most used. 1038 | """ 1039 | 1040 | 1041 | R: TypeAlias = ResizeAlgorithm 1042 | 1043 | 1044 | class ResizeAlgorithm: 1045 | """A filtering algorithm that is used to resize an image.""" 1046 | Nearest: R 1047 | Box: R 1048 | Bilinear: R 1049 | Hamming: R 1050 | Bicubic: R 1051 | Mitchell: R 1052 | Lanczos3: R 1053 | 1054 | 1055 | D: TypeAlias = DisposalMethod 1056 | 1057 | 1058 | class DisposalMethod: 1059 | """The method used to dispose a frame before transitioning to the next frame in an image sequence.""" 1060 | Keep: D 1061 | Background: D 1062 | Previous: D 1063 | 1064 | 1065 | W: TypeAlias = WrapStyle 1066 | 1067 | class WrapStyle: 1068 | """The style used to wrap text.""" 1069 | Repeat: W 1070 | Reflect: W 1071 | Clamp: W 1072 | 1073 | 1074 | O: TypeAlias = OverlayMode 1075 | 1076 | 1077 | class OverlayMode: 1078 | """The mode used to overlay an image onto another image.""" 1079 | Replace: O 1080 | Merge: O 1081 | 1082 | 1083 | H: TypeAlias = HorizontalAnchor 1084 | 1085 | 1086 | class HorizontalAnchor: 1087 | """The horizontal anchor of a text.""" 1088 | Left: H 1089 | Center: H 1090 | Right: H 1091 | 1092 | 1093 | V: TypeAlias = VerticalAnchor 1094 | 1095 | 1096 | class VerticalAnchor: 1097 | """The vertical anchor of a text.""" 1098 | Top: V 1099 | Center: V 1100 | Bottom: V --------------------------------------------------------------------------------