Repository: GuardKenzie/chafa.py (language: python)

Issue:
Spelling mistakes
This PR fixes some spelling mistakes in the docs pointed out by @jwilk 


Discussion / hints:

Fix patch:
diff --git a/.cirrus.yml b/.cirrus.yml
index 684d65c..899aba8 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -1,24 +1,14 @@
+env:
+  CHAFA_VERSION: master
+
 build_and_store_wheels: &BUILD_AND_STORE_WHEELS
   install_cibuildwheel_script:
-    - python -m pip install cibuildwheel==2.12.0
+    - python -m pip install cibuildwheel==2.21.1
   run_cibuildwheel_script:
     - cibuildwheel
   wheels_artifacts:
     path: "wheelhouse/*"
 
-macos_arm64_task:
-  name: 💪🍎 Build MacOS arm wheels.
-  alias: macos_arm_wheels
-  macos_instance:
-    image: ghcr.io/cirruslabs/macos-monterey-xcode
-
-  env:
-    PATH: /opt/homebrew/opt/python@3.10/bin:$PATH
-  install_pre_requirements_script:
-    - brew install python@3.10
-    - ln -s python3 /opt/homebrew/opt/python@3.10/bin/python
-  <<: *BUILD_AND_STORE_WHEELS
-
 linux_aarch64_task:
   name: 💪🐧 Build Linux arm wheels.
   alias: linux_arm_wheels
diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml
index 262dd48..8166441 100644
--- a/.github/workflows/build_wheels.yml
+++ b/.github/workflows/build_wheels.yml
@@ -11,16 +11,19 @@ jobs:
   build_wheels:
     name: 🎡 ${{ matrix.os }} wheels
     runs-on: ${{ matrix.os }}
+    env:
+      CHAFA_VERSION: master
     strategy:
       matrix:
-        os: [ubuntu-20.04, windows-2019, macos-11]
+        os: [ubuntu-latest, windows-latest, macos-13, macos-14]
+        # os: [ubuntu-20.04, windows-2019, macos-11]
 
     steps:
       - uses: actions/checkout@v4
 
       - name: Setup UCRT64
         uses: msys2/setup-msys2@v2
-        if: matrix.os == 'windows-2019'
+        if: matrix.os == 'windows-latest'
         with:
           msystem: ucrt64
           install: >-
@@ -35,31 +38,33 @@ jobs:
             make
 
       - name: Build Windows DLLs
-        if: matrix.os == 'windows-2019'
+        if: matrix.os == 'windows-latest'
         shell: msys2 {0}
         run: |
-          git clone --branch 1.12.5 https://github.com/hpjansson/chafa libchafa_src
+          git clone --branch $CHAFA_VERSION https://github.com/hpjansson/chafa libchafa_src
           cd libchafa_src
           ./autogen.sh --without-tools --host=mingw64 && make
 
       - name: Build wheels
-        uses: pypa/cibuildwheel@v2.16.2
+        uses: pypa/cibuildwheel@v2.21.1
 
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         with:
+          name: wheels-${{ matrix.os }}
           path: ./wheelhouse/*.whl
 
   make_sdist:
     name: 💽 Make SDist
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v3
 
     - name: Build SDist
       run: pipx run build --sdist
 
-    - uses: actions/upload-artifact@v3
+    - uses: actions/upload-artifact@v4
       with:
+        name: sdist
         path: dist/*.tar.gz
 
   upload_all:
@@ -68,12 +73,11 @@ jobs:
     runs-on: ubuntu-latest
     if: github.event_name == 'release' && github.event.action == 'published'
     steps:
-    - uses: actions/download-artifact@v3
+    - uses: actions/download-artifact@v4
       with:
-        name: artifact
         path: dist
 
-    - uses: pypa/gh-action-pypi-publish@v1.8.10
+    - uses: pypa/gh-action-pypi-publish@release/v1
       with:
         user: __token__
         password: ${{ secrets.pypi_password }}
diff --git a/.github/workflows/deliver_cirrus.yml b/.github/workflows/deliver_cirrus.yml
index fdf8f41..8bde703 100644
--- a/.github/workflows/deliver_cirrus.yml
+++ b/.github/workflows/deliver_cirrus.yml
@@ -2,7 +2,7 @@ name: 📬 Deliver from Cirrus
 
 on:
   check_suite:
-    type: ['completed']
+    types: ['completed']
 
 jobs:
   upload_all_cirrus:
@@ -17,13 +17,11 @@ jobs:
     steps:
     - name: Downloading artifacts
       run: |
-        wget -O macos.zip https://api.cirrus-ci.com/v1/artifact/github/GuardKenzie/chafa.py/macos_arm_wheels/wheels.zip
         wget -O linux.zip https://api.cirrus-ci.com/v1/artifact/github/GuardKenzie/chafa.py/linux_arm_wheels/wheels.zip
-        unzip macos.zip
         unzip linux.zip
       
     - name: Uploading to test PyPi
-      uses: pypa/gh-action-pypi-publish@v1.8.10
+      uses: pypa/gh-action-pypi-publish@release/v1
       with:
         packages-dir: wheelhouse/
         user: __token__
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index ac16653..9e02a6f 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -6,7 +6,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
       - name: Install dependencies
         run: |
           pip install sphinx sphinxemoji sphinxcontrib-prettyspecialmethods sphinx-toolbox
@@ -17,7 +17,7 @@ jobs:
       # - name: Setup Pages
       #   uses: actions/configure-pages@v3
       - name: Upload artifact
-        uses: actions/upload-pages-artifact@v2
+        uses: actions/upload-pages-artifact@v3
         with:
           path: './docs/_build/html'
 
@@ -35,6 +35,6 @@ jobs:
     steps:
       - name: Deploy to GitHub Pages
         id:   deployment
-        uses: actions/deploy-pages@v1
+        uses: actions/deploy-pages@v4
         with:
-          repo-token: ${{ secrets.GITHUB_TOKEN }}
+          token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index e5aaf32..1f35f59 100644
--- a/.gitignore
+++ b/.gitignore
@@ -134,4 +134,7 @@ dmypy.json
 # Build stuff to ignore
 libs/**.dll
 libs/**.so
-wheelhouse/*
\ No newline at end of file
+wheelhouse/*
+
+# Testing
+src/snake.jpg
\ No newline at end of file
diff --git a/TODO.md b/TODO.md
index b139413..fbfbcad 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,77 +1,4 @@
-# CanvasConfig
-
-- [x] Peaking at symbol maps?
-
-- [x] fill_symbol_map
-- [x] transparency_threshold
-- [x] fg_only
-- [x] fg and bg color
-- [x] work factor
-- [x] dithering
-    - [x] mode
-    - [x] intensity
-- [x] optimizations
-- [x] **DOCS**
-
-# Canvas
-
-- [ ] new_similar
-- [x] Peeking at config
-- [x] inspect characters at (x, y)
-- [x] inspect color at (x, y)
-- [x] raw color
-- [x] **DOCS**
-- [x] What argument type should `draw_pixels` accept for the pixel array? <- This is pretty much done
-    1. array (pretty fast)
-    2. python list and convert to array (slower than array)
-    3. Look into `from_buffer` for use with pyvips (faster than array)
-    4. Only ctypes array and make user handle conversion (This would be bad)
-
-# Symbol map
-
-- [x] copy
-- [x] adding
-    - [x] by tags
-    - [x] by range
-- [x] removing
-    - [x] by tags
-    - [x] by range
-
-- [x] selectors
-
-- [ ] allow builtin glyphs
-- [ ] get glyph
-- [ ] add glyph
-- [x] **DOCS**
-
-# TermDb
-
-- [x] copy
-- [x] fallback info
-- [x] **DOCS**
-
-# TermInfo
-
-- [x] copy
-- [ ] sequences
-    - [ ] get
-    - [ ] set
-- [x] supplement
-- [ ] **emitters!!!**
-- [x] **DOCS**
-
-# Misc
-- [x] Properly figure out how the loader should work (pretty happy with it for now but could be faster)
-- [x] Error handling (I think this is done?)
-- [ ] ~~Splitting classes into separate files?~~ (not for now)
-
-# Docs
-- [x] Add remaining enums
-  - [x] Dither mode
-  - [x] Pixel Type
-
-- [x] Docs for the loader
-
-- [x] Write a tutorial
-- [x] Add some examples
-- [x] Installation
+## Canvas
+- [ ] Bindings for print_rows
+- [ ] Bindings for print_rows_strv
+- [ ] **DOCS!!**
\ No newline at end of file
diff --git a/docs/_static/custom.css b/docs/_static/custom.css
index 6a8e111..bb31256 100644
--- a/docs/_static/custom.css
+++ b/docs/_static/custom.css
@@ -25,7 +25,7 @@
     --rosewater: #f2d5cf;
     --flamingo:  #eebebe;
     --pink:      #f4b8e4;
-    --mauve:     #ca9ee6;
+    --mauve:     hsl(277, 59%, 76%);
     --red:       #e78284;
     --peach:     #ef9f76;
     --maroon:    #ea999c;
@@ -47,13 +47,29 @@
     --property:  var(--green);
     --attribute: var(--green);
 
-    --links: var(--peach);
+    --links: hsl(277, 49%, 65%);;
+}
+
+a:visited {
+    color: var(--links);
 }
 
 div.sphinxsidebarwrapper p.blurb {
     text-align: center;
 }
 
+div.sphinxsidebarwrapper code.xref.py-class:hover {
+    background-color: initial;
+}
+
+div.sphinxsidebar a:hover code {
+    background-color: inherit;
+}
+
+div.sphinxsidebar code {
+    color: inherit;
+}
+
 img {
     border-radius: .5rem;
 }
@@ -62,7 +78,7 @@ img {
     display: inline-block;
 }
 
-div.body h1.blocked-header {
+div.body h1 {
     line-height: 1em;
     margin: .6em 0;
 }
diff --git a/docs/api/Canvas.rst b/docs/api/Canvas.rst
index 6559188..5d37742 100644
--- a/docs/api/Canvas.rst
+++ b/docs/api/Canvas.rst
@@ -31,7 +31,9 @@ A :py:class:`Canvas` is a canvas that can render its contents as text strings.
 
 When you want to make a :py:class:`Canvas`, you can specify it's properties like the width, height, output pixel mode etc. by first creating a :py:class:`CanvasConfig` and then initialising :py:class:`Canvas` with that.
 
-You can draw an image to the canvas using :py:meth:`Canvas.draw_all_pixels` and convert it to output bytes for printing with :py:meth:`Canvas.print`. *(pssst: check out the :py:class:`loader.Loader` to make drawing images easier)*.
+You can draw an image to the canvas using :py:meth:`Canvas.draw_all_pixels` and convert it to output bytes for printing with :py:meth:`Canvas.print`. Check out :ref:`pillow-example` for an example on how to load an image using `Pillow`_.
+
+Alternatively, you can assign the :py:class:`Canvas` a :py:class:`Placement` with the :py:attr:`Canvas.placement` property if you want to control the alignment of the image on the :py:class:`Canvas`. If you go this route, you do not need to use :py:meth:`Canvas.draw_all_pixels`, and can go straight to :py:meth:`Canvas.print`.
 
 The :py:class:`Canvas` supports indexing (and slicing) with ``[]``! This will return a :py:class:`CanvasInspector` or a `generator`_ for the relevant :py:class:`CanvasInspector` objects.
 
@@ -78,7 +80,25 @@ The :py:class:`Canvas` supports indexing (and slicing) with ``[]``! This will re
         :param int|slice|tuple pos: The position to index
 
         :rtype: CanvasInspector
-        
+    
+
+    .. py:property:: placement
+
+        :type: Placement
+
+        The :py:class:`Placement` of an :py:class:`Image` containing a :py:class:`Frame` on the canvas. Use this as an alternative to :py:meth:`draw_all_pixels` and for control over the image's alignment with :py:class:`Align` and :py:class:`Tuck`.
+
+        .. versionadded:: 1.2.0
+
+
+    .. py:method:: new_similar()
+
+        Returns a new :py:class:`Canvas` configured similarly to this one.
+
+        :rtype: Canvas
+
+        .. versionadded:: 1.2.0
+
 
     .. py:method:: peek_config()
 
@@ -97,7 +117,7 @@ The :py:class:`Canvas` supports indexing (and slicing) with ``[]``! This will re
         To output the data after drawing, use the :py:meth:`print` method.
 
         .. note::
-            Best performance is achieved by passing a :py:class:`ctypes.Array` for src_pixels. The :py:class:`chafa.loader.Loader` class provides convenient (and reasonably fast) methods for this using the `MagickWand`_ C-library.
+            Best performance is achieved by passing a :py:class:`ctypes.Array` for src_pixels. A fast way to do this is to use `Pillow`_; specifically the `` `Image.tobytes <https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.tobytes>`_ `` method. 
 
         :param PixelType src_pixel_type: The pixel type of src_pixels. This will determine what order the color channels will be read in and whether there is an alpha channel.
         :param list|tuple|array.array|ctypes.Array src_pixels: The source pixel data. This is a one dimensional array where every block of 3 (or 4 depending on the :py:class:`PixelType`) values represents one pixel of the image. The order of the channels is determined by src_pixel_type.
@@ -216,3 +236,4 @@ You can think of the :py:class:`CanvasInspector` like a detective with a magnify
 
 .. _`generator`: https://docs.python.org/3/glossary.html#term-generator
 .. _`MagickWand`: https://imagemagick.org/script/magick-wand.php
+.. _`Pillow`: https://python-pillow.org/
diff --git a/docs/api/FrameImagePlacement.rst b/docs/api/FrameImagePlacement.rst
new file mode 100644
index 0000000..bc681bc
--- /dev/null
+++ b/docs/api/FrameImagePlacement.rst
@@ -0,0 +1,86 @@
+.. currentmodule:: chafa
+
+==========================
+Frame, Image and Placement
+==========================
+
+These three objects are useful for animations or for image alignment within the :py:class:`Canvas`.
+The most important thing to remember is:
+
+   :py:class:`Frame` < :py:class:`Image` < :py:class:`Placement` < :py:class:`Canvas`
+
+That is, a :py:class:`Placement` contains a :py:class:`Image` which contains a :py:class:`Frame`. Once that's all done, the :py:class:`Placement` can be placed on the :py:class:`Canvas`.
+
+Frame
+-----
+
+.. py:class:: Frame(src_pixel_type: PixelType, src_pixels, src_width: int, src_height: int, src_rowstride: int)
+
+    This defines a frame of an image. This has to be added to an :py:class:`Image`. The inputs are the same as in :py:meth:`Canvas.draw_all_pixels`.
+
+    .. note::
+        Best performance is achieved by passing a :py:class:`ctypes.Array` for src_pixels. A fast way to do this is to use `Pillow <https://pillow.readthedocs.io/en/stable/>`_; specifically the `Image.tobytes <https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.tobytes>`_ method.
+
+    :param PixelType src_pixel_type: The pixel type of src_pixels. This will determine what order the color channels will be read in and whether there is an alpha channel.
+    :param list|tuple|array.array|ctypes.Array src_pixels: The source pixel data. This is a one dimensional array where every block of 3 (or 4 depending on the :py:class:`PixelType`) values represents one pixel of the image. The order of the channels is determined by src_pixel_type.
+
+    :param int src_width:  The width of the source image.
+    :param int src_height: The width of the source image.
+    :param int src_rowstride: The number of values in src_image that represents one line pixels in the source image. Typically this will be the number of channels in the source image multiplied by src_width, e.g. for an image with no alpha channel and a width of 300 pixels, this will be ``3*300``.
+
+    :raises ValueError: if src_width, src_height or src_rowstride are less than or equal to 0.
+
+    .. versionadded:: 1.2.0
+
+Image
+-----
+
+.. py:class:: Image
+
+    A container for a :py:class:`Frame`. To be placed on a :py:class:`Placement`.
+
+    .. versionadded:: 1.2.0
+
+    .. py:property:: frame
+
+        :type: Frame
+
+        The :py:class:`Frame` for the image
+    
+
+Placement
+---------
+After assigning a :py:class:`Placement` to a :py:class:`Canvas` with :py:attr:`Canvas.placement`, you can print the :py:class:`Canvas` using :py:meth:`Canvas.print`.
+
+
+.. py:class:: Placement(image: Image, id: int = 0)
+
+    This class defines the placement of an :py:class:`Image` on a :py:class:`Canvas`. 
+
+    .. note::
+        None of placement's properties have any effect if the :py:class:`Canvas`'s pixel mode is set to :py:attr:`PixelMode.CHAFA_PIXEL_MODE_SYMBOLS`.
+
+    :param image: The :py:class:`Image` to be placed.
+    :param id: An id to assign to the image. Leave as ``0`` to assign one automatically.
+
+    .. versionadded:: 1.2.0
+
+    .. py:property:: tuck
+
+        :type: Tuck
+
+        This describes how the :py:class:`Image` is resized to fit on the :py:class:`Canvas`, and defaults to :py:attr:`Tuck.CHAFA_TUCK_STRETCH`.
+
+    
+    .. py:property:: halign
+
+        :type: Align
+
+        Describes the horizontal alignment of the :py:class:`Image` on the :py:class:`Canvas`.
+
+    
+    .. py:property:: valign
+
+        :type: Align
+
+        Describes the horizontal alignment of the :py:class:`Image` on the :py:class:`Canvas`.
\ No newline at end of file
diff --git a/docs/api/TermInfo.rst b/docs/api/TermInfo.rst
index 32cbdcb..c5c80c8 100644
--- a/docs/api/TermInfo.rst
+++ b/docs/api/TermInfo.rst
@@ -45,6 +45,20 @@ A :py:class:`TermInfo` describes the characteristics of one particular kind of d
 
         :rtype: bool
 
+    .. py:method:: emit(*args)
+
+        Returns the terminal sequences asked for as :py:class:`bytes`.
+
+        .. note:: To "use" the sequence with :py:func:`print` you will first need to decode it with :py:meth:`bytes.decode`
+        
+        For an example, check out the :ref:`sequence example` example.
+
+        :param TermSeq \*args: The sequences to emit
+
+        :rtype: bytes
+
+        .. versionadded:: 1.2.0
+
     .. py:method:: detect_capabilities()
 
         A method that tries to automatically detect the current terminal's capabilities.
diff --git a/docs/api/enums.rst b/docs/api/enums.rst
index de8f95f..fc1d4ed 100644
--- a/docs/api/enums.rst
+++ b/docs/api/enums.rst
@@ -245,6 +245,59 @@ An enumerator for optimizations. When enabled, these may produce more compact ou
         All optimizations enabled.
 
 
+Tuck
+----
+An enumerator for tucking styles. To be used in a :py:class:`Placement` to specify what to do if the image doesn't fit within the :py:class:`Canvas`'s bounds.
+
+.. py:class:: Tuck
+
+    :bases: :py:class:`enum.IntEnum`
+
+    .. py:attribute:: CHAFA_TUCK_STRETCH
+	
+        Resize element to fit the area exactly, changing its aspect ratio.
+
+    .. py:attribute:: CHAFA_TUCK_FIT  
+
+        Resize element to fit the area, preserving its aspect ratio by adding padding.
+
+    .. py:attribute:: CHAFA_TUCK_SHRINK_TO_FIT   
+
+        Like CHAFA_TUCK_FIT , but prohibit enlargement.
+
+    .. py:attribute:: CHAFA_TUCK_MAX
+        
+        Last supported tucking policy, plus one.
+
+    .. versionadded:: 1.2.0
+
+Align
+-----
+An enumerator for :py:class:`Placement`'s available vertical- and horizontal alignments.
+
+.. py:class:: Align
+
+    :bases: :py:class:`enum.IntEnum`
+
+    .. py:attribute:: CHAFA_ALIGN_START
+	
+        Align flush with beginning of the area (top or left in LTR locales).
+
+    .. py:attribute:: CHAFA_ALIGN_END
+	
+        Align flush with end of the area (bottom or right in LTR locales).
+
+    .. py:attribute:: CHAFA_ALIGN_CENTER
+	
+        Align in the middle of the area.
+
+    .. py:attribute:: CHAFA_ALIGN_MAX
+	
+        Last supported alignment, plus one.
+    
+    .. versionadded:: 1.2.0
+
+
 Symbol Tags
 ___________
 An enumerator for symbol tags. This can be used in :py:meth:`SymbolMap.add_by_tags` to specify which symbols to use in the output of a :py:class:`Canvas`.
diff --git a/docs/conf.py b/docs/conf.py
index f2bc523..602527f 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -12,7 +12,7 @@
 project = 'chafa.py'
 copyright = '2022, Erica Ferrua Edwardsdóttir'
 author = 'Erica Ferrua Edwardsdóttir'
-release = '1.1.2'
+release = '1.2.0'
 
 # -- General configuration ---------------------------------------------------
 # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
diff --git a/docs/index.rst b/docs/index.rst
index 87c3ec8..50c8d62 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -89,7 +89,7 @@ Getting started
 
    usage/installation
    usage/tutorial
-   usage/examples
+   usage/examples/examples
 
 API Reference
 ---------------
@@ -102,6 +102,7 @@ API Reference
    api/SymbolMap
    api/TermDb
    api/TermInfo
+   api/FrameImagePlacement
    api/Loader
    api/Functions
    api/enums
diff --git a/docs/usage/examples.rst b/docs/usage/examples.rst
index 9b8f6dd..e69de29 100644
--- a/docs/usage/examples.rst
+++ b/docs/usage/examples.rst
@@ -1,372 +0,0 @@
-.. _examples:
-
-========
-Examples
-========
-
-On this page you will find a few useful examples (which you can also find in the `GitHub repo`_.) when it comes to using chafa.py. These should cover most of the basics for reading and printing an image and they also include some messing around with :py:class:`chafa.CanvasConfig`.
-
-If you are the sort of person who likes getting their hands dirty right away, take a look at these and read up on the methods and classes used in the api. That should be enough to get you to a pretty good point!
-
-All the examples will be using this image and calling it ``snake.jpg``
-
-.. image:: snake.jpg
-    :width: 300
-    :align: center
-
-Reading an image
-================
-
-Here are several different methods of reading images for using them with chafa.py. They perform similarly so just pick the one you like the most!
-
-.. py:currentmodule:: chafa.loader
-
-Using :py:class:`Loader`
-------------------------
-
-The :py:class:`Loader` is a class included with chafa.py which aims making it easy to read and draw images to the :py:class:`chafa.Canvas`, however, it does depend on the `MagickWand`_ C-library.
-
-::
-
-    from chafa import *
-    from chafa.loader import Loader
-
-    # Init canvas config
-    config = CanvasConfig()
-
-    # Set canvas height and width
-    config.height = 30
-    config.width  = 30
-
-    # Open image with the loader
-    image = Loader("./snake.jpg")
-
-    width      = image.width
-    height     = image.height
-    rowstride  = image.rowstride
-
-    # Init the canvas
-    canvas = Canvas(config)
-
-    # Draw to canvas
-    canvas.draw_all_pixels(
-        image.pixel_type,
-        image.get_pixels(),
-        width, 
-        height,
-        rowstride
-    )
-
-    # Write picture
-    print(canvas.print().decode())
-
-
-Using `Pillow`_
----------------
-
-When reading pixel data from an image opened with `Pillow`_, we get a 2D array which needs to be reshaped into a 1D array before passing to chafa. This is still pretty fast since both Pillow and NumPy are well optimized.
-
-::
-
-    from chafa import *
-    from PIL import Image
-
-    # Init canvas config
-    config = CanvasConfig()
-
-    # Set canvas height and width
-    config.height = 30
-    config.width  = 30
-
-    # Open image with PIL
-    image = Image.open("./snake.jpg")
-
-    width  = image.width
-    height = image.height
-    bands  = len(image.getbands())
-
-    # Put image into correct format
-    pixels = image.tobytes()
-
-    # Init the canvas
-    canvas = Canvas(config)
-
-    # Draw to canvas
-    canvas.draw_all_pixels(
-        PixelType.CHAFA_PIXEL_RGB8,
-        pixels,
-        width,
-        height,
-        width * bands
-    )
-
-    # Write picture
-    print(canvas.print().decode())
-
-
-Using `pyvips`_
----------------
-
-The `pyvips`_ library is another fast image manipulation library which can be used to read images in a pretty straight forward manner.
-
-::
-
-    import pyvips
-    from chafa import *
-
-    # Init canvas config
-    config = CanvasConfig()
-
-    # Set canvas height and width
-    config.height = 30 
-    config.width  = 30
-
-    # Open image with vips
-    image = pyvips.Image.new_from_file("./snake.jpg")
-
-    width  = image.width
-    height = image.height
-    bands  = image.bands
-
-    # Init the canvas
-    canvas = Canvas(config)
-
-    # Draw to canvas
-    canvas.draw_all_pixels(
-        PixelType.CHAFA_PIXEL_RGB8,
-        image.write_to_memory(),
-        width,
-        height,
-        width * bands
-    )
-
-    # Write picture
-    print(canvas.print().decode())
-
-
-.. currentmodule:: chafa
-
-Detecting terminal capabilities
-===============================
-
-Here is an example of a program that uses :py:meth:`TermInfo.detect_capabilities()` to figure out what the current terminal is capable of.
-
-::
-
-    from chafa import *
-    from chafa.loader import Loader
-
-    # Init canvas config
-    config = CanvasConfig()
-
-    # Set canvas height and width
-    config.height = 30
-    config.width  = 30
-
-    # Init database and get terminal info
-    term_db   = TermDb()
-    term_info = term_db.detect()
-
-    # Get terminal capabilities
-    capabilities = term_info.detect_capabilities()
-
-    # Set config to appropriate modes
-    config.canvas_mode = capabilities.canvas_mode
-    config.pixel_mode  = capabilities.pixel_mode
-
-    # Open image with the loader
-    image = Loader("./snake.jpg")
-
-    width      = image.width
-    height     = image.height
-    rowstride  = image.rowstride
-
-    # Init the canvas
-    canvas = Canvas(config)
-
-    # Draw to canvas
-    canvas.draw_all_pixels(
-        image.pixel_type,
-        image.get_pixels(),
-        width, 
-        height,
-        rowstride
-    )
-
-    # Write picture
-    print(canvas.print().decode())
-
-
-
-Accurate aspect ratio
-=====================
-
-Here is an example that outputs the image in the correct aspect ratio.
-
-::
-
-    from chafa import *
-    from chafa.loader import Loader
-
-    FONT_WIDTH  = 11
-    FONT_HEIGHT = 24
-
-    # Init canvas config
-    config = CanvasConfig()
-
-    # Set canvas height, width and cell geometry
-    config.height = 30
-    config.width  = 30
-
-    # Set the canvas cell geometry
-    config.cell_height = FONT_HEIGHT
-    config.cell_width  = FONT_WIDTH
-
-    # Open image with the loader
-    image = Loader("./snake.jpg")
-
-    width      = image.width
-    height     = image.height
-    rowstride  = image.rowstride
-
-    # Calculate the appropriate geometry for the canvas
-    config.calc_canvas_geometry(width, height, FONT_WIDTH/FONT_HEIGHT)
-
-    # Init the canvas
-    canvas = Canvas(config)
-
-    # Draw to canvas
-    canvas.draw_all_pixels(
-        image.pixel_type,
-        image.get_pixels(),
-        width, 
-        height,
-        rowstride
-    )
-
-    # Write picture
-    print(canvas.print().decode())
-
-
-Converting image to HTML
-========================
-
-This example produces a ``.html`` file version of an image, similar to the one at `index`_. You need to set the display font to some monospace font (e.g. `JetBrains Mono`_) in order for it to work.
-
-:: 
-
-    from chafa import *
-    from chafa.loader import Loader
-
-    img_loc     = "snake.jpg"
-    output_loc  = "out.html"
-
-    def format_pixel(char, fg, bg):
-        # Formats the pixel to html with color applied through style
-        style       = f"color: rgb{fg}; background-color: rgb{bg}"
-        opening_tag = f"<span class=\"pixel\" style=\"{style}\">"
-        closing_tag = f"</span>"
-
-        return f"{opening_tag}{char}{closing_tag}"
-
-    # Font ratio of JetBrains Mono
-    FONT_RATIO = 11/24
-
-    # Init config
-    config = CanvasConfig()
-
-    config.width  = 40
-    config.height = 40
-
-    # Load image
-    image = Loader(img_loc)
-
-    # Set up canvas
-    config.calc_canvas_geometry(image.width, image.height, FONT_RATIO)
-
-    canvas = Canvas(config)
-    canvas.draw_all_pixels(
-        image.pixel_type,
-        image.get_pixels(),
-        image.width, image.height,
-        image.rowstride
-    )
-
-    # Write the file
-    with open(output_loc, "w") as f:
-        f.write("<div class=\"chafa-img\">\n")
-
-        # Iterate over pixels in image
-        for row in canvas[:]:
-            f.write("    <p class=\"row\">\n")
-            f.write("        ")
-            for pixel in row:
-                f.write(
-                    format_pixel(
-                        pixel.char,
-                        pixel.fg_color,
-                        pixel.bg_color
-                    )
-                )
-
-            f.write("\n    </p>\n")
-
-        f.write("</div class=\"chafa-img\">")
-
-
-Playing around with raw color
-=============================
-
-::
-
-    from chafa import *
-
-    # Create config
-    config = CanvasConfig()
-
-    config.width = 36
-    config.height = (256 - (16+24))  // 36 + 2
-
-
-    config.canvas_mode = CanvasMode.CHAFA_CANVAS_MODE_INDEXED_256
-
-    # Init an empty canvas
-    pixels = [0, 0, 0, 0]
-
-    canvas = Canvas(config)
-    canvas.draw_all_pixels(
-        PixelType.CHAFA_PIXEL_RGBA8_PREMULTIPLIED,
-        pixels,
-        1, 1, 3
-    )
-
-    # Draw all 256 colors
-
-    # Set the characters to half blocks
-    for row in canvas[:]:
-        for pix in row:
-            pix.char = "▀"
-            pix.remove_foreground()
-
-    
-    # Draw the primary 16
-    for pix in canvas[0,:16]:
-        pix.raw_fg_color = pix.x
-
-    # Draw the rest of the colors
-    for row in canvas[1:-1, :]:
-        for pix in row:
-            pix.raw_fg_color = pix.x + 16 + ((pix.y-1) * 5 * config.width//5)
-
-    # Draw the greyscale gradient
-    for pix in canvas[-1, :24]:
-        pix.raw_fg_color = 255 - (23 - pix.x)
-
-    print(canvas.print().decode())
-
-.. _`MagickWand`: https://imagemagick.org/script/magick-wand.php
-.. _`Pillow`: https://python-pillow.org/
-.. _`pyvips`: https://github.com/libvips/pyvips
-.. _`GitHub repo`: https://github.com/guardkenzie/chafa.py
-.. _`JetBrains Mono`: https://www.jetbrains.com/lp/mono/
-.. _`index`: https://chafapy.mage.black
diff --git a/docs/usage/examples/aspect_ratio.rst b/docs/usage/examples/aspect_ratio.rst
new file mode 100644
index 0000000..97eec9d
--- /dev/null
+++ b/docs/usage/examples/aspect_ratio.rst
@@ -0,0 +1,57 @@
+=====================
+Accurate aspect ratio
+=====================
+
+Here is an example that outputs the image in the correct aspect ratio.
+
+::
+
+    from chafa import *
+    from chafa.loader import Loader
+
+    FONT_WIDTH  = 11
+    FONT_HEIGHT = 24
+
+    # Init canvas config
+    config = CanvasConfig()
+
+    # Set canvas height, width and cell geometry
+    config.height = 30
+    config.width  = 30
+
+    # Set the canvas cell geometry
+    config.cell_height = FONT_HEIGHT
+    config.cell_width  = FONT_WIDTH
+
+    # Open image with the loader
+    image = Loader("./snake.jpg")
+
+    width      = image.width
+    height     = image.height
+    rowstride  = image.rowstride
+
+    # Calculate the appropriate geometry for the canvas
+    config.calc_canvas_geometry(width, height, FONT_WIDTH/FONT_HEIGHT)
+
+    # Init the canvas
+    canvas = Canvas(config)
+
+    # Draw to canvas
+    canvas.draw_all_pixels(
+        image.pixel_type,
+        image.get_pixels(),
+        width, 
+        height,
+        rowstride
+    )
+
+    # Write picture
+    print(canvas.print().decode())
+
+
+.. _`MagickWand`: https://imagemagick.org/script/magick-wand.php
+.. _`Pillow`: https://python-pillow.org/
+.. _`pyvips`: https://github.com/libvips/pyvips
+.. _`GitHub repo`: https://github.com/guardkenzie/chafa.py
+.. _`JetBrains Mono`: https://www.jetbrains.com/lp/mono/
+.. _`index`: https://chafapy.mage.black
diff --git a/docs/usage/examples/converting_to_html.rst b/docs/usage/examples/converting_to_html.rst
new file mode 100644
index 0000000..a7655ea
--- /dev/null
+++ b/docs/usage/examples/converting_to_html.rst
@@ -0,0 +1,73 @@
+========================
+Converting image to HTML
+========================
+
+This example produces a ``.html`` file version of an image, similar to the one at `index`_. You need to set the display font to some monospace font (e.g. `JetBrains Mono`_) in order for it to work.
+
+:: 
+
+    from chafa import *
+    from chafa.loader import Loader
+
+    img_loc     = "snake.jpg"
+    output_loc  = "out.html"
+
+    def format_pixel(char, fg, bg):
+        # Formats the pixel to html with color applied through style
+        style       = f"color: rgb{fg}; background-color: rgb{bg}"
+        opening_tag = f"<span class=\"pixel\" style=\"{style}\">"
+        closing_tag = f"</span>"
+
+        return f"{opening_tag}{char}{closing_tag}"
+
+    # Font ratio of JetBrains Mono
+    FONT_RATIO = 11/24
+
+    # Init config
+    config = CanvasConfig()
+
+    config.width  = 40
+    config.height = 40
+
+    # Load image
+    image = Loader(img_loc)
+
+    # Set up canvas
+    config.calc_canvas_geometry(image.width, image.height, FONT_RATIO)
+
+    canvas = Canvas(config)
+    canvas.draw_all_pixels(
+        image.pixel_type,
+        image.get_pixels(),
+        image.width, image.height,
+        image.rowstride
+    )
+
+    # Write the file
+    with open(output_loc, "w") as f:
+        f.write("<div class=\"chafa-img\">\n")
+
+        # Iterate over pixels in image
+        for row in canvas[:]:
+            f.write("    <p class=\"row\">\n")
+            f.write("        ")
+            for pixel in row:
+                f.write(
+                    format_pixel(
+                        pixel.char,
+                        pixel.fg_color,
+                        pixel.bg_color
+                    )
+                )
+
+            f.write("\n    </p>\n")
+
+        f.write("</div class=\"chafa-img\">")
+
+
+.. _`MagickWand`: https://imagemagick.org/script/magick-wand.php
+.. _`Pillow`: https://python-pillow.org/
+.. _`pyvips`: https://github.com/libvips/pyvips
+.. _`GitHub repo`: https://github.com/guardkenzie/chafa.py
+.. _`JetBrains Mono`: https://www.jetbrains.com/lp/mono/
+.. _`index`: https://chafapy.mage.black
diff --git a/docs/usage/examples/detecting_capabilities.rst b/docs/usage/examples/detecting_capabilities.rst
new file mode 100644
index 0000000..0892dbb
--- /dev/null
+++ b/docs/usage/examples/detecting_capabilities.rst
@@ -0,0 +1,58 @@
+===============================
+Detecting terminal capabilities
+===============================
+
+Here is an example of a program that uses :py:meth:`TermInfo.detect_capabilities()` to figure out what the current terminal is capable of.
+
+::
+
+    from chafa import *
+    from chafa.loader import Loader
+
+    # Init canvas config
+    config = CanvasConfig()
+
+    # Set canvas height and width
+    config.height = 30
+    config.width  = 30
+
+    # Init database and get terminal info
+    term_db   = TermDb()
+    term_info = term_db.detect()
+
+    # Get terminal capabilities
+    capabilities = term_info.detect_capabilities()
+
+    # Set config to appropriate modes
+    config.canvas_mode = capabilities.canvas_mode
+    config.pixel_mode  = capabilities.pixel_mode
+
+    # Open image with the loader
+    image = Loader("./snake.jpg")
+
+    width      = image.width
+    height     = image.height
+    rowstride  = image.rowstride
+
+    # Init the canvas
+    canvas = Canvas(config)
+
+    # Draw to canvas
+    canvas.draw_all_pixels(
+        image.pixel_type,
+        image.get_pixels(),
+        width, 
+        height,
+        rowstride
+    )
+
+    # Write picture
+    print(canvas.print().decode())
+
+
+.. _`MagickWand`: https://imagemagick.org/script/magick-wand.php
+.. _`Pillow`: https://python-pillow.org/
+.. _`pyvips`: https://github.com/libvips/pyvips
+.. _`GitHub repo`: https://github.com/guardkenzie/chafa.py
+.. _`JetBrains Mono`: https://www.jetbrains.com/lp/mono/
+.. _`index`: https://chafapy.mage.black
diff --git a/docs/usage/examples/emitting_control_sequences.rst b/docs/usage/examples/emitting_control_sequences.rst
new file mode 100644
index 0000000..7328500
--- /dev/null
+++ b/docs/usage/examples/emitting_control_sequences.rst
@@ -0,0 +1,82 @@
+.. _sequence example:
+
+================================
+Animating with control sequences
+================================
+
+.. py:currentmodule:: chafa
+
+This is a fun little example that demonstrates how to use the :py:meth:`TermInfo.emit` method.
+
+.. image:: example_img/animation.gif
+    :width: 300
+    :align: center
+
+The code that produced this animation follows:
+
+::
+
+    from chafa import *
+    from time import sleep
+    import sys
+
+    SL=15
+
+    def snake(n, step):
+        if step < 0: turns = ["┏","┛"]
+        else:        turns = ["┓", "┗"]
+
+        if n == 1:
+            return turns[0]
+        elif n == SL - 1:
+            return turns[1]
+
+        return "┃"
+
+    def gradient(x, y):
+        return (80 + 5*x, 255 - (100 + 10*y), 200)
+
+    # Grab our info
+    db = TermDb()
+    info = db.detect()
+
+    # Clear the terminal
+    clear     = info.emit(TermSeq.CHAFA_TERM_SEQ_CLEAR)
+    no_cursor = info.emit(TermSeq.CHAFA_TERM_SEQ_DISABLE_CURSOR)
+
+    print(clear.decode())
+    print(no_cursor.decode())
+
+    rows = list(range(1,SL))
+    cols = list(range(1,32))
+
+    for x in cols:
+        # We want to step backwards if column is even
+        step = 2 * (x%2) - 1
+
+        for y in rows[::step]:
+            # Grab char
+            char = snake(y, step)
+
+            # Move to position
+            out = info.emit(
+                TermSeq.CHAFA_TERM_SEQ_CURSOR_TO_POS, 
+                x, y
+            )
+
+            # Set color
+            color = info.emit(
+                TermSeq.CHAFA_TERM_SEQ_SET_COLOR_FG_DIRECT,
+                *gradient(x,y)
+            )
+
+            sys.stdout.buffer.write(out)
+            sys.stdout.buffer.write(color)
+            sys.stdout.write(char)
+            sys.stdout.flush()
+
+            sleep(0.01)
+
+    # All back to normal!
+    reset = info.emit(TermSeq.CHAFA_TERM_SEQ_RESET_TERMINAL_HARD)
+    print(reset.decode())
diff --git a/docs/usage/examples/example_img/animation.gif b/docs/usage/examples/example_img/animation.gif
new file mode 100644
index 0000000..1c50a21
Binary files /dev/null and b/docs/usage/examples/example_img/animation.gif differ
diff --git a/docs/usage/examples/example_img/center_snake.png b/docs/usage/examples/example_img/center_snake.png
new file mode 100644
index 0000000..33b9f1a
Binary files /dev/null and b/docs/usage/examples/example_img/center_snake.png differ
diff --git a/docs/usage/examples/examples.rst b/docs/usage/examples/examples.rst
new file mode 100644
index 0000000..1176abf
--- /dev/null
+++ b/docs/usage/examples/examples.rst
@@ -0,0 +1,41 @@
+.. _examples:
+
+========
+Examples
+========
+
+On this page you will find a few useful examples (which you can also find in the `GitHub repo`_.) when it comes to using chafa.py. These should cover most of the basics for reading and printing an image and they also include some messing around with :py:class:`chafa.CanvasConfig`.
+
+If you are the sort of person who likes getting their hands dirty right away, take a look at these and read up on the methods and classes used in the api. That should be enough to get you to a pretty good point!
+
+All the examples will be using this image and calling it ``snake.jpg``
+
+Some more examples might be found in the `examples folder <https://github.com/GuardKenzie/chafa.py/tree/main/examples>`_ on GitHub.
+
+.. image:: ../snake.jpg
+    :width: 300
+    :align: center
+
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Here's all the examples on the webpage.
+
+   reading_an_image
+   aspect_ratio
+   detecting_capabilities
+   raw_color
+   emitting_control_sequences
+   converting_to_html
+   using_placement
+   
+
+
+
+
+.. _`MagickWand`: https://imagemagick.org/script/magick-wand.php
+.. _`Pillow`: https://python-pillow.org/
+.. _`pyvips`: https://github.com/libvips/pyvips
+.. _`GitHub repo`: https://github.com/guardkenzie/chafa.py
+.. _`JetBrains Mono`: https://www.jetbrains.com/lp/mono/
+.. _`index`: https://chafapy.mage.black
diff --git a/docs/usage/examples/raw_color.rst b/docs/usage/examples/raw_color.rst
new file mode 100644
index 0000000..61d8805
--- /dev/null
+++ b/docs/usage/examples/raw_color.rst
@@ -0,0 +1,58 @@
+=============================
+Playing around with raw color
+=============================
+
+::
+
+    from chafa import *
+
+    # Create config
+    config = CanvasConfig()
+
+    config.width = 36
+    config.height = (256 - (16+24))  // 36 + 2
+
+
+    config.canvas_mode = CanvasMode.CHAFA_CANVAS_MODE_INDEXED_256
+
+    # Init an empty canvas
+    pixels = [0, 0, 0, 0]
+
+    canvas = Canvas(config)
+    canvas.draw_all_pixels(
+        PixelType.CHAFA_PIXEL_RGBA8_PREMULTIPLIED,
+        pixels,
+        1, 1, 3
+    )
+
+    # Draw all 256 colors
+
+    # Set the characters to half blocks
+    for row in canvas[:]:
+        for pix in row:
+            pix.char = "▀"
+            pix.remove_foreground()
+
+    
+    # Draw the primary 16
+    for pix in canvas[0,:16]:
+        pix.raw_fg_color = pix.x
+
+    # Draw the rest of the colors
+    for row in canvas[1:-1, :]:
+        for pix in row:
+            pix.raw_fg_color = pix.x + 16 + ((pix.y-1) * 5 * config.width//5)
+
+    # Draw the greyscale gradient
+    for pix in canvas[-1, :24]:
+        pix.raw_fg_color = 255 - (23 - pix.x)
+
+    print(canvas.print().decode())
+
+
+.. _`MagickWand`: https://imagemagick.org/script/magick-wand.php
+.. _`Pillow`: https://python-pillow.org/
+.. _`pyvips`: https://github.com/libvips/pyvips
+.. _`GitHub repo`: https://github.com/guardkenzie/chafa.py
+.. _`JetBrains Mono`: https://www.jetbrains.com/lp/mono/
+.. _`index`: https://chafapy.mage.black
diff --git a/docs/usage/examples/reading_an_image.rst b/docs/usage/examples/reading_an_image.rst
new file mode 100644
index 0000000..24255ad
--- /dev/null
+++ b/docs/usage/examples/reading_an_image.rst
@@ -0,0 +1,139 @@
+================
+Reading an image
+================
+
+Here are several different methods of reading images for using them with chafa.py. They perform similarly so just pick the one you like the most!
+
+.. py:currentmodule:: chafa.loader
+
+Using :py:class:`Loader`
+------------------------
+
+The :py:class:`Loader` is a class included with chafa.py which aims making it easy to read and draw images to the :py:class:`chafa.Canvas`, however, it does depend on the `MagickWand`_ C-library.
+
+::
+
+    from chafa import *
+    from chafa.loader import Loader
+
+    # Init canvas config
+    config = CanvasConfig()
+
+    # Set canvas height and width
+    config.height = 30
+    config.width  = 30
+
+    # Open image with the loader
+    image = Loader("./snake.jpg")
+
+    width      = image.width
+    height     = image.height
+    rowstride  = image.rowstride
+
+    # Init the canvas
+    canvas = Canvas(config)
+
+    # Draw to canvas
+    canvas.draw_all_pixels(
+        image.pixel_type,
+        image.get_pixels(),
+        width, 
+        height,
+        rowstride
+    )
+
+    # Write picture
+    print(canvas.print().decode())
+
+
+.. _pillow-example:
+
+Using `Pillow`_
+---------------
+
+When reading pixel data from an image opened with `Pillow`_, we get a 2D array which needs to be reshaped into a 1D array before passing to chafa. This is still pretty fast since both Pillow and NumPy are well optimized.
+
+::
+
+    from chafa import *
+    from PIL import Image
+
+    # Init canvas config
+    config = CanvasConfig()
+
+    # Set canvas height and width
+    config.height = 30
+    config.width  = 30
+
+    # Open image with PIL
+    image = Image.open("./snake.jpg")
+
+    width  = image.width
+    height = image.height
+    bands  = len(image.getbands())
+
+    # Put image into correct format
+    pixels = image.tobytes()
+
+    # Init the canvas
+    canvas = Canvas(config)
+
+    # Draw to canvas
+    canvas.draw_all_pixels(
+        PixelType.CHAFA_PIXEL_RGB8,
+        pixels,
+        width,
+        height,
+        width * bands
+    )
+
+    # Write picture
+    print(canvas.print().decode())
+
+
+Using `pyvips`_
+---------------
+
+The `pyvips`_ library is another fast image manipulation library which can be used to read images in a pretty straight forward manner.
+
+::
+
+    import pyvips
+    from chafa import *
+
+    # Init canvas config
+    config = CanvasConfig()
+
+    # Set canvas height and width
+    config.height = 30 
+    config.width  = 30
+
+    # Open image with vips
+    image = pyvips.Image.new_from_file("./snake.jpg")
+
+    width  = image.width
+    height = image.height
+    bands  = image.bands
+
+    # Init the canvas
+    canvas = Canvas(config)
+
+    # Draw to canvas
+    canvas.draw_all_pixels(
+        PixelType.CHAFA_PIXEL_RGB8,
+        image.write_to_memory(),
+        width,
+        height,
+        width * bands
+    )
+
+    # Write picture
+    print(canvas.print().decode())
+
+
+.. _`MagickWand`: https://imagemagick.org/script/magick-wand.php
+.. _`Pillow`: https://python-pillow.org/
+.. _`pyvips`: https://github.com/libvips/pyvips
+.. _`GitHub repo`: https://github.com/guardkenzie/chafa.py
+.. _`JetBrains Mono`: https://www.jetbrains.com/lp/mono/
+.. _`index`: https://chafapy.mage.black
diff --git a/docs/usage/examples/using_placement.rst b/docs/usage/examples/using_placement.rst
new file mode 100644
index 0000000..1631d45
--- /dev/null
+++ b/docs/usage/examples/using_placement.rst
@@ -0,0 +1,72 @@
+.. py:currentmodule:: chafa
+===========================
+Using :py:class:`Placement`
+===========================
+
+Here is an example on how to use the :py:class:`Placement` class to draw an image in the center of the :py:class:`Canvas`.
+
+.. image:: example_img/center_snake.png
+    :width: 500
+    :align: center
+
+.. versionadded:: 1.2.0
+
+::
+
+    import chafa
+    from pathlib import Path
+    from PIL import Image
+
+    # Init canvas config
+    config = chafa.CanvasConfig()
+
+    # Set canvas height and width
+    config.width = 70
+    config.height = 20
+
+    # Set cell dimensions for accurate aspect ratio
+    config.cell_width  = 18
+    config.cell_height = 46
+
+    # Set pixel mode
+    config.pixel_mode = chafa.PixelMode.CHAFA_PIXEL_MODE_KITTY
+
+    # Open image with PIL
+    image = Image.open(Path(__file__).parent / "snake.jpg")
+
+    width  = image.width
+    height = image.height
+    bands  = len(image.getbands())
+
+    # Put image into correct format
+    pixels = image.tobytes()
+
+    # Init the canvas
+    canvas = chafa.Canvas(config)
+
+    # Init frame
+    frame = chafa.Frame(
+        chafa.PixelType.CHAFA_PIXEL_RGB8,
+        pixels,
+        height,
+        width,
+        width * bands
+    )
+
+    # Init image and assign our frame to it
+    chafa_image = chafa.Image()
+    chafa_image.frame = frame
+
+    # Init the placement of our image
+    placement = chafa.Placement(chafa_image)
+
+    # Set our desired placement
+    placement.tuck   = chafa.Tuck.CHAFA_TUCK_SHRINK_TO_FIT
+    placement.halign = chafa.Align.CHAFA_ALIGN_CENTER
+    placement.valign = chafa.Align.CHAFA_ALIGN_CENTER
+
+    # Put the image on the canvas
+    canvas.placement = placement
+
+    # Write picture
+    print(canvas.print().decode())
\ No newline at end of file
diff --git a/docs/usage/installation.rst b/docs/usage/installation.rst
index 04c1217..fc9f5a0 100644
--- a/docs/usage/installation.rst
+++ b/docs/usage/installation.rst
@@ -6,7 +6,7 @@ Installation
 
 Here are some installation instructions for getting going. It is preferred to install from `PyPI`_, however, you are welcome to build from source if you so desire.
 
-Currently only Linux, Windows 64-bit and MacOS systems (Intel and ARM) are supported.
+Currently every major 64-bit OS is supported except Windows on ARM.
 
 For all methods, if you want to use the included :py:class:`Loader` class to load images, you will also need the `MagickWand <https://imagemagick.org/script/magick-wand.php>`_ C-library. The installation of MagickWand is fairly straight forward.
 
@@ -56,7 +56,6 @@ Dependencies
 ============
 
 - `Chafa <https://hpjansson.org/chafa/download/>`_
-- `MagickWand <https://imagemagick.org/script/magick-wand.php>`_
 - `Hatchling <https://pypi.org/project/hatchling/>`_ (for building) 
 - Python 3.8 or later
 
diff --git a/examples/example_emit.py b/examples/example_emit.py
new file mode 100644
index 0000000..181d410
--- /dev/null
+++ b/examples/example_emit.py
@@ -0,0 +1,64 @@
+from chafa import *
+from time import sleep
+import sys
+
+SL=15
+
+def snake(n, step):
+    if step < 0: turns = ["┏","┛"]
+    else:        turns = ["┓", "┗"]
+
+    if n == 1:
+        return turns[0]
+    elif n == SL - 1:
+        return turns[1]
+
+    return "┃"
+
+def gradient(x, y):
+    return (80 + 5*x, 255 - (100 + 10*y), 200)
+
+# Grab our info
+db = TermDb()
+info = db.detect()
+
+# Clear the terminal
+clear     = info.emit(TermSeq.CHAFA_TERM_SEQ_CLEAR)
+no_cursor = info.emit(TermSeq.CHAFA_TERM_SEQ_DISABLE_CURSOR)
+
+print(clear.decode())
+print(no_cursor.decode())
+
+rows = list(range(1,SL))
+cols = list(range(1,32))
+
+for x in cols:
+    # We want to step backwards if column is even
+    step = 2 * (x%2) - 1
+
+    for y in rows[::step]:
+        # Grab char
+        char = snake(y, step)
+
+        # Move to position
+        out = info.emit(
+            TermSeq.CHAFA_TERM_SEQ_CURSOR_TO_POS, 
+            x, y
+        )
+
+        # Set color
+        color = info.emit(
+            TermSeq.CHAFA_TERM_SEQ_SET_COLOR_FG_DIRECT,
+            *gradient(x,y)
+        )
+
+        sys.stdout.buffer.write(out)
+        sys.stdout.buffer.write(color)
+        sys.stdout.write(char)
+        sys.stdout.flush()
+
+        sleep(0.01)
+
+# All back to normal!
+reset = info.emit(TermSeq.CHAFA_TERM_SEQ_RESET_TERMINAL_HARD)
+print(reset.decode())
diff --git a/examples/example_placement.py b/examples/example_placement.py
new file mode 100644
index 0000000..6ca095a
--- /dev/null
+++ b/examples/example_placement.py
@@ -0,0 +1,57 @@
+import chafa
+from pathlib import Path
+from PIL import Image
+
+# Init canvas config
+config = chafa.CanvasConfig()
+
+# Set canvas height and width
+config.width = 70
+config.height = 20
+
+# Set cell dimensions for accurate aspect ratio
+config.cell_width  = 18
+config.cell_height = 46
+
+# Set pixel mode
+config.pixel_mode = chafa.PixelMode.CHAFA_PIXEL_MODE_KITTY
+
+# Open image with PIL
+image = Image.open(Path(__file__).parent / "snake.jpg")
+
+width  = image.width
+height = image.height
+bands  = len(image.getbands())
+
+# Put image into correct format
+pixels = image.tobytes()
+
+# Init the canvas
+canvas = chafa.Canvas(config)
+
+# Init frame
+frame = chafa.Frame(
+    chafa.PixelType.CHAFA_PIXEL_RGB8,
+    pixels,
+    height,
+    width,
+    width * bands
+)
+
+# Init image and assign our frame to it
+chafa_image = chafa.Image()
+chafa_image.frame = frame
+
+# Init the placement of our image
+placement = chafa.Placement(chafa_image)
+
+# Set our desired placement
+placement.tuck   = chafa.Tuck.CHAFA_TUCK_SHRINK_TO_FIT
+placement.halign = chafa.Align.CHAFA_ALIGN_CENTER
+placement.valign = chafa.Align.CHAFA_ALIGN_CENTER
+
+# Put the image on the canvas
+canvas.placement = placement
+
+# Write picture
+print(canvas.print().decode())
\ No newline at end of file
diff --git a/examples/example_rows.py b/examples/example_rows.py
new file mode 100644
index 0000000..f08cbe8
--- /dev/null
+++ b/examples/example_rows.py
@@ -0,0 +1,42 @@
+from chafa import *
+from chafa.loader import Loader
+
+FONT_WIDTH  = 11
+FONT_HEIGHT = 24
+
+# Init canvas config
+config = CanvasConfig()
+
+# Set canvas height, width and cell geometry
+config.height = 30
+config.width  = 30
+
+# Set the canvas cell geometry
+config.cell_height = FONT_HEIGHT
+config.cell_width  = FONT_WIDTH
+
+# Open image with the loader
+image = Loader("./snake.jpg")
+
+width      = image.width
+height     = image.height
+rowstride  = image.rowstride
+
+# Calculate the appropriate geometry for the canvas
+config.calc_canvas_geometry(width, height, FONT_WIDTH/FONT_HEIGHT)
+
+# Init the canvas
+canvas = Canvas(config)
+
+# Draw to canvas
+canvas.draw_all_pixels(
+    image.pixel_type,
+    image.get_pixels(),
+    width,
+    height,
+    rowstride
+)
+
+# Write picture
+for row in canvas.print_rows(fallback=True):
+    print(row.decode())
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 531f333..4d9fcdf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
 
 [project]
 name    = "chafa.py"
-version = "1.1.2"
+version = "1.2.0"
 license = "LGPL-3.0"
 
 authors = [
@@ -32,22 +32,24 @@ classifiers = [
 
 [tool.cibuildwheel]
 build = "cp3*-*"
-skip  = ["*-manylinux_i686", "*-musllinux_*", "*-win32"]
+skip  = ["*-manylinux_i686", "*-musllinux_*", "*-win32", "cp38-macosx_arm64"]
 test-skip = "cp38-macosx*"
+environment-pass = "CHAFA_VERSION"
 
-manylinux-x86_64-image = "manylinux2014"
+manylinux-x86_64-image = "manylinux_2_28"
+manylinux-aarch64-image = "manylinux_2_28"
 
 test-requires = ["pytest", "Pillow"]
 test-command  = "pytest -rP {package}/tests"
 
 
-
-
 [tool.cibuildwheel.linux]
 before-all = [
-    "yum install -y libtool ImageMagick-devel",
+    "dnf group install -y 'Development Tools'",
+    "dnf install -y epel-release",
+    "dnf install -y ImageMagick-devel glib2-devel pkgconf-pkg-config",
     "cd libs",
-    "git clone --branch 1.12.5 https://github.com/hpjansson/chafa libchafa_src",
+    "git clone --branch $CHAFA_VERSION https://github.com/hpjansson/chafa libchafa_src",
     "cd libchafa_src",
     "mkdir m4",
     "cp /usr/share/aclocal/pkg.m4 m4/",
@@ -59,28 +61,34 @@ before-all = [
 
 repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}"
 
-[[tool.cibuildwheel.overrides]]
-select = "*linux_aarch64"
+# [[tool.cibuildwheel.overrides]]
+# select = "*linux_aarch64"
+
+# before-all = [
+#     "yum group install -y 'Development Tools'",
+#     "yum install -y epel-release",
+#     "yum install -y ImageMagick-devel",
+#     "cd libs",
+#     "git clone --branch $CHAFA_VERSION https://github.com/hpjansson/chafa libchafa_src",
+#     "cd libchafa_src",
+#     "mkdir m4",
+#     "cp /usr/share/aclocal/pkg.m4 m4/",
+#     "./autogen.sh --without-tools",
+#     "make",
+#     "cd ../",
+#     "cp libchafa_src/chafa/.libs/*chafa.so linux/"
+# ]
 
-before-all = [
-    "yum install -y libtool ImageMagick-devel",
-    "cd libs",
-    "git clone --branch 1.12.5 https://github.com/hpjansson/chafa libchafa_src",
-    "cd libchafa_src",
-    "mkdir m4",
-    "cp /usr/share/aclocal/pkg.m4 m4/",
-    "./autogen.sh --without-tools",
-    "make",
-    "cd ../",
-    "cp libchafa_src/chafa/.libs/*chafa.so linux/"
-]
 
 [tool.cibuildwheel.macos]
+
+environment = { MACOSX_DEPLOYMENT_TARGET="13.0" }
+
 before-all = [
     "NONINTERACTIVE=1 /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"",
-    "brew install make libtool pkg-config glib automake",
+    "brew install --force --overwrite make imagemagick libtool pkg-config glib automake",
     "ln -s $(which glibtoolize) /usr/local/bin/libtoolize",
-    "git clone --branch 1.12.5 https://github.com/hpjansson/chafa libchafa_src",
+    "git clone --branch $CHAFA_VERSION https://github.com/hpjansson/chafa libchafa_src",
     "cd libchafa_src",
     "./autogen.sh --without-tools",
     "gmake",
@@ -94,10 +102,12 @@ test-command = "MAGICK_HOME=/usr/local/Cellar/imagemagick/ pytest -rP {package}/
 [[tool.cibuildwheel.overrides]]
 select = "*macosx_arm64"
 
+environment = { MACOSX_DEPLOYMENT_TARGET="14.0" }
+
 before-all = [
     "brew install make libtool pkg-config glib automake imagemagick",
     "sudo ln -s $(which glibtoolize) /usr/local/bin/libtoolize",
-    "git clone --branch 1.12.5 https://github.com/hpjansson/chafa libchafa_src",
+    "git clone --branch $CHAFA_VERSION https://github.com/hpjansson/chafa libchafa_src",
     "cd libchafa_src",
     "./autogen.sh --without-tools",
     "make",
diff --git a/src/chafa/__init__.py b/src/chafa/__init__.py
index 05c52bc..ed90825 100644
--- a/src/chafa/__init__.py
+++ b/src/chafa/__init__.py
@@ -1,20 +1,30 @@
-from .chafa import PixelMode
-from .chafa import DitherMode
-from .chafa import CanvasMode
-from .chafa import PixelType
-from .chafa import SymbolTags
-from .chafa import Optimizations
-from .chafa import TermSeq
-from .chafa import ColorSpace
-from .chafa import ColorExtractor
+from .enums import PixelMode
+from .enums import DitherMode
+from .enums import CanvasMode
+from .enums import PixelType
+from .enums import SymbolTags
+from .enums import Optimizations
+from .enums import TermSeq
+from .enums import ColorSpace
+from .enums import ColorExtractor
+from .enums import Passthrough
+from .enums import Tuck
+from .enums import Align
 
-from .chafa import SymbolMap
-from .chafa import ReadOnlySymbolMap
-from .chafa import Canvas
-from .chafa import CanvasConfig
-from .chafa import ReadOnlyCanvasConfig
-from .chafa import TermDb
-from .chafa import TermInfo
-from .chafa import Canvas
+from .symbol_map import SymbolMap
+from .symbol_map import ReadOnlySymbolMap
+
+from .canvas_config import CanvasConfig
+from .canvas_config import ReadOnlyCanvasConfig
+
+from .canvas import Canvas
+
+from .term_db import TermDb
+
+from .term_info import TermInfo
+
+from .placement import Placement
+from .frame import Frame
+from .image import Image
 
 from .chafa import get_device_attributes
diff --git a/src/chafa/canvas.py b/src/chafa/canvas.py
new file mode 100644
index 0000000..a1695f8
--- /dev/null
+++ b/src/chafa/canvas.py
@@ -0,0 +1,960 @@
+from __future__ import annotations
+import ctypes
+from typing import Iterable
+import array
+from typing import Tuple, Union, Generator
+
+from .libraries import _Chafa
+from .canvas_config import ReadOnlyCanvasConfig, CanvasConfig
+from .enums import *
+from .term_info import TermInfo
+from .term_db import TermDb
+from .placement import Placement
+
+class Canvas:
+    def __init__(self, config: CanvasConfig):
+        """
+        :param CanvasConfig|None config: The config to initialise the 
+        canvas with. If None is passed, the canvas will be initialised 
+        with hard-coded defaults.
+
+        :raises TypeError: If term_info is not None or :py:class:`TermInfo`
+        :raises TypeError: If config is not None or :py:class:`CanvasConfig`
+        """
+        # Init config
+        if config is None:
+            _Chafa.chafa_canvas_new.argtypes = [ctypes.c_size_t]
+            config = ctypes.c_size_t(0)
+
+        elif not isinstance(config, CanvasConfig):
+            raise TypeError(f"config must be of type CanvasConfig or None, not {type(config)}")
+
+        else:
+            _Chafa.chafa_canvas_new.argtypes = [ctypes.c_void_p]
+            config = config._canvas_config
+
+        # Init canvas
+        _Chafa.chafa_canvas_new.restype  =  ctypes.c_void_p
+
+        self._canvas = _Chafa.chafa_canvas_new(config)
+
+        # Placement
+        self._placement = None
+
+
+    class GString(ctypes.Structure):
+        _fields_ = [('str',         ctypes.c_char_p),
+                    ('len',           ctypes.c_uint),
+                    ('allocated_len', ctypes.c_uint)]
+
+
+    @property
+    def placement(self) -> Placement:
+        """
+        The :py:class:`Placement` of an :py:class:`Image`
+        containing a :py:class:`Frame` on the canvas. Use 
+        this as an alternative to :py:meth:`draw_all_pixels` 
+        and for control over the image's alignment with :py:class:`Align` 
+        and :py:class:`Tuck`.
+        """
+
+        return self._placement
+    
+
+    @placement.setter
+    def placement(self, new_placement: Placement):
+        self._set_placement(new_placement)
+
+
+    def _set_placement(self, new_placement: Placement):
+        """
+        Bindings for chafa_canvas_set_placement
+        """
+        _Chafa.chafa_canvas_set_placement.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_canvas_set_placement(self._canvas, new_placement._placement)
+    
+
+    def new_similar(self) -> Canvas:
+        """
+        Creates a new canvas configured similarly to this one.
+
+        :rtype: Canvas
+        """
+
+        # types
+        _Chafa.chafa_canvas_new_similar.argtypes = [
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_canvas_new_similar.restype = ctypes.c_void_p
+
+        # Get new pointer
+        new_pointer = _Chafa.chafa_canvas_new_similar(self._canvas)
+
+        # Init canvas
+        new_canvas = Canvas(None)
+
+        # Assign the new pointer to the newly created canvas
+        new_canvas._canvas = new_pointer
+
+        return new_canvas
+
+
+    def peek_config(self) -> ReadOnlyCanvasConfig:
+        """
+        Returns a read only version of the :py:class:`CanvasConfig` 
+        used to initialise the canvas.
+
+        .. attention:: 
+            This :py:class:`ReadOnlyCanvasConfig`'s properties can 
+            be inspected but not changed.
+
+        :rtype: ReadOnlyCanvasConfig
+        """
+
+        # Types
+        _Chafa.chafa_canvas_peek_config.argtypes = [
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_canvas_peek_config.restype = ctypes.c_void_p
+
+        # Get the new pointer
+        new_pointer = _Chafa.chafa_canvas_peek_config(self._canvas)
+
+        # Init RO CanvasConfig
+        config = ReadOnlyCanvasConfig()
+        config._canvas_config = new_pointer
+
+        return config
+
+
+    def _canvas_slice(self, y_slice=slice(None), x_slice=slice(None), axis=0, y=None, x=None):
+        # Get slice of rows
+        # Check slice component types
+
+        # Define the hard stop
+        config = self.peek_config()
+        hard_stop = (config.height, config.width)
+        hard_stop = hard_stop[axis]
+
+        which_slice = (y_slice, x_slice)[axis]
+        
+        # Evaluate defaults
+        start = which_slice.start or 0
+        stop  = which_slice.stop  or hard_stop
+        step  = which_slice.step  or 1
+
+        # Check 0 step
+        if step == 0:
+            raise ValueError("Slice step cannot be zero.")
+        
+        # Evaluate negative start and stop
+        if start < 0:
+            start = hard_stop + start
+        
+        if stop < 0:
+            stop = hard_stop + stop
+
+        # Evaluators for when to stop the while loop
+        def positive_stop_evaluator(index):
+            return index < min(hard_stop, stop)
+
+        def negative_stop_evaluator(index):
+            return index >= 0
+
+        stop_evaluator = positive_stop_evaluator
+
+        # Negative step means we reverse start and stop
+        if step < 0:
+            aux   = start
+            start = stop - 1
+            stop  = aux
+
+            stop_evaluator = negative_stop_evaluator
+
+        if any([not isinstance(val, int) for val in (start, stop, step)]):
+            raise TypeError("Slice indices must be integers or None.")
+
+        # We are slicing over y
+        if axis == 0:
+            y = start
+
+            while stop_evaluator(y):
+                yield self[y, x_slice] if x is None else self[y, x]
+                y += step
+
+            return
+
+        # We are slicing over x
+        if axis == 1:
+            x = start
+
+            while stop_evaluator(x):
+                yield self[y, x]
+                x += step
+
+            return
+
+    def __getitem__(self, pos) -> CanvasInspector:
+        """
+        You can inspect pixels in the canvas by indexing, 
+        similar to if the canvas were a 2d array. When indexing, 
+        the first coordinate represents the row (or y coordinate) 
+        and the second represents the column (or x coordinate).
+
+        When indexing using a single value, a `generator`_ of 
+        relevant :py:class:`CanvasInspector` objects will be 
+        returned, representing each pixel in the given row.
+
+        When indexing using two values, a single :py:class:`CanvasInspector` 
+        will be returned representing the given pixel.
+
+        Slicing is also supported! You can slice either the row or 
+        column coordinates and this will return generators as 
+        expected. For example; if you index using ``[:,3]`` you will 
+        get a generator for each pixel in the 3rd column of the 
+        canvas (0 indexed). 
+        
+        Slicing using ``[3:6,:5]`` will return generators for rows 3 to 5 
+        inclusive. Each of these rows will be represented by a generator 
+        for :py:class:`CanvasInspector` objects representing pixels 0 to 
+        4 inclusive.
+
+        The take-away from this all is that indexing and slicing should work 
+        (mostly) the same as you would expect a 2d array to work. Check out 
+        the tutorial for more details.
+
+        :param int|slice|tuple pos: The position to index
+
+        :rtype: CanvasInspector
+        """
+
+        if isinstance(pos, int):
+            return self[pos, :]
+        
+        if isinstance(pos, slice):
+            return self._canvas_slice(y_slice=pos)
+
+        if not isinstance(pos, tuple):
+            raise TypeError(f"Indices of invalid type. Got {type(pos)}")
+
+        # None of the above means tuple
+
+        # Check for size
+        if len(pos) > 2:
+            raise IndexError(f"Too many indices for Canvas. Canvas is 2-dimensional, but got {len(pos)} indices.")
+        
+        # Empty tuple should return all rows
+        if len(pos) == 0:
+            return self[:]
+        
+        # Exact coordinates
+        if isinstance(pos[0], int) and isinstance(pos[1], int):
+            return CanvasInspector(self, *pos)
+        
+        # Slice of rows and columns
+        if isinstance(pos[0], slice) and isinstance(pos[1], slice):
+            return self._canvas_slice(y_slice=pos[0], x_slice=pos[1])
+        
+        # Slice of rows with exact x coordinate
+        if isinstance(pos[0], slice):
+            return self._canvas_slice(y_slice=pos[0], x=pos[1])
+        
+        # Slice of columns with exact y coordinate
+        if isinstance(pos[1], slice):
+            return self._canvas_slice(x_slice=pos[1], axis=1, y=pos[0])
+
+        raise TypeError(f"Indices of invalid type. Got {type(pos)}")
+
+
+    def _get_char_at(self, x:int, y:int) -> str:
+        """
+        Wrapper for chafa_canvas_get_char_at
+        """
+
+        # Define types
+        _Chafa.chafa_canvas_get_char_at.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_int,
+            ctypes.c_int
+        ]
+
+        _Chafa.chafa_canvas_get_char_at.restype = ctypes.c_wchar
+
+        # Get char
+        char = _Chafa.chafa_canvas_get_char_at(
+            self._canvas,
+            x, y
+        )
+
+        return char
+
+
+    def _set_char_at(self, x:int, y:int, char: str):
+        """
+        Wrapper for chafa_canvas_set_char_at
+        """
+
+        # Define types
+        _Chafa.chafa_canvas_set_char_at.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_int,
+            ctypes.c_int,
+            ctypes.c_wchar
+        ]
+
+        # Set char
+        _Chafa.chafa_canvas_set_char_at(
+            self._canvas,
+            x, y,
+            char
+        )
+
+
+    def _get_colors_at(self, x: int, y:int) -> Tuple[int, int]:
+        """
+        Wrapper for chafa_canvas_get_colors_at
+        """
+
+        # Set types
+        _Chafa.chafa_canvas_get_colors_at.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_int,
+            ctypes.c_int,
+            ctypes.POINTER(ctypes.c_int),
+            ctypes.POINTER(ctypes.c_int)
+        ]
+
+        # Define storage
+        bg_color = ctypes.pointer(ctypes.c_int(0))
+        fg_color = ctypes.pointer(ctypes.c_int(0))
+
+        # Get colors
+        _Chafa.chafa_canvas_get_colors_at(
+            self._canvas,
+            x, y,
+            fg_color,
+            bg_color
+        )
+
+        return fg_color.contents.value, bg_color.contents.value
+
+
+    def _get_raw_colors_at(self, x: int, y:int) -> Tuple[int, int]:
+        """
+        Wrapper for chafa_canvas_get_raw_colors_at
+        """
+
+        # Set types
+        _Chafa.chafa_canvas_get_raw_colors_at.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_int,
+            ctypes.c_int,
+            ctypes.POINTER(ctypes.c_int),
+            ctypes.POINTER(ctypes.c_int)
+        ]
+
+        # Define storage
+        bg_color = ctypes.pointer(ctypes.c_int(0))
+        fg_color = ctypes.pointer(ctypes.c_int(0))
+
+        # Get colors
+        _Chafa.chafa_canvas_get_raw_colors_at(
+            self._canvas,
+            x, y,
+            fg_color,
+            bg_color
+        )
+
+        return fg_color.contents.value, bg_color.contents.value
+
+
+    def _set_colors_at(self, x:int, y: int, fg: int, bg: int):
+        """
+        Wrapper for chafa_canvas_set_colors_at
+        """
+
+        # Set types
+        _Chafa.chafa_canvas_set_colors_at.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_int,
+            ctypes.c_int,
+            ctypes.c_int,
+            ctypes.c_int
+        ]
+
+        # Set colors
+        _Chafa.chafa_canvas_set_colors_at(
+            self._canvas,
+            x, y,
+            fg,
+            bg
+        )
+
+
+    def _set_raw_colors_at(self, x:int, y: int, fg: int, bg: int):
+        """
+        Wrapper for chafa_canvas_set_raw_colors_at
+        """
+
+        # Set types
+        _Chafa.chafa_canvas_set_raw_colors_at.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_int,
+            ctypes.c_int,
+            ctypes.c_int,
+            ctypes.c_int
+        ]
+
+        # Set colors
+        _Chafa.chafa_canvas_set_raw_colors_at(
+            self._canvas,
+            x, y,
+            fg,
+            bg
+        )
+
+
+    def draw_all_pixels(
+        self, 
+        src_pixel_type: PixelType, 
+        src_pixels:     Union[list, Tuple, array.array, ctypes.Array], 
+        src_width:      int, 
+        src_height:     int, 
+        src_rowstride:  int
+    ):
+        """
+        Draws the given src_pixels to the canvas. Depending on your 
+        set :py:class:`PixelMode`, this will be symbols, kitty sequences 
+        or sixel sequences. 
+
+        To output the data after drawing, use the :py:meth:`print` method.
+
+        .. note::
+            Best performance is achieved by passing a :py:class:`ctypes.Array` 
+            for src_pixels. The :py:class:`chafa.loader.Loader` 
+            class provides convenient (and reasonably fast) 
+            methods for this using the `MagickWand 
+            <https://imagemagick.org/script/magick-wand.php>`_ 
+            C-library.
+
+        :param PixelType src_pixel_type: The pixel type of src_pixels. This 
+            will determine what order the color channels will be read in 
+            and whether there is an alpha channel.
+
+        :param list|tuple|array.array|ctypes.Array src_pixels: The 
+            source pixel data. This is a one dimensional array where 
+            every block of 3 (or 4 depending on the :py:class:`PixelType`) 
+            values represents one pixel of the image. The order of the 
+            channels is determined by src_pixel_type.
+
+        :param int src_width:  The width of the source image.
+        :param int src_height: The width of the source image.
+
+        :param int src_rowstride: The number of values in src_image that 
+            represents one line pixels in the source image. Typically this 
+            will be the number of channels in the source image multiplied 
+            by src_width, e.g. for an image with no alpha channel and a 
+            width of 300 pixels, this will be ``3*300``.
+
+        :raises ValueError: if src_width, src_height or src_rowstride 
+            are less than or equal to 0.
+        """
+
+        # Convert src_pixels to appropriate format
+        if isinstance(src_pixels, ctypes.Array):
+            src_pixels = src_pixels
+        else:
+            try:
+                src_pixels = (ctypes.c_uint8 * len(src_pixels)).from_buffer(src_pixels)
+
+            except TypeError:
+                src_pixels = array.array("B", src_pixels)
+                src_pixels = (ctypes.c_uint8 * len(src_pixels)).from_buffer(src_pixels)
+
+        # Make sure types match
+        src_pixel_type = PixelType(src_pixel_type)
+
+        src_width     = int(src_width)
+        src_height    = int(src_height)
+        src_rowstride = int(src_rowstride)
+
+        # Value errors
+        if src_width <= 0:
+            raise ValueError("src_width must be greater than 0")
+
+        if src_height <= 0:
+            raise ValueError("src_height must be greater than 0")
+
+        if src_rowstride <= 0:
+            raise ValueError("src_rowstride must be greater than 0")
+        
+        # Specify types
+        _Chafa.chafa_canvas_draw_all_pixels.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_uint,
+            ctypes.POINTER(ctypes.c_uint8),
+            ctypes.c_uint,
+            ctypes.c_uint,
+            ctypes.c_uint
+        ]
+
+        # Draw pixels
+        _Chafa.chafa_canvas_draw_all_pixels(
+            self._canvas,
+            src_pixel_type,
+            src_pixels,
+            src_width,
+            src_height,
+            src_rowstride,
+        )
+
+
+    def print(self, term_info: TermInfo=None, fallback=False) -> bytes:
+        """
+        Builds a UTF-8 string of terminal control sequences and symbols 
+        representing the canvas' current contents. This can e.g. be 
+        printed to a terminal. The exact choice of escape sequences and 
+        symbols, dimensions, etc. is determined by the configuration 
+        assigned to canvas on its creation.
+
+        All output lines except for the last one will end in a newline.
+
+        :param TermInfo term_info: The :py:class:`TermInfo` that will 
+        provide the control sequences used when printing. If None is 
+        specified, the term_info will be initialised with 
+        :py:meth:`TermDb.detect`
+
+        :param bool fallback: If True, the term_info (the one provided by
+        :py:meth:`TermDb.detect` or the one provided by the user) will 
+        be supplemented with fallback control sequences.
+        """
+
+        term_db = None
+
+        # Check for term info
+        if term_info is None:
+            term_db = TermDb()
+            term_info = term_db.detect()
+
+        elif not isinstance(term_info, TermInfo):
+            raise TypeError(f"term_info must be None or of type TermInfo or None, not {type(term_info)}")
+
+        # Supplement with fallback sequences
+        if fallback:
+            if term_db is None:
+                term_db = TermDb()
+
+            fallback_info = term_db.get_fallback_info()
+            term_info.supplement(fallback_info)
+
+
+
+        _Chafa.chafa_canvas_print.argtypes = [
+            ctypes.c_void_p, 
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_canvas_print.restype  = ctypes.c_void_p
+
+        output = _Chafa.chafa_canvas_print(self._canvas, term_info._term_info)
+        output = self.GString.from_address(output)
+
+        return output.str
+
+
+    def print_rows(self, term_info: TermInfo=None, fallback=False) -> Generator[bytes]:
+        term_db = None
+
+        # Check for term info
+        if term_info is None:
+            term_db = TermDb()
+            term_info = term_db.detect()
+
+        elif not isinstance(term_info, TermInfo):
+            raise TypeError(f"term_info must be None or of type TermInfo or None, not {type(term_info)}")
+
+        # Supplement with fallback sequences
+        if fallback:
+            if term_db is None:
+                term_db = TermDb()
+
+            fallback_info = term_db.get_fallback_info()
+            term_info.supplement(fallback_info)
+
+        config = self.peek_config()
+
+        max_length = config.height
+
+        # Define storage for output with max_length to accomodate all the possible rows
+        output_array = ctypes.POINTER(ctypes.POINTER(self.GString) * max_length)()
+        output_rows = ctypes.c_int()
+
+        _Chafa.chafa_canvas_print_rows.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_void_p,
+            ctypes.POINTER(ctypes.POINTER(ctypes.POINTER(self.GString)*max_length)),
+            ctypes.POINTER(ctypes.c_int)
+        ]
+
+        _Chafa.chafa_canvas_print_rows(self._canvas, term_info._term_info, output_array, output_rows)
+
+        # Create a generator for the output
+        current_row = 0
+        while current_row < output_rows.value:
+            yield output_array.contents[current_row].contents.str
+            current_row += 1
+
+
+
+
+class CanvasInspector:
+    def __init__(self, canvas: Canvas, y: int, x: int):
+        # Get the configured height and width of the canvas
+        canvas_config = canvas.peek_config()
+        width  = canvas_config.width
+        height = canvas_config.height
+
+        # Check if x and y are within bounds
+        if (
+            (x >= width or y >= height)
+            or (x < 0 and width  < abs(x)) # If we have a negative we need to make sure it 
+            or (y < 0 and height < abs(y)) # is not going past the start of the canvas
+        ):
+            raise ValueError(
+                f"Coordinates ({x},{y}) are out of bounds for canvas with dimensions {width}x{height}."
+            )
+        
+        self._canvas = canvas
+
+        # Use modulo to cast negatives into positives
+        self._x = x % width
+        self._y = y % height
+
+
+    # === FG COLOR ===
+
+    @staticmethod
+    def packed_8bit_to_tuple(color: int) -> Tuple[int, int, int]:
+        """
+        A static method that takes an integer representing a packed
+        8bit RGB value and converts it into a tuple (R,G,B)
+
+        :rtype: tuple[int, int, int]
+        """
+
+        # Convert to bytes
+        bit_length = 3
+        order      = "big"
+        
+        color = color.to_bytes(bit_length, order)
+
+        return (color[0], color[1], color[2])
+
+    @staticmethod
+    def tuple_to_packed_8bit(color_tuple: Tuple[int, int, int]) -> int:
+        """
+        A static method that takes a tuple (R,G,B) representing a color
+        and converts it into a packed 8bit RGB value represented by an
+        integer.
+
+        :rtype: tuple[int, int, int]
+        """
+
+        offset = 1
+        color  = 0
+
+        # Convert color to packed bytes
+        for col in color_tuple[::-1]:
+            col = int(col)
+
+            if 255 < col or col < 0:
+                raise ValueError("Each value of color must be in the range [0,255]")
+            
+            color  += offset * col
+            offset *= 16**2
+
+        return color
+        
+
+    @property
+    def fg_color(self) -> Union[Tuple[int, int, int], None]:
+        """
+        :type: tuple[int, int, int] | None
+
+        The foreground color at the inspector's pixel. The color 
+        is represented as None if transparent or a tuple of 3 
+        integers in range [0,255], representing the color in 
+        (R, G, B) format.
+
+        For double-width characters, both cells will be set to the 
+        same color.
+
+        :raises TypeError:  if fg_color is not an :py:class:`Iterable` 
+            other than :py:class:`str`.
+        :raises ValueError: if fg_color is not None and does not contain 
+            exactly 3 values. 
+        :raises ValueError: if fg_color contains a value greater than 255 
+            or less than 0.
+        """
+        # Get the color at pixel
+        color = self._canvas._get_colors_at(self.x, self.y)[0]
+
+        # Return None for transparency
+        if color == -1:
+            return None
+
+        # convert to tuple
+        return self.packed_8bit_to_tuple(color)
+
+
+    @fg_color.setter
+    def fg_color(self, fg_color: Tuple[int, int, int]):
+        # Remove foreground if we get none
+        if fg_color is None:
+            self.remove_foreground()
+            return
+    	
+        # Check types
+        if isinstance(fg_color, str):
+            raise TypeError(f"fg_color must not be a string")
+
+        if not isinstance(fg_color, Iterable):
+            raise TypeError(f"fg_color must be iterable, not {type(fg_color)}")
+
+        if len(fg_color) != 3:
+            raise ValueError("fg_color must have exactly 3 values")
+
+        color = self.tuple_to_packed_8bit(fg_color)
+
+        bg_color = self.bg_color
+
+        if bg_color is None:
+            bg_color = -1
+
+        else:
+            bg_color = self.tuple_to_packed_8bit(bg_color)
+
+        self._canvas._set_colors_at(self.x, self.y, color, bg_color)
+
+    @property
+    def raw_fg_color(self) -> int:
+        """
+        :type: int
+
+        The raw foreground color at the inspector's pixel. The colors 
+        are -1 for transparency, a packed 8-bit RGB value 
+        (``0x00RRGGBB``) in truecolor mode, or the raw pen value (0-255) 
+        in indexed modes.
+        """
+        # Get the color at pixel
+        color = self._canvas._get_raw_colors_at(self.x, self.y)[0]
+
+        return int(color)
+
+
+    @raw_fg_color.setter
+    def raw_fg_color(self, fg_color: int):
+
+        if fg_color < -1 or fg_color > 0xFFFFFF:
+            raise ValueError("color must be in range [-1, 0xFFFFFF]")
+
+        self._canvas._set_raw_colors_at(self.x, self.y, fg_color, self.raw_bg_color)
+
+
+    # === BG COLOR ===
+    
+    @property
+    def bg_color(self) -> Union[Tuple[int, int, int], None]:
+        """
+        :type: tuple[int, int, int] | None
+
+        The background color at the inspector's pixel. The color is 
+        represented as None if transparent or a tuple of 3 integers 
+        in range [0,255], representing the color in (R, G, B) format.
+
+        For double-width characters, both cells will be set to the same color.
+
+        :raises TypeError:  if bg_color is not an :py:class:`Iterable` 
+            other than :py:class:`str`.
+        :raises ValueError: if bg_color is not None and does not 
+            contain exactly 3 values. 
+        :raises ValueError: if bg_color contains a value greater than 255 
+            or less than 0.
+        """
+        # Get the color at pixel
+        color = self._canvas._get_colors_at(self.x, self.y)[1]
+
+        if color == -1:
+            return None
+
+        # convert to tuple
+        return self.packed_8bit_to_tuple(color)
+    
+    @bg_color.setter
+    def bg_color(self, bg_color: Tuple[int, int, int]):
+
+        # Remove background if we get none
+        if bg_color is None:
+            self.remove_background()
+            return
+
+        # Check types
+        if isinstance(bg_color, str):
+            raise TypeError(f"bg_color must not be a string")
+
+        if not isinstance(bg_color, Iterable):
+            raise TypeError(f"bg_color must be iterable, not {type(bg_color)}")
+
+        if len(bg_color) != 3:
+            raise ValueError("bg_color must have exactly 3 values")
+
+        color  = self.tuple_to_packed_8bit(bg_color)
+
+        fg_color = self.fg_color
+
+        if fg_color is None:
+            fg_color = -1
+
+        else:
+            fg_color = self.tuple_to_packed_8bit(fg_color)
+
+        self._canvas._set_colors_at(self.x, self.y, fg_color, color)
+
+    @property
+    def raw_bg_color(self) -> int:
+        """
+        :type: int
+
+        The raw background color at the inspector's pixel. The colors 
+        are -1 for transparency, a packed 8-bit RGB value 
+        (``0x00RRGGBB``) in truecolor mode, or the raw pen value (0-255) 
+        in indexed modes.
+        """
+        # Get the color at pixel
+        color = self._canvas._get_raw_colors_at(self.x, self.y)[1]
+
+        return int(color)
+
+
+    @raw_bg_color.setter
+    def raw_bg_color(self, bg_color: int):
+
+        if bg_color < -1 or bg_color > 0xFFFFFF:
+            raise ValueError("color must be in range [-1, 0xFFFFFF]")
+
+        self._canvas._set_raw_colors_at(self.x, self.y, self.raw_fg_color, bg_color)
+
+    # === CHAR ===
+
+    @property
+    def char(self) -> str:
+        """
+        :type: str
+
+        The character at the inspector's pixel. For double-width 
+        characters, the leftmost cell must contain the character 
+        and the cell to the right of it will automatically be set 
+        to 0.
+
+        :raises ValueError: if char is not of length 1.
+        """
+        return self._canvas._get_char_at(self.x, self.y)
+
+    @char.setter
+    def char(self, char: str):
+        char = str(char)
+
+        if len(char) != 1:
+            raise ValueError(f"char must be of length 1")
+
+        self._canvas._set_char_at(self.x, self.y, char)
+
+
+    # === Coordinates ===
+
+    @property
+    def x(self) -> int:
+        """
+        :type: int
+
+        The x coordinate of the inspector.
+
+        :raises ValueError: if x is not less than the height of the canvas.
+        """
+        return self._x
+
+    @x.setter
+    def x(self, value: int):
+        width = self._canvas.peek_config().width
+
+        value = int(value)
+
+        if value >= width:
+            raise ValueError(
+                f"x-coordinate {value} is out of bounds for canvas with width {width}."
+            )
+        
+        self._x = value
+
+    @property
+    def y(self) -> int:
+        """
+        :type: int
+
+        The y coordinate of the inspector.
+
+        :raises ValueError: if y is not less than the height of the canvas.
+        """
+        return self._y
+
+    @y.setter
+    def y(self, value: int):
+        height = self._canvas.peek_config().height
+
+        value = int(value)
+
+        if value >= height:
+            raise ValueError(
+                f"y-coordinate {value} is out of bounds for canvas with width {height}."
+            )
+        
+        self._y = value
+
+    
+    def remove_background(self):
+        """
+        A function that sets the background color at the 
+        inspectors pixel to be transparent.
+        """
+
+        fg_color = self.fg_color
+
+        if fg_color is None:
+            fg_color = -1
+
+        else:
+            fg_color = self.tuple_to_packed_8bit(fg_color)
+
+        self._canvas._set_colors_at(self.x, self.y, fg_color, -1)
+
+    def remove_foreground(self):
+        """
+        A function that sets the foreground color at the 
+        inspectors pixel to be transparent.
+        """
+
+        bg_color = self.bg_color
+
+        if bg_color is None:
+            bg_color = -1
+
+        else:
+            bg_color = self.tuple_to_packed_8bit(bg_color)
+
+        self._canvas._set_colors_at(self.x, self.y, -1, bg_color)
diff --git a/src/chafa/canvas_config.py b/src/chafa/canvas_config.py
new file mode 100644
index 0000000..0590c12
--- /dev/null
+++ b/src/chafa/canvas_config.py
@@ -0,0 +1,1319 @@
+from __future__ import annotations
+import ctypes
+from typing import Tuple, Iterable
+
+from .libraries import _Chafa
+from .symbol_map import ReadOnlySymbolMap, SymbolMap
+from .enums import *
+
+def packed_8bit_to_tuple(color: int) -> Tuple[int, int, int]:
+    """
+    A static method that takes an integer representing a packed
+    8bit RGB value and converts it into a tuple (R,G,B)
+
+    :rtype: tuple[int, int, int]
+    """
+
+    # Convert to bytes
+    bit_length = 3
+    order      = "big"
+    
+    color = color.to_bytes(bit_length, order)
+
+    return (color[0], color[1], color[2])
+
+def tuple_to_packed_8bit(color_tuple: Tuple[int, int, int]) -> int:
+    """
+    A static method that takes a tuple (R,G,B) representing a color
+    and converts it into a packed 8bit RGB value represented by an
+    integer.
+
+    :rtype: tuple[int, int, int]
+    """
+
+    offset = 1
+    color  = 0
+
+    # Convert color to packed bytes
+    for col in color_tuple[::-1]:
+        col = int(col)
+
+        if 255 < col or col < 0:
+            raise ValueError("Each value of color must be in the range [0,255]")
+        
+        color  += offset * col
+        offset *= 16**2
+
+    return color
+
+
+class ReadOnlyCanvasConfig:
+    def __init__(self):
+        # Init config
+        _Chafa.chafa_canvas_config_new.restype = ctypes.c_void_p
+        self._canvas_config = _Chafa.chafa_canvas_config_new()
+
+
+    # === Width & Height property ===
+
+    @property
+    def height(self) -> int:
+        """
+        :type: int
+
+        Sets the config's height in character cells.
+        """
+
+        _, height = self.get_geometry()
+
+        return height
+
+    @property
+    def width(self) -> int:
+        """
+        :type: int
+
+        Sets the config's width in character cells.
+        """
+        width, _ = self.get_geometry()
+
+        return width
+
+
+    # === pixel mode property ===
+
+    @property
+    def pixel_mode(self) -> PixelMode:
+        """
+        :type: PixelMode
+
+        Sets config's stored :py:class:`PixelMode`. 
+        This determines how pixel graphics are rendered 
+        in the output.
+        """
+
+        return self._get_pixel_mode()
+
+
+    # === color extractor property ===
+
+    @property
+    def color_extractor(self) -> ColorExtractor:
+        """
+        :type: ColorExtractor
+
+        The config's stored :py:class:`ColorExtractor`. 
+        This determines how colours are approximated in 
+        character symbol output. e.g. 
+        :py:attr:`PixelMode.CHAFA_PIXEL_MODE_SYMBOLS`.
+        """
+        return self._get_color_extractor()
+
+    
+    # === color space property ===
+
+    @property
+    def color_space(self) -> ColorSpace:
+        """
+        :type: ColorSpace
+
+        The config's stored :py:class:`ColorSpace`.
+        """
+        return self._get_color_space()
+
+
+    # === canvas mode property ===
+
+    @property
+    def canvas_mode(self) -> CanvasMode:
+        """
+        :type: CanvasMode
+
+        Sets config's stored `CanvasMode`. 
+        This determines how colours (and colour control codes) 
+        are used in the output.
+        """
+        return self._get_canvas_mode()
+
+
+    # === preprocessing property ===
+
+    @property
+    def preprocessing(self) -> bool:
+        """
+        :type: bool
+
+        Indicates whether automatic image preprocessing should 
+        be enabled. This allows Chafa to boost contrast and 
+        saturation in an attempt to improve legibility. 
+        """
+        return self._get_preprocessing_enabled()
+
+    
+    # === dither grain width & height ===
+
+    @property
+    def dither_width(self) -> int:
+        """
+        :type: int
+
+        Sets config's stored dither grain width in pixels. These 
+        values can be 1, 2, 4 or 8. 8 corresponds to the size of 
+        an entire character cell. 
+
+        The default is 4 pixels.
+        """
+        width, _, = self._get_dither_grain_size()
+        return width
+
+
+    @property
+    def dither_height(self) -> int:
+        """
+        :type: int
+
+        Sets config's stored dither grain width in pixels. These 
+        values can be 1, 2, 4 or 8. 8 corresponds to the size of 
+        an entire character cell. 
+
+        The default is 4 pixels.
+
+        """
+        _, height, = self._get_dither_grain_size()
+        return height
+
+    
+    # === dither mode property ===
+
+    @property
+    def dither_mode(self) -> DitherMode:
+        """
+        :type: DitherMode
+
+        The config's stored :py:class:`DitherMode`.
+        """
+        return self._get_dither_mode()
+
+
+    # === dither intensity ===
+
+    @property
+    def dither_intensity(self) -> float:
+        """
+        :type: float
+
+        Returns the relative intensity of the dithering 
+        pattern applied during image conversion. 1.0 
+        is the default, corresponding to a moderate intensity.
+        """
+        return self._get_dither_intensity()
+
+
+    # === optimizations ===
+
+    @property
+    def optimizations(self) -> Tuple[Optimizations, ...]:
+        """
+        :type: Tuple[Optimizations, ...] 
+
+        Returns config's optimization flags.
+        When enabled, these may produce more 
+        compact output at the cost of reduced 
+        compatibility and increased CPU use. 
+
+        The flags will be returned as a tuple
+        containing all enabled flags.
+
+        Output quality is unaffected.
+        """
+
+        out = []
+
+        flags = self._get_optimizations()
+
+        # Check if all optimizations are being used
+        if flags == Optimizations.CHAFA_OPTIMIZATION_ALL:
+            return (Optimizations.CHAFA_OPTIMIZATION_ALL, )
+        
+        # Loop to decipher which optimizations are in use
+        for optimization in Optimizations:
+            # Check if we have reached the end
+            if optimization == Optimizations.CHAFA_OPTIMIZATION_NONE:
+                break
+
+            # Check if flag is set
+            if flags & optimization == optimization:
+                out.append(optimization)
+
+        return tuple(map(Optimizations, out))
+
+
+    # === cell width & height ===
+
+    @property
+    def cell_width(self) -> int:
+        """
+        :type: int
+
+        Sets config's cell width in pixels.
+        """
+
+        width, _ = self._get_cell_geometry()
+        return width
+
+
+    @property
+    def cell_height(self) -> int:
+        """
+        :type: int
+
+        Sets config's cell height in pixels.
+        """
+        _, height = self._get_cell_geometry()
+        return height 
+
+
+    # === Transparency threshold ===
+
+    @property
+    def transparency_threshold(self) -> float:
+        """
+        :type: float
+
+        The threshold above which full transparency will be used.
+        """
+        return self._get_transparency_threshold()
+
+
+    # === Work factor ===
+
+    @property
+    def work_factor(self) -> float:
+        """
+        :type: float
+
+        Gets the work/quality tradeoff factor. A higher 
+        value means more time and memory will be spent 
+        towards a higher quality output.
+        """
+        return self._get_work_factor()
+
+    
+    # === fg only ===
+
+    @property
+    def fg_only(self) -> bool:
+        """
+        :type: bool
+
+        Queries whether to use foreground colors only, 
+        leaving the background unmodified in the canvas 
+        output.
+
+        When this is set, the canvas will emit escape codes 
+        to set the foreground color only.
+        
+        .. note::
+
+            This is relevant only when the :py:attr:`pixel_mode` 
+            is set to :py:attr:`PixelMode.CHAFA_PIXEL_MODE_SYMBOLS`.
+        """
+        return self._get_fg_only_enabled()
+
+
+    # === fg and bg colors ===
+
+    @property
+    def fg_color(self) -> Tuple[int, int, int]:
+        """
+        :type: Tuple[int, int, int]
+
+        The assumed foreground color of the output device. This 
+        is used to determine how to apply the foreground pen in 
+        FGBG modes like :py:attr:`CanvasMode.CHAFA_CANVAS_MODE_FGBG`.
+
+        The color is a tuple containing 3 integers in range [0,255] 
+        corresponding to red, green and blue respectively.
+        """
+        # Get the color
+        color = self._get_fg_color()
+
+        # convert to tuple
+        return packed_8bit_to_tuple(color)
+
+    
+    @property
+    def bg_color(self) -> Tuple[int, int, int]:
+        """
+        :type: Tuple[int, int, int]
+
+        The assumed foreground color of the output device. This 
+        is used to determine how to apply the foreground pen in 
+        FGBG modes like :py:attr:`CanvasMode.CHAFA_CANVAS_MODE_FGBG`.
+
+        The color is a tuple containing 3 integers in range [0,255] 
+        corresponding to red, green and blue respectively.
+        """
+        # Get the color
+        color = self._get_bg_color()
+
+        # convert to tuple
+        return packed_8bit_to_tuple(color)
+
+
+    @property
+    def passthrough(self) -> Passthrough:
+        # Get passthrough
+        through = self._get_passthrough()
+
+        # Convert to enum
+        return Passthrough(through)
+
+
+    def get_geometry(self) -> Tuple[int, int]:
+        """
+        Get the config's canvas geometry in character cells. 
+        This is the same as inspecting :py:attr:`width` 
+        and :py:attr:`height`
+
+        :rtype: typing.Tuple[int, int] of width and height.
+        """
+
+        # Init pointers
+        width  = ctypes.pointer(ctypes.c_int(-1))
+        height = ctypes.pointer(ctypes.c_int(-1))
+
+        # get geometry
+        _Chafa.chafa_canvas_config_get_geometry.argtypes = [
+            ctypes.c_void_p,
+            ctypes.POINTER(ctypes.c_int),
+            ctypes.POINTER(ctypes.c_int)
+        ]
+
+        _Chafa.chafa_canvas_config_get_geometry(
+            self._canvas_config,
+            width,
+            height
+        )
+
+        return width.contents.value, height.contents.value
+
+    def _get_fg_only_enabled(self) -> bool:
+        """
+        Wrapper for chafa_canvas_config_get_fg_only_enabled
+        """
+
+        # Set types
+        _Chafa.chafa_canvas_config_get_fg_only_enabled.argtypes = [
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_canvas_config_get_fg_only_enabled.restype = ctypes.c_bool
+
+        # Get fg_only 
+        fg_only = _Chafa.chafa_canvas_config_get_fg_only_enabled(self._canvas_config)
+
+        return fg_only
+
+
+    def _get_fg_color(self) -> int:
+        """
+        Wrapper for chafa_canvas_config_get_fg_color
+        """
+
+        # Set types
+        _Chafa.chafa_canvas_config_get_fg_color.argtypes = [
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_canvas_config_get_fg_color.restype = ctypes.c_uint32
+
+        # Get fg_color
+        color = _Chafa.chafa_canvas_config_get_fg_color(
+            self._canvas_config 
+        )
+
+        return color
+
+    
+    def _get_bg_color(self) -> int:
+        """
+        Wrapper for chafa_canvas_config_get_bg_color
+        """
+
+        # Set types
+        _Chafa.chafa_canvas_config_get_bg_color.argtypes = [
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_canvas_config_get_bg_color.restype = ctypes.c_uint32
+
+        # Get bg_color
+        color = _Chafa.chafa_canvas_config_get_bg_color(
+            self._canvas_config 
+        )
+
+        return color
+
+    
+    def _get_transparency_threshold(self) -> float:
+        """
+        Wrapper for chafa_canvas_config_get_transparency_threshold
+        """
+
+        # Set types
+        _Chafa.chafa_canvas_config_get_transparency_threshold.argtypes = [
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_canvas_config_get_transparency_threshold.restype = ctypes.c_float
+
+        # Get threshold
+        threshold = _Chafa.chafa_canvas_config_get_transparency_threshold(self._canvas_config)
+
+        return threshold
+
+
+    def _get_work_factor(self) -> float:
+        """
+        Wrapper for chafa_canvas_config_get_work_factor
+        """
+
+        # Set types
+        _Chafa.chafa_canvas_config_get_work_factor.argtypes = [
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_canvas_config_get_work_factor.restype = ctypes.c_float
+
+        # Get factor
+        factor = _Chafa.chafa_canvas_config_get_work_factor(self._canvas_config)
+
+        return factor
+
+
+    def _get_pixel_mode(self) -> PixelMode:
+        """
+        Wrapper for chafa_canvas_config_get_pixel_mode
+        """
+
+        # Set types
+        _Chafa.chafa_canvas_config_get_pixel_mode.argtypes = [ctypes.c_void_p]
+        _Chafa.chafa_canvas_config_get_pixel_mode.restype  = ctypes.c_uint
+
+        # Get mode
+        pixel_mode = _Chafa.chafa_canvas_config_get_pixel_mode(self._canvas_config)
+
+        return PixelMode(pixel_mode)
+
+    
+    def _get_dither_grain_size(self) -> Tuple[int, int]:
+        """
+        Wrapper for chafa_canvas_config_get_dither_grain_size
+        """
+
+        # Specify types
+        _Chafa.chafa_canvas_config_get_dither_grain_size.argtypes = [
+            ctypes.c_void_p,
+            ctypes.POINTER(ctypes.c_int),
+            ctypes.POINTER(ctypes.c_int)
+        ]
+
+        width_out  = ctypes.pointer(ctypes.c_int(0))
+        height_out = ctypes.pointer(ctypes.c_int(0))
+
+        # Set grain size
+        _Chafa.chafa_canvas_config_get_dither_grain_size(self._canvas_config, width_out, height_out)
+
+        return (width_out.contents.value, height_out.contents.value)
+
+    
+    def _get_cell_geometry(self) -> Tuple[int, int]:
+        """
+        Wrapper for chafa_canvas_config_get_dither_grain_size
+        """
+
+        # Specify types
+        _Chafa.chafa_canvas_config_get_cell_geometry.argtypes = [
+            ctypes.c_void_p,
+            ctypes.POINTER(ctypes.c_int),
+            ctypes.POINTER(ctypes.c_int)
+        ]
+
+        width_out  = ctypes.pointer(ctypes.c_int(0))
+        height_out = ctypes.pointer(ctypes.c_int(0))
+
+        # Set grain size
+        _Chafa.chafa_canvas_config_get_cell_geometry(self._canvas_config, width_out, height_out)
+
+        return (width_out.contents.value, height_out.contents.value)
+
+    
+    def _get_dither_mode(self) -> DitherMode:
+        """
+        Wrapper for chafa_canvas_config_get_cnavas_mode
+        """
+
+        # Specify types
+        _Chafa.chafa_canvas_config_get_dither_mode.argtypes = [ctypes.c_void_p]
+        _Chafa.chafa_canvas_config_get_dither_mode.restype  = ctypes.c_uint
+
+        # Get mode
+        mode = _Chafa.chafa_canvas_config_get_dither_mode(self._canvas_config)
+
+        return DitherMode(mode)
+
+
+    def _get_dither_intensity(self) -> float:
+        """
+        Wrapper for chafa_canvas_config_get_dither_intensity
+        """
+
+        # Specify types
+        _Chafa.chafa_canvas_config_get_dither_intensity.argtypes = [
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_canvas_config_get_dither_intensity.restype = ctypes.c_float
+
+        # Get intensity
+        intensity = _Chafa.chafa_canvas_config_get_dither_intensity(self._canvas_config)
+
+        return intensity
+
+    
+    def _get_canvas_mode(self) -> CanvasMode:
+        """
+        Wrapper for chafa_canvas_config_get_cnavas_mode
+        """
+
+        # Specify types
+        _Chafa.chafa_canvas_config_get_canvas_mode.argtypes = [ctypes.c_void_p]
+        _Chafa.chafa_canvas_config_get_canvas_mode.restype  = ctypes.c_uint
+
+        # Get mode
+        mode = _Chafa.chafa_canvas_config_get_canvas_mode(self._canvas_config)
+
+        return CanvasMode(mode)
+
+    
+    def _get_color_extractor(self) -> ColorExtractor:
+        """
+        Wrapper for chafa_canvas_config_get_color_extractor
+        """
+
+        # Set types
+        _Chafa.chafa_canvas_config_get_color_extractor.argtypes = [ctypes.c_void_p]
+        _Chafa.chafa_canvas_config_get_color_extractor.restype  = ctypes.c_uint
+
+        # Get extractor
+        extractor = _Chafa.chafa_canvas_config_get_color_extractor(self._canvas_config)
+
+        return ColorExtractor(extractor)
+
+    
+    def _get_color_space(self) -> ColorSpace:
+        """
+        Wrapper for chafa_canvas_config_get_color_space
+        """
+
+        # Set types
+        _Chafa.chafa_canvas_config_get_color_space.argtypes = [ctypes.c_void_p]
+        _Chafa.chafa_canvas_config_get_color_space.restype  = ctypes.c_uint
+
+        # Get space
+        space = _Chafa.chafa_canvas_config_get_color_space(self._canvas_config)
+
+        return ColorSpace(space)
+
+
+    def _get_preprocessing_enabled(self) -> bool:
+        """
+        Wrapper for chafa_canvas_config_get_preprocessing_enabled
+        """
+
+        # Set types
+        _Chafa.chafa_canvas_config_get_preprocessing_enabled.argtypes = [ctypes.c_void_p]
+        _Chafa.chafa_canvas_config_get_preprocessing_enabled.restype  = ctypes.c_bool
+
+        # Get preprocessing value 
+        preprocessing = _Chafa.chafa_canvas_config_get_preprocessing_enabled(self._canvas_config)
+
+        return preprocessing
+
+
+    def _get_optimizations(self) -> int:
+        """
+        Wrapper for chafa_canvas_config_get_optimizations
+        """
+
+        # Specify types
+        _Chafa.chafa_canvas_config_get_optimizations.argtypes = [
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_canvas_config_get_optimizations.restype = ctypes.c_uint
+
+        return _Chafa.chafa_canvas_config_get_optimizations(self._canvas_config)
+
+    
+    def _get_passthrough(self) -> int:
+        """
+        Wrapper for chafa_canvas_config_get_passthrough
+        """
+
+        # Specify types
+        _Chafa.chafa_canvas_config_get_passthrough.argtypes = [
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_canvas_config_get_passthrough.restype = ctypes.c_uint
+
+        return _Chafa.chafa_canvas_config_get_passthrough(self._canvas_config)
+
+
+    def peek_symbol_map(self) -> ReadOnlySymbolMap:
+        """
+        Wrapper for chafa_canvas_config_peek_symbol_map
+        """
+
+        # define types
+        _Chafa.chafa_canvas_config_peek_symbol_map.argtypes = [
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_canvas_config_peek_symbol_map.restype = ctypes.c_void_p
+
+        # Get new pointer
+        new_pointer = _Chafa.chafa_canvas_config_peek_symbol_map(self._canvas_config)
+
+        # Init RO SymbolMap
+        symbol_map = ReadOnlySymbolMap()
+        symbol_map._symbol_map = new_pointer
+
+        return symbol_map
+
+
+
+class CanvasConfig(ReadOnlyCanvasConfig):
+    # === Width & Height property ===
+    @ReadOnlyCanvasConfig.height.setter
+    def height(self, value: int):
+        value = int(value)
+
+        self._set_geometry(self.width, value)
+
+    @ReadOnlyCanvasConfig.width.setter
+    def width(self, value: int):
+        value = int(value)
+
+        self._set_geometry(value, self.height)
+
+
+    # === pixel mode property ===
+    @ReadOnlyCanvasConfig.pixel_mode.setter
+    def pixel_mode(self, mode: PixelMode):
+        mode = PixelMode(mode)
+
+        self._set_pixel_mode(mode)
+
+    
+    # === color extractor property ===
+
+    @ReadOnlyCanvasConfig.color_extractor.setter
+    def color_extractor(self, extractor: ColorExtractor):
+        extractor = ColorExtractor(extractor)
+
+        self._set_color_extractor(extractor)
+
+
+    # === color space property ===
+
+    @ReadOnlyCanvasConfig.color_space.setter
+    def color_space(self, space: ColorSpace):
+        space = ColorSpace(space)
+
+        self._set_color_space(space)
+
+
+    # === canvas mode property ===
+
+    @ReadOnlyCanvasConfig.canvas_mode.setter
+    def canvas_mode(self, mode: CanvasMode):
+        mode = CanvasMode(mode)
+
+        self._set_canvas_mode(mode)
+
+
+    # === preprocessing property ===
+
+    @ReadOnlyCanvasConfig.preprocessing.setter
+    def preprocessing(self, preproc: bool):
+        if preproc is None:
+            raise TypeError("preprocessing must not be None")
+
+        preproc = bool(preproc)
+
+        self._set_preprocessing_enabled(preproc)
+    
+    
+    # === dither grain width & height ===
+    
+    @ReadOnlyCanvasConfig.dither_width.setter
+    def dither_width(self, width: int):
+        width = int(width)
+
+        self._set_dither_grain_size(width, self.dither_height)
+
+    
+    @ReadOnlyCanvasConfig.dither_height.setter
+    def dither_height(self, height: int):
+        height = int(height)
+
+        self._set_dither_grain_size(self.dither_width, height)
+
+
+    # === dither mode property ===
+    
+    @ReadOnlyCanvasConfig.dither_mode.setter
+    def dither_mode(self, mode: DitherMode):
+        mode = DitherMode(mode)
+
+        self._set_dither_mode(mode)
+
+    
+    # === dither intensity ===
+
+    @ReadOnlyCanvasConfig.dither_intensity.setter
+    def dither_intensity(self, intensity: float):
+        intensity = float(intensity)
+
+        if intensity < 0:
+            raise ValueError("Dither intensity must be positive.")
+
+        self._set_dither_intensity(intensity)
+    
+
+    # === optimizations ===
+
+    @ReadOnlyCanvasConfig.optimizations.setter
+    def optimizations(self, optimizations: Tuple):
+        # Convert optimizations to proper type to
+        # raise appropriate errors
+        if not isinstance(optimizations, Iterable):
+            raise TypeError(f"optimizations must be iterable, not {type(optimizations)}")
+
+        # Or all optimizations together
+        compounded = 0
+        for flag in optimizations:
+            # Convert flag to Optimizations to get error if invalid
+            flag = Optimizations(flag)
+
+            compounded |= flag
+
+        self._set_optimizations(compounded)
+
+
+    # === cell width & height ===
+    
+    @ReadOnlyCanvasConfig.cell_width.setter
+    def cell_width(self, width: int):
+        width = int(width)
+
+        self._set_cell_geometry(width, self.cell_height)
+
+    
+    @ReadOnlyCanvasConfig.cell_height.setter
+    def cell_height(self, height: int):
+        height = int(height)
+
+        self._set_cell_geometry(self.cell_width, height)
+
+
+    # === Transparency threshold ===
+    
+    @ReadOnlyCanvasConfig.transparency_threshold.setter
+    def transparency_threshold(self, threshold: float):
+        threshold = float(threshold)
+
+        if 1 < threshold or threshold < 0:
+            raise ValueError("Transparency threshold must be in range [0,1]")
+
+        self._set_transparency_threshold(threshold)
+
+
+    # === Work factor ===
+
+    @ReadOnlyCanvasConfig.work_factor.setter
+    def work_factor(self, factor: float):
+        factor = float(factor)
+
+        if 1 < factor or factor < 0:
+            raise ValueError("Work factor must be in range [0,1]")
+
+        self._set_work_factor(factor)
+        
+
+    # === fg only ===
+
+    @ReadOnlyCanvasConfig.fg_only.setter
+    def fg_only(self, fg_only: bool):
+        if fg_only is None:
+            raise TypeError("fg_only must not be None")
+
+        fg_only = bool(fg_only)
+
+        self._set_fg_only_enabled(fg_only)
+        
+
+    # === fg and bg colors ===
+
+    @ReadOnlyCanvasConfig.fg_color.setter
+    def fg_color(self, fg_color: Tuple[int, int, int]):
+
+        if isinstance(fg_color, str):
+            raise TypeError(f"fg_color must not be a string")
+
+        if not isinstance(fg_color, Iterable):
+            raise TypeError(f"fg_color must be iterable, not {type(fg_color)}")
+
+        if len(fg_color) != 3:
+            raise ValueError("fg_color must have exactly 3 values")
+
+        color  = tuple_to_packed_8bit(fg_color)
+
+        self._set_fg_color(color)
+
+
+    @ReadOnlyCanvasConfig.bg_color.setter
+    def bg_color(self, bg_color: Tuple[int, int, int]):
+
+        if isinstance(bg_color, str):
+            raise TypeError(f"bg_color must not be a string")
+
+        if not isinstance(bg_color, Iterable):
+            raise TypeError(f"bg_color must be iterable, not {type(bg_color)}")
+
+        if len(bg_color) != 3:
+            raise ValueError("bg_color must have exactly 3 values")
+
+        color = tuple_to_packed_8bit(bg_color)
+
+        self._set_bg_color(color)
+
+    
+    @ReadOnlyCanvasConfig.passthrough.setter
+    def passthrough(self, through: Passthrough):
+        through = Passthrough(through)
+
+        self._set_passthrough(through)
+
+
+
+    def copy(self) -> CanvasConfig:
+        """
+        Creates a new :py:class:`CanvasConfig` that is a copy of this config.
+
+        :rtype: CanvasConfig
+        """
+
+        # define types
+        _Chafa.chafa_canvas_config_copy.argtypes = [
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_canvas_config_copy.restype = ctypes.c_void_p
+
+        # Init new config
+        new_config = CanvasConfig()
+
+        # Get new pointer
+        config_copy = _Chafa.chafa_canvas_config_copy(self._canvas_config)
+
+        new_config._canvas_config = config_copy
+
+        return new_config
+
+
+    def _set_geometry(self, width: int, height: int):
+        """
+        Wrapper for chafa_canvas_config_set_geometry
+        """
+
+        _Chafa.chafa_canvas_config_set_geometry.argtypes = [
+            ctypes.c_void_p, 
+            ctypes.c_uint, 
+            ctypes.c_uint
+        ]
+
+        _Chafa.chafa_canvas_config_set_geometry(
+            self._canvas_config, 
+            width, 
+            height
+        )
+
+
+    def _set_fg_only_enabled(self, fg_only: bool):
+        """
+        Wrapper for chafa_canvas_config_set_fg_only_enabled
+        """
+
+        # Set types
+        _Chafa.chafa_canvas_config_set_fg_only_enabled.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_bool
+        ]
+
+        # Set threshold
+        _Chafa.chafa_canvas_config_set_fg_only_enabled(
+            self._canvas_config,
+            fg_only
+        )
+
+
+    def _set_fg_color(self, fg_color: int):
+        """
+        Wrapper for chafa_canvas_config_set_fg_color
+        """
+
+        # Set types
+        _Chafa.chafa_canvas_config_set_fg_color.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_uint32
+        ]
+
+        # Set fg_color
+        color = _Chafa.chafa_canvas_config_set_fg_color(
+            self._canvas_config,
+            fg_color
+        )
+
+
+    def _set_bg_color(self, bg_color: int):
+        """
+        Wrapper for chafa_canvas_config_set_bg_color
+        """
+
+        # Set types
+        _Chafa.chafa_canvas_config_set_bg_color.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_uint32
+        ]
+
+        # Set bg_color
+        color = _Chafa.chafa_canvas_config_set_bg_color(
+            self._canvas_config,
+            bg_color
+        )
+
+
+    def _set_transparency_threshold(self, threshold: float):
+        """
+        Wrapper for chafa_canvas_config_set_transparency_threshold
+        """
+
+        # Set types
+        _Chafa.chafa_canvas_config_set_transparency_threshold.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_float
+        ]
+
+        # Set threshold
+        _Chafa.chafa_canvas_config_set_transparency_threshold(
+            self._canvas_config,
+            threshold
+        )
+
+
+    def _set_work_factor(self, factor: float):
+        """
+        Wrapper for chafa_canvas_config_set_work_factor
+        """
+
+        # Set types
+        _Chafa.chafa_canvas_config_set_work_factor.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_float
+        ]
+
+        # Set factor
+        _Chafa.chafa_canvas_config_set_work_factor(
+            self._canvas_config,
+            factor
+        )
+
+    
+    def _set_pixel_mode(self, mode: PixelMode):
+        """
+        Wrapper for chafa_canvas_config_set_pixel_mode
+        """
+
+        # Specify types
+        _Chafa.chafa_canvas_config_set_pixel_mode.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_uint
+        ]
+
+        _Chafa.chafa_canvas_config_set_pixel_mode(self._canvas_config, mode)
+
+
+    def _set_dither_grain_size(self, width: int, height: int):
+        """
+        Wrapper for chafa_canvas_config_set_dither_grain_size
+        """
+
+        # Specify types
+        _Chafa.chafa_canvas_config_set_dither_grain_size.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_int,
+            ctypes.c_int
+        ]
+
+        # Set grain size
+        _Chafa.chafa_canvas_config_set_dither_grain_size(self._canvas_config, width, height)
+
+
+    def _set_cell_geometry(self, width: int, height: int):
+        """
+        Wrapper for chafa_canvas_config_set_cell_geometry
+        """
+
+        # Specify types
+        _Chafa.chafa_canvas_config_set_cell_geometry.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_int,
+            ctypes.c_int
+        ]
+
+        # Set grain size
+        _Chafa.chafa_canvas_config_set_cell_geometry(self._canvas_config, width, height)
+
+    
+    def _set_dither_mode(self, mode: DitherMode):
+        """
+            wrapper for chafa_canvas_config_set_dither_mode
+        """
+
+        # Specify types
+        _Chafa.chafa_canvas_config_set_dither_mode.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_uint
+        ]
+
+        _Chafa.chafa_canvas_config_set_dither_mode(self._canvas_config, mode)
+
+
+    def _set_dither_intensity(self, intensity: float):
+        """
+        Wrapper for chafa_canvas_config_set_dither_intensity
+        """
+
+        # Specify types
+        _Chafa.chafa_canvas_config_set_dither_intensity.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_float
+        ]
+
+        # Set intensity
+        _Chafa.chafa_canvas_config_set_dither_intensity(
+            self._canvas_config,
+            intensity
+        )
+
+    
+    def _set_canvas_mode(self, mode: CanvasMode):
+        """
+            wrapper for chafa_canvas_config_set_canvas_mode
+        """
+
+        # Specify types
+        _Chafa.chafa_canvas_config_set_canvas_mode.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_uint
+        ]
+
+        _Chafa.chafa_canvas_config_set_canvas_mode(self._canvas_config, mode)
+
+    
+    def _set_color_extractor(self, extractor: ColorExtractor):
+        """
+        Wrapper for chafa_canvas_config_set_color_extractor
+        """
+
+        # Specify types
+        _Chafa.chafa_canvas_config_set_color_extractor.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_uint
+        ]
+
+        _Chafa.chafa_canvas_config_set_color_extractor(self._canvas_config, extractor)
+
+    
+    def _set_color_space(self, space: ColorSpace):
+        """
+        Wrapper for chafa_canvas_config_set_color_space
+        """
+
+        # Specify types
+        _Chafa.chafa_canvas_config_set_color_space.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_uint
+        ]
+
+        _Chafa.chafa_canvas_config_set_color_space(self._canvas_config, space)
+
+    
+    def _set_preprocessing_enabled(self, preproc: bool):
+        """
+        Wrapper for chafa_canvas_config_set_preprocessing_enabled
+        """
+
+        # Specify types
+        _Chafa.chafa_canvas_config_set_preprocessing_enabled.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_bool
+        ]
+
+        _Chafa.chafa_canvas_config_set_preprocessing_enabled(self._canvas_config, preproc)
+
+
+    def _set_optimizations(self, optimizations: int):
+        """
+        Wrapper for chafa_canvas_config_set_optimizations
+        """
+
+        # Specify types
+        _Chafa.chafa_canvas_config_set_optimizations.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_uint
+        ]
+
+        # Set optimizations
+        _Chafa.chafa_canvas_config_set_optimizations(
+            self._canvas_config,
+            optimizations
+        )
+
+
+    def _set_passthrough(self, through: Passthrough):
+        """
+        Wrapper for chafa_canvas_config_set_passthrough
+        """
+
+        # Specify types
+        _Chafa.chafa_canvas_config_set_passthrough.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_uint
+        ]
+
+        # Set passthrough
+        _Chafa.chafa_canvas_config_set_passthrough(
+            self._canvas_config,
+            through
+        )
+
+
+    def set_symbol_map(self, symbol_map: SymbolMap):
+        """
+        Assigns a copy of symbol_map to config.
+
+        :param SymbolMap symbol_map: The symbol_map.
+        """
+        
+        if not isinstance(symbol_map, SymbolMap):
+            raise TypeError(f"symbol_map must be a SymbolMap, not {type(symbol_map)}")
+
+        # Specify types
+        _Chafa.chafa_canvas_config_set_symbol_map.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_canvas_config_set_symbol_map(self._canvas_config, symbol_map._symbol_map)
+
+
+    def set_fill_symbol_map(self, fill_symbol_map: SymbolMap):
+        """
+        Assigns a copy of fill_symbol_map to config.
+
+        Fill symbols are assigned according to their overall 
+        foreground to background coverage, disregarding shape. ???
+
+        :param SymbolMap fill_symbol_map: The fill symbol map.
+        """
+
+        if not isinstance(fill_symbol_map, SymbolMap):
+            raise TypeError(f"fill_symbol_map must be a SymbolMap, not {type(fill_symbol_map)}")
+
+        # Specify types
+        _Chafa.chafa_canvas_config_set_fill_symbol_map.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_canvas_config_set_fill_symbol_map(self._canvas_config, fill_symbol_map._symbol_map)
+
+    def calc_canvas_geometry(self, src_width: int, src_height: int, font_ratio: float, zoom: bool=False, stretch: bool=False):
+        """
+        Calculates an optimal geometry for a :py:class:`Canvas` given 
+        the width and height of an input image, font ratio, zoom and 
+        stretch preferences. This will then set the config's width and 
+        height to the calculated values.
+
+        :param int src_width: Width of the input image in pixels.
+        :param int src_height: Height of the input image in pixels.
+        :param float font_ratio: The font's width divided by its height.
+        :param bool zoom: Upscale the image to fit the canvas.
+        :param bool stretch: Ignore the aspect ratio of source.
+
+        :raises ValueError: if src_width or src_height are <= 0
+        """
+
+        src_width  = int(src_width)
+        src_height = int(src_height)
+
+        font_ratio = float(font_ratio)
+
+        if zoom is None:
+            raise TypeError("zoom must not be None")
+
+        if stretch is None:
+            raise TypeError("stretch must not be None")
+
+        zoom       = bool(zoom)
+        stretch    = bool(stretch)
+
+
+        if src_width <= 0:
+            raise ValueError("src_width must be greater than 0")
+
+        if src_height <= 0:
+            raise ValueError("src_height must be greater than 0")
+
+        if font_ratio <= 0:
+            raise ValueError("font_ratio must be greater than 0")
+
+        _Chafa.chafa_calc_canvas_geometry.argtypes = [
+            ctypes.c_uint,
+            ctypes.c_uint,
+            ctypes.POINTER(ctypes.c_uint),
+            ctypes.POINTER(ctypes.c_uint),
+            ctypes.c_float,
+            ctypes.c_bool,
+            ctypes.c_bool
+        ]
+
+        new_width  = ctypes.pointer(ctypes.c_uint(self.width))
+        new_height = ctypes.pointer(ctypes.c_uint(self.height))
+        
+        _Chafa.chafa_calc_canvas_geometry(
+            src_width, src_height,
+            new_width, new_height,
+            font_ratio,
+            zoom,
+            stretch
+        )
+
+        self.width  = new_width. contents.value
+        self.height = new_height.contents.value
+
diff --git a/src/chafa/chafa.py b/src/chafa/chafa.py
index 22e025a..cb73ef8 100644
--- a/src/chafa/chafa.py
+++ b/src/chafa/chafa.py
@@ -1,310 +1,17 @@
-import ctypes
-import ctypes.util
-from typing import Tuple, Sequence, Union # PEP 484 
-from enum import IntEnum
-from collections.abc import Iterable
-import array
-from pathlib import Path
-import os
+from __future__ import annotations
 import sys
 import platform
 from io import UnsupportedOperation
 
-#  CHAFA LETS GOOOOOOO!!!
-_root_dir = Path(os.path.dirname(__file__)) 
-
-def find_chafa():
-    search_names = ["chafa", "libchafa", "libchafa-0"]
-    for name in search_names:
-        chafa = ctypes.util.find_library(name)
-        if chafa:
-            break
-    
-    return chafa
-
-def find_glib():
-    search_names  = ["glib", "glib-2.0", "libglib", "libglib-2.0", "libglib-2.0-0"]
-    for name in search_names:
-        glib = ctypes.util.find_library(name)
-        if glib:
-            break
-    
-    return glib
-
-# Figure out which libraries we need to import
 if platform.system() == "Linux":
     import termios
-    _lib_glib = "libglib-2.0.so"
-    _lib      = _root_dir / "libs" / "libchafa.so"
-
-    try:
-        ctypes.CDLL(_lib_glib)
-    except OSError:
-        _lib_glib = find_glib()
-
-    if not _lib.exists():
-        _lib = find_chafa()
-
-elif platform.system() == "Windows":
-    os.add_dll_directory(os.path.dirname(__file__))
-
-    _lib_glib = _root_dir / "libs" / "libglib-2.0-0.dll"
-    _lib      = _root_dir / "libs" / "libchafa.dll"
-
-    if not _lib_glib.exists():
-        _lib_glib = find_glib()
-
-    if not _lib.exists():
-        _lib = find_chafa()
-
-
-elif platform.system() == "Darwin":
-    _lib_glib = _root_dir / ".dylibs" / "libglib-2.0.0.dylib"
-    _lib      = _root_dir / "libs"    / "libchafa.dylib"
-
-    if not _lib_glib.exists():
-        _lib_glib = find_glib()
-
-    if not _lib.exists():
-        _lib = find_chafa()
-
-else:
-    raise ImportError("You appear to be running on an unsupported system.")
-
-
-if _lib is None:
-    raise ImportError("libchafa was not found on your system.")
-
-if _lib_glib is None:
-    raise ImportError("libglib-2.0 was not found on your system.")
-
-_lib      = str(_lib)
-_lib_glib = str(_lib_glib)
-
-_Chafa = ctypes.CDLL(_lib)
-
-#
-# === PIXEL MODES ===
-#
-
-class PixelMode(IntEnum):
-    CHAFA_PIXEL_MODE_SYMBOLS = 0
-    CHAFA_PIXEL_MODE_SIXELS  = 1
-    CHAFA_PIXEL_MODE_KITTY   = 2
-    CHAFA_PIXEL_MODE_ITERM2  = 3
-    CHAFA_PIXEL_MODE_MAX     = 4
-
-
-#
-# === CANVAS MODES ===
-#
-
-class CanvasMode(IntEnum):
-    CHAFA_CANVAS_MODE_TRUECOLOR     = 0
-    CHAFA_CANVAS_MODE_INDEXED_256   = 1
-    CHAFA_CANVAS_MODE_INDEXED_240   = 2
-    CHAFA_CANVAS_MODE_INDEXED_16    = 3
-    CHAFA_CANVAS_MODE_FGBG_BGFG     = 4
-    CHAFA_CANVAS_MODE_FGBG          = 5
-    CHAFA_CANVAS_MODE_INDEXED_8     = 6
-    CHAFA_CANVAS_MODE_INDEXED_16_8  = 7
-
-    CHAFA_CANVAS_MODE_MAX           = 8
-
-
-#
-# === DITHER MODES ===
-#
-
-class DitherMode(IntEnum):
-    CHAFA_DITHER_MODE_NONE      = 0
-    CHAFA_DITHER_MODE_ORDERED   = 1
-    CHAFA_DITHER_MODE_DIFFUSION = 2
-
-    CHAFA_DITHER_MODE_MAX       = 3
-
-
-#
-# === COLOR SPACE ===
-#
-
-class ColorSpace(IntEnum):
-    CHAFA_COLOR_SPACE_RGB    = 0
-    CHAFA_COLOR_SPACE_DIN99D = 1
-
-    CHAFA_COLOR_SPACE_MAX    = 2
-
-
-#
-# === COLOR EXTRACTOR ===
-#
-
-class ColorExtractor(IntEnum):
-    CHAFA_COLOR_EXTRACTOR_AVERAGE = 0
-    CHAFA_COLOR_EXTRACTOR_MEDIAN  = 1
-
-    CHAFA_COLOR_SPACE_MAX         = 2
-
-
-#
-# === OPTIMIZATIONS ===
-#
-
-class Optimizations(IntEnum):
-    CHAFA_OPTIMIZATION_REUSE_ATTRIBUTES = (1 << 0),
-    CHAFA_OPTIMIZATION_SKIP_CELLS       = (1 << 1),
-    CHAFA_OPTIMIZATION_REPEAT_CELLS     = (1 << 2),
-
-    CHAFA_OPTIMIZATION_NONE             = 0,
-    CHAFA_OPTIMIZATION_ALL              = 0x7fffffff
-
-
-#
-# === PIXEL TYPE ===
-#
-
-class PixelType(IntEnum):
-    CHAFA_PIXEL_RGBA8_PREMULTIPLIED = 0
-    CHAFA_PIXEL_BGRA8_PREMULTIPLIED = 1
-    CHAFA_PIXEL_ARGB8_PREMULTIPLIED = 2
-    CHAFA_PIXEL_ABGR8_PREMULTIPLIED = 3
-
-    CHAFA_PIXEL_RGBA8_UNASSOCIATED = 4
-    CHAFA_PIXEL_BGRA8_UNASSOCIATED = 5
-    CHAFA_PIXEL_ARGB8_UNASSOCIATED = 6
-    CHAFA_PIXEL_ABGR8_UNASSOCIATED = 7
-    
-    CHAFA_PIXEL_RGB8 = 8
-    CHAFA_PIXEL_BGR8 = 9
-    CHAFA_PIXEL_MAX  = 10
-
-
-#
-# === SYMBOL TAGS ===
-#
-
-class SymbolTags(IntEnum):
-    CHAFA_SYMBOL_TAG_NONE        = 0
-
-    CHAFA_SYMBOL_TAG_SPACE       = (1 <<  0)
-    CHAFA_SYMBOL_TAG_SOLID       = (1 <<  1)
-    CHAFA_SYMBOL_TAG_STIPPLE     = (1 <<  2)
-    CHAFA_SYMBOL_TAG_BLOCK       = (1 <<  3)
-    CHAFA_SYMBOL_TAG_BORDER      = (1 <<  4)
-    CHAFA_SYMBOL_TAG_DIAGONAL    = (1 <<  5)
-    CHAFA_SYMBOL_TAG_DOT         = (1 <<  6)
-    CHAFA_SYMBOL_TAG_QUAD        = (1 <<  7)
-    CHAFA_SYMBOL_TAG_HHALF       = (1 <<  8)
-    CHAFA_SYMBOL_TAG_VHALF       = (1 <<  9)
-    CHAFA_SYMBOL_TAG_HALF        = ((CHAFA_SYMBOL_TAG_HHALF) | (CHAFA_SYMBOL_TAG_VHALF))
-    CHAFA_SYMBOL_TAG_INVERTED    = (1 << 10)
-    CHAFA_SYMBOL_TAG_BRAILLE     = (1 << 11)
-    CHAFA_SYMBOL_TAG_TECHNICAL   = (1 << 12)
-    CHAFA_SYMBOL_TAG_GEOMETRIC   = (1 << 13)
-    CHAFA_SYMBOL_TAG_ASCII       = (1 << 14)
-    CHAFA_SYMBOL_TAG_ALPHA       = (1 << 15)
-    CHAFA_SYMBOL_TAG_DIGIT       = (1 << 16)
-    CHAFA_SYMBOL_TAG_ALNUM       = CHAFA_SYMBOL_TAG_ALPHA | CHAFA_SYMBOL_TAG_DIGIT
-    CHAFA_SYMBOL_TAG_NARROW      = (1 << 17)
-    CHAFA_SYMBOL_TAG_WIDE        = (1 << 18)
-    CHAFA_SYMBOL_TAG_AMBIGUOUS   = (1 << 19)
-    CHAFA_SYMBOL_TAG_UGLY        = (1 << 20)
-    CHAFA_SYMBOL_TAG_LEGACY      = (1 << 21)
-    CHAFA_SYMBOL_TAG_SEXTANT     = (1 << 22)
-    CHAFA_SYMBOL_TAG_WEDGE       = (1 << 23)
-    CHAFA_SYMBOL_TAG_LATIN       = (1 << 24)
-    CHAFA_SYMBOL_TAG_EXTRA       = (1 << 30)
-    CHAFA_SYMBOL_TAG_BAD         = CHAFA_SYMBOL_TAG_AMBIGUOUS | CHAFA_SYMBOL_TAG_UGLY
-    CHAFA_SYMBOL_TAG_ALL         = ~(CHAFA_SYMBOL_TAG_EXTRA | CHAFA_SYMBOL_TAG_BAD)
-
-
-#
-# === Term sequences ===
-#
-
-class TermSeq(IntEnum):
-    CHAFA_TERM_SEQ_RESET_TERMINAL_SOFT = 0
-    CHAFA_TERM_SEQ_RESET_TERMINAL_HARD = 1
-    CHAFA_TERM_SEQ_RESET_ATTRIBUTES    = 2
-
-    CHAFA_TERM_SEQ_CLEAR         = 3
-    CHAFA_TERM_SEQ_INVERT_COLORS = 4
-
-    CHAFA_TERM_SEQ_CURSOR_TO_TOP_LEFT    = 5
-    CHAFA_TERM_SEQ_CURSOR_TO_BOTTOM_LEFT = 6
-    CHAFA_TERM_SEQ_CURSOR_TO_POS         = 7
-    CHAFA_TERM_SEQ_CURSOR_UP_1           = 8
-    CHAFA_TERM_SEQ_CURSOR_UP             = 9
-    CHAFA_TERM_SEQ_CURSOR_DOWN_1         = 10
-    CHAFA_TERM_SEQ_CURSOR_DOWN           = 11
-    CHAFA_TERM_SEQ_CURSOR_LEFT_1         = 12
-    CHAFA_TERM_SEQ_CURSOR_LEFT           = 13
-    CHAFA_TERM_SEQ_CURSOR_RIGHT_1        = 14
-    CHAFA_TERM_SEQ_CURSOR_RIGHT          = 15
-    CHAFA_TERM_SEQ_CURSOR_UP_SCROLL      = 16
-    CHAFA_TERM_SEQ_CURSOR_DOWN_SCROLL    = 17
-
-    CHAFA_TERM_SEQ_INSERT_CELLS = 18
-    CHAFA_TERM_SEQ_DELETE_CELLS = 19
-    CHAFA_TERM_SEQ_INSERT_ROWS  = 20
-    CHAFA_TERM_SEQ_DELETE_ROWS  = 21
-
-    CHAFA_TERM_SEQ_SET_SCROLLING_ROWS = 22
-
-    CHAFA_TERM_SEQ_ENABLE_INSERT  = 23
-    CHAFA_TERM_SEQ_DISABLE_INSERT = 24
-
-    CHAFA_TERM_SEQ_ENABLE_CURSOR  = 25
-    CHAFA_TERM_SEQ_DISABLE_CURSOR = 26
-
-    CHAFA_TERM_SEQ_ENABLE_ECHO  = 27
-    CHAFA_TERM_SEQ_DISABLE_ECHO = 28
-
-    CHAFA_TERM_SEQ_ENABLE_WRAP  = 29
-    CHAFA_TERM_SEQ_DISABLE_WRAP = 30
-
-    CHAFA_TERM_SEQ_SET_COLOR_FG_DIRECT   = 31
-    CHAFA_TERM_SEQ_SET_COLOR_BG_DIRECT   = 32
-    CHAFA_TERM_SEQ_SET_COLOR_FGBG_DIRECT = 33
-
-    CHAFA_TERM_SEQ_SET_COLOR_FG_256   = 34
-    CHAFA_TERM_SEQ_SET_COLOR_BG_256   = 35
-    CHAFA_TERM_SEQ_SET_COLOR_FGBG_256 = 36
-
-    CHAFA_TERM_SEQ_SET_COLOR_FG_16   = 37
-    CHAFA_TERM_SEQ_SET_COLOR_BG_16   = 38
-    CHAFA_TERM_SEQ_SET_COLOR_FGBG_16 = 39
-
-    CHAFA_TERM_SEQ_BEGIN_SIXELS = 40
-    CHAFA_TERM_SEQ_END_SIXELS   = 41
-    CHAFA_TERM_SEQ_REPEAT_CHAR  = 42
-
-    CHAFA_TERM_SEQ_BEGIN_KITTY_IMMEDIATE_IMAGE_V1 = 43
-    CHAFA_TERM_SEQ_END_KITTY_IMAGE                = 44
-    CHAFA_TERM_SEQ_BEGIN_KITTY_IMAGE_CHUNK        = 45
-    CHAFA_TERM_SEQ_END_KITTY_IMAGE_CHUNK          = 46
-
-    CHAFA_TERM_SEQ_BEGIN_ITERM2_IMAGE = 47
-    CHAFA_TERM_SEQ_END_ITERM2_IMAGE   = 48
-
-    CHAFA_TERM_SEQ_ENABLE_SIXEL_SCROLLING  = 49
-    CHAFA_TERM_SEQ_DISABLE_SIXEL_SCROLLING = 50
-
-    CHAFA_TERM_SEQ_ENABLE_BOLD = 51
-
-    CHAFA_TERM_SEQ_SET_COLOR_FG_8   = 52
-    CHAFA_TERM_SEQ_SET_COLOR_BG_8   = 53
-    CHAFA_TERM_SEQ_SET_COLOR_FGBG_8 = 54
-
-    CHAFA_TERM_SEQ_MAX = 55
-
 
 
 def get_device_attributes():
     """
     A functino that returns an array containing the current
     terminal's device attributes, fetched by reading the string
-    provided by emitting the ``\e[c`` control sequence. (See
+    provided by emitting the ``\\e[c`` control sequence. (See
     `Xterm Control Sequences on invisible-island.net 
     <https://invisible-island.net/xterm/ctlseqs/ctlseqs.html>_`.
 
@@ -364,2579 +71,3 @@ def get_device_attributes():
 
     return out
 
-
-class ReadOnlySymbolMap():
-    def __init__(self):
-        # Init map
-        _Chafa.chafa_symbol_map_new.restype = ctypes.c_void_p
-        self._symbol_map = _Chafa.chafa_symbol_map_new()
-
-    
-    def copy(self) -> 'SymbolMap':
-        """
-        Returns a new :py:class:`SymbolMap` that's a copy of this one.
-
-        :rtype: SymbolMap
-        """
-
-        # Argtypes
-        _Chafa.chafa_symbol_map_copy.argtypes = [
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_symbol_map_copy.restype = ctypes.c_void_p
-
-        # Get new pointer
-        new_pointer = _Chafa.chafa_symbol_map_copy(self._symbol_map)
-
-        # Init symbol map
-        symbol_map = SymbolMap()
-        symbol_map._symbol_map = new_pointer
-
-        return symbol_map
-
-
-
-class SymbolMap(ReadOnlySymbolMap):
-    def add_by_tags(self, tags: SymbolTags):
-        """
-        Adds symbols matching the set of tags to the symbol map.
-
-        :param SymbolTags tags: The set of tags to add to the map.
-        """
-
-        # Set types
-        _Chafa.chafa_symbol_map_add_by_tags.argtypes = [
-            ctypes.c_void_p, 
-            ctypes.c_uint
-        ]
-
-        _Chafa.chafa_symbol_map_add_by_tags(self._symbol_map, tags)
-
-    
-    def remove_by_tags(self, tags: SymbolTags):
-        """
-        Removes symbols matching the set of tags from the symbol map.
-
-        :param SymbolTags tags: The set of tags to remove from the map.
-        """
-
-        # If we did not get passed a SymbolTags
-        # try to convert to SymbolTags which will give
-        # an appropriate error if invalid
-        if not isinstance(tags, SymbolTags):
-            tags = SymbolTags(SymbolTags)
-
-        # Set types
-        _Chafa.chafa_symbol_map_remove_by_tags.argtypes = [
-            ctypes.c_void_p, 
-            ctypes.c_uint
-        ]
-
-        _Chafa.chafa_symbol_map_remove_by_tags(self._symbol_map, tags)
-
-    
-    def add_by_range(self, first: str, last: str):
-        """
-        Adds symbols in the code point range starting with the character first and ending with the character last to the symbol map.
-
-        For example, if first is given as ``a`` and last is given as ``f``, all characters ``a, b, c, d, e, f`` will be added to the map.
-
-        :param str first: First code point to add, inclusive.
-        :param str last: Last code point to add, inclusive.
-
-        :raises TypeError: if first or last are not of type str.
-        :raises ValueError: if first or last have length other than 1.
-        """
-
-        # Check types
-        if not isinstance(first, str):
-            raise TypeError(f"code point 'first' must be of type str. Got {type(first)}")
-
-        if not isinstance(last, str):
-            raise TypeError(f"code point 'last' must be of type str. Got {type(last)}")
-
-        # check for chars
-        if len(first) != 1:
-            raise ValueError("code point 'first' must be of length 1")
-
-        if len(last) != 1:
-            raise ValueError("code point 'last' must be of length 1")
-
-        # Set types
-        _Chafa.chafa_symbol_map_add_by_range.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_wchar,
-            ctypes.c_wchar
-        ]
-
-        # add tags
-        _Chafa.chafa_symbol_map_add_by_range(
-            self._symbol_map,
-            first,
-            last
-        )
-
-
-    def remove_by_range(self, first: str, last: str):
-        """
-        Removes symbols in the code point range starting with the character first and ending with the character last from the symbol map.
-
-        :param str first: First code point to remove, inclusive.
-        :param str last: Last code point to remove, inclusive.
-
-        :raises TypeError: if first or last are not of type str.
-        :raises ValueError: if first or last have length other than 1.
-        """
-
-        # Check types
-        if not isinstance(first, str):
-            raise TypeError(f"code point 'first' must be of type str. Got {type(first)}")
-
-        if not isinstance(last, str):
-            raise TypeError(f"code point 'last' must be of type str. Got {type(last)}")
-
-        # check for chars
-        if len(first) != 1:
-            raise ValueError("code point 'first' must be of length 1")
-
-        if len(last) != 1:
-            raise ValueError("code point 'last' must be of length 1")
-
-        # Set types
-        _Chafa.chafa_symbol_map_remove_by_range.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_wchar,
-            ctypes.c_wchar
-        ]
-
-        # remove tags
-        _Chafa.chafa_symbol_map_remove_by_range(
-            self._symbol_map,
-            first,
-            last
-        )
-
-    
-    def apply_selectors(self, selectors: str):
-        """
-        Parses a string consisting of symbol tags separated by ``+-,`` and applies the pattern to the symbol map. If the string begins with ``+`` or ``-``, it's understood to be relative to the current set in the symbol map, otherwise the map is cleared first.
-
-        The symbol tags are string versions of :py:class:`SymbolTags`, i.e.
-
-        ================================================  ===========
-        :py:class:`SymbolTags`                            String
-        ================================================  ===========
-        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_ALL`        all
-        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_NONE`       none
-        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_SPACE`      space
-        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_SOLID`      solid
-        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_STIPPLE`    stipple
-        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_BLOCK`      block
-        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_BORDER`     border
-        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_DIAGONAL`   diagonal
-        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_DOT`        dot
-        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_QUAD`       quad
-        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_HALF`       half
-        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_EXTRA`      extra
-        ================================================  ===========
-
-        other :py:class:`SymbolTags` follow the same format and are supported.
-
-        For example: ``block,border`` sets map to contain symbols matching either of those tags. ``+block,border-dot,stipple`` adds block and border symbols then removes dot and stipple symbols.
-
-        :param str selectors: The string of selectors to apply.
-
-        :raises ValueError: if the selectors string is invalid.
-        """
-
-        # Check type
-        if not isinstance(selectors, str):
-            raise TypeError(f"selectors must be of type str. Got {type(selectors)}")
-
-        class GError(ctypes.Structure):
-            _fields_ = [('domain',   ctypes.c_uint32),
-                        ('code',     ctypes.c_int),
-                        ('message',  ctypes.c_char_p)]
-
-        # Set types
-
-        _Chafa.chafa_symbol_map_apply_selectors.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_char_p,
-            ctypes.POINTER(ctypes.POINTER(GError))
-        ]
-
-        _Chafa.chafa_symbol_map_apply_selectors.restype = ctypes.c_bool
-
-        selectors = ctypes.c_char_p(bytes(selectors, "utf8"))
-
-        # Init error
-        error = ctypes.POINTER(GError)()
-
-        success = _Chafa.chafa_symbol_map_apply_selectors(
-            self._symbol_map,
-            selectors,
-            ctypes.byref(error)
-        )
-        
-        if not success:
-            error = error.contents.message.decode()
-            raise ValueError(error)
-
-        return success
-
-
-class ReadOnlyCanvasConfig:
-    def __init__(self):
-        # Init config
-        _Chafa.chafa_canvas_config_new.restype = ctypes.c_void_p
-        self._canvas_config = _Chafa.chafa_canvas_config_new()
-
-
-    # === Width & Height property ===
-
-    @property
-    def height(self) -> int:
-        """
-        :type: int
-
-        Sets the config's height in character cells.
-        """
-
-        _, height = self.get_geometry()
-
-        return height
-
-    @property
-    def width(self) -> int:
-        """
-        :type: int
-
-        Sets the config's width in character cells.
-        """
-        width, _ = self.get_geometry()
-
-        return width
-
-
-    # === pixel mode property ===
-
-    @property
-    def pixel_mode(self) -> PixelMode:
-        """
-        :type: PixelMode
-
-        Sets config's stored :py:class:`PixelMode`. 
-        This determines how pixel graphics are rendered 
-        in the output.
-        """
-
-        return self._get_pixel_mode()
-
-
-    # === color extractor property ===
-
-    @property
-    def color_extractor(self) -> ColorExtractor:
-        """
-        :type: ColorExtractor
-
-        The config's stored :py:class:`ColorExtractor`. 
-        This determines how colours are approximated in 
-        character symbol output. e.g. 
-        :py:attr:`PixelMode.CHAFA_PIXEL_MODE_SYMBOLS`.
-        """
-        return self._get_color_extractor()
-
-    
-    # === color space property ===
-
-    @property
-    def color_space(self) -> ColorSpace:
-        """
-        :type: ColorSpace
-
-        The config's stored :py:class:`ColorSpace`.
-        """
-        return self._get_color_space()
-
-
-    # === canvas mode property ===
-
-    @property
-    def canvas_mode(self) -> CanvasMode:
-        """
-        :type: CanvasMode
-
-        Sets config's stored `CanvasMode`. 
-        This determines how colours (and colour control codes) 
-        are used in the output.
-        """
-        return self._get_canvas_mode()
-
-
-    # === preprocessing property ===
-
-    @property
-    def preprocessing(self) -> bool:
-        """
-        :type: bool
-
-        Indicates whether automatic image preprocessing should 
-        be enabled. This allows Chafa to boost contrast and 
-        saturation in an attempt to improve legibility. 
-        """
-        return self._get_preprocessing_enabled()
-
-    
-    # === dither grain width & height ===
-
-    @property
-    def dither_width(self) -> int:
-        """
-        :type: int
-
-        Sets config's stored dither grain width in pixels. These 
-        values can be 1, 2, 4 or 8. 8 corresponds to the size of 
-        an entire character cell. 
-
-        The default is 4 pixels.
-        """
-        width, _, = self._get_dither_grain_size()
-        return width
-
-
-    @property
-    def dither_height(self) -> int:
-        """
-        :type: int
-
-        Sets config's stored dither grain width in pixels. These 
-        values can be 1, 2, 4 or 8. 8 corresponds to the size of 
-        an entire character cell. 
-
-        The default is 4 pixels.
-
-        """
-        _, height, = self._get_dither_grain_size()
-        return height
-
-    
-    # === dither mode property ===
-
-    @property
-    def dither_mode(self) -> DitherMode:
-        """
-        :type: DitherMode
-
-        The config's stored :py:class:`DitherMode`.
-        """
-        return self._get_dither_mode()
-
-
-    # === dither intensity ===
-
-    @property
-    def dither_intensity(self) -> float:
-        """
-        :type: float
-
-        Returns the relative intensity of the dithering 
-        pattern applied during image conversion. 1.0 
-        is the default, corresponding to a moderate intensity.
-        """
-        return self._get_dither_intensity()
-
-
-    # === optimizations ===
-
-    @property
-    def optimizations(self) -> Tuple[Optimizations, ...]:
-        """
-        :type: Tuple[Optimizations, ...] 
-
-        Returns config's optimization flags.
-        When enabled, these may produce more 
-        compact output at the cost of reduced 
-        compatibility and increased CPU use. 
-
-        The flags will be returned as a tuple
-        containing all enabled flags.
-
-        Output quality is unaffected.
-        """
-
-        out = []
-
-        flags = self._get_optimizations()
-
-        # Check if all optimizations are being used
-        if flags == Optimizations.CHAFA_OPTIMIZATION_ALL:
-            return (Optimizations.CHAFA_OPTIMIZATION_ALL, )
-        
-        # Loop to decipher which optimizations are in use
-        for optimization in Optimizations:
-            # Check if we have reached the end
-            if optimization == Optimizations.CHAFA_OPTIMIZATION_NONE:
-                break
-
-            # Check if flag is set
-            if flags & optimization == optimization:
-                out.append(optimization)
-
-        return tuple(map(Optimizations, out))
-
-
-    # === cell width & height ===
-
-    @property
-    def cell_width(self) -> int:
-        """
-        :type: int
-
-        Sets config's cell width in pixels.
-        """
-
-        width, _ = self._get_cell_geometry()
-        return width
-
-
-    @property
-    def cell_height(self) -> int:
-        """
-        :type: int
-
-        Sets config's cell height in pixels.
-        """
-        _, height = self._get_cell_geometry()
-        return height 
-
-
-    # === Transparency threshold ===
-
-    @property
-    def transparency_threshold(self) -> float:
-        """
-        :type: float
-
-        The threshold above which full transparency will be used.
-        """
-        return self._get_transparency_threshold()
-
-
-    # === Work factor ===
-
-    @property
-    def work_factor(self) -> float:
-        """
-        :type: float
-
-        Gets the work/quality tradeoff factor. A higher 
-        value means more time and memory will be spent 
-        towards a higher quality output.
-        """
-        return self._get_work_factor()
-
-    
-    # === fg only ===
-
-    @property
-    def fg_only(self) -> bool:
-        """
-        :type: bool
-
-        Queries whether to use foreground colors only, 
-        leaving the background unmodified in the canvas 
-        output.
-
-        When this is set, the canvas will emit escape codes 
-        to set the foreground color only.
-        
-        .. note::
-
-            This is relevant only when the :py:attr:`pixel_mode` 
-            is set to :py:attr:`PixelMode.CHAFA_PIXEL_MODE_SYMBOLS`.
-        """
-        return self._get_fg_only_enabled()
-
-
-    # === fg and bg colors ===
-
-    @property
-    def fg_color(self) -> Tuple[int, int, int]:
-        """
-        :type: Tuple[int, int, int]
-
-        The assumed foreground color of the output device. This 
-        is used to determine how to apply the foreground pen in 
-        FGBG modes like :py:attr:`CanvasMode.CHAFA_CANVAS_MODE_FGBG`.
-
-        The color is a tuple containing 3 integers in range [0,255] 
-        corresponding to red, green and blue respectively.
-        """
-        # Get the color
-        color = self._get_fg_color()
-
-        # convert to tuple
-        return CanvasInspector.packed_8bit_to_tuple(color)
-
-    
-    @property
-    def bg_color(self) -> Tuple[int, int, int]:
-        """
-        :type: Tuple[int, int, int]
-
-        The assumed foreground color of the output device. This 
-        is used to determine how to apply the foreground pen in 
-        FGBG modes like :py:attr:`CanvasMode.CHAFA_CANVAS_MODE_FGBG`.
-
-        The color is a tuple containing 3 integers in range [0,255] 
-        corresponding to red, green and blue respectively.
-        """
-        # Get the color
-        color = self._get_bg_color()
-
-        # convert to tuple
-        return CanvasInspector.packed_8bit_to_tuple(color)
-
-
-    def get_geometry(self) -> Tuple[int, int]:
-        """
-        Get the config's canvas geometry in character cells. 
-        This is the same as inspecting :py:attr:`width` 
-        and :py:attr:`height`
-
-        :rtype: typing.Tuple[int, int] of width and height.
-        """
-
-        # Init pointers
-        width  = ctypes.pointer(ctypes.c_int(-1))
-        height = ctypes.pointer(ctypes.c_int(-1))
-
-        # get geometry
-        _Chafa.chafa_canvas_config_get_geometry.argtypes = [
-            ctypes.c_void_p,
-            ctypes.POINTER(ctypes.c_int),
-            ctypes.POINTER(ctypes.c_int)
-        ]
-
-        _Chafa.chafa_canvas_config_get_geometry(
-            self._canvas_config,
-            width,
-            height
-        )
-
-        return width.contents.value, height.contents.value
-
-    def _get_fg_only_enabled(self) -> bool:
-        """
-        Wrapper for chafa_canvas_config_get_fg_only_enabled
-        """
-
-        # Set types
-        _Chafa.chafa_canvas_config_get_fg_only_enabled.argtypes = [
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_canvas_config_get_fg_only_enabled.restype = ctypes.c_bool
-
-        # Get fg_only 
-        fg_only = _Chafa.chafa_canvas_config_get_fg_only_enabled(self._canvas_config)
-
-        return fg_only
-
-
-    def _get_fg_color(self) -> int:
-        """
-        Wrapper for chafa_canvas_config_get_fg_color
-        """
-
-        # Set types
-        _Chafa.chafa_canvas_config_get_fg_color.argtypes = [
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_canvas_config_get_fg_color.restype = ctypes.c_uint32
-
-        # Get fg_color
-        color = _Chafa.chafa_canvas_config_get_fg_color(
-            self._canvas_config 
-        )
-
-        return color
-
-    
-    def _get_bg_color(self) -> int:
-        """
-        Wrapper for chafa_canvas_config_get_bg_color
-        """
-
-        # Set types
-        _Chafa.chafa_canvas_config_get_bg_color.argtypes = [
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_canvas_config_get_bg_color.restype = ctypes.c_uint32
-
-        # Get bg_color
-        color = _Chafa.chafa_canvas_config_get_bg_color(
-            self._canvas_config 
-        )
-
-        return color
-
-    
-    def _get_transparency_threshold(self) -> float:
-        """
-        Wrapper for chafa_canvas_config_get_transparency_threshold
-        """
-
-        # Set types
-        _Chafa.chafa_canvas_config_get_transparency_threshold.argtypes = [
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_canvas_config_get_transparency_threshold.restype = ctypes.c_float
-
-        # Get threshold
-        threshold = _Chafa.chafa_canvas_config_get_transparency_threshold(self._canvas_config)
-
-        return threshold
-
-
-    def _get_work_factor(self) -> float:
-        """
-        Wrapper for chafa_canvas_config_get_work_factor
-        """
-
-        # Set types
-        _Chafa.chafa_canvas_config_get_work_factor.argtypes = [
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_canvas_config_get_work_factor.restype = ctypes.c_float
-
-        # Get factor
-        factor = _Chafa.chafa_canvas_config_get_work_factor(self._canvas_config)
-
-        return factor
-
-
-    def _get_pixel_mode(self) -> PixelMode:
-        """
-        Wrapper for chafa_canvas_config_get_pixel_mode
-        """
-
-        # Set types
-        _Chafa.chafa_canvas_config_get_pixel_mode.argtypes = [ctypes.c_void_p]
-        _Chafa.chafa_canvas_config_get_pixel_mode.restype  = ctypes.c_uint
-
-        # Get mode
-        pixel_mode = _Chafa.chafa_canvas_config_get_pixel_mode(self._canvas_config)
-
-        return PixelMode(pixel_mode)
-
-    
-    def _get_dither_grain_size(self) -> Tuple[int, int]:
-        """
-        Wrapper for chafa_canvas_config_get_dither_grain_size
-        """
-
-        # Specify types
-        _Chafa.chafa_canvas_config_get_dither_grain_size.argtypes = [
-            ctypes.c_void_p,
-            ctypes.POINTER(ctypes.c_int),
-            ctypes.POINTER(ctypes.c_int)
-        ]
-
-        width_out  = ctypes.pointer(ctypes.c_int(0))
-        height_out = ctypes.pointer(ctypes.c_int(0))
-
-        # Set grain size
-        _Chafa.chafa_canvas_config_get_dither_grain_size(self._canvas_config, width_out, height_out)
-
-        return (width_out.contents.value, height_out.contents.value)
-
-    
-    def _get_cell_geometry(self) -> Tuple[int, int]:
-        """
-        Wrapper for chafa_canvas_config_get_dither_grain_size
-        """
-
-        # Specify types
-        _Chafa.chafa_canvas_config_get_cell_geometry.argtypes = [
-            ctypes.c_void_p,
-            ctypes.POINTER(ctypes.c_int),
-            ctypes.POINTER(ctypes.c_int)
-        ]
-
-        width_out  = ctypes.pointer(ctypes.c_int(0))
-        height_out = ctypes.pointer(ctypes.c_int(0))
-
-        # Set grain size
-        _Chafa.chafa_canvas_config_get_cell_geometry(self._canvas_config, width_out, height_out)
-
-        return (width_out.contents.value, height_out.contents.value)
-
-    
-    def _get_dither_mode(self) -> DitherMode:
-        """
-        Wrapper for chafa_canvas_config_get_cnavas_mode
-        """
-
-        # Specify types
-        _Chafa.chafa_canvas_config_get_dither_mode.argtypes = [ctypes.c_void_p]
-        _Chafa.chafa_canvas_config_get_dither_mode.restype  = ctypes.c_uint
-
-        # Get mode
-        mode = _Chafa.chafa_canvas_config_get_dither_mode(self._canvas_config)
-
-        return DitherMode(mode)
-
-
-    def _get_dither_intensity(self) -> float:
-        """
-        Wrapper for chafa_canvas_config_get_dither_intensity
-        """
-
-        # Specify types
-        _Chafa.chafa_canvas_config_get_dither_intensity.argtypes = [
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_canvas_config_get_dither_intensity.restype = ctypes.c_float
-
-        # Get intensity
-        intensity = _Chafa.chafa_canvas_config_get_dither_intensity(self._canvas_config)
-
-        return intensity
-
-    
-    def _get_canvas_mode(self) -> CanvasMode:
-        """
-        Wrapper for chafa_canvas_config_get_cnavas_mode
-        """
-
-        # Specify types
-        _Chafa.chafa_canvas_config_get_canvas_mode.argtypes = [ctypes.c_void_p]
-        _Chafa.chafa_canvas_config_get_canvas_mode.restype  = ctypes.c_uint
-
-        # Get mode
-        mode = _Chafa.chafa_canvas_config_get_canvas_mode(self._canvas_config)
-
-        return CanvasMode(mode)
-
-    
-    def _get_color_extractor(self) -> ColorExtractor:
-        """
-        Wrapper for chafa_canvas_config_get_color_extractor
-        """
-
-        # Set types
-        _Chafa.chafa_canvas_config_get_color_extractor.argtypes = [ctypes.c_void_p]
-        _Chafa.chafa_canvas_config_get_color_extractor.restype  = ctypes.c_uint
-
-        # Get extractor
-        extractor = _Chafa.chafa_canvas_config_get_color_extractor(self._canvas_config)
-
-        return ColorExtractor(extractor)
-
-    
-    def _get_color_space(self) -> ColorSpace:
-        """
-        Wrapper for chafa_canvas_config_get_color_space
-        """
-
-        # Set types
-        _Chafa.chafa_canvas_config_get_color_space.argtypes = [ctypes.c_void_p]
-        _Chafa.chafa_canvas_config_get_color_space.restype  = ctypes.c_uint
-
-        # Get space
-        space = _Chafa.chafa_canvas_config_get_color_space(self._canvas_config)
-
-        return ColorSpace(space)
-
-
-    def _get_preprocessing_enabled(self) -> bool:
-        """
-        Wrapper for chafa_canvas_config_get_preprocessing_enabled
-        """
-
-        # Set types
-        _Chafa.chafa_canvas_config_get_preprocessing_enabled.argtypes = [ctypes.c_void_p]
-        _Chafa.chafa_canvas_config_get_preprocessing_enabled.restype  = ctypes.c_bool
-
-        # Get preprocessing value 
-        preprocessing = _Chafa.chafa_canvas_config_get_preprocessing_enabled(self._canvas_config)
-
-        return preprocessing
-
-
-    def _get_optimizations(self) -> int:
-        """
-        Wrapper for chafa_canvas_config_get_optimizations
-        """
-
-        # Specify types
-        _Chafa.chafa_canvas_config_get_optimizations.argtypes = [
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_canvas_config_get_optimizations.restype = ctypes.c_uint
-
-        return _Chafa.chafa_canvas_config_get_optimizations(self._canvas_config)
-
-
-    def peek_symbol_map(self) -> ReadOnlySymbolMap:
-        """
-        Wrapper for chafa_canvas_config_peek_symbol_map
-        """
-
-        # define types
-        _Chafa.chafa_canvas_config_peek_symbol_map.argtypes = [
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_canvas_config_peek_symbol_map.restype = ctypes.c_void_p
-
-        # Get new pointer
-        new_pointer = _Chafa.chafa_canvas_config_peek_symbol_map(self._canvas_config)
-
-        # Init RO SymbolMap
-        symbol_map = ReadOnlySymbolMap()
-        symbol_map._symbol_map = new_pointer
-
-        return symbol_map
-
-
-
-class CanvasConfig(ReadOnlyCanvasConfig):
-    # === Width & Height property ===
-    @ReadOnlyCanvasConfig.height.setter
-    def height(self, value: int):
-        value = int(value)
-
-        self._set_geometry(self.width, value)
-
-    @ReadOnlyCanvasConfig.width.setter
-    def width(self, value: int):
-        value = int(value)
-
-        self._set_geometry(value, self.height)
-
-
-    # === pixel mode property ===
-    @ReadOnlyCanvasConfig.pixel_mode.setter
-    def pixel_mode(self, mode: PixelMode):
-        mode = PixelMode(mode)
-
-        self._set_pixel_mode(mode)
-
-    
-    # === color extractor property ===
-
-    @ReadOnlyCanvasConfig.color_extractor.setter
-    def color_extractor(self, extractor: ColorExtractor):
-        extractor = ColorExtractor(extractor)
-
-        self._set_color_extractor(extractor)
-
-
-    # === color space property ===
-
-    @ReadOnlyCanvasConfig.color_space.setter
-    def color_space(self, space: ColorSpace):
-        space = ColorSpace(space)
-
-        self._set_color_space(space)
-
-
-    # === canvas mode property ===
-
-    @ReadOnlyCanvasConfig.canvas_mode.setter
-    def canvas_mode(self, mode: CanvasMode):
-        mode = CanvasMode(mode)
-
-        self._set_canvas_mode(mode)
-
-
-    # === preprocessing property ===
-
-    @ReadOnlyCanvasConfig.preprocessing.setter
-    def preprocessing(self, preproc: bool):
-        if preproc is None:
-            raise TypeError("preprocessing must not be None")
-
-        preproc = bool(preproc)
-
-        self._set_preprocessing_enabled(preproc)
-    
-    
-    # === dither grain width & height ===
-    
-    @ReadOnlyCanvasConfig.dither_width.setter
-    def dither_width(self, width: int):
-        width = int(width)
-
-        self._set_dither_grain_size(width, self.dither_height)
-
-    
-    @ReadOnlyCanvasConfig.dither_height.setter
-    def dither_height(self, height: int):
-        height = int(height)
-
-        self._set_dither_grain_size(self.dither_width, height)
-
-
-    # === dither mode property ===
-    
-    @ReadOnlyCanvasConfig.dither_mode.setter
-    def dither_mode(self, mode: DitherMode):
-        mode = DitherMode(mode)
-
-        self._set_dither_mode(mode)
-
-    
-    # === dither intensity ===
-
-    @ReadOnlyCanvasConfig.dither_intensity.setter
-    def dither_intensity(self, intensity: float):
-        intensity = float(intensity)
-
-        if intensity < 0:
-            raise ValueError("Dither intensity must be positive.")
-
-        self._set_dither_intensity(intensity)
-    
-
-    # === optimizations ===
-
-    @ReadOnlyCanvasConfig.optimizations.setter
-    def optimizations(self, optimizations: Tuple):
-        # Convert optimizations to proper type to
-        # raise appropriate errors
-        if not isinstance(optimizations, Iterable):
-            raise TypeError(f"optimizations must be iterable, not {type(optimizations)}")
-
-        # Or all optimizations together
-        compounded = 0
-        for flag in optimizations:
-            # Convert flag to Optimizations to get error if invalid
-            flag = Optimizations(flag)
-
-            compounded |= flag
-
-        self._set_optimizations(compounded)
-
-
-    # === cell width & height ===
-    
-    @ReadOnlyCanvasConfig.cell_width.setter
-    def cell_width(self, width: int):
-        width = int(width)
-
-        self._set_cell_geometry(width, self.cell_height)
-
-    
-    @ReadOnlyCanvasConfig.cell_height.setter
-    def cell_height(self, height: int):
-        height = int(height)
-
-        self._set_cell_geometry(self.cell_width, height)
-
-
-    # === Transparency threshold ===
-    
-    @ReadOnlyCanvasConfig.transparency_threshold.setter
-    def transparency_threshold(self, threshold: float):
-        threshold = float(threshold)
-
-        if 1 < threshold or threshold < 0:
-            raise ValueError("Transparency threshold must be in range [0,1]")
-
-        self._set_transparency_threshold(threshold)
-
-
-    # === Work factor ===
-
-    @ReadOnlyCanvasConfig.work_factor.setter
-    def work_factor(self, factor: float):
-        factor = float(factor)
-
-        if 1 < factor or factor < 0:
-            raise ValueError("Work factor must be in range [0,1]")
-
-        self._set_work_factor(factor)
-        
-
-    # === fg only ===
-
-    @ReadOnlyCanvasConfig.fg_only.setter
-    def fg_only(self, fg_only: bool):
-        if fg_only is None:
-            raise TypeError("fg_only must not be None")
-
-        fg_only = bool(fg_only)
-
-        self._set_fg_only_enabled(fg_only)
-        
-
-    # === fg and bg colors ===
-
-    @ReadOnlyCanvasConfig.fg_color.setter
-    def fg_color(self, fg_color: Tuple[int, int, int]):
-
-        if isinstance(fg_color, str):
-            raise TypeError(f"fg_color must not be a string")
-
-        if not isinstance(fg_color, Iterable):
-            raise TypeError(f"fg_color must be iterable, not {type(fg_color)}")
-
-        if len(fg_color) != 3:
-            raise ValueError("fg_color must have exactly 3 values")
-
-        color  = CanvasInspector.tuple_to_packed_8bit(fg_color)
-
-        self._set_fg_color(color)
-
-
-    @ReadOnlyCanvasConfig.bg_color.setter
-    def bg_color(self, bg_color: Tuple[int, int, int]):
-
-        if isinstance(bg_color, str):
-            raise TypeError(f"bg_color must not be a string")
-
-        if not isinstance(bg_color, Iterable):
-            raise TypeError(f"bg_color must be iterable, not {type(bg_color)}")
-
-        if len(bg_color) != 3:
-            raise ValueError("bg_color must have exactly 3 values")
-
-        color = CanvasInspector.tuple_to_packed_8bit(bg_color)
-
-        self._set_bg_color(color)
-
-
-
-    def copy(self) -> 'CanvasConfig':
-        """
-        Creates a new :py:class:`CanvasConfig` that is a copy of this config.
-
-        :rtype: CanvasConfig
-        """
-
-        # define types
-        _Chafa.chafa_canvas_config_copy.argtypes = [
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_canvas_config_copy.restype = ctypes.c_void_p
-
-        # Init new config
-        new_config = CanvasConfig()
-
-        # Get new pointer
-        config_copy = _Chafa.chafa_canvas_config_copy(self._canvas_config)
-
-        new_config._canvas_config = config_copy
-
-        return new_config
-
-
-    def _set_geometry(self, width: int, height: int):
-        """
-        Wrapper for chafa_canvas_config_set_geometry
-        """
-
-        _Chafa.chafa_canvas_config_set_geometry.argtypes = [
-            ctypes.c_void_p, 
-            ctypes.c_uint, 
-            ctypes.c_uint
-        ]
-
-        _Chafa.chafa_canvas_config_set_geometry(
-            self._canvas_config, 
-            width, 
-            height
-        )
-
-
-    def _set_fg_only_enabled(self, fg_only: bool):
-        """
-        Wrapper for chafa_canvas_config_set_fg_only_enabled
-        """
-
-        # Set types
-        _Chafa.chafa_canvas_config_set_fg_only_enabled.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_bool
-        ]
-
-        # Set threshold
-        _Chafa.chafa_canvas_config_set_fg_only_enabled(
-            self._canvas_config,
-            fg_only
-        )
-
-
-    def _set_fg_color(self, fg_color: int):
-        """
-        Wrapper for chafa_canvas_config_set_fg_color
-        """
-
-        # Set types
-        _Chafa.chafa_canvas_config_set_fg_color.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_uint32
-        ]
-
-        # Set fg_color
-        color = _Chafa.chafa_canvas_config_set_fg_color(
-            self._canvas_config,
-            fg_color
-        )
-
-
-    def _set_bg_color(self, bg_color: int):
-        """
-        Wrapper for chafa_canvas_config_set_bg_color
-        """
-
-        # Set types
-        _Chafa.chafa_canvas_config_set_bg_color.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_uint32
-        ]
-
-        # Set bg_color
-        color = _Chafa.chafa_canvas_config_set_bg_color(
-            self._canvas_config,
-            bg_color
-        )
-
-
-    def _set_transparency_threshold(self, threshold: float):
-        """
-        Wrapper for chafa_canvas_config_set_transparency_threshold
-        """
-
-        # Set types
-        _Chafa.chafa_canvas_config_set_transparency_threshold.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_float
-        ]
-
-        # Set threshold
-        _Chafa.chafa_canvas_config_set_transparency_threshold(
-            self._canvas_config,
-            threshold
-        )
-
-
-    def _set_work_factor(self, factor: float):
-        """
-        Wrapper for chafa_canvas_config_set_work_factor
-        """
-
-        # Set types
-        _Chafa.chafa_canvas_config_set_work_factor.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_float
-        ]
-
-        # Set factor
-        _Chafa.chafa_canvas_config_set_work_factor(
-            self._canvas_config,
-            factor
-        )
-
-    
-    def _set_pixel_mode(self, mode: PixelMode):
-        """
-        Wrapper for chafa_canvas_config_set_pixel_mode
-        """
-
-        # Specify types
-        _Chafa.chafa_canvas_config_set_pixel_mode.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_uint
-        ]
-
-        _Chafa.chafa_canvas_config_set_pixel_mode(self._canvas_config, mode)
-
-
-    def _set_dither_grain_size(self, width: int, height: int):
-        """
-        Wrapper for chafa_canvas_config_set_dither_grain_size
-        """
-
-        # Specify types
-        _Chafa.chafa_canvas_config_set_dither_grain_size.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_int,
-            ctypes.c_int
-        ]
-
-        # Set grain size
-        _Chafa.chafa_canvas_config_set_dither_grain_size(self._canvas_config, width, height)
-
-
-    def _set_cell_geometry(self, width: int, height: int):
-        """
-        Wrapper for chafa_canvas_config_set_cell_geometry
-        """
-
-        # Specify types
-        _Chafa.chafa_canvas_config_set_cell_geometry.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_int,
-            ctypes.c_int
-        ]
-
-        # Set grain size
-        _Chafa.chafa_canvas_config_set_cell_geometry(self._canvas_config, width, height)
-
-    
-    def _set_dither_mode(self, mode: DitherMode):
-        """
-            wrapper for chafa_canvas_config_set_dither_mode
-        """
-
-        # Specify types
-        _Chafa.chafa_canvas_config_set_dither_mode.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_uint
-        ]
-
-        _Chafa.chafa_canvas_config_set_dither_mode(self._canvas_config, mode)
-
-
-    def _set_dither_intensity(self, intensity: float):
-        """
-        Wrapper for chafa_canvas_config_set_dither_intensity
-        """
-
-        # Specify types
-        _Chafa.chafa_canvas_config_set_dither_intensity.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_float
-        ]
-
-        # Set intensity
-        _Chafa.chafa_canvas_config_set_dither_intensity(
-            self._canvas_config,
-            intensity
-        )
-
-    
-    def _set_canvas_mode(self, mode: CanvasMode):
-        """
-            wrapper for chafa_canvas_config_set_canvas_mode
-        """
-
-        # Specify types
-        _Chafa.chafa_canvas_config_set_canvas_mode.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_uint
-        ]
-
-        _Chafa.chafa_canvas_config_set_canvas_mode(self._canvas_config, mode)
-
-    
-    def _set_color_extractor(self, extractor: ColorExtractor):
-        """
-        Wrapper for chafa_canvas_config_set_color_extractor
-        """
-
-        # Specify types
-        _Chafa.chafa_canvas_config_set_color_extractor.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_uint
-        ]
-
-        _Chafa.chafa_canvas_config_set_color_extractor(self._canvas_config, extractor)
-
-    
-    def _set_color_space(self, space: ColorSpace):
-        """
-        Wrapper for chafa_canvas_config_set_color_space
-        """
-
-        # Specify types
-        _Chafa.chafa_canvas_config_set_color_space.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_uint
-        ]
-
-        _Chafa.chafa_canvas_config_set_color_space(self._canvas_config, space)
-
-    
-    def _set_preprocessing_enabled(self, preproc: bool):
-        """
-        Wrapper for chafa_canvas_config_set_preprocessing_enabled
-        """
-
-        # Specify types
-        _Chafa.chafa_canvas_config_set_preprocessing_enabled.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_bool
-        ]
-
-        _Chafa.chafa_canvas_config_set_preprocessing_enabled(self._canvas_config, preproc)
-
-
-    def _set_optimizations(self, optimizations: int):
-        """
-        Wrapper for chafa_canvas_config_set_optimizations
-        """
-
-        # Specify types
-        _Chafa.chafa_canvas_config_set_optimizations.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_uint
-        ]
-
-        # Set optimizations
-        _Chafa.chafa_canvas_config_set_optimizations(
-            self._canvas_config,
-            optimizations
-        )
-
-
-    def set_symbol_map(self, symbol_map: SymbolMap):
-        """
-        Assigns a copy of symbol_map to config.
-
-        :param SymbolMap symbol_map: The symbol_map.
-        """
-        
-        if not isinstance(symbol_map, SymbolMap):
-            raise TypeError(f"symbol_map must be a SymbolMap, not {type(symbol_map)}")
-
-        # Specify types
-        _Chafa.chafa_canvas_config_set_symbol_map.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_canvas_config_set_symbol_map(self._canvas_config, symbol_map._symbol_map)
-
-
-    def set_fill_symbol_map(self, fill_symbol_map: SymbolMap):
-        """
-        Assigns a copy of fill_symbol_map to config.
-
-        Fill symbols are assigned according to their overall 
-        foreground to background coverage, disregarding shape. ???
-
-        :param SymbolMap fill_symbol_map: The fill symbol map.
-        """
-
-        if not isinstance(fill_symbol_map, SymbolMap):
-            raise TypeError(f"fill_symbol_map must be a SymbolMap, not {type(fill_symbol_map)}")
-
-        # Specify types
-        _Chafa.chafa_canvas_config_set_fill_symbol_map.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_canvas_config_set_fill_symbol_map(self._canvas_config, fill_symbol_map._symbol_map)
-
-    def calc_canvas_geometry(self, src_width: int, src_height: int, font_ratio: float, zoom: bool=False, stretch: bool=False):
-        """
-        Calculates an optimal geometry for a :py:class:`Canvas` given 
-        the width and height of an input image, font ratio, zoom and 
-        stretch preferences. This will then set the config's width and 
-        height to the calculated values.
-
-        :param int src_width: Width of the input image in pixels.
-        :param int src_height: Height of the input image in pixels.
-        :param float font_ratio: The font's width divided by its height.
-        :param bool zoom: Upscale the image to fit the canvas.
-        :param bool stretch: Ignore the aspect ratio of source.
-
-        :raises ValueError: if src_width or src_height are <= 0
-        """
-
-        src_width  = int(src_width)
-        src_height = int(src_height)
-
-        font_ratio = float(font_ratio)
-
-        if zoom is None:
-            raise TypeError("zoom must not be None")
-
-        if stretch is None:
-            raise TypeError("stretch must not be None")
-
-        zoom       = bool(zoom)
-        stretch    = bool(stretch)
-
-
-        if src_width <= 0:
-            raise ValueError("src_width must be greater than 0")
-
-        if src_height <= 0:
-            raise ValueError("src_height must be greater than 0")
-
-        if font_ratio <= 0:
-            raise ValueError("font_ratio must be greater than 0")
-
-        _Chafa.chafa_calc_canvas_geometry.argtypes = [
-            ctypes.c_uint,
-            ctypes.c_uint,
-            ctypes.POINTER(ctypes.c_uint),
-            ctypes.POINTER(ctypes.c_uint),
-            ctypes.c_float,
-            ctypes.c_bool,
-            ctypes.c_bool
-        ]
-
-        new_width  = ctypes.pointer(ctypes.c_uint(self.width))
-        new_height = ctypes.pointer(ctypes.c_uint(self.height))
-        
-        _Chafa.chafa_calc_canvas_geometry(
-            src_width, src_height,
-            new_width, new_height,
-            font_ratio,
-            zoom,
-            stretch
-        )
-
-        self.width  = new_width. contents.value
-        self.height = new_height.contents.value
-
-
-
-class TermDb():
-    def __init__(self, no_defaults: bool=False):
-        no_defaults = bool(no_defaults)
-
-        # Init term db
-        if no_defaults:
-            _Chafa.chafa_term_db_new.restype = ctypes.c_void_p
-            self._term_db = _Chafa.chafa_term_db_new()
-        else:
-            _Chafa.chafa_term_db_get_default.restype = ctypes.c_void_p
-            self._term_db = _Chafa.chafa_term_db_get_default()
-
-    def detect(self):
-        """
-        :rtype: TermInfo
-
-        Builds a new :py:class:`TermInfo` with capabilities implied by 
-        the system environment variables (principally the ``TERM`` 
-        variable, but also others).
-        """
-        # Init glib
-        glib = ctypes.CDLL(_lib_glib)
-        glib.g_get_environ.restype = ctypes.c_void_p
-
-        # Get environment
-        environment = glib.g_get_environ()
-
-        _Chafa.chafa_term_db_detect.restype  = ctypes.c_void_p
-        _Chafa.chafa_term_db_detect.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_void_p
-        ]
-
-        new_term_info = _Chafa.chafa_term_db_detect(
-            self._term_db,
-            environment
-        )
-
-        term_info = TermInfo()
-        term_info._term_info = new_term_info
-
-        return term_info
-
-
-    def get_fallback_info(self) -> 'TermInfo':
-        """
-        :rtype: TermInfo
-
-        Builds a new :py:class:`TermInfo` with fallback control 
-        sequences. This can be used with unknown but presumably 
-        modern terminals, or to supplement missing capabilities 
-        in a detected terminal.
-        """
-
-        # Define types
-        _Chafa.chafa_term_db_get_fallback_info.argtypes = [
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_term_db_get_fallback_info.restype = ctypes.c_void_p
-
-        # Get pointer to fallback info
-        fallback_info_pointer = _Chafa.chafa_term_db_get_fallback_info(self._term_db)
-
-        # Init fallback info
-        fallback_info = TermInfo()
-        fallback_info._term_info = fallback_info_pointer
-
-        return fallback_info
-
-
-    def copy(self) -> 'TermDb':
-        """
-        :rtype: TermDb
-
-        Returns a new :py:class:`TermDb` which is a copy of this one.
-        """
-
-        # Argtypes
-        _Chafa.chafa_term_db_copy.argtypes = [
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_term_db_copy.restype = ctypes.c_void_p
-
-        # Grab new pointer
-        new_pointer = _Chafa.chafa_term_db_copy(self._term_db)
-
-        # Init new term_db
-        term_db = TermDb()
-        term_db._term_db = new_pointer
-
-        return term_db
-
-
-class TermInfo():
-    def __init__(self):
-        # Init term_info
-        _Chafa.chafa_term_info_new.restype = ctypes.c_void_p
-
-        self._term_info = _Chafa.chafa_term_info_new()
-
-
-    class TerminalCapabilities:
-        def __init__(self, canvas_mode, pixel_mode):
-            self.canvas_mode = canvas_mode
-            self.pixel_mode = pixel_mode
-
-        def __repr__(self):
-            return f"TerminalCapabilities(canvas_mode={self.canvas_mode.name}, pixel_mode={self.pixel_mode.name})"
-
-        def __eq__(self, other):
-            return self.canvas_mode == other.canvas_mode and self.pixel_mode == other.pixel_mode
-
-
-    def copy(self) -> 'TermInfo':
-        """
-        Returns a new :py:class:`TermInfo` that is a copy of this one.
-
-        :rtype: TermInfo
-        """
-
-        # Argtypes
-        _Chafa.chafa_term_info_copy.argtypes = [
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_term_info_copy.restype = ctypes.c_void_p
-
-        # Grab new pointer
-        new_pointer = _Chafa.chafa_term_info_copy(self._term_info)
-
-        # Init new term_info
-        term_info = TermInfo()
-        term_info._term_info = new_pointer
-
-        return term_info
-
-    
-    def supplement(self, source: 'TermInfo'):
-        """
-        Supplements missing sequences in this 
-        :py:class:`TermInfo` with ones copied 
-        from source.
-
-        :param TermInfo source: The :py:class:`TermInfo` to copy sequences from.
-        """
-
-        if not isinstance(source, TermInfo):
-            raise TypeError(f"source must be of type TermInfo, not {type(source)}")
-
-        self._supplement(source._term_info)
-
-
-    def _supplement(self, source: ctypes.c_void_p):
-        """
-        Wrapper for chafa_term_info_supplement
-        """
-
-        _Chafa.chafa_term_info_supplement.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_term_info_supplement(self._term_info, source)
-
-
-    def have_seq(self, seq: TermSeq) -> bool:
-        """
-        Checks if :py:class:`TermInfo` can emit seq.
-        
-        :param TermSeq seq: A :py:class:`TermSeq` to query for.
-
-        :rtype: bool
-        """
-
-        seq = TermSeq(seq)
-
-        # Set types
-        _Chafa.chafa_term_info_have_seq.argtypes = [
-            ctypes.c_void_p, 
-            ctypes.c_int
-        ]
-        
-        _Chafa.chafa_term_info_have_seq.restype = ctypes.c_bool
-
-        # Check for sequence
-        return _Chafa.chafa_term_info_have_seq(self._term_info, seq)
-
-
-    def detect_capabilities(self) -> TerminalCapabilities:
-        """
-        A function that tries to detect the capabilities of the
-        terminal and return the appropriate canvas and pixel modes
-        """
-        # === Canvas mode ===
-
-        color_direct = self.have_seq(TermSeq.CHAFA_TERM_SEQ_SET_COLOR_FGBG_DIRECT) \
-            and        self.have_seq(TermSeq.CHAFA_TERM_SEQ_SET_COLOR_FG_DIRECT) \
-            and        self.have_seq(TermSeq.CHAFA_TERM_SEQ_SET_COLOR_BG_DIRECT)
-
-        color_256    = self.have_seq(TermSeq.CHAFA_TERM_SEQ_SET_COLOR_FGBG_256) \
-            and        self.have_seq(TermSeq.CHAFA_TERM_SEQ_SET_COLOR_FG_256) \
-            and        self.have_seq(TermSeq.CHAFA_TERM_SEQ_SET_COLOR_BG_256)
-
-        color_16     = self.have_seq(TermSeq.CHAFA_TERM_SEQ_SET_COLOR_FGBG_16) \
-            and        self.have_seq(TermSeq.CHAFA_TERM_SEQ_SET_COLOR_FG_16) \
-            and        self.have_seq(TermSeq.CHAFA_TERM_SEQ_SET_COLOR_BG_16)
-
-        color_2      = self.have_seq(TermSeq.CHAFA_TERM_SEQ_INVERT_COLORS) \
-            and        self.have_seq(TermSeq.CHAFA_TERM_SEQ_RESET_ATTRIBUTES)
-
-        if color_direct:
-            canvas_mode = CanvasMode.CHAFA_CANVAS_MODE_TRUECOLOR
-
-        elif color_256:
-            canvas_mode = CanvasMode.CHAFA_CANVAS_MODE_INDEXED_240
-
-        elif color_16:
-            canvas_mode = CanvasMode.CHAFA_CANVAS_MODE_INDEXED_16
-
-        elif color_2:
-            canvas_mode = CanvasMode.CHAFA_CANVAS_MODE_FGBG_BGFG
-
-        else:
-            canvas_mode = CanvasMode.CHAFA_CANVAS_MODE_FGBG
-
-        # === Pixel mode ===
-
-        # Also check device attributes if we are on xterm
-        terminal = os.environ.get("TERM", "")
-        xterm_sixels = False
-
-        if "xterm" in terminal and platform.system() == "Linux":
-            attributes = get_device_attributes()
-
-            xterm_sixels = 4 in attributes
-
-        # Check for sixels
-        sixel_capable = self.have_seq(TermSeq.CHAFA_TERM_SEQ_BEGIN_SIXELS) \
-            and         self.have_seq(TermSeq.CHAFA_TERM_SEQ_END_SIXELS) \
-
-        sixel_capable = sixel_capable or xterm_sixels
-
-        # Check for kitty
-        kitty_capable = self.have_seq(TermSeq.CHAFA_TERM_SEQ_BEGIN_KITTY_IMMEDIATE_IMAGE_V1)
-
-        if kitty_capable:
-            pixel_mode = PixelMode.CHAFA_PIXEL_MODE_KITTY
-
-        elif sixel_capable:
-            pixel_mode = PixelMode.CHAFA_PIXEL_MODE_SIXELS
-
-        else:
-            pixel_mode = PixelMode.CHAFA_PIXEL_MODE_SYMBOLS
-
-        # Init capabilities
-        terminal_capabilities = self.TerminalCapabilities(canvas_mode, pixel_mode)
-
-        return terminal_capabilities
-
-    
-class Canvas:
-    def __init__(self, config: CanvasConfig):
-        """
-        :param CanvasConfig|None config: The config to initialise the 
-        canvas with. If None is passed, the canvas will be initialised 
-        with hard-coded defaults.
-
-        :raises TypeError: If term_info is not None or :py:class:`TermInfo`
-        :raises TypeError: If config is not None or :py:class:`CanvasConfig`
-        """
-        # Init config
-        if config is None:
-            _Chafa.chafa_canvas_new.argtypes = [ctypes.c_size_t]
-            config = ctypes.c_size_t(0)
-
-        elif not isinstance(config, CanvasConfig):
-            raise TypeError(f"config must be of type CanvasConfig or None, not {type(config)}")
-
-        else:
-            _Chafa.chafa_canvas_new.argtypes = [ctypes.c_void_p]
-            config = config._canvas_config
-
-        # Init canvas
-        _Chafa.chafa_canvas_new.restype  =  ctypes.c_void_p
-
-        self._canvas = _Chafa.chafa_canvas_new(config)
-
-    
-    def new_similar(self) -> 'Canvas':
-        """
-        Creates a new canvas configured similarly to this one.
-
-        :rtype: Canvas
-        """
-
-        # types
-        _Chafa.chafa_canvas_new_similar.argtypes = [
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_canvas_new_similar.restype = ctypes.c_void_p
-
-        # Get new pointer
-        new_pointer = _Chafa.chafa_canvas_new_similar(self._canvas)
-
-        # Init canvas
-        new_canvas = Canvas(None)
-
-        # Assign the new pointer to the newly created canvas
-        new_canvas._canvas = new_pointer
-
-        return new_canvas
-
-
-    def peek_config(self) -> ReadOnlyCanvasConfig:
-        """
-        Returns a read only version of the :py:class:`CanvasConfig` 
-        used to initialise the canvas.
-
-        .. attention:: 
-            This :py:class:`ReadOnlyCanvasConfig`'s properties can 
-            be inspected but not changed.
-
-        :rtype: ReadOnlyCanvasConfig
-        """
-
-        # Types
-        _Chafa.chafa_canvas_peek_config.argtypes = [
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_canvas_peek_config.restype = ctypes.c_void_p
-
-        # Get the new pointer
-        new_pointer = _Chafa.chafa_canvas_peek_config(self._canvas)
-
-        # Init RO CanvasConfig
-        config = ReadOnlyCanvasConfig()
-        config._canvas_config = new_pointer
-
-        return config
-
-
-    def _canvas_slice(self, y_slice=slice(None), x_slice=slice(None), axis=0, y=None, x=None):
-        # Get slice of rows
-        # Check slice component types
-
-        # Define the hard stop
-        config = self.peek_config()
-        hard_stop = (config.height, config.width)
-        hard_stop = hard_stop[axis]
-
-        which_slice = (y_slice, x_slice)[axis]
-        
-        # Evaluate defaults
-        start = which_slice.start or 0
-        stop  = which_slice.stop  or hard_stop
-        step  = which_slice.step  or 1
-
-        # Check 0 step
-        if step == 0:
-            raise ValueError("Slice step cannot be zero.")
-        
-        # Evaluate negative start and stop
-        if start < 0:
-            start = hard_stop + start
-        
-        if stop < 0:
-            stop = hard_stop + stop
-
-        # Evaluators for when to stop the while loop
-        def positive_stop_evaluator(index):
-            return index < min(hard_stop, stop)
-
-        def negative_stop_evaluator(index):
-            return index >= 0
-
-        stop_evaluator = positive_stop_evaluator
-
-        # Negative step means we reverse start and stop
-        if step < 0:
-            aux   = start
-            start = stop - 1
-            stop  = aux
-
-            stop_evaluator = negative_stop_evaluator
-
-        if any([not isinstance(val, int) for val in (start, stop, step)]):
-            raise TypeError("Slice indices must be integers or None.")
-
-        # We are slicing over y
-        if axis == 0:
-            y = start
-
-            while stop_evaluator(y):
-                yield self[y, x_slice] if x is None else self[y, x]
-                y += step
-
-            return
-
-        # We are slicing over x
-        if axis == 1:
-            x = start
-
-            while stop_evaluator(x):
-                yield self[y, x]
-                x += step
-
-            return
-
-    def __getitem__(self, pos):
-        """
-        You can inspect pixels in the canvas by indexing, 
-        similar to if the canvas were a 2d array. When indexing, 
-        the first coordinate represents the row (or y coordinate) 
-        and the second represents the column (or x coordinate).
-
-        When indexing using a single value, a `generator`_ of 
-        relevant :py:class:`CanvasInspector` objects will be 
-        returned, representing each pixel in the given row.
-
-        When indexing using two values, a single :py:class:`CanvasInspector` 
-        will be returned representing the given pixel.
-
-        Slicing is also supported! You can slice either the row or 
-        column coordinates and this will return generators as 
-        expected. For example; if you index using ``[:,3]`` you will 
-        get a generator for each pixel in the 3rd column of the 
-        canvas (0 indexed). 
-        
-        Slicing using ``[3:6,:5]`` will return generators for rows 3 to 5 
-        inclusive. Each of these rows will be represented by a generator 
-        for :py:class:`CanvasInspector` objects representing pixels 0 to 
-        4 inclusive.
-
-        The take-away from this all is that indexing and slicing should work 
-        (mostly) the same as you would expect a 2d array to work. Check out 
-        the tutorial for more details.
-
-        :param int|slice|tuple pos: The position to index
-
-        :rtype: CanvasInspector
-        """
-
-        if isinstance(pos, int):
-            return self[pos, :]
-        
-        if isinstance(pos, slice):
-            return self._canvas_slice(y_slice=pos)
-
-        if not isinstance(pos, tuple):
-            raise TypeError(f"Indices of invalid type. Got {type(pos)}")
-
-        # None of the above means tuple
-
-        # Check for size
-        if len(pos) > 2:
-            raise IndexError(f"Too many indices for Canvas. Canvas is 2-dimensional, but got {len(pos)} indices.")
-        
-        # Empty tuple should return all rows
-        if len(pos) == 0:
-            return self[:]
-        
-        # Exact coordinates
-        if isinstance(pos[0], int) and isinstance(pos[1], int):
-            return CanvasInspector(self, *pos)
-        
-        # Slice of rows and columns
-        if isinstance(pos[0], slice) and isinstance(pos[1], slice):
-            return self._canvas_slice(y_slice=pos[0], x_slice=pos[1])
-        
-        # Slice of rows with exact x coordinate
-        if isinstance(pos[0], slice):
-            return self._canvas_slice(y_slice=pos[0], x=pos[1])
-        
-        # Slice of columns with exact y coordinate
-        if isinstance(pos[1], slice):
-            return self._canvas_slice(x_slice=pos[1], axis=1, y=pos[0])
-
-        raise TypeError(f"Indices of invalid type. Got {type(pos)}")
-
-
-    def _get_char_at(self, x:int, y:int) -> str:
-        """
-        Wrapper for chafa_canvas_get_char_at
-        """
-
-        # Define types
-        _Chafa.chafa_canvas_get_char_at.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_int,
-            ctypes.c_int
-        ]
-
-        _Chafa.chafa_canvas_get_char_at.restype = ctypes.c_wchar
-
-        # Get char
-        char = _Chafa.chafa_canvas_get_char_at(
-            self._canvas,
-            x, y
-        )
-
-        return char
-
-
-    def _set_char_at(self, x:int, y:int, char: str):
-        """
-        Wrapper for chafa_canvas_set_char_at
-        """
-
-        # Define types
-        _Chafa.chafa_canvas_set_char_at.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_int,
-            ctypes.c_int,
-            ctypes.c_wchar
-        ]
-
-        # Set char
-        _Chafa.chafa_canvas_set_char_at(
-            self._canvas,
-            x, y,
-            char
-        )
-
-
-    def _get_colors_at(self, x: int, y:int) -> Tuple[int, int]:
-        """
-        Wrapper for chafa_canvas_get_colors_at
-        """
-
-        # Set types
-        _Chafa.chafa_canvas_get_colors_at.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_int,
-            ctypes.c_int,
-            ctypes.POINTER(ctypes.c_int),
-            ctypes.POINTER(ctypes.c_int)
-        ]
-
-        # Define storage
-        bg_color = ctypes.pointer(ctypes.c_int(0))
-        fg_color = ctypes.pointer(ctypes.c_int(0))
-
-        # Get colors
-        _Chafa.chafa_canvas_get_colors_at(
-            self._canvas,
-            x, y,
-            fg_color,
-            bg_color
-        )
-
-        return fg_color.contents.value, bg_color.contents.value
-
-
-    def _get_raw_colors_at(self, x: int, y:int) -> Tuple[int, int]:
-        """
-        Wrapper for chafa_canvas_get_raw_colors_at
-        """
-
-        # Set types
-        _Chafa.chafa_canvas_get_raw_colors_at.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_int,
-            ctypes.c_int,
-            ctypes.POINTER(ctypes.c_int),
-            ctypes.POINTER(ctypes.c_int)
-        ]
-
-        # Define storage
-        bg_color = ctypes.pointer(ctypes.c_int(0))
-        fg_color = ctypes.pointer(ctypes.c_int(0))
-
-        # Get colors
-        _Chafa.chafa_canvas_get_raw_colors_at(
-            self._canvas,
-            x, y,
-            fg_color,
-            bg_color
-        )
-
-        return fg_color.contents.value, bg_color.contents.value
-
-
-    def _set_colors_at(self, x:int, y: int, fg: int, bg: int):
-        """
-        Wrapper for chafa_canvas_set_colors_at
-        """
-
-        # Set types
-        _Chafa.chafa_canvas_set_colors_at.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_int,
-            ctypes.c_int,
-            ctypes.c_int,
-            ctypes.c_int
-        ]
-
-        # Set colors
-        _Chafa.chafa_canvas_set_colors_at(
-            self._canvas,
-            x, y,
-            fg,
-            bg
-        )
-
-
-    def _set_raw_colors_at(self, x:int, y: int, fg: int, bg: int):
-        """
-        Wrapper for chafa_canvas_set_raw_colors_at
-        """
-
-        # Set types
-        _Chafa.chafa_canvas_set_raw_colors_at.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_int,
-            ctypes.c_int,
-            ctypes.c_int,
-            ctypes.c_int
-        ]
-
-        # Set colors
-        _Chafa.chafa_canvas_set_raw_colors_at(
-            self._canvas,
-            x, y,
-            fg,
-            bg
-        )
-
-
-    def draw_all_pixels(
-        self, 
-        src_pixel_type: PixelType, 
-        src_pixels:     Union[list, Tuple, array.array, ctypes.Array], 
-        src_width:      int, 
-        src_height:     int, 
-        src_rowstride:  int
-    ):
-        """
-        Draws the given src_pixels to the canvas. Depending on your 
-        set :py:class:`PixelMode`, this will be symbols, kitty sequences 
-        or sixel sequences. 
-
-        To output the data after drawing, use the :py:meth:`print` method.
-
-        .. note::
-            Best performance is achieved by passing a :py:class:`ctypes.Array` 
-            for src_pixels. The :py:class:`chafa.loader.Loader` 
-            class provides convenient (and reasonably fast) 
-            methods for this using the `MagickWand 
-            <https://imagemagick.org/script/magick-wand.php>`_ 
-            C-library.
-
-        :param PixelType src_pixel_type: The pixel type of src_pixels. This 
-            will determine what order the color channels will be read in 
-            and whether there is an alpha channel.
-
-        :param list|tuple|array.array|ctypes.Array src_pixels: The 
-            source pixel data. This is a one dimensional array where 
-            every block of 3 (or 4 depending on the :py:class:`PixelType`) 
-            values represents one pixel of the image. The order of the 
-            channels is determined by src_pixel_type.
-
-        :param int src_width:  The width of the source image.
-        :param int src_height: The width of the source image.
-
-        :param int src_rowstride: The number of values in src_image that 
-            represents one line pixels in the source image. Typically this 
-            will be the number of channels in the source image multiplied 
-            by src_width, e.g. for an image with no alpha channel and a 
-            width of 300 pixels, this will be ``3*300``.
-
-        :raises ValueError: if src_width, src_height or src_rowstride 
-            are less than or equal to 0.
-        """
-
-        # Convert src_pixels to appropriate format
-        if isinstance(src_pixels, ctypes.Array):
-            src_pixels = src_pixels
-        else:
-            try:
-                src_pixels = (ctypes.c_uint8 * len(src_pixels)).from_buffer(src_pixels)
-
-            except TypeError:
-                src_pixels = array.array("B", src_pixels)
-                src_pixels = (ctypes.c_uint8 * len(src_pixels)).from_buffer(src_pixels)
-
-        # Make sure types match
-        src_pixel_type = PixelType(src_pixel_type)
-
-        src_width     = int(src_width)
-        src_height    = int(src_height)
-        src_rowstride = int(src_rowstride)
-
-        # Value errors
-        if src_width <= 0:
-            raise ValueError("src_width must be greater than 0")
-
-        if src_height <= 0:
-            raise ValueError("src_height must be greater than 0")
-
-        if src_rowstride <= 0:
-            raise ValueError("src_rowstride must be greater than 0")
-        
-        # Specify types
-        _Chafa.chafa_canvas_draw_all_pixels.argtypes = [
-            ctypes.c_void_p,
-            ctypes.c_uint,
-            ctypes.POINTER(ctypes.c_uint8),
-            ctypes.c_uint,
-            ctypes.c_uint,
-            ctypes.c_uint
-        ]
-
-        # Draw pixels
-        _Chafa.chafa_canvas_draw_all_pixels(
-            self._canvas,
-            src_pixel_type,
-            src_pixels,
-            src_width,
-            src_height,
-            src_rowstride,
-        )
-
-
-    def print(self, term_info: TermInfo=None, fallback=False) -> bytes:
-        """
-        Builds a UTF-8 string of terminal control sequences and symbols 
-        representing the canvas' current contents. This can e.g. be 
-        printed to a terminal. The exact choice of escape sequences and 
-        symbols, dimensions, etc. is determined by the configuration 
-        assigned to canvas on its creation.
-
-        All output lines except for the last one will end in a newline.
-
-        :param TermInfo term_info: The :py:class:`TermInfo` that will 
-        provide the control sequences used when printing. If None is 
-        specified, the term_info will be initialised with 
-        :py:meth:`TermDb.detect`
-
-        :param bool fallback: If True, the term_info (the one provided by
-        :py:meth:`TermDb.detect` or the one provided by the user) will 
-        be supplemented with fallback control sequences.
-        """
-
-        term_db = None
-
-        # Check for term info
-        if term_info is None:
-            term_db = TermDb()
-            term_info = term_db.detect()
-
-        elif not isinstance(term_info, TermInfo):
-            raise TypeError(f"term_info must be None or of type TermInfo or None, not {type(term_info)}")
-
-        # Supplement with fallback sequences
-        if fallback:
-            if term_db is None:
-                term_db = TermDb()
-
-            fallback_info = term_db.get_fallback_info()
-            term_info.supplement(fallback_info)
-
-
-        class GString(ctypes.Structure):
-            _fields_ = [('str',         ctypes.c_char_p),
-                        ('len',           ctypes.c_uint),
-                        ('allocated_len', ctypes.c_uint)]
-
-        _Chafa.chafa_canvas_print.argtypes = [
-            ctypes.c_void_p, 
-            ctypes.c_void_p
-        ]
-
-        _Chafa.chafa_canvas_print.restype  = ctypes.c_void_p
-
-        output = _Chafa.chafa_canvas_print(self._canvas, term_info._term_info)
-        output = GString.from_address(output)
-
-        return output.str
-
-
-
-class CanvasInspector:
-    def __init__(self, canvas: Canvas, y: int, x: int):
-        # Get the configured height and width of the canvas
-        canvas_config = canvas.peek_config()
-        width  = canvas_config.width
-        height = canvas_config.height
-
-        # Check if x and y are within bounds
-        if (
-            (x >= width or y >= height)
-            or (x < 0 and width  < abs(x)) # If we have a negative we need to make sure it 
-            or (y < 0 and height < abs(y)) # is not going past the start of the canvas
-        ):
-            raise ValueError(
-                f"Coordinates ({x},{y}) are out of bounds for canvas with dimensions {width}x{height}."
-            )
-        
-        self._canvas = canvas
-
-        # Use modulo to cast negatives into positives
-        self._x = x % width
-        self._y = y % height
-
-
-    # === FG COLOR ===
-
-    @staticmethod
-    def packed_8bit_to_tuple(color: int) -> Tuple[int, int, int]:
-        """
-        A static method that takes an integer representing a packed
-        8bit RGB value and converts it into a tuple (R,G,B)
-
-        :rtype: tuple[int, int, int]
-        """
-
-        # Convert to bytes
-        bit_length = 3
-        order      = "big"
-        
-        color = color.to_bytes(bit_length, order)
-
-        return (color[0], color[1], color[2])
-
-    @staticmethod
-    def tuple_to_packed_8bit(color_tuple: Tuple[int, int, int]) -> int:
-        """
-        A static method that takes a tuple (R,G,B) representing a color
-        and converts it into a packed 8bit RGB value represented by an
-        integer.
-
-        :rtype: tuple[int, int, int]
-        """
-
-        offset = 1
-        color  = 0
-
-        # Convert color to packed bytes
-        for col in color_tuple[::-1]:
-            col = int(col)
-
-            if 255 < col or col < 0:
-                raise ValueError("Each value of color must be in the range [0,255]")
-            
-            color  += offset * col
-            offset *= 16**2
-
-        return color
-        
-
-    @property
-    def fg_color(self) -> Union[Tuple[int, int, int], None]:
-        """
-        :type: tuple[int, int, int] | None
-
-        The foreground color at the inspector's pixel. The color 
-        is represented as None if transparent or a tuple of 3 
-        integers in range [0,255], representing the color in 
-        (R, G, B) format.
-
-        For double-width characters, both cells will be set to the 
-        same color.
-
-        :raises TypeError:  if fg_color is not an :py:class:`Iterable` 
-            other than :py:class:`str`.
-        :raises ValueError: if fg_color is not None and does not contain 
-            exactly 3 values. 
-        :raises ValueError: if fg_color contains a value greater than 255 
-            or less than 0.
-        """
-        # Get the color at pixel
-        color = self._canvas._get_colors_at(self.x, self.y)[0]
-
-        # Return None for transparency
-        if color == -1:
-            return None
-
-        # convert to tuple
-        return self.packed_8bit_to_tuple(color)
-
-
-    @fg_color.setter
-    def fg_color(self, fg_color: Tuple[int, int, int]):
-        # Remove foreground if we get none
-        if fg_color is None:
-            self.remove_foreground()
-            return
-    	
-        # Check types
-        if isinstance(fg_color, str):
-            raise TypeError(f"fg_color must not be a string")
-
-        if not isinstance(fg_color, Iterable):
-            raise TypeError(f"fg_color must be iterable, not {type(fg_color)}")
-
-        if len(fg_color) != 3:
-            raise ValueError("fg_color must have exactly 3 values")
-
-        color = self.tuple_to_packed_8bit(fg_color)
-
-        bg_color = self.bg_color
-
-        if bg_color is None:
-            bg_color = -1
-
-        else:
-            bg_color = self.tuple_to_packed_8bit(bg_color)
-
-        self._canvas._set_colors_at(self.x, self.y, color, bg_color)
-
-    @property
-    def raw_fg_color(self) -> int:
-        """
-        :type: int
-
-        The raw foreground color at the inspector's pixel. The colors 
-        are -1 for transparency, a packed 8-bit RGB value 
-        (``0x00RRGGBB``) in truecolor mode, or the raw pen value (0-255) 
-        in indexed modes.
-        """
-        # Get the color at pixel
-        color = self._canvas._get_raw_colors_at(self.x, self.y)[0]
-
-        return int(color)
-
-
-    @raw_fg_color.setter
-    def raw_fg_color(self, fg_color: int):
-
-        if fg_color < -1 or fg_color > 0xFFFFFF:
-            raise ValueError("color must be in range [-1, 0xFFFFFF]")
-
-        self._canvas._set_raw_colors_at(self.x, self.y, fg_color, self.raw_bg_color)
-
-
-    # === BG COLOR ===
-    
-    @property
-    def bg_color(self) -> Union[Tuple[int, int, int], None]:
-        """
-        :type: tuple[int, int, int] | None
-
-        The background color at the inspector's pixel. The color is 
-        represented as None if transparent or a tuple of 3 integers 
-        in range [0,255], representing the color in (R, G, B) format.
-
-        For double-width characters, both cells will be set to the same color.
-
-        :raises TypeError:  if bg_color is not an :py:class:`Iterable` 
-            other than :py:class:`str`.
-        :raises ValueError: if bg_color is not None and does not 
-            contain exactly 3 values. 
-        :raises ValueError: if bg_color contains a value greater than 255 
-            or less than 0.
-        """
-        # Get the color at pixel
-        color = self._canvas._get_colors_at(self.x, self.y)[1]
-
-        if color == -1:
-            return None
-
-        # convert to tuple
-        return self.packed_8bit_to_tuple(color)
-    
-    @bg_color.setter
-    def bg_color(self, bg_color: Tuple[int, int, int]):
-
-        # Remove background if we get none
-        if bg_color is None:
-            self.remove_background()
-            return
-
-        # Check types
-        if isinstance(bg_color, str):
-            raise TypeError(f"bg_color must not be a string")
-
-        if not isinstance(bg_color, Iterable):
-            raise TypeError(f"bg_color must be iterable, not {type(bg_color)}")
-
-        if len(bg_color) != 3:
-            raise ValueError("bg_color must have exactly 3 values")
-
-        color  = self.tuple_to_packed_8bit(bg_color)
-
-        fg_color = self.fg_color
-
-        if fg_color is None:
-            fg_color = -1
-
-        else:
-            fg_color = self.tuple_to_packed_8bit(fg_color)
-
-        self._canvas._set_colors_at(self.x, self.y, fg_color, color)
-
-    @property
-    def raw_bg_color(self) -> int:
-        """
-        :type: int
-
-        The raw background color at the inspector's pixel. The colors 
-        are -1 for transparency, a packed 8-bit RGB value 
-        (``0x00RRGGBB``) in truecolor mode, or the raw pen value (0-255) 
-        in indexed modes.
-        """
-        # Get the color at pixel
-        color = self._canvas._get_raw_colors_at(self.x, self.y)[1]
-
-        return int(color)
-
-
-    @raw_bg_color.setter
-    def raw_bg_color(self, bg_color: int):
-
-        if bg_color < -1 or bg_color > 0xFFFFFF:
-            raise ValueError("color must be in range [-1, 0xFFFFFF]")
-
-        self._canvas._set_raw_colors_at(self.x, self.y, self.raw_fg_color, bg_color)
-
-    # === CHAR ===
-
-    @property
-    def char(self) -> str:
-        """
-        :type: str
-
-        The character at the inspector's pixel. For double-width 
-        characters, the leftmost cell must contain the character 
-        and the cell to the right of it will automatically be set 
-        to 0.
-
-        :raises ValueError: if char is not of length 1.
-        """
-        return self._canvas._get_char_at(self.x, self.y)
-
-    @char.setter
-    def char(self, char: str):
-        char = str(char)
-
-        if len(char) != 1:
-            raise ValueError(f"char must be of length 1")
-
-        self._canvas._set_char_at(self.x, self.y, char)
-
-
-    # === Coordinates ===
-
-    @property
-    def x(self) -> int:
-        """
-        :type: int
-
-        The x coordinate of the inspector.
-
-        :raises ValueError: if x is not less than the height of the canvas.
-        """
-        return self._x
-
-    @x.setter
-    def x(self, value: int):
-        width = self._canvas.peek_config().width
-
-        value = int(value)
-
-        if value >= width:
-            raise ValueError(
-                f"x-coordinate {value} is out of bounds for canvas with width {width}."
-            )
-        
-        self._x = value
-
-    @property
-    def y(self) -> int:
-        """
-        :type: int
-
-        The y coordinate of the inspector.
-
-        :raises ValueError: if y is not less than the height of the canvas.
-        """
-        return self._y
-
-    @y.setter
-    def y(self, value: int):
-        height = self._canvas.peek_config().height
-
-        value = int(value)
-
-        if value >= height:
-            raise ValueError(
-                f"y-coordinate {value} is out of bounds for canvas with width {height}."
-            )
-        
-        self._y = value
-
-    
-    def remove_background(self):
-        """
-        A function that sets the background color at the 
-        inspectors pixel to be transparent.
-        """
-
-        fg_color = self.fg_color
-
-        if fg_color is None:
-            fg_color = -1
-
-        else:
-            fg_color = self.tuple_to_packed_8bit(fg_color)
-
-        self._canvas._set_colors_at(self.x, self.y, fg_color, -1)
-
-    def remove_foreground(self):
-        """
-        A function that sets the foreground color at the 
-        inspectors pixel to be transparent.
-        """
-
-        bg_color = self.bg_color
-
-        if bg_color is None:
-            bg_color = -1
-
-        else:
-            bg_color = self.tuple_to_packed_8bit(bg_color)
-
-        self._canvas._set_colors_at(self.x, self.y, -1, bg_color)
diff --git a/src/chafa/enums.py b/src/chafa/enums.py
new file mode 100644
index 0000000..cb52e16
--- /dev/null
+++ b/src/chafa/enums.py
@@ -0,0 +1,249 @@
+from enum import IntEnum
+
+#
+# === PIXEL MODES ===
+#
+
+class PixelMode(IntEnum):
+    CHAFA_PIXEL_MODE_SYMBOLS = 0
+    CHAFA_PIXEL_MODE_SIXELS  = 1
+    CHAFA_PIXEL_MODE_KITTY   = 2
+    CHAFA_PIXEL_MODE_ITERM2  = 3
+    CHAFA_PIXEL_MODE_MAX     = 4
+
+
+#
+# === CANVAS MODES ===
+#
+
+class CanvasMode(IntEnum):
+    CHAFA_CANVAS_MODE_TRUECOLOR     = 0
+    CHAFA_CANVAS_MODE_INDEXED_256   = 1
+    CHAFA_CANVAS_MODE_INDEXED_240   = 2
+    CHAFA_CANVAS_MODE_INDEXED_16    = 3
+    CHAFA_CANVAS_MODE_FGBG_BGFG     = 4
+    CHAFA_CANVAS_MODE_FGBG          = 5
+    CHAFA_CANVAS_MODE_INDEXED_8     = 6
+    CHAFA_CANVAS_MODE_INDEXED_16_8  = 7
+
+    CHAFA_CANVAS_MODE_MAX           = 8
+
+
+#
+# === DITHER MODES ===
+#
+
+class DitherMode(IntEnum):
+    CHAFA_DITHER_MODE_NONE      = 0
+    CHAFA_DITHER_MODE_ORDERED   = 1
+    CHAFA_DITHER_MODE_DIFFUSION = 2
+
+    CHAFA_DITHER_MODE_MAX       = 3
+
+
+#
+# === COLOR SPACE ===
+#
+
+class ColorSpace(IntEnum):
+    CHAFA_COLOR_SPACE_RGB    = 0
+    CHAFA_COLOR_SPACE_DIN99D = 1
+
+    CHAFA_COLOR_SPACE_MAX    = 2
+
+
+#
+# === COLOR EXTRACTOR ===
+#
+
+class ColorExtractor(IntEnum):
+    CHAFA_COLOR_EXTRACTOR_AVERAGE = 0
+    CHAFA_COLOR_EXTRACTOR_MEDIAN  = 1
+
+    CHAFA_COLOR_SPACE_MAX         = 2
+
+
+#
+# === OPTIMIZATIONS ===
+#
+
+class Optimizations(IntEnum):
+    CHAFA_OPTIMIZATION_REUSE_ATTRIBUTES = (1 << 0),
+    CHAFA_OPTIMIZATION_SKIP_CELLS       = (1 << 1),
+    CHAFA_OPTIMIZATION_REPEAT_CELLS     = (1 << 2),
+
+    CHAFA_OPTIMIZATION_NONE             = 0,
+    CHAFA_OPTIMIZATION_ALL              = 0x7fffffff
+
+
+#
+# === PIXEL TYPE ===
+#
+
+class PixelType(IntEnum):
+    CHAFA_PIXEL_RGBA8_PREMULTIPLIED = 0
+    CHAFA_PIXEL_BGRA8_PREMULTIPLIED = 1
+    CHAFA_PIXEL_ARGB8_PREMULTIPLIED = 2
+    CHAFA_PIXEL_ABGR8_PREMULTIPLIED = 3
+
+    CHAFA_PIXEL_RGBA8_UNASSOCIATED = 4
+    CHAFA_PIXEL_BGRA8_UNASSOCIATED = 5
+    CHAFA_PIXEL_ARGB8_UNASSOCIATED = 6
+    CHAFA_PIXEL_ABGR8_UNASSOCIATED = 7
+    
+    CHAFA_PIXEL_RGB8 = 8
+    CHAFA_PIXEL_BGR8 = 9
+    CHAFA_PIXEL_MAX  = 10
+
+
+#
+# === SYMBOL TAGS ===
+#
+
+class SymbolTags(IntEnum):
+    CHAFA_SYMBOL_TAG_NONE        = 0
+
+    CHAFA_SYMBOL_TAG_SPACE       = (1 <<  0)
+    CHAFA_SYMBOL_TAG_SOLID       = (1 <<  1)
+    CHAFA_SYMBOL_TAG_STIPPLE     = (1 <<  2)
+    CHAFA_SYMBOL_TAG_BLOCK       = (1 <<  3)
+    CHAFA_SYMBOL_TAG_BORDER      = (1 <<  4)
+    CHAFA_SYMBOL_TAG_DIAGONAL    = (1 <<  5)
+    CHAFA_SYMBOL_TAG_DOT         = (1 <<  6)
+    CHAFA_SYMBOL_TAG_QUAD        = (1 <<  7)
+    CHAFA_SYMBOL_TAG_HHALF       = (1 <<  8)
+    CHAFA_SYMBOL_TAG_VHALF       = (1 <<  9)
+    CHAFA_SYMBOL_TAG_HALF        = ((CHAFA_SYMBOL_TAG_HHALF) | (CHAFA_SYMBOL_TAG_VHALF))
+    CHAFA_SYMBOL_TAG_INVERTED    = (1 << 10)
+    CHAFA_SYMBOL_TAG_BRAILLE     = (1 << 11)
+    CHAFA_SYMBOL_TAG_TECHNICAL   = (1 << 12)
+    CHAFA_SYMBOL_TAG_GEOMETRIC   = (1 << 13)
+    CHAFA_SYMBOL_TAG_ASCII       = (1 << 14)
+    CHAFA_SYMBOL_TAG_ALPHA       = (1 << 15)
+    CHAFA_SYMBOL_TAG_DIGIT       = (1 << 16)
+    CHAFA_SYMBOL_TAG_ALNUM       = CHAFA_SYMBOL_TAG_ALPHA | CHAFA_SYMBOL_TAG_DIGIT
+    CHAFA_SYMBOL_TAG_NARROW      = (1 << 17)
+    CHAFA_SYMBOL_TAG_WIDE        = (1 << 18)
+    CHAFA_SYMBOL_TAG_AMBIGUOUS   = (1 << 19)
+    CHAFA_SYMBOL_TAG_UGLY        = (1 << 20)
+    CHAFA_SYMBOL_TAG_LEGACY      = (1 << 21)
+    CHAFA_SYMBOL_TAG_SEXTANT     = (1 << 22)
+    CHAFA_SYMBOL_TAG_WEDGE       = (1 << 23)
+    CHAFA_SYMBOL_TAG_LATIN       = (1 << 24)
+    CHAFA_SYMBOL_TAG_EXTRA       = (1 << 30)
+    CHAFA_SYMBOL_TAG_BAD         = CHAFA_SYMBOL_TAG_AMBIGUOUS | CHAFA_SYMBOL_TAG_UGLY
+    CHAFA_SYMBOL_TAG_ALL         = ~(CHAFA_SYMBOL_TAG_EXTRA | CHAFA_SYMBOL_TAG_BAD)
+
+
+#
+# === Term sequences ===
+#
+
+class TermSeq(IntEnum):
+    CHAFA_TERM_SEQ_RESET_TERMINAL_SOFT = 0
+    CHAFA_TERM_SEQ_RESET_TERMINAL_HARD = 1
+    CHAFA_TERM_SEQ_RESET_ATTRIBUTES    = 2
+
+    CHAFA_TERM_SEQ_CLEAR         = 3
+    CHAFA_TERM_SEQ_INVERT_COLORS = 4
+
+    CHAFA_TERM_SEQ_CURSOR_TO_TOP_LEFT    = 5
+    CHAFA_TERM_SEQ_CURSOR_TO_BOTTOM_LEFT = 6
+    CHAFA_TERM_SEQ_CURSOR_TO_POS         = 7
+    CHAFA_TERM_SEQ_CURSOR_UP_1           = 8
+    CHAFA_TERM_SEQ_CURSOR_UP             = 9
+    CHAFA_TERM_SEQ_CURSOR_DOWN_1         = 10
+    CHAFA_TERM_SEQ_CURSOR_DOWN           = 11
+    CHAFA_TERM_SEQ_CURSOR_LEFT_1         = 12
+    CHAFA_TERM_SEQ_CURSOR_LEFT           = 13
+    CHAFA_TERM_SEQ_CURSOR_RIGHT_1        = 14
+    CHAFA_TERM_SEQ_CURSOR_RIGHT          = 15
+    CHAFA_TERM_SEQ_CURSOR_UP_SCROLL      = 16
+    CHAFA_TERM_SEQ_CURSOR_DOWN_SCROLL    = 17
+
+    CHAFA_TERM_SEQ_INSERT_CELLS = 18
+    CHAFA_TERM_SEQ_DELETE_CELLS = 19
+    CHAFA_TERM_SEQ_INSERT_ROWS  = 20
+    CHAFA_TERM_SEQ_DELETE_ROWS  = 21
+
+    CHAFA_TERM_SEQ_SET_SCROLLING_ROWS = 22
+
+    CHAFA_TERM_SEQ_ENABLE_INSERT  = 23
+    CHAFA_TERM_SEQ_DISABLE_INSERT = 24
+
+    CHAFA_TERM_SEQ_ENABLE_CURSOR  = 25
+    CHAFA_TERM_SEQ_DISABLE_CURSOR = 26
+
+    CHAFA_TERM_SEQ_ENABLE_ECHO  = 27
+    CHAFA_TERM_SEQ_DISABLE_ECHO = 28
+
+    CHAFA_TERM_SEQ_ENABLE_WRAP  = 29
+    CHAFA_TERM_SEQ_DISABLE_WRAP = 30
+
+    CHAFA_TERM_SEQ_SET_COLOR_FG_DIRECT   = 31
+    CHAFA_TERM_SEQ_SET_COLOR_BG_DIRECT   = 32
+    CHAFA_TERM_SEQ_SET_COLOR_FGBG_DIRECT = 33
+
+    CHAFA_TERM_SEQ_SET_COLOR_FG_256   = 34
+    CHAFA_TERM_SEQ_SET_COLOR_BG_256   = 35
+    CHAFA_TERM_SEQ_SET_COLOR_FGBG_256 = 36
+
+    CHAFA_TERM_SEQ_SET_COLOR_FG_16   = 37
+    CHAFA_TERM_SEQ_SET_COLOR_BG_16   = 38
+    CHAFA_TERM_SEQ_SET_COLOR_FGBG_16 = 39
+
+    CHAFA_TERM_SEQ_BEGIN_SIXELS = 40
+    CHAFA_TERM_SEQ_END_SIXELS   = 41
+    CHAFA_TERM_SEQ_REPEAT_CHAR  = 42
+
+    CHAFA_TERM_SEQ_BEGIN_KITTY_IMMEDIATE_IMAGE_V1 = 43
+    CHAFA_TERM_SEQ_END_KITTY_IMAGE                = 44
+    CHAFA_TERM_SEQ_BEGIN_KITTY_IMAGE_CHUNK        = 45
+    CHAFA_TERM_SEQ_END_KITTY_IMAGE_CHUNK          = 46
+
+    CHAFA_TERM_SEQ_BEGIN_ITERM2_IMAGE = 47
+    CHAFA_TERM_SEQ_END_ITERM2_IMAGE   = 48
+
+    CHAFA_TERM_SEQ_ENABLE_SIXEL_SCROLLING  = 49
+    CHAFA_TERM_SEQ_DISABLE_SIXEL_SCROLLING = 50
+
+    CHAFA_TERM_SEQ_ENABLE_BOLD = 51
+
+    CHAFA_TERM_SEQ_SET_COLOR_FG_8   = 52
+    CHAFA_TERM_SEQ_SET_COLOR_BG_8   = 53
+    CHAFA_TERM_SEQ_SET_COLOR_FGBG_8 = 54
+
+    CHAFA_TERM_SEQ_MAX = 55
+
+
+#
+# === Passthrough ===
+#
+
+class Passthrough(IntEnum):
+    CHAFA_PASSTHROUGH_NONE    = 0
+    CHAFA_PASSTHROUGH_SCREEN  = 1
+    CHAFA_PASSTHROUGH_TMUX    = 2
+    CHAFA_PASSTHROUGH_MAX     = 3
+
+
+#
+# === Tuck ===
+#
+
+class Tuck(IntEnum):
+    CHAFA_TUCK_STRETCH        = 0
+    CHAFA_TUCK_FIT            = 1
+    CHAFA_TUCK_SHRINK_TO_FIT  = 2
+    CHAFA_TUCK_MAX            = 3
+
+
+#
+# === Align ===
+#
+    
+class Align(IntEnum):
+    CHAFA_ALIGN_START   = 0
+    CHAFA_ALIGN_END     = 1
+    CHAFA_ALIGN_CENTER  = 2
+    CHAFA_ALIGN_MAX     = 3
\ No newline at end of file
diff --git a/src/chafa/frame.py b/src/chafa/frame.py
new file mode 100644
index 0000000..ff1ea4f
--- /dev/null
+++ b/src/chafa/frame.py
@@ -0,0 +1,92 @@
+from __future__ import annotations
+from typing import Union, Tuple
+import array
+import ctypes
+
+from .libraries import _Chafa
+from .enums import PixelType
+
+class Frame:
+    def __init__(
+        self, 
+        src_pixel_type: PixelType, 
+        src_pixels:     Union[list, Tuple, array.array, ctypes.Array], 
+        src_width:      int, 
+        src_height:     int, 
+        src_rowstride:  int
+    ):
+        """
+        This defines a frame of an image. This has to be added to an 
+        :py:class:`Image`. The inputs are the same as in 
+        :py:meth:`Canvas.draw_all_pixels`.
+
+        .. note::
+            Best performance is achieved by passing a :py:class:`ctypes.Array` 
+            for src_pixels. A fast way to do this is to use 
+            `Pillow <https://pillow.readthedocs.io/en/stable/>`_; specifically the 
+            `Image.tobytes <https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.tobytes>`_ method.
+
+        :param PixelType src_pixel_type: The pixel type of src_pixels.
+            This will determine what order the color channels will be read 
+            in and whether there is an alpha channel.
+        :param list|tuple|array.array|ctypes.Array src_pixels: The source pixel
+            data. This is a one dimensional array where every block of 3 (or 4 depending 
+            on the :py:class:`PixelType`) values represents one pixel of the image. 
+            The order of the channels is determined by src_pixel_type.
+        :param int src_width:  The width of the source image.
+        :param int src_height: The width of the source image.
+        :param int src_rowstride: The number of values in src_image that 
+            represents one line pixels in the source image. Typically this will be 
+            the number of channels in the source image multiplied by src_width, 
+            e.g. for an image with no alpha channel and a width of 300 pixels, this 
+            will be ``3*300``.
+
+        :raises ValueError: if src_width, src_height or src_rowstride are less 
+            than or equal to 0.
+        """
+        
+        if isinstance(src_pixels, ctypes.Array):
+            src_pixels = src_pixels
+        else:
+            try:
+                src_pixels = (ctypes.c_uint8 * len(src_pixels)).from_buffer(src_pixels)
+
+            except TypeError:
+                src_pixels = array.array("B", src_pixels)
+                src_pixels = (ctypes.c_uint8 * len(src_pixels)).from_buffer(src_pixels)
+
+        # Make sure types match
+        src_pixel_type = PixelType(src_pixel_type)
+
+        src_width     = int(src_width)
+        src_height    = int(src_height)
+        src_rowstride = int(src_rowstride)
+
+        # Value errors
+        if src_width <= 0:
+            raise ValueError("src_width must be greater than 0")
+
+        if src_height <= 0:
+            raise ValueError("src_height must be greater than 0")
+
+        if src_rowstride <= 0:
+            raise ValueError("src_rowstride must be greater than 0")
+        
+        _Chafa.chafa_frame_new.argtypes = [
+            ctypes.POINTER(ctypes.c_uint8),
+            ctypes.c_uint,
+            ctypes.c_uint,
+            ctypes.c_uint,
+            ctypes.c_uint
+        ]
+
+        _Chafa.chafa_frame_new.restype = ctypes.c_void_p
+
+        # Draw pixels
+        self._frame = _Chafa.chafa_frame_new(
+            src_pixels,
+            src_pixel_type,
+            src_width,
+            src_height,
+            src_rowstride,
+        )
\ No newline at end of file
diff --git a/src/chafa/image.py b/src/chafa/image.py
new file mode 100644
index 0000000..af05184
--- /dev/null
+++ b/src/chafa/image.py
@@ -0,0 +1,50 @@
+from __future__ import annotations
+import ctypes
+
+from .libraries import _Chafa
+from .frame import Frame
+
+class Image:
+    def __init__(self):
+        """
+        A container for a :py:class:`Frame`. To be placed on a 
+        :py:class:`Placement`.
+        """
+        
+        _Chafa.chafa_image_new.restype = ctypes.c_void_p
+
+        self._image = _Chafa.chafa_image_new()
+        self._frame = None
+
+
+    # === Frame property ===
+
+    @property
+    def frame(self) -> Frame:
+        """
+        :type: Frame
+
+        The :py:class:`Frame` for the image
+        """
+        
+        return self._frame
+    
+
+    @frame.setter
+    def frame(self, new_frame: Frame):
+        self._set_frame(new_frame)
+    
+
+    def _set_frame(self, new_frame: Frame):
+        """
+        Bindings for chafa_image_set_frame
+        """
+
+        _Chafa.chafa_image_set_frame.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_void_p    
+        ]
+
+        _Chafa.chafa_image_set_frame(self._image, new_frame._frame)
+
+        self._frame = new_frame
\ No newline at end of file
diff --git a/src/chafa/libraries.py b/src/chafa/libraries.py
new file mode 100644
index 0000000..db11d33
--- /dev/null
+++ b/src/chafa/libraries.py
@@ -0,0 +1,77 @@
+import os
+from pathlib import Path
+import ctypes
+import ctypes.util
+import platform
+
+#  CHAFA LETS GOOOOOOO!!!
+_root_dir = Path(os.path.dirname(__file__)) 
+
+def find_chafa():
+    search_names = ["chafa", "libchafa", "libchafa-0"]
+    for name in search_names:
+        chafa = ctypes.util.find_library(name)
+        if chafa:
+            break
+    
+    return chafa
+
+def find_glib():
+    search_names  = ["glib", "glib-2.0", "libglib", "libglib-2.0", "libglib-2.0-0"]
+    for name in search_names:
+        glib = ctypes.util.find_library(name)
+        if glib:
+            break
+    
+    return glib
+
+# Figure out which libraries we need to import
+if platform.system() == "Linux":
+    _lib_glib = "libglib-2.0.so"
+    _lib      = _root_dir / "libs" / "libchafa.so"
+
+    try:
+        ctypes.CDLL(_lib_glib)
+    except OSError:
+        _lib_glib = find_glib()
+
+    if not _lib.exists():
+        _lib = find_chafa()
+
+elif platform.system() == "Windows":
+    os.add_dll_directory(os.path.dirname(__file__))
+
+    _lib_glib = _root_dir / "libs" / "libglib-2.0-0.dll"
+    _lib      = _root_dir / "libs" / "libchafa.dll"
+
+    if not _lib_glib.exists():
+        _lib_glib = find_glib()
+
+    if not _lib.exists():
+        _lib = find_chafa()
+
+
+elif platform.system() == "Darwin":
+    _lib_glib = _root_dir / ".dylibs" / "libglib-2.0.0.dylib"
+    _lib      = _root_dir / "libs"    / "libchafa.dylib"
+
+    if not _lib_glib.exists():
+        _lib_glib = find_glib()
+
+    if not _lib.exists():
+        _lib = find_chafa()
+
+else:
+    raise ImportError("You appear to be running on an unsupported system.")
+
+
+if _lib is None:
+    raise ImportError("libchafa was not found on your system.")
+
+if _lib_glib is None:
+    raise ImportError("libglib-2.0 was not found on your system.")
+
+_lib      = str(_lib)
+_lib_glib = str(_lib_glib)
+
+_Chafa = ctypes.CDLL(_lib)
\ No newline at end of file
diff --git a/src/chafa/loader.py b/src/chafa/loader.py
index da16cc5..8c59fdb 100644
--- a/src/chafa/loader.py
+++ b/src/chafa/loader.py
@@ -1,7 +1,7 @@
 import ctypes.util
 import ctypes
 from pathlib import Path
-from chafa import PixelType
+from .enums import PixelType
 import platform
 import os
 
diff --git a/src/chafa/placement.py b/src/chafa/placement.py
new file mode 100644
index 0000000..8cc58fb
--- /dev/null
+++ b/src/chafa/placement.py
@@ -0,0 +1,168 @@
+from __future__ import annotations
+import ctypes
+
+from .libraries import _Chafa
+from .enums import Tuck, Align
+from .image import Image
+
+
+class Placement:
+    def __init__(self, image: Image, id: int = 0):
+        """
+        This class defines the placement of an :py:class:`Image` on a 
+        :py:class:`Canvas`. 
+
+        .. note::
+            None of placement's properties have any effect if the 
+            :py:class:`Canvas`'s pixel mode is set to 
+            :py:attr:`PixelMode.CHAFA_PIXEL_MODE_SYMBOLS`.
+
+        :param image: The :py:class:`Image` to be placed.
+        :param id: An id to assign to the image. Leave as ``0`` to assign 
+            one automatically.
+        """
+        
+        _Chafa.chafa_placement_new.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_int
+        ]
+
+        _Chafa.chafa_placement_new.restype = ctypes.c_void_p
+
+        self._placement = _Chafa.chafa_placement_new(image._image, id)
+        self._image = image
+
+
+    # === Tuck property ===
+    
+    @property
+    def tuck(self) -> Tuck:
+        """
+        :type: Tuck
+
+        This describes how the :py:class:`Image` is resized to fit on the 
+        :py:class:`Canvas`, and defaults to :py:attr:`Tuck.CHAFA_TUCK_STRETCH`.
+        """
+        
+        return self._get_tuck()
+
+
+    @tuck.setter
+    def tuck(self, new_tuck: Tuck):
+        new_tuck = Tuck(new_tuck)
+
+        self._set_tuck(new_tuck)
+
+
+    # === Halign property === 
+
+    @property
+    def halign(self) -> Align:
+        """
+        :type: Align
+
+        Describes the horizontal alignment of the :py:class:`Image` on the 
+        :py:class:`Canvas`.
+        """
+        
+        return self._get_halign()
+
+
+    @halign.setter
+    def halign(self, new_halign: Align):
+        new_halign = Align(new_halign)
+
+        self._set_halign(new_halign)
+
+
+    # === Valign property ===
+
+    @property
+    def valign(self) -> Align:
+        """
+        :type: Align
+
+        Describes the horizontal alignment of the :py:class:`Image` on the 
+        :py:class:`Canvas`.
+        """
+        
+        return self._get_valign()
+
+
+    @valign.setter
+    def valign(self, new_valign: Align):
+        new_valign = Align(new_valign)
+
+        self._set_valign(new_valign)
+    
+
+    def _get_tuck(self) -> Tuck:
+        """
+        Bindings for chafa_placement_get_tuck
+        """
+        _Chafa.chafa_placement_get_tuck.argtypes = [ctypes.c_void_p]
+
+        _Chafa.chafa_placement_get_tuck.restype  = ctypes.c_uint
+
+        tuck =_Chafa.chafa_placement_get_tuck(self._placement)
+        return Tuck(tuck)
+    
+
+    def _set_tuck(self, new_tuck: Tuck):
+        """
+        Bindings for chafa_placement_set_tuck
+        """
+        _Chafa.chafa_placement_set_tuck.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_uint
+        ]
+
+        _Chafa.chafa_placement_set_tuck(self._placement, new_tuck)
+
+
+    def _get_halign(self) -> Align:
+        """
+        Bindings for chafa_placement_get_halign
+        """
+        _Chafa.chafa_placement_get_halign.argtypes = [ctypes.c_void_p]
+
+        _Chafa.chafa_placement_get_halign.restype  = ctypes.c_uint
+
+        halign =_Chafa.chafa_placement_get_halign()
+        return Align(halign)
+    
+
+    def _set_halign(self, new_halign: Align):
+        """
+        Bindings for chafa_placement_set_halign
+        """
+        _Chafa.chafa_placement_set_halign.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_uint
+        ]
+
+        _Chafa.chafa_placement_set_halign(self._placement, new_halign)
+
+
+    def _get_valign(self) -> Align:
+        """
+        Bindings for chafa_placement_get_valign
+        """
+        _Chafa.chafa_placement_get_valign.argtypes = [ctypes.c_void_p]
+
+        _Chafa.chafa_placement_get_valign.restype  = ctypes.c_uint
+
+        valign =_Chafa.chafa_placement_get_valign()
+        return Align(valign)
+    
+
+    def _set_valign(self, new_valign: Align):
+        """
+        Bindings for chafa_placement_set_valign
+        """
+        _Chafa.chafa_placement_set_valign.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_uint
+        ]
+
+        _Chafa.chafa_placement_set_valign(self._placement, new_valign)
\ No newline at end of file
diff --git a/src/chafa/symbol_map.py b/src/chafa/symbol_map.py
new file mode 100644
index 0000000..57e2449
--- /dev/null
+++ b/src/chafa/symbol_map.py
@@ -0,0 +1,226 @@
+from __future__ import annotations
+import ctypes
+
+from .libraries import _Chafa
+from .enums import *
+
+class ReadOnlySymbolMap():
+    def __init__(self):
+        # Init map
+        _Chafa.chafa_symbol_map_new.restype = ctypes.c_void_p
+        self._symbol_map = _Chafa.chafa_symbol_map_new()
+
+    
+    def copy(self) -> SymbolMap:
+        """
+        Returns a new :py:class:`SymbolMap` that's a copy of this one.
+
+        :rtype: SymbolMap
+        """
+
+        # Argtypes
+        _Chafa.chafa_symbol_map_copy.argtypes = [
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_symbol_map_copy.restype = ctypes.c_void_p
+
+        # Get new pointer
+        new_pointer = _Chafa.chafa_symbol_map_copy(self._symbol_map)
+
+        # Init symbol map
+        symbol_map = SymbolMap()
+        symbol_map._symbol_map = new_pointer
+
+        return symbol_map
+
+
+
+class SymbolMap(ReadOnlySymbolMap):
+    def add_by_tags(self, tags: SymbolTags):
+        """
+        Adds symbols matching the set of tags to the symbol map.
+
+        :param SymbolTags tags: The set of tags to add to the map.
+        """
+
+        # Set types
+        _Chafa.chafa_symbol_map_add_by_tags.argtypes = [
+            ctypes.c_void_p, 
+            ctypes.c_uint
+        ]
+
+        _Chafa.chafa_symbol_map_add_by_tags(self._symbol_map, tags)
+
+    
+    def remove_by_tags(self, tags: SymbolTags):
+        """
+        Removes symbols matching the set of tags from the symbol map.
+
+        :param SymbolTags tags: The set of tags to remove from the map.
+        """
+
+        # If we did not get passed a SymbolTags
+        # try to convert to SymbolTags which will give
+        # an appropriate error if invalid
+        if not isinstance(tags, SymbolTags):
+            tags = SymbolTags(SymbolTags)
+
+        # Set types
+        _Chafa.chafa_symbol_map_remove_by_tags.argtypes = [
+            ctypes.c_void_p, 
+            ctypes.c_uint
+        ]
+
+        _Chafa.chafa_symbol_map_remove_by_tags(self._symbol_map, tags)
+
+    
+    def add_by_range(self, first: str, last: str):
+        """
+        Adds symbols in the code point range starting with the character first and ending with the character last to the symbol map.
+
+        For example, if first is given as ``a`` and last is given as ``f``, all characters ``a, b, c, d, e, f`` will be added to the map.
+
+        :param str first: First code point to add, inclusive.
+        :param str last: Last code point to add, inclusive.
+
+        :raises TypeError: if first or last are not of type str.
+        :raises ValueError: if first or last have length other than 1.
+        """
+
+        # Check types
+        if not isinstance(first, str):
+            raise TypeError(f"code point 'first' must be of type str. Got {type(first)}")
+
+        if not isinstance(last, str):
+            raise TypeError(f"code point 'last' must be of type str. Got {type(last)}")
+
+        # check for chars
+        if len(first) != 1:
+            raise ValueError("code point 'first' must be of length 1")
+
+        if len(last) != 1:
+            raise ValueError("code point 'last' must be of length 1")
+
+        # Set types
+        _Chafa.chafa_symbol_map_add_by_range.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_wchar,
+            ctypes.c_wchar
+        ]
+
+        # add tags
+        _Chafa.chafa_symbol_map_add_by_range(
+            self._symbol_map,
+            first,
+            last
+        )
+
+
+    def remove_by_range(self, first: str, last: str):
+        """
+        Removes symbols in the code point range starting with the character first and ending with the character last from the symbol map.
+
+        :param str first: First code point to remove, inclusive.
+        :param str last: Last code point to remove, inclusive.
+
+        :raises TypeError: if first or last are not of type str.
+        :raises ValueError: if first or last have length other than 1.
+        """
+
+        # Check types
+        if not isinstance(first, str):
+            raise TypeError(f"code point 'first' must be of type str. Got {type(first)}")
+
+        if not isinstance(last, str):
+            raise TypeError(f"code point 'last' must be of type str. Got {type(last)}")
+
+        # check for chars
+        if len(first) != 1:
+            raise ValueError("code point 'first' must be of length 1")
+
+        if len(last) != 1:
+            raise ValueError("code point 'last' must be of length 1")
+
+        # Set types
+        _Chafa.chafa_symbol_map_remove_by_range.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_wchar,
+            ctypes.c_wchar
+        ]
+
+        # remove tags
+        _Chafa.chafa_symbol_map_remove_by_range(
+            self._symbol_map,
+            first,
+            last
+        )
+
+    
+    def apply_selectors(self, selectors: str):
+        """
+        Parses a string consisting of symbol tags separated by ``+-,`` and applies the pattern to the symbol map. If the string begins with ``+`` or ``-``, it's understood to be relative to the current set in the symbol map, otherwise the map is cleared first.
+
+        The symbol tags are string versions of :py:class:`SymbolTags`, i.e.
+
+        ================================================  ===========
+        :py:class:`SymbolTags`                            String
+        ================================================  ===========
+        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_ALL`        all
+        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_NONE`       none
+        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_SPACE`      space
+        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_SOLID`      solid
+        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_STIPPLE`    stipple
+        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_BLOCK`      block
+        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_BORDER`     border
+        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_DIAGONAL`   diagonal
+        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_DOT`        dot
+        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_QUAD`       quad
+        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_HALF`       half
+        :py:meth:`SymbolTags.CHAFA_SYMBOL_TAG_EXTRA`      extra
+        ================================================  ===========
+
+        other :py:class:`SymbolTags` follow the same format and are supported.
+
+        For example: ``block,border`` sets map to contain symbols matching either of those tags. ``+block,border-dot,stipple`` adds block and border symbols then removes dot and stipple symbols.
+
+        :param str selectors: The string of selectors to apply.
+
+        :raises ValueError: if the selectors string is invalid.
+        """
+
+        # Check type
+        if not isinstance(selectors, str):
+            raise TypeError(f"selectors must be of type str. Got {type(selectors)}")
+
+        class GError(ctypes.Structure):
+            _fields_ = [('domain',   ctypes.c_uint32),
+                        ('code',     ctypes.c_int),
+                        ('message',  ctypes.c_char_p)]
+
+        # Set types
+
+        _Chafa.chafa_symbol_map_apply_selectors.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_char_p,
+            ctypes.POINTER(ctypes.POINTER(GError))
+        ]
+
+        _Chafa.chafa_symbol_map_apply_selectors.restype = ctypes.c_bool
+
+        selectors = ctypes.c_char_p(bytes(selectors, "utf8"))
+
+        # Init error
+        error = ctypes.POINTER(GError)()
+
+        success = _Chafa.chafa_symbol_map_apply_selectors(
+            self._symbol_map,
+            selectors,
+            ctypes.byref(error)
+        )
+        
+        if not success:
+            error = error.contents.message.decode()
+            raise ValueError(error)
+
+        return success
diff --git a/src/chafa/term_db.py b/src/chafa/term_db.py
new file mode 100644
index 0000000..73e0e4b
--- /dev/null
+++ b/src/chafa/term_db.py
@@ -0,0 +1,99 @@
+from __future__ import annotations
+import ctypes
+
+from .libraries import _Chafa, _lib_glib
+from .term_info import TermInfo
+
+class TermDb():
+    def __init__(self, no_defaults: bool=False):
+        no_defaults = bool(no_defaults)
+
+        # Init term db
+        if no_defaults:
+            _Chafa.chafa_term_db_new.restype = ctypes.c_void_p
+            self._term_db = _Chafa.chafa_term_db_new()
+        else:
+            _Chafa.chafa_term_db_get_default.restype = ctypes.c_void_p
+            self._term_db = _Chafa.chafa_term_db_get_default()
+
+    def detect(self):
+        """
+        :rtype: TermInfo
+
+        Builds a new :py:class:`TermInfo` with capabilities implied by 
+        the system environment variables (principally the ``TERM`` 
+        variable, but also others).
+        """
+        # Init glib
+        glib = ctypes.CDLL(_lib_glib)
+        glib.g_get_environ.restype = ctypes.c_void_p
+
+        # Get environment
+        environment = glib.g_get_environ()
+
+        _Chafa.chafa_term_db_detect.restype  = ctypes.c_void_p
+        _Chafa.chafa_term_db_detect.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_void_p
+        ]
+
+        new_term_info = _Chafa.chafa_term_db_detect(
+            self._term_db,
+            environment
+        )
+
+        term_info = TermInfo()
+        term_info._term_info = new_term_info
+
+        return term_info
+
+
+    def get_fallback_info(self) -> TermInfo:
+        """
+        :rtype: TermInfo
+
+        Builds a new :py:class:`TermInfo` with fallback control 
+        sequences. This can be used with unknown but presumably 
+        modern terminals, or to supplement missing capabilities 
+        in a detected terminal.
+        """
+
+        # Define types
+        _Chafa.chafa_term_db_get_fallback_info.argtypes = [
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_term_db_get_fallback_info.restype = ctypes.c_void_p
+
+        # Get pointer to fallback info
+        fallback_info_pointer = _Chafa.chafa_term_db_get_fallback_info(self._term_db)
+
+        # Init fallback info
+        fallback_info = TermInfo()
+        fallback_info._term_info = fallback_info_pointer
+
+        return fallback_info
+
+
+    def copy(self) -> TermDb:
+        """
+        :rtype: TermDb
+
+        Returns a new :py:class:`TermDb` which is a copy of this one.
+        """
+
+        # Argtypes
+        _Chafa.chafa_term_db_copy.argtypes = [
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_term_db_copy.restype = ctypes.c_void_p
+
+        # Grab new pointer
+        new_pointer = _Chafa.chafa_term_db_copy(self._term_db)
+
+        # Init new term_db
+        term_db = TermDb()
+        term_db._term_db = new_pointer
+
+        return term_db
diff --git a/src/chafa/term_info.py b/src/chafa/term_info.py
new file mode 100644
index 0000000..fa9fdd5
--- /dev/null
+++ b/src/chafa/term_info.py
@@ -0,0 +1,221 @@
+from __future__ import annotations
+import ctypes
+import os
+import platform
+
+from .libraries import _Chafa
+from .chafa import get_device_attributes
+from .enums import *
+
+class TermInfo():
+    def __init__(self):
+        # Init term_info
+        _Chafa.chafa_term_info_new.restype = ctypes.c_void_p
+
+        self._term_info = _Chafa.chafa_term_info_new()
+
+
+    class TerminalCapabilities:
+        def __init__(self, canvas_mode, pixel_mode):
+            self.canvas_mode = canvas_mode
+            self.pixel_mode = pixel_mode
+
+        def __repr__(self):
+            return f"TerminalCapabilities(canvas_mode={self.canvas_mode.name}, pixel_mode={self.pixel_mode.name})"
+
+        def __eq__(self, other):
+            return self.canvas_mode == other.canvas_mode and self.pixel_mode == other.pixel_mode
+
+
+    def copy(self) -> TermInfo:
+        """
+        Returns a new :py:class:`TermInfo` that is a copy of this one.
+
+        :rtype: TermInfo
+        """
+
+        # Argtypes
+        _Chafa.chafa_term_info_copy.argtypes = [
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_term_info_copy.restype = ctypes.c_void_p
+
+        # Grab new pointer
+        new_pointer = _Chafa.chafa_term_info_copy(self._term_info)
+
+        # Init new term_info
+        term_info = TermInfo()
+        term_info._term_info = new_pointer
+
+        return term_info
+
+    
+    def supplement(self, source: 'TermInfo'):
+        """
+        Supplements missing sequences in this 
+        :py:class:`TermInfo` with ones copied 
+        from source.
+
+        :param TermInfo source: The :py:class:`TermInfo` to copy sequences from.
+        """
+
+        if not isinstance(source, TermInfo):
+            raise TypeError(f"source must be of type TermInfo, not {type(source)}")
+
+        self._supplement(source._term_info)
+
+
+    def _supplement(self, source: ctypes.c_void_p):
+        """
+        Wrapper for chafa_term_info_supplement
+        """
+
+        _Chafa.chafa_term_info_supplement.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_void_p
+        ]
+
+        _Chafa.chafa_term_info_supplement(self._term_info, source)
+
+
+    def have_seq(self, seq: TermSeq) -> bool:
+        """
+        Checks if :py:class:`TermInfo` can emit seq.
+        
+        :param TermSeq seq: A :py:class:`TermSeq` to query for.
+
+        :rtype: bool
+        """
+
+        seq = TermSeq(seq)
+
+        # Set types
+        _Chafa.chafa_term_info_have_seq.argtypes = [
+            ctypes.c_void_p, 
+            ctypes.c_int
+        ]
+        
+        _Chafa.chafa_term_info_have_seq.restype = ctypes.c_bool
+
+        # Check for sequence
+        return _Chafa.chafa_term_info_have_seq(self._term_info, seq)
+
+
+    def detect_capabilities(self) -> TerminalCapabilities:
+        """
+        A function that tries to detect the capabilities of the
+        terminal and return the appropriate canvas and pixel modes
+        """
+        # === Canvas mode ===
+
+        color_direct = self.have_seq(TermSeq.CHAFA_TERM_SEQ_SET_COLOR_FGBG_DIRECT) \
+            and        self.have_seq(TermSeq.CHAFA_TERM_SEQ_SET_COLOR_FG_DIRECT) \
+            and        self.have_seq(TermSeq.CHAFA_TERM_SEQ_SET_COLOR_BG_DIRECT)
+
+        color_256    = self.have_seq(TermSeq.CHAFA_TERM_SEQ_SET_COLOR_FGBG_256) \
+            and        self.have_seq(TermSeq.CHAFA_TERM_SEQ_SET_COLOR_FG_256) \
+            and        self.have_seq(TermSeq.CHAFA_TERM_SEQ_SET_COLOR_BG_256)
+
+        color_16     = self.have_seq(TermSeq.CHAFA_TERM_SEQ_SET_COLOR_FGBG_16) \
+            and        self.have_seq(TermSeq.CHAFA_TERM_SEQ_SET_COLOR_FG_16) \
+            and        self.have_seq(TermSeq.CHAFA_TERM_SEQ_SET_COLOR_BG_16)
+
+        color_2      = self.have_seq(TermSeq.CHAFA_TERM_SEQ_INVERT_COLORS) \
+            and        self.have_seq(TermSeq.CHAFA_TERM_SEQ_RESET_ATTRIBUTES)
+
+        if color_direct:
+            canvas_mode = CanvasMode.CHAFA_CANVAS_MODE_TRUECOLOR
+
+        elif color_256:
+            canvas_mode = CanvasMode.CHAFA_CANVAS_MODE_INDEXED_240
+
+        elif color_16:
+            canvas_mode = CanvasMode.CHAFA_CANVAS_MODE_INDEXED_16
+
+        elif color_2:
+            canvas_mode = CanvasMode.CHAFA_CANVAS_MODE_FGBG_BGFG
+
+        else:
+            canvas_mode = CanvasMode.CHAFA_CANVAS_MODE_FGBG
+
+        # === Pixel mode ===
+
+        # Also check device attributes if we are on xterm
+        terminal = os.environ.get("TERM", "")
+        xterm_sixels = False
+
+        if "xterm" in terminal and platform.system() == "Linux":
+            attributes = get_device_attributes()
+
+            xterm_sixels = 4 in attributes
+
+        # Check for sixels
+        sixel_capable = self.have_seq(TermSeq.CHAFA_TERM_SEQ_BEGIN_SIXELS) \
+            and         self.have_seq(TermSeq.CHAFA_TERM_SEQ_END_SIXELS) \
+
+        sixel_capable = sixel_capable or xterm_sixels
+
+        # Check for kitty
+        kitty_capable = self.have_seq(TermSeq.CHAFA_TERM_SEQ_BEGIN_KITTY_IMMEDIATE_IMAGE_V1)
+
+        if kitty_capable:
+            pixel_mode = PixelMode.CHAFA_PIXEL_MODE_KITTY
+
+        elif sixel_capable:
+            pixel_mode = PixelMode.CHAFA_PIXEL_MODE_SIXELS
+
+        else:
+            pixel_mode = PixelMode.CHAFA_PIXEL_MODE_SYMBOLS
+
+        # Init capabilities
+        terminal_capabilities = self.TerminalCapabilities(canvas_mode, pixel_mode)
+
+        return terminal_capabilities
+
+
+    def emit(self, sequence: TermSeq, *args) -> bytes:
+        """
+        Returns the asked for terminal sequences as bytes.
+
+        :param TermSeq *args: The sequences to emit
+
+        :rtype: bytes
+        """
+        # Make sure we have a terminal sequence
+        sequence = TermSeq(sequence)
+
+        # Check if the terminal has the sequence
+        if not self.have_seq(sequence):
+            raise ValueError(f"Your terminal does not appear the sequence {sequence.name}")
+        
+        # Terminate the args
+        args = (*args, -1)
+
+        # Grab the sequence
+        out = self._emit_seq(sequence, *args)
+
+        # Check if we actually got anything
+        if out is None:
+            raise TypeError(f"Wrong number of arguments passed for sequence {sequence.name}")
+
+
+        return out
+
+
+    def _emit_seq(self, seq: TermSeq, *args):
+        """
+        wrapper for chafa_term_info_emit_seq
+        """
+
+        _Chafa.chafa_term_info_emit_seq.argtypes = [
+            ctypes.c_void_p,
+            ctypes.c_int
+        ]
+
+        _Chafa.chafa_term_info_emit_seq.restype  = ctypes.c_char_p
+
+        res = _Chafa.chafa_term_info_emit_seq(self._term_info, seq, *args)
+
+        return res
+    


Test patch:
diff --git a/tests/5_passthrough_test.py b/tests/5_passthrough_test.py
new file mode 100644
index 0000000..a5377c6
--- /dev/null
+++ b/tests/5_passthrough_test.py
@@ -0,0 +1,13 @@
+from chafa import *
+
+def test_passthrough():
+    config = CanvasConfig()
+
+    through = config.passthrough
+    print("Before:", through)
+
+    config.passthrough = Passthrough.CHAFA_PASSTHROUGH_SCREEN
+
+    print("After:", config.passthrough)
+
+    assert config.passthrough != through
\ No newline at end of file
diff --git a/tests/6_print_rows_test.py b/tests/6_print_rows_test.py
new file mode 100644
index 0000000..5641fe5
--- /dev/null
+++ b/tests/6_print_rows_test.py
@@ -0,0 +1,45 @@
+from chafa import *
+from chafa.loader import Loader
+
+from pathlib import Path
+
+FONT_WIDTH  = 11
+FONT_HEIGHT = 24
+
+def test_print_rows():
+    # Init canvas config
+    config = CanvasConfig()
+
+    # Set canvas height, width and cell geometry
+    config.height = 30
+    config.width  = 30
+
+    # Set the canvas cell geometry
+    config.cell_height = FONT_HEIGHT
+    config.cell_width  = FONT_WIDTH
+
+    # Open image with the loader
+    image = Loader(Path(__file__).parent / "snake.jpg")
+
+    width      = image.width
+    height     = image.height
+    rowstride  = image.rowstride
+
+    # Calculate the appropriate geometry for the canvas
+    config.calc_canvas_geometry(width, height, FONT_WIDTH/FONT_HEIGHT)
+
+    # Init the canvas
+    canvas = Canvas(config)
+
+    # Draw to canvas
+    canvas.draw_all_pixels(
+        image.pixel_type,
+        image.get_pixels(),
+        width,
+        height,
+        rowstride
+    )
+
+    # Write picture
+    for row in canvas.print_rows(fallback=True):
+        print(row.decode())