|
24 | 24 | Filter, |
25 | 25 | FitMode, |
26 | 26 | Glow, |
| 27 | + Grain, |
27 | 28 | ImageEffect, |
28 | 29 | ImageLayer, |
29 | 30 | LayerType, |
@@ -575,7 +576,10 @@ def _render_background_layer(self, image: Image.Image, layer: BackgroundLayer): |
575 | 576 | return |
576 | 577 |
|
577 | 578 | for effect in layer.effects: |
578 | | - layer_image = self._apply_filter(layer_image, effect) |
| 579 | + if isinstance(effect, Grain): |
| 580 | + layer_image = self._apply_grain(layer_image, effect) |
| 581 | + else: |
| 582 | + layer_image = self._apply_filter(layer_image, effect) |
579 | 583 |
|
580 | 584 | if layer.opacity < 1.0 and not layer.color: |
581 | 585 | layer_image = self._apply_opacity(layer_image, layer.opacity) |
@@ -652,6 +656,90 @@ def _apply_filter(self, image: Image.Image, effect: Filter) -> Image.Image: |
652 | 656 | image = self._apply_saturation(image, effect.saturation) |
653 | 657 | return image |
654 | 658 |
|
| 659 | + @staticmethod |
| 660 | + def _generate_noise_image( |
| 661 | + size: tuple[int, int], |
| 662 | + intensity: float, |
| 663 | + monochrome: bool, |
| 664 | + seed: int | None, |
| 665 | + ) -> Image.Image | None: |
| 666 | + import random as _random |
| 667 | + |
| 668 | + pixel_count = size[0] * size[1] |
| 669 | + max_val = int(intensity * 255) |
| 670 | + |
| 671 | + if max_val == 0: |
| 672 | + return None |
| 673 | + |
| 674 | + lut = bytes(i * max_val // 255 for i in range(256)) |
| 675 | + |
| 676 | + if seed is not None: |
| 677 | + rng = _random.Random(seed) |
| 678 | + if monochrome: |
| 679 | + raw = rng.randbytes(pixel_count) |
| 680 | + else: |
| 681 | + raw_r, raw_g, raw_b = ( |
| 682 | + rng.randbytes(pixel_count), |
| 683 | + rng.randbytes(pixel_count), |
| 684 | + rng.randbytes(pixel_count), |
| 685 | + ) |
| 686 | + else: |
| 687 | + if monochrome: |
| 688 | + raw = os.urandom(pixel_count) |
| 689 | + else: |
| 690 | + raw_r, raw_g, raw_b = ( |
| 691 | + os.urandom(pixel_count), |
| 692 | + os.urandom(pixel_count), |
| 693 | + os.urandom(pixel_count), |
| 694 | + ) |
| 695 | + |
| 696 | + if monochrome: |
| 697 | + ch = Image.frombytes("L", size, raw).point(lut) |
| 698 | + noise_img = Image.merge("RGB", [ch, ch, ch]) |
| 699 | + else: |
| 700 | + noise_img = Image.merge( |
| 701 | + "RGB", |
| 702 | + [ |
| 703 | + Image.frombytes("L", size, raw_r).point(lut), |
| 704 | + Image.frombytes("L", size, raw_g).point(lut), |
| 705 | + Image.frombytes("L", size, raw_b).point(lut), |
| 706 | + ], |
| 707 | + ) |
| 708 | + return noise_img.convert("RGBA") |
| 709 | + |
| 710 | + def _blend_grain( |
| 711 | + self, |
| 712 | + image: Image.Image, |
| 713 | + intensity: float, |
| 714 | + monochrome: bool, |
| 715 | + seed: int | None, |
| 716 | + blend_mode: str, |
| 717 | + opacity: float, |
| 718 | + ) -> Image.Image: |
| 719 | + if opacity == 0.0: |
| 720 | + return image |
| 721 | + noise = self._generate_noise_image(image.size, intensity, monochrome, seed) |
| 722 | + if noise is None: |
| 723 | + return image |
| 724 | + r, g, b, original_alpha = image.split() |
| 725 | + blended = self._apply_blend_mode(image, noise, blend_mode) |
| 726 | + br, bg, bb, _ = blended.split() |
| 727 | + if opacity < 1.0: |
| 728 | + br = Image.blend(r, br, opacity) |
| 729 | + bg = Image.blend(g, bg, opacity) |
| 730 | + bb = Image.blend(b, bb, opacity) |
| 731 | + return Image.merge("RGBA", (br, bg, bb, original_alpha)) |
| 732 | + |
| 733 | + def _apply_grain(self, image: Image.Image, effect: Grain) -> Image.Image: |
| 734 | + return self._blend_grain( |
| 735 | + image, |
| 736 | + effect.intensity, |
| 737 | + effect.monochrome, |
| 738 | + effect.seed, |
| 739 | + effect.blend_mode, |
| 740 | + effect.opacity, |
| 741 | + ) |
| 742 | + |
655 | 743 | def _apply_opacity_to_color(self, color: tuple[int, ...], opacity: float) -> tuple[int, ...]: |
656 | 744 | r, g, b = color[:3] |
657 | 745 |
|
@@ -799,6 +887,8 @@ def _render_image_layer(self, image: Image.Image, layer: ImageLayer): |
799 | 887 | for effect in layer.effects: |
800 | 888 | if isinstance(effect, Filter): |
801 | 889 | img = self._apply_filter(img, effect) |
| 890 | + elif isinstance(effect, Grain): |
| 891 | + img = self._apply_grain(img, effect) |
802 | 892 |
|
803 | 893 | for effect in layer.effects: |
804 | 894 | if isinstance(effect, Glow): |
|
0 commit comments