Skip to content

Commit

Permalink
Merge pull request #5 from tfuxu/simplify-palette-wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
tfuxu committed Sep 2, 2023
2 parents 497073f + 3775897 commit b7ab855
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 32 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ except Exception as e:
print(f"Couldn't load the requested image. Exception: {e}")

# This is the color palette used in output image
palette = dither_go.create_palette(
RGBA(0, 0, 0, 255),
RGBA(255, 255, 255, 255),
palette = dither_go.create_palette([
[0, 0, 0],
[255, 255, 255],
# You can put here any color you want
)
])

# Create new `Ditherer` object using a constructor
ditherer = dither_go.new_ditherer(palette)
Expand Down
8 changes: 4 additions & 4 deletions README_PyPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ except Exception as e:
print(f"Couldn't load the requested image. Exception: {e}")

# This is the color palette used in output image
palette = dither_go.create_palette(
RGBA(0, 0, 0, 255),
RGBA(255, 255, 255, 255),
palette = dither_go.create_palette([
[0, 0, 0],
[255, 255, 255],
# You can put here any color you want
)
])

# Create new `Ditherer` object using a constructor
ditherer = dither_go.new_ditherer(palette)
Expand Down
1 change: 1 addition & 0 deletions dither_go/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@

from .wrapper import *
from .matrices import *
from .exceptions import DitherGoError, InvalidColorError
9 changes: 9 additions & 0 deletions dither_go/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright 2023, tfuxu <https://github.com/tfuxu>
# SPDX-License-Identifier: GPL-3.0-or-later

class DitherGoError(Exception):
""" Base exception class used by modules in Dither Go. """


class InvalidColorError(DitherGoError):
""" Raised when there is an error during parsing/converting a color value. """
Empty file added dither_go/utils/__init__.py
Empty file.
122 changes: 122 additions & 0 deletions dither_go/utils/color.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Copyright 2023, tfuxu <https://github.com/tfuxu>
# SPDX-License-Identifier: GPL-3.0-or-later

from typing import List, Union

from dither_go.bindings import dither_go
from dither_go.exceptions import InvalidColorError


class ColorUtils:
"""
A class for internal color parsing/manipulation utility methods.
"""

def __init__(self):
pass

def color_to_rgba(self, color_value: Union[List[int], str]):
"""
Converts either an list of RGB color channels or hexadecimal representation
to the `color.RGBA` Golang object.
:param color_value: Either an list of RGB color channels or hex color code.
:type color_value: Union[List[int], str]
:raises InvalidColorError: When there is an error during color code parsing or
reading a color representation type.
:returns: An `color.RGBA` Golang object for use in color palettes.
"""

if not isinstance(color_value, list) and not isinstance(color_value, str):
raise InvalidColorError("Invalid format of color value provided")

if isinstance(color_value, list):
rgba_list = self.is_valid_rgba(color_value)
elif isinstance(color_value, str):
rgba_list = self.is_valid_hex(color_value)

if len(rgba_list) == 3:
rgba_list.append(255)

r, g, b, a = rgba_list

return dither_go.CreateRGBA(r, g, b, a)

def is_valid_hex(self, hex_value: str) -> List[int]:
"""
Checks if provided hexadecimal value is a valid representation
of a hexadecimal color code and returns an list of RGB color channels
upon success.
It supports short hex codes (eg. #fff), normal sized codes and
extended form with alpha channel (transparency).
:param hex_value: A hexadecimal value with `#` prefix.
:type hex_value: :class:`str`
:raises InvalidColorError: When there is an error during color code parsing.
:returns: An list of RGB color channels converted from hexadecimal value.
:rtype: List[int]
"""

if not hex_value.startswith("#"):
raise InvalidColorError("Color code isn't prefixed with hash (#) character")

hex_value = hex_value.lstrip("#")

if len(hex_value) not in [3, 6, 8]:
raise InvalidColorError(f"Provided hexadecimal code has an invalid length: {len(hex_value)}")

index_tuple = (0, 2, 4)
channel_length = 2

if len(hex_value) == 8:
index_tuple = index_tuple + (6,)

if len(hex_value) == 3:
index_tuple = (0, 1, 2)
channel_length = 1

rgba_list = []
for i in index_tuple:
try:
rgba_list.append(int(hex_value[i:i+channel_length], 16))
except ValueError as exc:
raise InvalidColorError("Color channel value in color code isn't an valid hexadecimal number") from exc

return rgba_list

def is_valid_rgba(self, rgba_list: List[int]) -> List[int]:
"""
Checks if provided list of color channel values represents
a valid RGB-formatted color.
It supports RGB values with and without the forth alpha channel provided
(transparency).
:param rgba_list: A list of RGB color channels.
:type rgba_list: List[int]
:raises InvalidColorError: When there is an error during color channel parsing.
:returns: The provided list as an indication of success.
:rtype: List[int]
"""

if len(rgba_list) not in [3, 4]:
raise InvalidColorError(f"Provided color channel list contains invalid amount of values: {len(rgba_list)}")

for channel in rgba_list:
if not isinstance(channel, int):
try:
channel = int(channel)
except ValueError as exc:
raise InvalidColorError("An color channel in provided list is an instance of the unsupported data type") from exc

if channel not in range(0, 256):
raise InvalidColorError("Color channel value is outside the (0, 255) range")

return rgba_list
67 changes: 48 additions & 19 deletions dither_go/wrapper.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Copyright 2023, tfuxu <https://github.com/tfuxu>
# SPDX-License-Identifier: GPL-3.0-or-later

from typing import List, Union

from dither_go.utils.color import ColorUtils
from dither_go.bindings import dither, dither_go


Expand Down Expand Up @@ -62,20 +65,29 @@ def new_ditherer(palette):
If the palette is None, then None will be returned.
.. note:: All palette colors should be opaque.
:param palette: A color palette created using ``create_palette`` function.
:returns: A new ``Ditherer`` Golang object with provided color palette.
"""

# TODO: Use inherited Dither class instead
return dither.NewDitherer(palette)


# ---- Library Functions ---
def round_clamp(i: float) -> int:
def round_clamp(number: float) -> int:
"""
Clamps the number and rounds it, rounding ties to the nearest even number.
This should be used if you're writing your own PixelMapper.
:param number: An floating-point number.
:type number: :class:`float`
:rtype: :class:`int`
"""

return dither.RoundClamp(i)
return dither.RoundClamp(number)

def error_diffusion_strength(edm, strength: float):
"""
Expand All @@ -99,12 +111,17 @@ def error_diffusion_strength(edm, strength: float):
# ---- Helper Functions ---
def open_image(path: str):
"""
Opens image file and decodes its contents using image.Decode function.
:raises Exception: If there is a failure in i/o operations or image decoding.
Opens image file and decodes its contents using ``image.Decode`` Golang function.
.. note:: Check ``format_matrix.md`` document for information about
supported image formats.
:param path: An path to the location of the image.
:type path: :class:`str`
:raises Exception: If there is a failure in I/O operations or image decoding.
:returns: An ``image.Image`` Golang object containing image data.
"""

try:
Expand All @@ -119,33 +136,45 @@ def save_image(img_data, output_path: str, encode_format: str) -> None:
Saves provided image data in specified output path and
encodes it to the supported format.
:raises Exception: If there is a failure in i/o operations or image encoding.
.. note:: Check ``format_matrix.md`` document for information about
supported image formats and their names used in ``encode_format``.
:param output_path: An path to the output location of the image.
:type output_path: :class:`str`
:param encode_format: A name of the image format used in encoding.
:type encode_format: :class:`str`
:raises Exception: If there is a failure in I/O operations or image encoding.
:rtype: :class:`None`
"""

try:
dither_go.SaveImage(img_data, output_path, encode_format)
except Exception as exc:
raise exc

def create_palette(*args):
def create_palette(color_list: List[Union[str, List[int]]]):
"""
Creates a list of `color.RGBA` objects.
Creates a new color palette for use in dithered images.
.. warning:: Always create `color.RGBA` values with `RGBA()` constructor
to avoid accessing invalid memory addresses.
"""
It supports mixing hexadecimal color codes (in short, normal and extended forms),
with lists of RGB color channels (with and without alpha channel provided).
return dither_go.CreatePalette(*args)
:param color_list: A list with hex color values and/or lists of RGBA channel
value representations written using integers.
:type color_list: List[Union[str, List[int]]]
def RGBA(red: int, green: int, blue: int, alpha: int):
"""
Creates new ``color.RGBA`` object with provided (red, green, blue, alpha)
color channels.
:raises InvalidColorError: When there is an error during color parsing/conversion.
.. note:: Values higher than 255 will result in 'Out of range' exceptions
:returns: An list of ``color.RGBA`` Golang objects for use in image dithering.
"""

return dither_go.CreateRGBA(red, green, blue, alpha)
color_utils = ColorUtils()

palette = []
for value in color_list:
palette.append(color_utils.color_to_rgba(value))

return dither_go.CreatePalette(*palette)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ classifiers = [
"Bug Tracker" = "https://github.com/tfuxu/dither-go/issues"

[tool.setuptools]
packages = ["dither_go", "dither_go.bindings"]
packages = ["dither_go", "dither_go.bindings", "dither_go.utils"]
2 changes: 0 additions & 2 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +0,0 @@
import sys
sys.path.append('..')
6 changes: 4 additions & 2 deletions tests/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
# SPDX-License-Identifier: GPL-3.0-or-later

import dither_go
from dither_go import RGBA

try:
img = dither_go.open_image("tests/input.jpg")
except Exception as e:
raise e

palette = dither_go.create_palette(RGBA(0, 0, 0, 255), RGBA(255, 255, 255, 255))
palette = dither_go.create_palette([
[0, 0, 0, 255],
[255, 255, 255, 255],
])

dither_object = dither_go.new_ditherer(palette)
dither_object.SetOrdered(dither_go.OrderedDitherers.ClusteredDot4x4, 1.0)
Expand Down
32 changes: 32 additions & 0 deletions tests/test_color.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright 2023, tfuxu <https://github.com/tfuxu>
# SPDX-License-Identifier: GPL-3.0-or-later

from dither_go.utils.color import ColorUtils


def test_is_valid_hex():
"""
Tests if `is_valid_hex()` helper method can properly parse hexadecimal codes
and translate them to correct RGBA color channels.
"""

color_utils = ColorUtils()

test_hex_codes = ["#fff", "#128", "#abdfbe", "#deadbeef"]
valid_results = [[15, 15, 15], [1, 2, 8], [171, 223, 190], [222, 173, 190, 239]]

for value, result in zip(test_hex_codes, valid_results):
assert color_utils.is_valid_hex(value) == result

def test_is_valid_rgba():
"""
Tests if `is_valid_rgba()` helper method can properly parse RGBA color channel lists.
"""

color_utils = ColorUtils()

test_rgba_lists = [[0, 0, 0, 32], [132, 247, 89], [44, 114, 148], [255, 255, 255, 255]]
valid_results = test_rgba_lists[:]

for value, result in zip(test_rgba_lists, valid_results):
assert color_utils.is_valid_rgba(value) == result

0 comments on commit b7ab855

Please sign in to comment.