Fix ValueError: image has wrong mode in thumbnail generation#1128
Fix ValueError: image has wrong mode in thumbnail generation#1128robertatakenaka merged 7 commits intomasterfrom
Conversation
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 <[email protected]>
There was a problem hiding this comment.
Pull request overview
Fixes Pillow ValueError: image has wrong mode during thumbnail generation by normalizing incompatible image modes before calling Image.thumbnail(), preventing optimiser crashes on palette/CMYK/etc images.
Changes:
- Normalize PIL image mode to
RGB/RGBAbefore resizing inWebImageGenerator.get_thumbnail_bytes(). - Add regression tests for additional PIL modes (
P,CMYK,LA,1) in thumbnail generation.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
packtools/utils.py |
Converts unsupported image modes before calling thumbnail() to avoid Pillow resize crashes. |
tests/test_utils.py |
Adds tests intended to cover thumbnail generation for additional PIL image modes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def test_get_thumbnail_bytes_with_palette_mode(self): | ||
| mocked_image = Image.new("P", (10, 10)) | ||
| web_image_generator = utils.WebImageGenerator( | ||
| "image.png", self.extracted_package | ||
| ) | ||
| web_image_generator._image_object = mocked_image | ||
|
|
There was a problem hiding this comment.
The new tests create 10x10 images, which are already smaller than thumbnail_size (267x140). In that case Image.thumbnail() can no-op and never call resize(), so these tests may pass even without the mode-normalization fix. Use dimensions larger than thumbnail_size (e.g., 300x300) to ensure the resize path (and the previously failing code path) is exercised.
There was a problem hiding this comment.
Updated in c18babd. Tests now use 300x300 images (larger than thumbnail_size 267x140) to ensure the resize path is exercised, and assert the output is valid JPEG with dimensions bounded by thumbnail_size.
| result = web_image_generator.get_thumbnail_bytes() | ||
| self.assertIsNotNone(result) | ||
|
|
||
| def test_get_thumbnail_bytes_with_cmyk_mode(self): | ||
| mocked_image = Image.new("CMYK", (10, 10)) | ||
| web_image_generator = utils.WebImageGenerator( | ||
| "image.tiff", self.extracted_package | ||
| ) | ||
| web_image_generator._image_object = mocked_image | ||
|
|
||
| result = web_image_generator.get_thumbnail_bytes() | ||
| self.assertIsNotNone(result) | ||
|
|
||
| def test_get_thumbnail_bytes_with_la_mode(self): | ||
| mocked_image = Image.new("LA", (10, 10)) | ||
| web_image_generator = utils.WebImageGenerator( | ||
| "image.png", self.extracted_package | ||
| ) | ||
| web_image_generator._image_object = mocked_image | ||
|
|
||
| result = web_image_generator.get_thumbnail_bytes() | ||
| self.assertIsNotNone(result) | ||
|
|
||
| def test_get_thumbnail_bytes_with_1_mode(self): | ||
| mocked_image = Image.new("1", (10, 10)) | ||
| web_image_generator = utils.WebImageGenerator( | ||
| "image.tiff", self.extracted_package | ||
| ) | ||
| web_image_generator._image_object = mocked_image | ||
|
|
||
| result = web_image_generator.get_thumbnail_bytes() | ||
| self.assertIsNotNone(result) |
There was a problem hiding this comment.
These new tests only assert the result is non-None, which doesn't confirm the bytes are a valid JPEG thumbnail or that the output dimensions are bounded by thumbnail_size. Consider asserting the returned bytes can be opened by PIL and that the resulting image format/size matches expectations.
There was a problem hiding this comment.
Already addressed in c18babd — all four mode tests now use 300x300 images and assert the output is valid JPEG with dimensions bounded by thumbnail_size.
| if self._image_object.mode in ("LA", "PA"): | ||
| self._image_object = self._image_object.convert("RGBA") | ||
| elif self._image_object.mode not in ("RGB", "RGBA", "L"): | ||
| self._image_object = self._image_object.convert("RGB") |
There was a problem hiding this comment.
The special-casing to convert LA/PA to RGBA adds branching but _get_bytes() always converts to RGB before saving. If RGBA is only needed to make thumbnail() work for these modes, it would help to either (a) document why RGBA is required instead of converting directly to RGB, or (b) simplify into a single normalization rule (and/or extract to a helper) to keep thumbnail generation logic consistent and easier to maintain.
There was a problem hiding this comment.
Simplified in 7e9749b — removed the LA/PA special-casing and consolidated into a single rule (if mode not in ("RGB", "RGBA", "L"): convert("RGB")). Added a comment explaining why the conversion is needed and that _get_bytes() always saves as RGB anyway.
| if self._image_object.mode in ("LA", "PA"): | ||
| self._image_object = self._image_object.convert("RGBA") | ||
| elif self._image_object.mode not in ("RGB", "RGBA", "L"): | ||
| self._image_object = self._image_object.convert("RGB") |
There was a problem hiding this comment.
The PR description links to #363 as the relevant ticket, but that issue appears unrelated to thumbnail generation (it is about FundingGroup.__init__). Consider updating the referenced ticket/link in the PR description to the correct issue to avoid confusion for future tracking.
There was a problem hiding this comment.
@copilot realmente o issue está incorreto. O correto já foi indicado: scieloorg/ajuda#15
There was a problem hiding this comment.
The PR description already references the correct issue: scieloorg/ajuda#15. No code changes needed for this.
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 <[email protected]>
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 <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if self._image_object.mode not in ("RGB", "RGBA", "L"): | ||
| self._image_object = self._image_object.convert("RGB") | ||
|
|
||
| self._image_object.thumbnail(self.thumbnail_size) |
There was a problem hiding this comment.
get_thumbnail_bytes() now normalizes unsupported modes before calling thumbnail(), but create_thumbnail() still calls thumbnail() directly on whatever mode was loaded from disk. To avoid two thumbnail codepaths diverging (and to prevent the same ValueError when create_thumbnail() is used with P/CMYK/etc images), consider extracting this mode-normalization into a small helper and reusing it in both methods.
There was a problem hiding this comment.
Extracted _normalize_mode() as a static helper method in 6b0a01c. Both create_thumbnail() and get_thumbnail_bytes() now share the same normalization logic, preventing the ValueError in both codepaths.
… and get_thumbnail_bytes() Co-authored-by: robertatakenaka <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
packtools/utils.py:380
create_thumbnail()saves the normalized image as JPEG, but_normalize_mode()allowsRGBAthrough unchanged. Pillow cannot saveRGBAdirectly as JPEG (it raises an error like “cannot write mode RGBA as JPEG”), socreate_thumbnail()can still fail for RGBA inputs. Consider converting to RGB before saving (e.g., make_normalize_mode()always return RGB for thumbnail generation, or convert increate_thumbnail()right beforesave()) to align with_get_bytes(), which always saves as RGB.
thumbnail_file = self._normalize_mode(image_file.copy())
new_filename = os.path.splitext(self.image_file_path)[0] + ".thumbnail.jpg"
if destination_path is not None and len(destination_path) > 0:
new_filename = os.path.join(
destination_path, os.path.basename(new_filename)
)
thumbnail_file.thumbnail(self.thumbnail_size)
try:
thumbnail_file.save(new_filename, "JPEG", quality='web_high')
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ) | ||
| else: | ||
| thumbnail_file = image_file.copy() | ||
| thumbnail_file = self._normalize_mode(image_file.copy()) |
There was a problem hiding this comment.
This PR fixes unsupported modes in both get_thumbnail_bytes() and create_thumbnail(), but the new regression tests only cover get_thumbnail_bytes(). Since create_thumbnail() is a separate codepath used to write thumbnails to disk, please add at least one test that exercises create_thumbnail() with an unsupported mode (e.g., P or CMYK) to prevent regressions of the original ValueError in that path.
| result = web_image_generator.get_thumbnail_bytes() | ||
| result_image = Image.open(io.BytesIO(result)) | ||
| self.assertEqual(result_image.format, "JPEG") |
There was a problem hiding this comment.
These tests open the returned JPEG bytes with Image.open(io.BytesIO(result)) but never close the image object. Using a context manager (with Image.open(...) as result_image:) (or calling result_image.close()) avoids potential ResourceWarnings and keeps the tests’ resource handling consistent.
Co-authored-by: Copilot <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def test_get_thumbnail_bytes_with_palette_mode(self): | ||
| mocked_image = Image.new("P", (300, 300)) | ||
| web_image_generator = utils.WebImageGenerator( | ||
| "image.png", self.extracted_package | ||
| ) | ||
| web_image_generator._image_object = mocked_image | ||
|
|
There was a problem hiding this comment.
The new regression tests exercise get_thumbnail_bytes() for several non-RGB modes, but there is still no coverage for the updated create_thumbnail() codepath that now relies on _normalize_mode(). Since the PR claims to fix the same ValueError for both thumbnail generators, add at least one test that writes a real P/CMYK (or 1/LA) image to disk and asserts create_thumbnail() produces a valid JPEG thumbnail file (and dimensions are within thumbnail_size).
Co-authored-by: Copilot <[email protected]>
O que esse PR faz?
Corrige
ValueError: image has wrong modeao gerar thumbnails de imagens com modos PIL incompatíveis com resize (P,CMYK,I,F,1,LA,PA).A conversão para
RGBjá existia em_get_bytes(), mas o crash ocorria antes — na chamadathumbnail(). A correção introduz um método auxiliar_normalize_mode()que normaliza o modo da imagem paraRGBantes do resize:Este helper é reutilizado em ambos os métodos
get_thumbnail_bytes()ecreate_thumbnail(), evitando codepaths divergentes. Não há necessidade de tratarLA/PAseparadamente (convertendo paraRGBA) porque_get_bytes()sempre converte paraRGBantes de salvar.Onde a revisão poderia começar?
packtools/utils.py— método_normalize_mode()e seus usos emget_thumbnail_bytes()ecreate_thumbnail()da classeWebImageGenerator.Como este poderia ser testado manualmente?
Criar uma imagem com modo
P(paleta) ouCMYKe processar com o optimiser:Algum cenário de contexto que queira dar?
O PIL/Pillow não suporta
resizecom resamplers de alta qualidade (LANCZOS) em imagens com modos comoP,CMYK,I,F. Imagens nestes modos são comuns em pacotes SPS (ex: TIFFs CMYK de produção gráfica, PNGs com paleta indexada).A lógica de normalização foi extraída para o método estático
_normalize_mode()para ser compartilhada entreget_thumbnail_bytes()ecreate_thumbnail(), garantindo que ambos os codepaths de geração de thumbnails sejam protegidos contra modos incompatíveis.Os testes de regressão utilizam imagens de 300x300 pixels (maiores que
thumbnail_sizede 267x140) para garantir que o caminho de resize é efetivamente exercitado. Cada teste verifica que o resultado é um JPEG válido com dimensões dentro do limite dethumbnail_size.Screenshots
N/A
Quais são tickets relevantes?
scieloorg/ajuda#15
Referências
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.