Skip to content

Commit b872cee

Browse files
committed
rel 2024.1
1 parent 1f8dc44 commit b872cee

File tree

10 files changed

+295
-154
lines changed

10 files changed

+295
-154
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
!test
22
README.rst
33
requirements_optional.txt
4+
profile.*
45

56
# DepHell stuff
67
poetry.lock

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,21 @@
33
All major and minor version changes will be documented in this file. Details of
44
patch-level version changes can be found in [commit messages](../../commits/master).
55

6+
## 2024.1 - 2024/01/27
7+
8+
- remove `imagetools.renderWAlphaOffset` as `blendLayers` now supports this:
9+
```py
10+
def blendLayers(
11+
background: Image.Image,
12+
foreground: Image.Image,
13+
blendType: BlendType | tuple[str, ...],
14+
opacity: float = 1.0,
15+
offsets: tuple[int, int] = (0, 0),
16+
) -> Image.Image:
17+
```
18+
- better support for different sized images. Note if the background is smaller than the
19+
foreground then some of the foreground will be cut off
20+
621
## 2024.0.1 - 2024/01/19
722

823
- update dependencies

blendmodes/blend.py

Lines changed: 165 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ def destin(
272272
foregroundAlpha: np.ndarray,
273273
backgroundColour: np.ndarray,
274274
foregroundColour: np.ndarray,
275-
):
275+
) -> tuple[np.ndarray, np.ndarray]:
276276
"""'clip' composite mode.
277277
278278
All parts of 'layer above' which are alpha in 'layer below' will be made
@@ -300,7 +300,7 @@ def destout(
300300
foregroundAlpha: np.ndarray,
301301
backgroundColour: np.ndarray,
302302
foregroundColour: np.ndarray,
303-
):
303+
) -> tuple[np.ndarray, np.ndarray]:
304304
"""Reverse 'Clip' composite mode.
305305
306306
All parts of 'layer below' which are alpha in 'layer above' will be made
@@ -323,7 +323,7 @@ def destatop(
323323
foregroundAlpha: np.ndarray,
324324
backgroundColour: np.ndarray,
325325
foregroundColour: np.ndarray,
326-
):
326+
) -> tuple[np.ndarray, np.ndarray]:
327327
"""Place the layer below above the 'layer above' in places where the 'layer above' exists...
328328
329329
where 'layer below' does not exist, but 'layer above' does, place 'layer-above'
@@ -344,7 +344,7 @@ def srcatop(
344344
foregroundAlpha: np.ndarray,
345345
backgroundColour: np.ndarray,
346346
foregroundColour: np.ndarray,
347-
):
347+
) -> tuple[np.ndarray, np.ndarray]:
348348
"""Place the layer below above the 'layer above' in places where the 'layer above' exists."""
349349
outAlpha = (foregroundAlpha * backgroundAlpha) + (backgroundAlpha * (1 - foregroundAlpha))
350350
with np.errstate(divide="ignore", invalid="ignore"):
@@ -459,34 +459,150 @@ def blendLayers(
459459
foreground: Image.Image,
460460
blendType: BlendType | tuple[str, ...],
461461
opacity: float = 1.0,
462+
offsets: tuple[int, int] = (0, 0),
462463
) -> Image.Image:
463-
"""Blend layers using numpy array.
464+
"""Blend two layers (background, and foreground).
465+
466+
Note if the background is smaller than the foreground then some of the foreground will be cut
467+
off
464468
465469
Args:
466470
----
467-
background (Image.Image): background layer
468-
foreground (Image.Image): foreground layer (must be same size as background)
469-
blendType (BlendType): The blendtype
470-
opacity (float): The opacity of the foreground image
471+
background (Image.Image): The background layer.
472+
foreground (Image.Image): The foreground layer (must be the same size as the background).
473+
blendType (BlendType): The blend type to be applied.
474+
opacity (float, optional): The opacity of the foreground image. Defaults to 1.0.
475+
offsets (Tuple[int, int], optional): Offsets for the foreground layer. Defaults to (0, 0).
471476
472477
Returns:
473478
-------
474-
Image.Image: combined image
479+
Image.Image: The combined image.
480+
481+
Examples:
482+
--------
483+
# Blend two layers with default parameters
484+
combined_image = blendLayers(background_image, foreground_image, BlendType.NORMAL)
485+
486+
# Blend two layers with custom opacity and offsets
487+
combined_image = blendLayers(
488+
background_image,
489+
foreground_image,
490+
BlendType.MULTIPLY,
491+
opacity=0.7,
492+
offsets=(100, 50)
493+
)
475494
"""
476-
# Convert the Image.Image to a numpy array
477-
npForeground: np.ndarray = imageIntToFloat(np.array(foreground.convert("RGBA")))
478-
npBackground: np.ndarray = imageIntToFloat(np.array(background.convert("RGBA")))
495+
arr = blendLayersArray(
496+
background=background,
497+
foreground=foreground,
498+
blendType=blendType,
499+
opacity=opacity,
500+
offsets=offsets,
501+
)
502+
503+
return Image.fromarray(np.uint8(np.around(arr, 0)))
504+
479505

480-
# Get the alpha from the layers
481-
backgroundAlpha = npBackground[:, :, 3]
482-
foregroundAlpha = npForeground[:, :, 3] * opacity
483-
combinedAlpha = backgroundAlpha * foregroundAlpha
506+
def blendLayersArray(
507+
background: np.ndarray | Image.Image,
508+
foreground: np.ndarray | Image.Image,
509+
blendType: BlendType,
510+
opacity: float = 1.0,
511+
offsets: tuple[int, int] = (0, 0),
512+
) -> np.ndarray:
513+
"""Blend two layers (background, and foreground).
484514
485-
# Get the colour from the layers
486-
backgroundColor = npBackground[:, :, 0:3]
487-
foregroundColor = npForeground[:, :, 0:3]
515+
Note if the background is smaller than the foreground then some of the foreground will be cut
516+
off
517+
518+
Args:
519+
----
520+
background (np.ndarray | Image.Image): The background layer.
521+
foreground (np.ndarray | Image.Image): The foreground layer (must be the same size as the background).
522+
blendType (BlendType): The blend type to be applied.
523+
opacity (float, optional): The opacity of the foreground image. Defaults to 1.0.
524+
offsets (Tuple[int, int], optional): Offsets for the foreground layer. Defaults to (0, 0).
525+
526+
Returns:
527+
-------
528+
np.ndarray: The combined image.
529+
530+
Examples:
531+
--------
532+
# Blend two layers with default parameters
533+
combined_image = blendLayers(background_image, foreground_image, BlendType.NORMAL)
534+
535+
# Blend two layers with custom opacity and offsets
536+
combined_image = blendLayers(
537+
background_image,
538+
foreground_image,
539+
BlendType.MULTIPLY,
540+
opacity=0.7,
541+
offsets=(100, 50)
542+
)
543+
"""
544+
# Convert the Image.Image to a numpy array if required
545+
if isinstance(background, Image.Image):
546+
background = np.array(background.convert("RGBA"))
547+
if isinstance(foreground, Image.Image):
548+
foreground = np.array(foreground.convert("RGBA"))
549+
550+
# do any offset shifting first
551+
if offsets[0] > 0:
552+
foreground = np.hstack(
553+
(np.zeros((foreground.shape[0], offsets[0], 4), dtype=np.float64), foreground)
554+
)
555+
elif offsets[0] < 0:
556+
if offsets[0] > -1 * foreground.shape[1]:
557+
foreground = foreground[:, -1 * offsets[0] :, :]
558+
else:
559+
# offset offscreen completely, there is nothing left
560+
return np.zeros(background.shape, dtype=np.float64)
561+
if offsets[1] > 0:
562+
foreground = np.vstack(
563+
(np.zeros((offsets[1], foreground.shape[1], 4), dtype=np.float64), foreground)
564+
)
565+
elif offsets[1] < 0:
566+
if offsets[1] > -1 * foreground.shape[0]:
567+
foreground = foreground[-1 * offsets[1] :, :, :]
568+
else:
569+
# offset offscreen completely, there is nothing left
570+
return np.zeros(background.shape, dtype=np.float64)
571+
572+
# resize array to fill small images with zeros
573+
if foreground.shape[0] < background.shape[0]:
574+
foreground = np.vstack(
575+
(
576+
foreground,
577+
np.zeros(
578+
(background.shape[0] - foreground.shape[0], foreground.shape[1], 4),
579+
dtype=np.float64,
580+
),
581+
)
582+
)
583+
if foreground.shape[1] < background.shape[1]:
584+
foreground = np.hstack(
585+
(
586+
foreground,
587+
np.zeros(
588+
(foreground.shape[0], background.shape[1] - foreground.shape[1], 4),
589+
dtype=np.float64,
590+
),
591+
)
592+
)
593+
594+
# crop the source if the backdrop is smaller
595+
foreground = foreground[: background.shape[0], : background.shape[1], :]
596+
597+
lower_norm = background / 255.0
598+
upper_norm = foreground / 255.0
599+
600+
upper_alpha = upper_norm[:, :, 3] * opacity
601+
lower_alpha = lower_norm[:, :, 3]
602+
603+
upper_rgb = upper_norm[:, :, :3]
604+
lower_rgb = lower_norm[:, :, :3]
488605

489-
# Some effects require alpha
490606
alphaFunc = {
491607
BlendType.DESTIN: destin,
492608
BlendType.DESTOUT: destout,
@@ -495,28 +611,34 @@ def blendLayers(
495611
}
496612

497613
if blendType in alphaFunc:
498-
return Image.fromarray(
499-
imageFloatToInt(
500-
np.clip(
501-
np.dstack(
502-
alphaFunc[blendType](
503-
backgroundAlpha, foregroundAlpha, backgroundColor, foregroundColor
504-
)
505-
),
506-
a_min=0,
507-
a_max=1,
508-
)
509-
)
614+
out_rgb, out_alpha = alphaFunc[blendType](lower_alpha, upper_alpha, lower_rgb, upper_rgb)
615+
else:
616+
out_rgb, out_alpha = alpha_comp_shell(
617+
lower_alpha, upper_alpha, lower_rgb, upper_rgb, blendType
510618
)
511619

512-
# Get the colours and the alpha for the new image
513-
colorComponents = (
514-
(backgroundAlpha - combinedAlpha)[:, :, None] * backgroundColor
515-
+ (foregroundAlpha - combinedAlpha)[:, :, None] * foregroundColor
516-
+ combinedAlpha[:, :, None] * blend(backgroundColor, foregroundColor, blendType)
517-
)
518-
alphaComponent = backgroundAlpha + foregroundAlpha - combinedAlpha
620+
return np.nan_to_num(np.dstack((out_rgb, out_alpha)), copy=False) * 255.0
519621

520-
return Image.fromarray(
521-
imageFloatToInt(np.clip(np.dstack((colorComponents, alphaComponent)), a_min=0, a_max=1))
522-
)
622+
623+
def alpha_comp_shell(
624+
lower_alpha: np.ndarray,
625+
upper_alpha: np.ndarray,
626+
lower_rgb: np.ndarray,
627+
upper_rgb: np.ndarray,
628+
blendType: BlendType | tuple[str, ...],
629+
) -> tuple[np.ndarray, np.ndarray]:
630+
"""
631+
Implement common transformations occurring in any blend or composite mode.
632+
"""
633+
634+
out_alpha = upper_alpha + lower_alpha - (upper_alpha * lower_alpha)
635+
636+
blend_rgb = blend(lower_rgb, upper_rgb, blendType)
637+
638+
lower_rgb_part = np.multiply(((1.0 - upper_alpha) * lower_alpha)[:, :, None], lower_rgb)
639+
upper_rgb_part = np.multiply(((1.0 - lower_alpha) * upper_alpha)[:, :, None], upper_rgb)
640+
blended_rgb_part = np.multiply((lower_alpha * upper_alpha)[:, :, None], blend_rgb)
641+
642+
out_rgb = np.divide((lower_rgb_part + upper_rgb_part + blended_rgb_part), out_alpha[:, :, None])
643+
644+
return out_rgb, out_alpha

blendmodes/imagetools.py

Lines changed: 0 additions & 27 deletions
This file was deleted.

documentation/reference/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,3 @@ A full list of `Blendmodes` project modules.
77
- [Blendmodes](blendmodes/index.md#blendmodes)
88
- [Blend](blendmodes/blend.md#blend)
99
- [BlendType](blendmodes/blendtype.md#blendtype)
10-
- [Imagetools](blendmodes/imagetools.md#imagetools)

0 commit comments

Comments
 (0)