Skip to content

Fix ValueError: image has wrong mode in thumbnail generation#1128

Merged
robertatakenaka merged 7 commits intomasterfrom
copilot/fix-airflow-package-optimization
Mar 11, 2026
Merged

Fix ValueError: image has wrong mode in thumbnail generation#1128
robertatakenaka merged 7 commits intomasterfrom
copilot/fix-airflow-package-optimization

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 10, 2026

O que esse PR faz?

Corrige ValueError: image has wrong mode ao gerar thumbnails de imagens com modos PIL incompatíveis com resize (P, CMYK, I, F, 1, LA, PA).

A conversão para RGB já existia em _get_bytes(), mas o crash ocorria antes — na chamada thumbnail(). A correção introduz um método auxiliar _normalize_mode() que normaliza o modo da imagem para RGB antes do resize:

@staticmethod
def _normalize_mode(image):
    if image.mode not in ("RGB", "RGBA", "L"):
        return image.convert("RGB")
    return image

Este helper é reutilizado em ambos os métodos get_thumbnail_bytes() e create_thumbnail(), evitando codepaths divergentes. Não há necessidade de tratar LA/PA separadamente (convertendo para RGBA) porque _get_bytes() sempre converte para RGB antes de salvar.

Onde a revisão poderia começar?

packtools/utils.py — método _normalize_mode() e seus usos em get_thumbnail_bytes() e create_thumbnail() da classe WebImageGenerator.

Como este poderia ser testado manualmente?

Criar uma imagem com modo P (paleta) ou CMYK e processar com o optimiser:

from PIL import Image
from packtools.utils import WebImageGenerator

img = Image.new("P", (300, 300))
gen = WebImageGenerator("test.png", "/tmp")
gen._image_object = img
gen.get_thumbnail_bytes()  # Antes: ValueError, agora: retorna bytes JPEG

Algum cenário de contexto que queira dar?

O PIL/Pillow não suporta resize com resamplers de alta qualidade (LANCZOS) em imagens com modos como P, 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 entre get_thumbnail_bytes() e create_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_size de 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 de thumbnail_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.

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]>
Copilot AI changed the title [WIP] Fix exception when optimizing package in Airflow Fix ValueError: image has wrong mode in thumbnail generation Mar 10, 2026
@robertatakenaka robertatakenaka marked this pull request as ready for review March 11, 2026 11:34
Copilot AI review requested due to automatic review settings March 11, 2026 11:34
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/RGBA before resizing in WebImageGenerator.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.

Comment thread tests/test_utils.py
Comment on lines +466 to +472
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

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tests/test_utils.py Outdated
Comment on lines +473 to +504
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)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packtools/utils.py Outdated
Comment on lines +421 to +424
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")
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packtools/utils.py Outdated
Comment on lines +421 to +424
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")
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot realmente o issue está incorreto. O correto já foi indicado: scieloorg/ajuda#15

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packtools/utils.py Outdated
Comment on lines 425 to 428
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)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() allows RGBA through unchanged. Pillow cannot save RGBA directly as JPEG (it raises an error like “cannot write mode RGBA as JPEG”), so create_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 in create_thumbnail() right before save()) 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.

Comment thread packtools/utils.py Outdated
Comment thread packtools/utils.py
)
else:
thumbnail_file = image_file.copy()
thumbnail_file = self._normalize_mode(image_file.copy())
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread tests/test_utils.py
Comment on lines +473 to +475
result = web_image_generator.get_thumbnail_bytes()
result_image = Image.open(io.BytesIO(result))
self.assertEqual(result_image.format, "JPEG")
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tests/test_utils.py
Comment on lines +466 to +472
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

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread packtools/utils.py Outdated
@robertatakenaka robertatakenaka merged commit 8a6145e into master Mar 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants