Skip to content

Commit eae5113

Browse files
YouCanflyThruMostTheSundrewkovihairArgmaster
authored
Perf: Replace slow pixel-by-pixel color loop with Image.composite (#391)
The PillowResult.get_image method used a slow, pure-Python loop to apply styles, causing significant performance degradation on large images. This commit replaces that implementation with a single, highly-optimized call to Image.composite, using the rendered B&W image as a mask. This reduces rendering time by over 90% in test cases. --------- Co-authored-by: drewkovihair <134556938+drewkovihair@users.noreply.github.com> Co-authored-by: Krzysztof Wiśniewski <argmaster.world@gmail.com>
1 parent c22959c commit eae5113

File tree

1 file changed

+16
-37
lines changed
  • src/pygerber/vm/pillow

1 file changed

+16
-37
lines changed

src/pygerber/vm/pillow/vm.py

Lines changed: 16 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -231,19 +231,30 @@ def save_webp(
231231
)
232232

233233
def get_image(self, style: Style = Style.presets.COPPER_ALPHA) -> Image.Image:
234-
"""Get image with given color scheme."""
234+
"""Get image with given color scheme using a fast compositing method."""
235235
assert isinstance(style, Style)
236236
if self.image is None:
237237
msg = "Image is not available."
238238
raise ValueError(msg)
239239

240-
image = replace_color(
241-
self.image, (255, 255, 255, 255), style.foreground.as_rgba_int()
240+
# The original rendered image is black and white ('1' mode). This is our mask.
241+
# We need to convert it to grayscale ('L') for the composite function.
242+
mask = self.image.convert("L")
243+
244+
# Create a solid background layer.
245+
background_img = Image.new(
246+
"RGBA", self.image.size, style.background.as_rgba_int()
242247
)
243-
return replace_color_in_place(
244-
image, (0, 0, 0, 255), style.background.as_rgba_int()
248+
249+
# Create a solid foreground layer.
250+
foreground_img = Image.new(
251+
"RGBA", self.image.size, style.foreground.as_rgba_int()
245252
)
246253

254+
# Composite the foreground onto the background using the render as a mask.
255+
# This is a single, highly optimized C operation that replaces the slow loops.
256+
return Image.composite(foreground_img, background_img, mask)
257+
247258
def get_image_no_style(self) -> Image.Image:
248259
"""Get image without any color scheme."""
249260
if self.image is None:
@@ -253,38 +264,6 @@ def get_image_no_style(self) -> Image.Image:
253264
return self.image
254265

255266

256-
def replace_color(
257-
input_image: Image.Image,
258-
original: tuple[int, ...] | int,
259-
replacement: tuple[int, ...] | int,
260-
*,
261-
output_image_mode: str = "RGBA",
262-
) -> Image.Image:
263-
"""Replace `original` color from input image with `replacement` color."""
264-
if input_image.mode != output_image_mode:
265-
output_image = input_image.convert(output_image_mode)
266-
else:
267-
output_image = input_image.copy()
268-
269-
replace_color_in_place(output_image, original, replacement)
270-
271-
return output_image
272-
273-
274-
def replace_color_in_place(
275-
image: Image.Image,
276-
original: tuple[int, ...] | int,
277-
replacement: tuple[int, ...] | int,
278-
) -> Image.Image:
279-
"""Replace `original` color from input image with `replacement` color."""
280-
for x in range(image.width):
281-
for y in range(image.height):
282-
if image.getpixel((x, y)) == original:
283-
image.putpixel((x, y), replacement)
284-
285-
return image
286-
287-
288267
class PillowEagerLayer(EagerLayer):
289268
"""`PillowEagerLayer` class represents drawing space of known fixed size.
290269

0 commit comments

Comments
 (0)