Skip to content
Closed
7 changes: 3 additions & 4 deletions src/PIL/GifImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -957,10 +957,9 @@ def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None:
optimise = _FORCE_OPTIMIZE or im.mode == "L"
if optimise or im.width * im.height < 512 * 512:
# check which colors are used
used_palette_colors = []
for i, count in enumerate(im.histogram()):
if count:
used_palette_colors.append(i)
used_palette_colors = [
i for i, count in enumerate(im.histogram()) if count
]

if optimise or max(used_palette_colors) >= len(used_palette_colors):
return used_palette_colors
Expand Down
149 changes: 85 additions & 64 deletions src/PIL/ImageDraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,29 +868,45 @@ def floodfill(
edge = {(x, y)}
# use a set to keep record of current and previous edge pixels
# to reduce memory consumption
full_edge = set()
while edge:
new_edge = set()
for x, y in edge: # 4 adjacent method
for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)):
# If already processed, or if a coordinate is negative, skip
if (s, t) in full_edge or s < 0 or t < 0:
continue
try:
p = pixel[s, t]
except (ValueError, IndexError):
pass
else:
full_edge.add((s, t))
if border is None:
fill = _color_diff(p, background) <= thresh
full_edge: set[tuple[int, int]] = set()
if border is None:
# Optimize the common case: no border, threshold-based fill
while edge:
new_edge: set[tuple[int, int]] = set()
for x, y in edge: # 4 adjacent method
for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)):
if (s, t) in full_edge or s < 0 or t < 0:
continue
try:
p = pixel[s, t]
except (ValueError, IndexError):
pass
else:
full_edge.add((s, t))
if _color_diff(p, background) <= thresh:
pixel[s, t] = value
new_edge.add((s, t))
full_edge = edge # discard pixels processed
edge = new_edge
else:
# Border-based fill
while edge:
new_edge = set()
for x, y in edge:
for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)):
if (s, t) in full_edge or s < 0 or t < 0:
continue
try:
p = pixel[s, t]
except (ValueError, IndexError):
pass
else:
fill = p not in (value, border)
if fill:
pixel[s, t] = value
new_edge.add((s, t))
full_edge = edge # discard pixels processed
edge = new_edge
full_edge.add((s, t))
if p != value and p != border:
pixel[s, t] = value
new_edge.add((s, t))
full_edge = edge
edge = new_edge


def _compute_regular_polygon_vertices(
Expand Down Expand Up @@ -983,44 +999,31 @@ def _compute_regular_polygon_vertices(
msg = "rotation should be an int or float" # type: ignore[unreachable]
raise ValueError(msg)

# 2. Define Helper Functions
def _apply_rotation(point: list[float], degrees: float) -> tuple[float, float]:
return (
round(
point[0] * math.cos(math.radians(360 - degrees))
- point[1] * math.sin(math.radians(360 - degrees))
+ centroid[0],
2,
),
round(
point[1] * math.cos(math.radians(360 - degrees))
+ point[0] * math.sin(math.radians(360 - degrees))
+ centroid[1],
2,
),
)

def _compute_polygon_vertex(angle: float) -> tuple[float, float]:
start_point = [polygon_radius, 0]
return _apply_rotation(start_point, angle)

def _get_angles(n_sides: int, rotation: float) -> list[float]:
angles = []
degrees = 360 / n_sides
# Start with the bottom left polygon vertex
current_angle = (270 - 0.5 * degrees) + rotation
for _ in range(n_sides):
angles.append(current_angle)
current_angle += degrees
if current_angle > 360:
current_angle -= 360
return angles

# 3. Variable Declarations
angles = _get_angles(n_sides, rotation)

# 4. Compute Vertices
return [_compute_polygon_vertex(angle) for angle in angles]
# 2. Compute vertices directly
# Since start_point is always [polygon_radius, 0], the rotation formula simplifies to:
# X = polygon_radius * cos(rad) + centroid_x
# Y = polygon_radius * sin(rad) + centroid_y
# where rad = radians(360 - angle)
degrees = 360 / n_sides
# Start with the bottom left polygon vertex
current_angle = (270 - 0.5 * degrees) + rotation
cx, cy = centroid[0], centroid[1]
_cos = math.cos
_sin = math.sin
_radians = math.radians

vertices: list[tuple[float, float]] = []
for _ in range(n_sides):
rad = _radians(360 - current_angle)
vertices.append((
round(polygon_radius * _cos(rad) + cx, 2),
round(polygon_radius * _sin(rad) + cy, 2),
))
current_angle += degrees
if current_angle > 360:
current_angle -= 360

return vertices


def _color_diff(
Expand All @@ -1029,7 +1032,25 @@ def _color_diff(
"""
Uses 1-norm distance to calculate difference between two values.
"""
first = color1 if isinstance(color1, tuple) else (color1,)
second = color2 if isinstance(color2, tuple) else (color2,)

return sum(abs(first[i] - second[i]) for i in range(len(second)))
if isinstance(color1, tuple):
if isinstance(color2, tuple):
# Fast path for common RGB (3) and RGBA (4) cases
n = len(color2)
if n == 3:
return (
abs(color1[0] - color2[0])
+ abs(color1[1] - color2[1])
+ abs(color1[2] - color2[2])
)
elif n == 4:
return (
abs(color1[0] - color2[0])
+ abs(color1[1] - color2[1])
+ abs(color1[2] - color2[2])
+ abs(color1[3] - color2[3])
)
return sum(abs(a - b) for a, b in zip(color1, color2))
return abs(color1[0] - color2)
if isinstance(color2, tuple):
return abs(color1 - color2[0])
return abs(color1 - color2)
69 changes: 38 additions & 31 deletions src/PIL/ImageFilter.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from __future__ import annotations

import abc
import functools
from collections.abc import Sequence
from typing import cast

Expand Down Expand Up @@ -78,7 +77,7 @@ def __init__(
) -> None:
if scale is None:
# default scale is sum of kernel
scale = functools.reduce(lambda a, b: a + b, kernel)
scale = sum(kernel)
if size[0] * size[1] != len(kernel):
msg = "not enough coefficients in kernel"
raise ValueError(msg)
Expand Down Expand Up @@ -446,16 +445,16 @@ def __init__(
# Convert to a flat list
if table and isinstance(table[0], (list, tuple)):
raw_table = cast(Sequence[Sequence[int]], table)
flat_table: list[int] = []
for pixel in raw_table:
if len(pixel) != channels:
msg = (
"The elements of the table should "
f"have a length of {channels}."
)
raise ValueError(msg)
flat_table.extend(pixel)
table = flat_table
from itertools import chain

table = list(chain.from_iterable(raw_table))

if wrong_size or len(table) != items * channels:
msg = (
Expand Down Expand Up @@ -507,15 +506,18 @@ def generate(
msg = "Only 3 or 4 output channels are supported"
raise ValueError(msg)

table: list[float] = [0] * (size_1d * size_2d * size_3d * channels)
idx_out = 0
for b in range(size_3d):
for g in range(size_2d):
for r in range(size_1d):
table[idx_out : idx_out + channels] = callback(
r / (size_1d - 1), g / (size_2d - 1), b / (size_3d - 1)
)
idx_out += channels
# Precompute normalized coordinate arrays to avoid repeated division
r_values = [r / (size_1d - 1) for r in range(size_1d)]
g_values = [g / (size_2d - 1) for g in range(size_2d)]
b_values = [b / (size_3d - 1) for b in range(size_3d)]

# Build table using extend to avoid slice assignment overhead
table: list[float] = []
table_extend = table.extend
for bv in b_values:
for gv in g_values:
for rv in r_values:
table_extend(callback(rv, gv, bv))

return cls(
(size_1d, size_2d, size_3d),
Expand Down Expand Up @@ -557,25 +559,30 @@ def transform(
ch_out = channels or ch_in
size_1d, size_2d, size_3d = self.size

table: list[float] = [0] * (size_1d * size_2d * size_3d * ch_out)
# Precompute normalized coordinates and use extend for efficiency
source_table = self.table
table: list[float] = []
table_extend = table.extend
idx_in = 0
idx_out = 0
for b in range(size_3d):
for g in range(size_2d):
for r in range(size_1d):
values = self.table[idx_in : idx_in + ch_in]
if with_normals:
values = callback(
r / (size_1d - 1),
g / (size_2d - 1),
b / (size_3d - 1),
*values,
if with_normals:
r_values = [r / (size_1d - 1) for r in range(size_1d)]
g_values = [g / (size_2d - 1) for g in range(size_2d)]
b_values = [b / (size_3d - 1) for b in range(size_3d)]
for bv in b_values:
for gv in g_values:
for rv in r_values:
table_extend(
callback(rv, gv, bv, *source_table[idx_in : idx_in + ch_in])
)
idx_in += ch_in
else:
for b in range(size_3d):
for g in range(size_2d):
for r in range(size_1d):
table_extend(
callback(*source_table[idx_in : idx_in + ch_in])
)
else:
values = callback(*values)
table[idx_out : idx_out + ch_out] = values
idx_in += ch_in
idx_out += ch_out
idx_in += ch_in

return type(self)(
self.size,
Expand Down
Loading