Skip to content

Commit 8a6145e

Browse files
CopilotrobertatakenakaCopilot
authored
Fix ValueError: image has wrong mode in thumbnail generation (#1128)
* Initial plan * Fix ValueError: image has wrong mode in thumbnail generation Convert images with unsupported PIL modes (P, CMYK, I, F, 1, LA, PA) to RGB or RGBA before calling thumbnail() in get_thumbnail_bytes(). This prevents the ValueError that occurs when PIL tries to resize images with these modes using high-quality resamplers. Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com> * Improve thumbnail tests: use 300x300 images and assert JPEG output Use image dimensions larger than thumbnail_size (267x140) to ensure the resize path is exercised. Assert the result is valid JPEG with dimensions bounded by thumbnail_size. Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com> * Simplify mode normalization: single rule converting to RGB Remove LA/PA special-casing since _get_bytes() always converts to RGB before saving. Add comment explaining why the conversion is needed. Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com> * Extract _normalize_mode() helper and reuse in both create_thumbnail() and get_thumbnail_bytes() Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 594b283 commit 8a6145e

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

packtools/utils.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ def create_thumbnail(self, destination_path=None):
369369
'Error opening image file "%s": %s' % (self.image_file_path, str(exc))
370370
)
371371
else:
372-
thumbnail_file = image_file.copy()
372+
thumbnail_file = self._normalize_mode(image_file.copy())
373373
new_filename = os.path.splitext(self.image_file_path)[0] + ".thumbnail.jpg"
374374
if destination_path is not None and len(destination_path) > 0:
375375
new_filename = os.path.join(
@@ -386,6 +386,32 @@ def create_thumbnail(self, destination_path=None):
386386
finally:
387387
image_file.close()
388388

389+
@staticmethod
390+
def _normalize_mode(image):
391+
"""Normalize the image mode so it is compatible with ``Image.thumbnail()``
392+
and with saving as JPEG.
393+
394+
Some modes such as P, CMYK, I, F, 1, LA, and PA are not accepted by
395+
``Image.thumbnail()`` and may raise ``ValueError``. Additionally, Pillow
396+
cannot save JPEGs from images with an alpha channel (e.g. RGBA). This
397+
helper converts images in unsupported modes to RGB, and composites RGBA
398+
images onto an RGB background, while leaving RGB and L images unchanged,
399+
so that both ``create_thumbnail()`` and ``get_thumbnail_bytes()`` share
400+
the same normalization logic and can be safely saved as JPEG.
401+
"""
402+
# Handle images with alpha channel explicitly so they can be saved as JPEG.
403+
if image.mode == "RGBA":
404+
# Composite onto a white background to remove transparency safely.
405+
background = Image.new("RGB", image.size, (255, 255, 255))
406+
alpha = image.split()[3]
407+
background.paste(image, mask=alpha)
408+
return background
409+
410+
# Convert any other unsupported mode to RGB.
411+
if image.mode not in ("RGB", "L"):
412+
return image.convert("RGB")
413+
return image
414+
389415
def _get_bytes(self, format):
390416
image_file = io.BytesIO()
391417
try:
@@ -418,6 +444,8 @@ def get_thumbnail_bytes(self):
418444
% self.filename
419445
)
420446

447+
self._image_object = self._normalize_mode(self._image_object)
448+
421449
self._image_object.thumbnail(self.thumbnail_size)
422450
return self._get_bytes("JPEG")
423451

tests/test_utils.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,58 @@ def test_get_thumbnail_bytes_ok(self):
463463
image_copy.save(image_expected, "JPEG")
464464
self.assertEqual(result, image_expected.getvalue())
465465

466+
def test_get_thumbnail_bytes_with_palette_mode(self):
467+
mocked_image = Image.new("P", (300, 300))
468+
web_image_generator = utils.WebImageGenerator(
469+
"image.png", self.extracted_package
470+
)
471+
web_image_generator._image_object = mocked_image
472+
473+
result = web_image_generator.get_thumbnail_bytes()
474+
result_image = Image.open(io.BytesIO(result))
475+
self.assertEqual(result_image.format, "JPEG")
476+
self.assertLessEqual(result_image.size[0], web_image_generator.thumbnail_size[0])
477+
self.assertLessEqual(result_image.size[1], web_image_generator.thumbnail_size[1])
478+
479+
def test_get_thumbnail_bytes_with_cmyk_mode(self):
480+
mocked_image = Image.new("CMYK", (300, 300))
481+
web_image_generator = utils.WebImageGenerator(
482+
"image.tiff", self.extracted_package
483+
)
484+
web_image_generator._image_object = mocked_image
485+
486+
result = web_image_generator.get_thumbnail_bytes()
487+
result_image = Image.open(io.BytesIO(result))
488+
self.assertEqual(result_image.format, "JPEG")
489+
self.assertLessEqual(result_image.size[0], web_image_generator.thumbnail_size[0])
490+
self.assertLessEqual(result_image.size[1], web_image_generator.thumbnail_size[1])
491+
492+
def test_get_thumbnail_bytes_with_la_mode(self):
493+
mocked_image = Image.new("LA", (300, 300))
494+
web_image_generator = utils.WebImageGenerator(
495+
"image.png", self.extracted_package
496+
)
497+
web_image_generator._image_object = mocked_image
498+
499+
result = web_image_generator.get_thumbnail_bytes()
500+
result_image = Image.open(io.BytesIO(result))
501+
self.assertEqual(result_image.format, "JPEG")
502+
self.assertLessEqual(result_image.size[0], web_image_generator.thumbnail_size[0])
503+
self.assertLessEqual(result_image.size[1], web_image_generator.thumbnail_size[1])
504+
505+
def test_get_thumbnail_bytes_with_1_mode(self):
506+
mocked_image = Image.new("1", (300, 300))
507+
web_image_generator = utils.WebImageGenerator(
508+
"image.tiff", self.extracted_package
509+
)
510+
web_image_generator._image_object = mocked_image
511+
512+
result = web_image_generator.get_thumbnail_bytes()
513+
result_image = Image.open(io.BytesIO(result))
514+
self.assertEqual(result_image.format, "JPEG")
515+
self.assertLessEqual(result_image.size[0], web_image_generator.thumbnail_size[0])
516+
self.assertLessEqual(result_image.size[1], web_image_generator.thumbnail_size[1])
517+
466518

467519
class TestXMLWebOptimiser(unittest.TestCase):
468520
def setUp(self):

0 commit comments

Comments
 (0)