Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add transform.hsl() #2398

Merged
merged 30 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c5f51bd
C implementation and stub.
itzpr3d4t0r Aug 14, 2023
1ad58db
first docs, fixes
itzpr3d4t0r Aug 14, 2023
8a62c62
more fixes
itzpr3d4t0r Aug 14, 2023
963c1cb
fix warning
itzpr3d4t0r Aug 14, 2023
a431200
changed algorithms for improved performance. Now accepts range -1 to …
itzpr3d4t0r Aug 15, 2023
4593fae
Added tests, made hue optional, fixed stubs, better error messages.
itzpr3d4t0r Aug 15, 2023
4e017bb
format and some fixes/small refactors
itzpr3d4t0r Aug 15, 2023
e9e47e2
Added one more test for the function return value.
itzpr3d4t0r Aug 16, 2023
9e17998
lazy fix
itzpr3d4t0r Aug 16, 2023
b3e360f
add thread stuff
itzpr3d4t0r Oct 27, 2023
89c27e0
Fix segfault
yunline Nov 30, 2023
fef615c
Merge branch 'main' into transform_hsl
yunline Nov 30, 2023
e6ff3b5
Update transform.c
yunline Nov 30, 2023
eee4aac
format the code
yunline Nov 30, 2023
a096df6
Update src_c/transform.c
yunline Dec 1, 2023
72857e2
Update src_c/transform.c
yunline Dec 1, 2023
d8730fe
Add locking/unlocking of both src & dst surfaces
MyreMylar Dec 2, 2023
158793d
review fixes
itzpr3d4t0r Dec 6, 2023
ffc0016
docstring update
itzpr3d4t0r Dec 6, 2023
6951846
explicit cast
itzpr3d4t0r Dec 6, 2023
498ca41
fix
itzpr3d4t0r Dec 6, 2023
add5285
format
itzpr3d4t0r Dec 6, 2023
d62b762
optimization for 4bpp surfaces
itzpr3d4t0r Dec 7, 2023
7cb8100
fixes
itzpr3d4t0r Dec 7, 2023
f16ef95
more optimizations
itzpr3d4t0r May 30, 2024
3ab71d9
extended optimized path to 3Bpp surfaces, minof fixes
itzpr3d4t0r May 30, 2024
a6f0d1e
fixes for SDL_BIG_ENDIAN and slow path
itzpr3d4t0r May 30, 2024
a89472f
more fixes
itzpr3d4t0r May 30, 2024
26241df
update versionadded tag
itzpr3d4t0r May 30, 2024
203b679
fix docs
itzpr3d4t0r May 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions buildconfig/stubs/pygame/transform.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,10 @@ def gaussian_blur(
repeat_edge_pixels: bool = True,
dest_surface: Optional[Surface] = None
) -> Surface: ...
def hsl(
surface: Surface,
hue: float = 0,
saturation: float = 0,
lightness: float = 0,
dest_surface: Optional[Surface] = None,
) -> Surface: ...
35 changes: 35 additions & 0 deletions docs/reST/ref/transform.rst
Original file line number Diff line number Diff line change
Expand Up @@ -410,4 +410,39 @@ Instead, always begin with the original image and scale to the desired size.)

.. ## pygame.transform.threshold ##

.. function:: hsl

| :sl:`Change the hue, saturation, and lightness of a surface.`
| :sg:`hsl(surface, hue, saturation, lightness, dest_surface=None) -> Surface`

This function allows you to modify the hue, saturation, and lightness of a given surface.

:param pygame.Surface surface: The surface to transform.

:param float hue: The amount to change the hue. Positive values rotate the hue clockwise,
while negative values rotate it counterclockwise. Value range: -360 to 360.

:param float saturation: The amount to change the saturation. Positive values increase saturation,
while negative values decrease it. Value range: -1 to 1.

:param float lightness: The amount to change the lightness. Positive values increase lightness,
while negative values decrease it. Value range: -1 to 1.

:param pygame.Surface dest_surface: An optional destination surface to store the transformed image.
If provided, it should have the same dimensions and depth as the source surface.

:returns: A new surface with the hue, saturation, and lightness transformed.

:Examples:

Apply a hue rotation of 30 degrees, increase saturation by 20%, and decrease lightness by 10% to a surface:

.. code-block:: python

new_surf = hsl(original_surf, 30, 0.2, -0.1)

.. versionadded:: 2.5.0

.. ## pygame.transform.hsl ##

.. ## pygame.transform ##
1 change: 1 addition & 0 deletions src_c/doc/transform_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@
#define DOC_TRANSFORM_INVERT "invert(surface, dest_surface=None) -> Surface\ninverts the RGB elements of a surface"
#define DOC_TRANSFORM_GRAYSCALE "grayscale(surface, dest_surface=None) -> Surface\ngrayscale a surface"
#define DOC_TRANSFORM_THRESHOLD "threshold(dest_surface, surface, search_color, threshold=(0,0,0,0), set_color=(0,0,0,0), set_behavior=1, search_surf=None, inverse_set=False) -> num_threshold_pixels\nfinds which, and how many pixels in a surface are within a threshold of a 'search_color' or a 'search_surf'."
#define DOC_TRANSFORM_HSL "hsl(surface, hue, saturation, lightness, dest_surface=None) -> Surface\nChange the hue, saturation, and lightness of a surface."
264 changes: 264 additions & 0 deletions src_c/transform.c
Original file line number Diff line number Diff line change
Expand Up @@ -2211,6 +2211,268 @@ surf_grayscale(PyObject *self, PyObject *args, PyObject *kwargs)
}
}

#define MIN3(a, b, c) MIN(MIN(a, b), c)
#define MAX3(a, b, c) MAX(MAX(a, b), c)

/* Following hsl code is based on code from
* https://gist.github.com/ciembor/1494530
* and
* http://en.wikipedia.org/wiki/HSL_color_space
*/
static PG_FORCEINLINE void
RGB_to_HSL(Uint8 r, Uint8 g, Uint8 b, float *h, float *s, float *l)
{
float r_1 = r / 255.0f;
float g_1 = g / 255.0f;
float b_1 = b / 255.0f;

float min = MIN3(r_1, g_1, b_1);
float max = MAX3(r_1, g_1, b_1);
float delta = max - min;

*l = (max + min) / 2.0f;

if (delta == 0) {
*h = *s = 0; // achromatic
}
else {
*s = *l > 0.5f ? delta / (2.0f - max - min) : delta / (max + min);

if (max == r_1) {
*h = (g_1 - b_1) / delta + (g_1 < b_1 ? 6 : 0);
}
else if (max == g_1) {
*h = (b_1 - r_1) / delta + 2;
}
else {
*h = (r_1 - g_1) / delta + 4;
}
*h /= 6;
}
}

static PG_FORCEINLINE float
hue_to_rgb(float p, float q, float t)
{
if (t < 0)
t += 1;
if (t > 1)
t -= 1;
if (t < 1 / 6.0f)
return p + (q - p) * 6 * t;
if (t < 1 / 2.0f)
return q;
if (t < 2 / 3.0f)
return p + (q - p) * (2 / 3.0f - t) * 6;
return p;
}

static PG_FORCEINLINE void
HSL_to_RGB(float h, float s, float l, Uint8 *r, Uint8 *g, Uint8 *b)
{
if (s == 0) {
*r = *g = *b = (Uint8)(l * 255);
}
else {
float q = l < 0.5f ? l * (1 + s) : l + s - l * s;
float p = 2 * l - q;
*r = (Uint8)(hue_to_rgb(p, q, h + 1 / 3.0f) * 255);
*g = (Uint8)(hue_to_rgb(p, q, h) * 255);
*b = (Uint8)(hue_to_rgb(p, q, h - 1 / 3.0f) * 255);
}
}

static void
modify_hsl(SDL_Surface *surf, SDL_Surface *dst, float h, float s, float l)
{
int x, y;
yunline marked this conversation as resolved.
Show resolved Hide resolved
Uint8 r, g, b, a;
float s_h = 0, s_s = 0, s_l = 0;
SDL_PixelFormat *fmt = surf->format;
Uint8 *srcp8 = (Uint8 *)surf->pixels;
Uint8 *dstp8 = (Uint8 *)dst->pixels;

int surf_locked = 0;
if (SDL_MUSTLOCK(surf)) {
if (SDL_LockSurface(surf) == 0) {
surf_locked = 1;
}
}
MyreMylar marked this conversation as resolved.
Show resolved Hide resolved
int dst_locked = 0;
if (SDL_MUSTLOCK(dst)) {
if (SDL_LockSurface(dst) == 0) {
dst_locked = 1;
}
}

if (fmt->BytesPerPixel == 4 || fmt->BytesPerPixel == 3) {
const int src_skip = surf->pitch - surf->w * fmt->BytesPerPixel;
const int dst_skip = dst->pitch - dst->w * fmt->BytesPerPixel;

#if SDL_BYTEORDER == SDL_LIL_ENDIAN
const int Ridx = fmt->Rshift >> 3;
const int Gidx = fmt->Gshift >> 3;
const int Bidx = fmt->Bshift >> 3;
#else
const int Ridx = 3 - (fmt->Rshift >> 3);
const int Gidx = 3 - (fmt->Gshift >> 3);
const int Bidx = 3 - (fmt->Bshift >> 3);
#endif

int height = surf->h;

while (height--) {
for (x = 0; x < surf->w; x++) {
RGB_to_HSL(srcp8[Ridx], srcp8[Gidx], srcp8[Bidx], &s_h, &s_s,
&s_l);

if (h) {
s_h += h;
if (s_h > 1)
s_h -= 1;
else if (s_h < 0)
s_h += 1;
}
if (s) {
s_s = s_s * (1 + s);
s_s = s_s > 1 ? 1 : s_s < 0 ? 0 : s_s;
}
if (l) {
s_l = l < 0 ? s_l * (1 + l) : s_l * (1 - l) + l;
s_l = s_l > 1 ? 1 : s_l < 0 ? 0 : s_l;
}

HSL_to_RGB(s_h, s_s, s_l, &r, &g, &b);
dstp8[Ridx] = r;
dstp8[Gidx] = g;
dstp8[Bidx] = b;

srcp8 += fmt->BytesPerPixel;
dstp8 += fmt->BytesPerPixel;
}
srcp8 += src_skip;
dstp8 += dst_skip;
}
}
else {
Uint8 *pix;
Uint32 pixel;
for (y = 0; y < surf->h; y++) {
for (x = 0; x < surf->w; x++) {
SURF_GET_AT(pixel, surf, x, y, srcp8, fmt, pix);
SDL_GetRGBA(pixel, fmt, &r, &g, &b, &a);
RGB_to_HSL(r, g, b, &s_h, &s_s, &s_l);

if (h) {
s_h += h;
if (s_h > 1)
s_h -= 1;
else if (s_h < 0)
s_h += 1;
}
if (s) {
s_s = s_s * (1 + s);
s_s = s_s > 1 ? 1 : s_s < 0 ? 0 : s_s;
}
if (l) {
s_l = l < 0 ? s_l * (1 + l) : s_l * (1 - l) + l;
s_l = s_l > 1 ? 1 : s_l < 0 ? 0 : s_l;
}

HSL_to_RGB(s_h, s_s, s_l, &r, &g, &b);
pixel = SDL_MapRGBA(fmt, r, g, b, a);
SURF_SET_AT(pixel, dst, x, y, dstp8, fmt, pix);
}
}
}

if (surf_locked) {
SDL_UnlockSurface(surf);
}
if (dst_locked) {
SDL_UnlockSurface(dst);
}
}

static PyObject *
surf_hsl(PyObject *self, PyObject *args, PyObject *kwargs)
{
pgSurfaceObject *surfobj;
pgSurfaceObject *surfobj2 = NULL;
SDL_Surface *dst, *src;
float h = 0, s = 0, l = 0;

static char *keywords[] = {"surface", "hue", "saturation",
Starbuck5 marked this conversation as resolved.
Show resolved Hide resolved
"lightness", "dest_surface", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!|fffO!", keywords,
&pgSurface_Type, &surfobj, &h, &s, &l,
&pgSurface_Type, &surfobj2)) {
return NULL;
}

if (s < -1 || s > 1) {
PyObject *value = PyFloat_FromDouble((double)s);
if (!value)
return NULL;

PyErr_Format(PyExc_ValueError,
"saturation value must be between -1 and 1, got %R",
value);
Py_DECREF(value);
return NULL;
}
if (l < -1 || l > 1) {
PyObject *value = PyFloat_FromDouble((double)l);
if (!value) {
return NULL;
}
PyErr_Format(PyExc_ValueError,
"lightness value must be between -1 and 1, got %R",
value);
Py_DECREF(value);
return NULL;
}

h = (float)(fmodf(h, 360.0) / 360.0);

src = pgSurface_AsSurface(surfobj);
SURF_INIT_CHECK(src);

if (!surfobj2) {
dst = newsurf_fromsurf(src, src->w, src->h);
if (!dst)
return NULL;
}
else {
dst = pgSurface_AsSurface(surfobj2);
}

if (dst->w != src->w || dst->h != src->h) {
return RAISE(
PyExc_ValueError,
"Destination surface must be the same size as source surface.");
}
if (src->format->Rmask != dst->format->Rmask ||
src->format->Gmask != dst->format->Gmask ||
src->format->Bmask != dst->format->Bmask ||
src->format->BytesPerPixel != dst->format->BytesPerPixel) {
return RAISE(PyExc_ValueError,
"Source and destination surfaces need the same format.");
}

Py_BEGIN_ALLOW_THREADS;
modify_hsl(src, dst, h, s, l);
Py_END_ALLOW_THREADS;

if (surfobj2) {
Py_INCREF(surfobj2);
return (PyObject *)surfobj2;
}
else {
return (PyObject *)pgSurface_New(dst);
}
}

/*
number to use for missing samples
*/
Expand Down Expand Up @@ -3486,6 +3748,8 @@ static PyMethodDef _transform_methods[] = {
DOC_TRANSFORM_INVERT},
{"grayscale", (PyCFunction)surf_grayscale, METH_VARARGS | METH_KEYWORDS,
DOC_TRANSFORM_GRAYSCALE},
{"hsl", (PyCFunction)surf_hsl, METH_VARARGS | METH_KEYWORDS,
DOC_TRANSFORM_HSL},
{NULL, NULL, 0, NULL}};

MODINIT_DEFINE(transform)
Expand Down
Loading
Loading