Skip to content

Conversation

mulamatamvinay22
Copy link

@mulamatamvinay22 mulamatamvinay22 commented Aug 4, 2025

Description

Add RunwayTextToImage node that integrates with RunwayML's text_to_image API, enabling text prompt-based image generation. Includes support for environment variables, polling, and error handling.

Changes Made

  • ✅ Created RunwayTextToImage node under api_nodes
  • ✅ Added complete Google-style docstring for documentation
  • ✅ Implemented polling mechanism to wait for image readiness
  • ✅ Integrated API key access from environment variable (RUNWAY_API_KEY)
  • ✅ Added pytest-based unit test with mock response
image

Logs

================================ test session starts =============================
platform win32 -- Python 3.12.5, pytest-8.4.1, pluggy-1.6.0
rootdir: C:\Users\mulam\DreamLayer
collected 1 item

tests\api_nodes\test_runway_text2img.py .                                 [100%]


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
  * Added support for generating images from text prompts using Runway's text-to-image API.
  * Introduced new test coverage for the text-to-image functionality.
  * Expanded image format support with plugins for AVIF, BDF, BLP, BMP, BUFR, CUR, DCX, DDS, EPS, FITS, FLI, FPX, FTEX, GBR, GD, GIF, GIMP gradients and palettes, GRIB, HDF5, ICNS, ICO, IM, IMT, IPTC, JPEG 2000, JPEG, JPEG presets, McIdas, MIC, MPEG, MPO, MSP, PostScript, and more.
  * Added modules for advanced image operations, color management, drawing, enhancements, filtering, font handling, math, morphology, palette management, path support, Qt/Tk/Win integration, statistics, and transformations.
* **Tests**
  * Added new automated tests for the Runway text-to-image feature.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Copy link
Contributor

coderabbitai bot commented Aug 4, 2025

Walkthrough

This update introduces a substantial set of new modules and plugins to the Python Imaging Library (PIL), adding support for a wide array of image formats, color management, drawing, transformations, and utility functions. It also adds a new ComfyUI-compatible node for Runway's text-to-image API and corresponding tests. The changes span core image handling, file format plugins, palette and font management, color operations, and platform-specific integrations.

Changes

Cohort / File(s) Change Summary
Runway Text-to-Image Node & Test
nodes/api_nodes/runway_text2img.py, tests/api_nodes/test_runway_text2img.py
Added a ComfyUI-compatible node for Runway's text-to-image API and a test with mocked HTTP requests.
PIL Core File Handling & Utilities
venv/Lib/site-packages/PIL/ImageFile.py, .../ImageMode.py, .../ImagePalette.py, .../ImageStat.py, .../ImageMath.py, .../ImageOps.py, .../ImageChops.py, .../ImageEnhance.py, .../ImageTransform.py, .../ImageSequence.py, .../ImageShow.py, .../ImageDraw.py, .../ImageDraw2.py, .../ImageColor.py, .../ImageFont.py, .../ImagePath.py, .../ImageGrab.py, .../ImageMorph.py
Introduced core image file classes, math, stats, palette, color, drawing, enhancement, transformation, and display utilities for PIL.
PIL File Format Plugins: Standard and Extended
venv/Lib/site-packages/PIL/AvifImagePlugin.py, .../BmpImagePlugin.py, .../BlpImagePlugin.py, .../BufrStubImagePlugin.py, .../CurImagePlugin.py, .../DcxImagePlugin.py, .../DdsImagePlugin.py, .../EpsImagePlugin.py, .../ExifTags.py, .../FitsImagePlugin.py, .../FliImagePlugin.py, .../FpxImagePlugin.py, .../FtexImagePlugin.py, .../GbrImagePlugin.py, .../GdImageFile.py, .../GifImagePlugin.py, .../GribStubImagePlugin.py, .../Hdf5StubImagePlugin.py, .../IcnsImagePlugin.py, .../IcoImagePlugin.py, .../ImImagePlugin.py, .../ImtImagePlugin.py, .../IptcImagePlugin.py, .../Jpeg2KImagePlugin.py, .../JpegImagePlugin.py, .../JpegPresets.py, .../McIdasImagePlugin.py, .../MicImagePlugin.py, .../MpegImagePlugin.py, .../MpoImagePlugin.py, .../MspImagePlugin.py
Added plugins for a broad range of image formats, including AVIF, BMP, BLP, BUFR, CUR, DCX, DDS, EPS, EXIF, FITS, FLI, FPX, FTEX, GBR, GD, GIF, GRIB, HDF5, ICNS, ICO, IM, IMT, IPTC, JPEG 2000, JPEG, JPEG presets, McIdas, MIC, MPEG, MPO, and MSP, with support for reading, writing, and metadata extraction.
PIL Palette and Gradient Utilities
venv/Lib/site-packages/PIL/PaletteFile.py, .../GimpPaletteFile.py, .../GimpGradientFile.py
Added support for reading and handling Teragon, GIMP palette, and GIMP gradient files.
PIL Container and Font Base Classes
venv/Lib/site-packages/PIL/ContainerIO.py, .../FontFile.py, .../BdfFontFile.py
Added a file-like container class for byte regions, a base class for raster font files, and a BDF font file parser.
PIL Platform Integrations
venv/Lib/site-packages/PIL/ImageTk.py, .../ImageQt.py, .../ImageWin.py
Added interfaces for displaying PIL images in Tkinter, Qt, and Windows environments.
PIL Color Management
venv/Lib/site-packages/PIL/ImageCms.py
Introduced LittleCMS-based color management, ICC profile support, and color transforms.
PIL PostScript Drawing
venv/Lib/site-packages/PIL/PSDraw.py
Added a class for PostScript graphics output, including image embedding and basic drawing primitives.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant RunwayTextToImage
    participant RunwayAPI
    participant Polling
    participant ImageLoader

    User->>RunwayTextToImage: Instantiate with prompt, timeout
    RunwayTextToImage->>RunwayAPI: POST /v1/text_to_image (prompt, API key)
    RunwayAPI-->>RunwayTextToImage: Respond with image_url
    loop Poll until image ready or timeout
        RunwayTextToImage->>Polling: GET image_url
        Polling-->>RunwayTextToImage: (image data or not ready)
    end
    RunwayTextToImage->>ImageLoader: Load image data as PIL.Image
    ImageLoader-->>RunwayTextToImage: PIL.Image.Image
    RunwayTextToImage-->>User: Return PIL.Image.Image
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Poem

A warren of formats, a garden of code,
New plugins and colors on every road!
From AVIF to GIF, from palette to font,
Now PIL can do almost all that you want.
With Runway and tests, and drawing anew—
This bunny’s quite proud of what’s hopping to you!

(_/)
( •_•)
/ >🍃 — Happy coding!

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 43

🔭 Outside diff range comments (7)
venv/Lib/site-packages/PIL/FontFile.py (1)

134-135: Fix syntax error: missing closing parenthesis.

Line 135 is missing a closing parenthesis for the puti16 function call.

-                    puti16(fp, m[0] + m[1] + m[2])
+                    puti16(fp, m[0] + m[1] + m[2])

Wait, that doesn't look right. Let me check the context again. The function call appears incomplete.

-                    puti16(fp, m[0] + m[1] + m[2])
+                    puti16(fp, m[0] + m[1] + m[2])

Actually, looking at the structure, this should be a tuple concatenation:

-                    puti16(fp, m[0] + m[1] + m[2])
+                    puti16(fp, m[0] + m[1] + m[2])
venv/Lib/site-packages/PIL/PaletteFile.py (1)

1-55: Remove vendored third-party code from version control

venv/Lib/site-packages/... belongs to the Python virtual-environment cache produced by pip install. Committing artefacts from a venv:

  • bloats the repository and causes huge diffs whenever dependencies are upgraded,
  • risks licensing conflicts (the Pillow copyright header is reproduced here),
  • hides security updates, because the code is pinned rather than fetched from PyPI at build time,
  • breaks reproducible builds on platforms with a different Python / Pillow version layout.

Add the whole venv/ directory to .gitignore, delete it from the PR, and rely on requirements.txt / pyproject.toml instead.

tests/api_nodes/test_runway_text2img.py (1)

8-35: Consider adding error condition tests.

The test only covers the happy path. Consider adding tests for error conditions like missing API key, API failures, timeout scenarios, and invalid responses.

Example additional tests to add:

def test_runway_text2img_node_missing_api_key():
    with mock.patch.dict(os.environ, {}, clear=True):
        node = RunwayTextToImage(prompt="test", timeout=5)
        with pytest.raises(RuntimeError, match="RUNWAY_API_KEY is not set"):
            node.run()

def test_runway_text2img_node_api_failure(mock_get, mock_post):
    with mock.patch.dict(os.environ, {"RUNWAY_API_KEY": "fake_key"}):
        mock_post.return_value.status_code = 400
        mock_post.return_value.text = "Bad Request"
        
        node = RunwayTextToImage(prompt="test", timeout=5)
        with pytest.raises(RuntimeError, match="API request failed"):
            node.run()
venv/Lib/site-packages/PIL/ImageQt.py (1)

1-221: Consider using PIL as a dependency instead of vendoring

This appears to be a vendored copy of PIL's ImageQt module. Vendoring third-party libraries increases maintenance burden and can lead to security issues if not regularly updated. Consider adding PIL/Pillow as a proper dependency in your requirements file instead.

venv/Lib/site-packages/PIL/IcnsImagePlugin.py (1)

1-412: Unexpected Pillow vendoring overshadowing RunwayTextToImage changes

I see the intended RunwayTextToImage node and its tests in:

  • nodes/api_nodes/runway_text2img.py
  • tests/api_nodes/test_runway_text2img.py

…however, the PR also adds the entire Pillow library under venv/Lib/site-packages/PIL/, which doesn’t align with the stated goal and dramatically inflates the diff.

Please:

  • Remove all vendored venv/Lib/site-packages/PIL/ files from this PR.
  • Declare Pillow as a project dependency (e.g. in requirements.txt or setup.py).
  • Verify that your new node and its tests still function correctly against the installed Pillow package.
venv/Lib/site-packages/PIL/ImageDraw2.py (1)

1-244: Remove committed virtual environment files

Your .gitignore already includes patterns for venv/, .venv, env/, etc., but the venv/ directory is still present in this PR. Please remove all committed virtual-environment files from version control. For example:

git rm -r --cached venv/
git commit -m "Remove committed virtual environment files"
git push

After this, verify that no files under venv/ remain in the PR. The existing .gitignore will prevent them from being re-added.

venv/Lib/site-packages/PIL/ImageCms.py (1)

1123-1124: Syntax error: incomplete return statement

Line 1124 appears to be truncated. The return statement is incomplete.

-    return _VERSION, core.littlecms_version, sys.version.split()[0], __version__
+    return (_VERSION, core.littlecms_version, sys.version.split()[0], __version__)
♻️ Duplicate comments (4)
venv/Lib/site-packages/PIL/ImageFilter.py (1)

1-605: Virtual environment file - should not be in version control

This is another PIL library file from the virtual environment. See the previous comment about removing all venv/ files from the PR.

venv/Lib/site-packages/PIL/GifImagePlugin.py (1)

1-1214: Virtual environment file - should not be in version control

This is another PIL library file from the virtual environment. See the previous comment about removing all venv/ files from the PR.

venv/Lib/site-packages/PIL/ImageChops.py (1)

1-312: Virtual environment file - should not be in version control

This is another PIL library file from the virtual environment. See the previous comment about removing all venv/ files from the PR.

venv/Lib/site-packages/PIL/ImageOps.py (1)

1-746: Virtual environment file - should not be in version control

This is another PIL library file from the virtual environment. See the previous comment about removing all venv/ files from the PR.

Based on the PR objectives mentioning a "RunwayTextToImage node", it seems the actual implementation files might be missing from this review. Would you like me to help locate the actual implementation files that should be reviewed? The expected files would be:

  • nodes/api_nodes/runway_text2img.py (the main implementation)
  • tests/api_nodes/test_runway_text2img.py (the test file)
🧹 Nitpick comments (65)
venv/Lib/site-packages/PIL/ImageMath.py (2)

46-49: Consider supporting broader numeric types.

The type check for constants only handles int and float, but users might pass other numeric types like numpy scalars, decimal.Decimal, or complex numbers. Consider using numbers.Number for broader compatibility.

-            if isinstance(im1, (int, float)) and self.im.mode in ("1", "L", "I"):
+            if isinstance(im1, (int, float, numbers.Number)) and self.im.mode in ("1", "L", "I"):

Don't forget to import numbers at the top of the file.


78-86: Document or reconsider the cropping behavior for mismatched image sizes.

When operating on images of different sizes, the implementation crops both images to their common minimum size from the top-left corner. This behavior might be unexpected as users could anticipate center cropping, padding, or an error instead.

Consider adding a parameter to control this behavior or at least document it clearly in the class docstring:

"""
Note: When performing binary operations on images of different sizes, 
both images are cropped to their common minimum dimensions from the 
top-left corner (0, 0).
"""
venv/Lib/site-packages/PIL/ImageSequence.py (1)

24-62: LGTM with minor suggestions for enhancement.

The Iterator class implementation is solid with proper error handling and adherence to Python's iterator protocol. The validation in __init__ and exception handling in __getitem__ and __next__ are well-implemented.

Consider these minor enhancements:

 class Iterator:
     """
     This class implements an iterator object that can be used to loop
     over an image sequence.

     You can use the ``[]`` operator to access elements by index. This operator
     will raise an :py:exc:`IndexError` if you try to access a nonexistent
     frame.

     :param im: An image object.
     """

     def __init__(self, im: Image.Image) -> None:
         if not hasattr(im, "seek"):
             msg = "im must have seek method"
             raise AttributeError(msg)
         self.im = im
         self.position = getattr(self.im, "_min_frame", 0)
+        self._initial_position = self.position

+    def reset(self) -> None:
+        """Reset the iterator to the beginning of the sequence."""
+        self.position = self._initial_position
venv/Lib/site-packages/PIL/FontFile.py (2)

26-34: Consider more flexible type hint for the values parameter.

The current type hint requires exactly 10 integers, which may be overly restrictive for a utility function.

Consider this more flexible approach:

-def puti16(
-    fp: BinaryIO, values: tuple[int, int, int, int, int, int, int, int, int, int]
-) -> None:
+def puti16(fp: BinaryIO, values: tuple[int, ...]) -> None:

36-62: Consider using a type alias for better readability.

The glyph tuple type is used multiple times and is quite verbose.

Add a type alias at the module level:

+from typing import BinaryIO, TypeAlias
+
+GlyphData: TypeAlias = tuple[
+    tuple[int, int],
+    tuple[int, int, int, int], 
+    tuple[int, int, int, int],
+    Image.Image,
+]
+
 class FontFile:
     """Base class for raster font file handlers."""
 
     bitmap: Image.Image | None = None
 
     def __init__(self) -> None:
         self.info: dict[bytes, bytes | int] = {}
-        self.glyph: list[
-            tuple[
-                tuple[int, int],
-                tuple[int, int, int, int],
-                tuple[int, int, int, int],
-                Image.Image,
-            ]
-            | None
-        ] = [None] * 256
+        self.glyph: list[GlyphData | None] = [None] * 256
 
-    def __getitem__(self, ix: int) -> (
-        tuple[
-            tuple[int, int],
-            tuple[int, int, int, int],
-            tuple[int, int, int, int],
-            Image.Image,
-        ]
-        | None
-    ):
+    def __getitem__(self, ix: int) -> GlyphData | None:
         return self.glyph[ix]
venv/Lib/site-packages/PIL/BdfFontFile.py (1)

112-115: Simplify nested if statements.

The nested if statements can be combined for better readability as suggested by static analysis.

-            if s[:i] in [b"COMMENT", b"COPYRIGHT"]:
-                if s.find(b"LogicalFontDescription") < 0:
-                    comments.append(s[i + 1 : -1].decode("ascii"))
+            if s[:i] in [b"COMMENT", b"COPYRIGHT"] and s.find(b"LogicalFontDescription") < 0:
+                comments.append(s[i + 1 : -1].decode("ascii"))
venv/Lib/site-packages/PIL/ImageTk.py (2)

134-143: Consider using contextlib.suppress for cleaner error handling.

The try-except-pass pattern can be replaced with contextlib.suppress for better readability.

+from contextlib import suppress
+
     def __del__(self) -> None:
-        try:
-            name = self.__photo.name
-        except AttributeError:
-            return
+        with suppress(AttributeError):
+            name = self.__photo.name
+        else:
+            return
         self.__photo.name = None
-        try:
+        with suppress(Exception):
             self.__photo.tk.call("image", "delete", name)
-        except Exception:
-            pass  # ignore internal errors

222-231: Consider using contextlib.suppress for cleaner error handling.

Same as the PhotoImage class, the try-except-pass pattern can be replaced with contextlib.suppress.

     def __del__(self) -> None:
-        try:
-            name = self.__photo.name
-        except AttributeError:
-            return
+        with suppress(AttributeError):
+            name = self.__photo.name
+        else:
+            return
         self.__photo.name = None
-        try:
+        with suppress(Exception):
             self.__photo.tk.call("image", "delete", name)
-        except Exception:
-            pass  # ignore internal errors
venv/Lib/site-packages/PIL/ImImagePlugin.py (4)

88-100: Consider consolidating repetitive loop structures

The three separate loops (lines 89-99) follow similar patterns and could be consolidated for better maintainability.

-# ifunc95 extensions
-for i in ["8", "8S", "16", "16S", "32", "32F"]:
-    OPEN[f"L {i} image"] = ("F", f"F;{i}")
-    OPEN[f"L*{i} image"] = ("F", f"F;{i}")
-for i in ["16", "16L", "16B"]:
-    OPEN[f"L {i} image"] = (f"I;{i}", f"I;{i}")
-    OPEN[f"L*{i} image"] = (f"I;{i}", f"I;{i}")
-for i in ["32S"]:
-    OPEN[f"L {i} image"] = ("I", f"I;{i}")
-    OPEN[f"L*{i} image"] = ("I", f"I;{i}")
-for j in range(2, 33):
-    OPEN[f"L*{j} image"] = ("F", f"F;{j}")
+# ifunc95 extensions
+# Define mappings for different format types
+format_mappings = [
+    (["8", "8S", "16", "16S", "32", "32F"], "F", "F"),
+    (["16", "16L", "16B"], "I", "I"),
+    (["32S"], "I", "I"),
+]
+
+for formats, mode_prefix, raw_prefix in format_mappings:
+    for i in formats:
+        OPEN[f"L {i} image"] = (f"{mode_prefix};{i}" if mode_prefix == "I" else mode_prefix, 
+                                f"{raw_prefix};{i}")
+        OPEN[f"L*{i} image"] = (f"{mode_prefix};{i}" if mode_prefix == "I" else mode_prefix,
+                                f"{raw_prefix};{i}")
+
+# Additional L* formats
+for j in range(2, 33):
+    OPEN[f"L*{j} image"] = ("F", f"F;{j}")

241-244: Simplify nested if statements

The nested if statements can be combined for better readability.

-            elif self.mode == "RGB":
-                if not greyscale or not linear:
-                    self.lut = list(palette)
+            elif self.mode == "RGB" and (not greyscale or not linear):
+                self.lut = list(palette)

249-249: Address the FIXME hack comment

The comment indicates this is a hack. Consider documenting why this is necessary or refactoring to avoid the hack.

Would you like me to investigate alternative approaches to avoid this hack?


299-303: Use ternary operator for bits calculation

The if-else block can be simplified using a ternary operator.

-        if self.mode == "1":
-            bits = 1
-        else:
-            bits = 8 * len(self.mode)
+        bits = 1 if self.mode == "1" else 8 * len(self.mode)
venv/Lib/site-packages/PIL/BlpImagePlugin.py (3)

44-46: Add missing format enum value

The Format enum only defines JPEG = 0, but the code also checks for compression == 1 (e.g., lines 353, 392). Consider adding a constant for this format type to improve code clarity.

 class Format(IntEnum):
     JPEG = 0
+    UNCOMPRESSED = 1

318-327: Remove unused loop variable in _read_palette

The loop variable i is not used in the palette reading loop.

 def _read_palette(self) -> list[tuple[int, int, int, int]]:
     ret = []
-    for i in range(256):
+    for _ in range(256):
         try:
             b, g, r, a = struct.unpack("<4B", self._safe_read(4))
         except struct.error:
             break
         ret.append((b, g, r, a))
     return ret

401-416: Replace unused loop variables with underscore

The loop variable yb is not used within the loop bodies for DXT decoding.

                 if self._alpha_encoding == AlphaEncoding.DXT1:
                     linesize = (self.state.xsize + 3) // 4 * 8
-                    for yb in range((self.state.ysize + 3) // 4):
+                    for _ in range((self.state.ysize + 3) // 4):
                         for d in decode_dxt1(self._safe_read(linesize), alpha):
                             data += d

                 elif self._alpha_encoding == AlphaEncoding.DXT3:
                     linesize = (self.state.xsize + 3) // 4 * 16
-                    for yb in range((self.state.ysize + 3) // 4):
+                    for _ in range((self.state.ysize + 3) // 4):
                         for d in decode_dxt3(self._safe_read(linesize)):
                             data += d

                 elif self._alpha_encoding == AlphaEncoding.DXT5:
                     linesize = (self.state.xsize + 3) // 4 * 16
-                    for yb in range((self.state.ysize + 3) // 4):
+                    for _ in range((self.state.ysize + 3) // 4):
                         for d in decode_dxt5(self._safe_read(linesize)):
                             data += d
venv/Lib/site-packages/PIL/McIdasImagePlugin.py (1)

55-55: Address the FIXME comment for memory map support.

The FIXME comment indicates missing memory map support for 32-bit images, which could impact performance for large files. Consider implementing this functionality or documenting the limitation.

venv/Lib/site-packages/PIL/MspImagePlugin.py (2)

44-46: Incomplete documentation comment

The comment states "This plugin supports both uncompressed (Windows 1.0)." but appears to be cut off. Based on the code, it supports both uncompressed (DanM) and compressed (LinS) formats.

-# Image plugin for Windows MSP images.  This plugin supports both
-# uncompressed (Windows 1.0).
+# Image plugin for Windows MSP images.  This plugin supports both
+# uncompressed (Windows 1.0 - DanM) and compressed (Windows 2.0 - LinS) formats.

122-124: Potential memory allocation issue with large ysize

The struct format string is dynamically constructed based on self.state.ysize. For very large ysize values, this could potentially cause excessive memory allocation.

Consider adding a sanity check for maximum image dimensions:

MAX_IMAGE_DIMENSION = 65535  # or another reasonable limit
if self.state.ysize > MAX_IMAGE_DIMENSION:
    msg = f"Image height {self.state.ysize} exceeds maximum supported dimension"
    raise OSError(msg)
venv/Lib/site-packages/PIL/PaletteFile.py (1)

48-49: Validate colour component range

r, g, b are written straight through to o8(). Pillow’s internal o8() will raise if the value is outside 0-255, but the resulting SyntaxError message (“bad palette file”) is misleading; it refers to line length earlier. Consider clamping or producing a clear diagnostic:

-                palette[i] = o8(r) + o8(g) + o8(b)
+                for comp in (r, g, b):
+                    if not 0 <= comp <= 255:
+                        raise ValueError(f"RGB component {comp} out of range 0-255")
+                palette[i] = o8(r) + o8(g) + o8(b)
venv/Lib/site-packages/PIL/GimpPaletteFile.py (1)

56-56: Optimize palette value appending.

Using a generator expression with += for list concatenation is inefficient.

-            palette += (int(v[i]) for i in range(3))
+            palette.extend(int(v[i]) for i in range(3))

Or even more directly:

-            palette += (int(v[i]) for i in range(3))
+            palette.extend([int(v[0]), int(v[1]), int(v[2])])
venv/Lib/site-packages/PIL/ImageStat.py (1)

57-59: Consider removing the type: ignore comment.

The # type: ignore[unreachable] comment suggests the type checker correctly identifies this as unreachable code. Since you're already validating the input types, this else block serves as defensive programming against runtime type errors.

Consider either:

  1. Remove the type ignore comment and keep the defensive check
  2. Remove the entire else block if you're confident in the type hints
         else:
-            msg = "first argument must be image or list"  # type: ignore[unreachable]
+            msg = "first argument must be image or list"
             raise TypeError(msg)
venv/Lib/site-packages/PIL/GimpGradientFile.py (1)

133-133: Rename unused loop variable.

The loop variable i is not used within the loop body.

-        for i in range(count):
+        for _ in range(count):
venv/Lib/site-packages/PIL/EpsImagePlugin.py (1)

124-130: Simplify device selection with ternary operator.

The if-else block can be simplified using a ternary operator as suggested by static analysis.

-    if transparency:
-        # "RGBA"
-        device = "pngalpha"
-    else:
-        # "pnmraw" automatically chooses between
-        # PBM ("1"), PGM ("L"), and PPM ("RGB").
-        device = "pnmraw"
+    # "pngalpha" for RGBA, "pnmraw" automatically chooses between PBM ("1"), PGM ("L"), and PPM ("RGB")
+    device = "pngalpha" if transparency else "pnmraw"
venv/Lib/site-packages/PIL/PSDraw.py (2)

24-24: Remove unused TYPE_CHECKING assignment.

The TYPE_CHECKING variable is set to False but should typically be imported from typing and used for type-only imports.

-TYPE_CHECKING = False

Import it properly if needed:

+from typing import TYPE_CHECKING

117-120: Consider using a ternary operator for cleaner code.

The if-else block for setting the default DPI can be simplified using a ternary operator.

-        if not dpi:
-            if im.mode == "1":
-                dpi = 200  # fax
-            else:
-                dpi = 100  # grayscale
+        if not dpi:
+            dpi = 200 if im.mode == "1" else 100  # fax vs grayscale
tests/api_nodes/test_runway_text2img.py (2)

10-10: Consider a more descriptive test function name.

The current name is generic. Consider renaming to better describe what specifically is being tested.

-def test_runway_text2img_node(mock_get, mock_post):
+def test_runway_text2img_node_successful_generation(mock_get, mock_post):

21-26: Add comment explaining the mock PNG data.

The minimal valid PNG bytes are well-crafted but could benefit from a comment explaining their purpose and structure.

-        # Mock GET response with minimal valid PNG bytes
+        # Mock GET response with minimal valid PNG bytes (1x1 pixel transparent PNG)
         mock_get.return_value.content = (
venv/Lib/site-packages/PIL/CurImagePlugin.py (2)

51-51: Rename unused loop variable.

The loop variable i is not used within the loop body. Consider renaming it to indicate it's intentionally unused.

-        for i in range(i16(s, 4)):
+        for _ in range(i16(s, 4)):

53-56: Simplify conditional logic using logical OR.

The if branches can be combined for better readability.

-            if not m:
-                m = s
-            elif s[0] > m[0] and s[1] > m[1]:
-                m = s
+            if not m or (s[0] > m[0] and s[1] > m[1]):
+                m = s
nodes/api_nodes/runway_text2img.py (3)

34-36: Consider adding prompt validation.

The constructor accepts any string as a prompt without validation. Consider adding basic validation for empty or overly long prompts.

 def __init__(self, prompt: str, timeout: int = 10):
+    if not prompt or not prompt.strip():
+        raise ValueError("Prompt cannot be empty")
+    if len(prompt) > 1000:  # Adjust limit as needed
+        raise ValueError("Prompt is too long")
     self.prompt = prompt
     self.timeout = timeout

53-57: Consider making API endpoint configurable.

The API endpoint is hardcoded. Consider making it configurable for flexibility (testing, different environments, API versioning).

+API_ENDPOINT = os.getenv("RUNWAY_API_ENDPOINT", "https://api.dev.runwayml.com/v1/text_to_image")

 response = requests.post(
-    "https://api.dev.runwayml.com/v1/text_to_image",
+    API_ENDPOINT,
     json=payload,
     headers=headers
 )

71-76: Consider making polling interval configurable.

The polling interval is hardcoded to 1 second. For better flexibility, consider making it configurable.

-def __init__(self, prompt: str, timeout: int = 10):
+def __init__(self, prompt: str, timeout: int = 10, poll_interval: float = 1.0):
     self.prompt = prompt
     self.timeout = timeout
+    self.poll_interval = poll_interval

-            time.sleep(1)  # Polling interval
+            time.sleep(self.poll_interval)
venv/Lib/site-packages/PIL/DcxImagePlugin.py (1)

30-30: Document the magic number properly

The comment "QUIZ: what's this value, then?" is not helpful for maintainability. Please document what this magic number represents.

-MAGIC = 0x3ADE68B1  # QUIZ: what's this value, then?
+MAGIC = 0x3ADE68B1  # DCX file format signature
venv/Lib/site-packages/PIL/FtexImagePlugin.py (1)

78-78: Store or validate the version number

The version is read but discarded. Consider storing it in self.info or validating it if there are known version constraints.

-        struct.unpack("<i", self.fp.read(4))  # version
+        version = struct.unpack("<i", self.fp.read(4))[0]
+        self.info["ftex_version"] = version
venv/Lib/site-packages/PIL/ImtImagePlugin.py (1)

44-45: Fix error message for consistency.

The error message should reference "IMT" instead of "IM" to match the format name.

-            msg = "not an IM file"
+            msg = "not an IMT file"
venv/Lib/site-packages/PIL/ImageColor.py (1)

43-43: Simplify dict.get() call.

The second argument None is redundant as it's the default return value.

-    rgb = colormap.get(color, None)
+    rgb = colormap.get(color)
venv/Lib/site-packages/PIL/MpoImagePlugin.py (3)

84-91: Consider using a ternary operator for cleaner code.

The conditional assignment of mptype can be simplified.

-        if i == 0:
-            mptype = 0x030000  # Baseline MP Primary Image
-        else:
-            mptype = 0x000000  # Undefined
+        mptype = 0x030000 if i == 0 else 0x000000  # Baseline MP Primary Image : Undefined

128-129: Address the FIXME hack comment.

There's a FIXME comment indicating a hack where self._fp is assigned from self.fp. This suggests a potential design issue that should be properly resolved.

Would you like me to help investigate the proper solution for this hack or open an issue to track this technical debt?


169-187: Consider the implications of runtime class mutation.

The adopt method changes the __class__ of an existing instance, which is an unusual pattern that could lead to unexpected behavior. While the docstring explains the purpose, this approach has risks:

  • It bypasses normal object initialization
  • It could break if JpegImageFile and MpoImageFile have incompatible internal states
  • It makes the code harder to understand and maintain

Consider alternative patterns like factory methods or proper inheritance initialization.

venv/Lib/site-packages/PIL/ContainerIO.py (2)

92-95: Simplify conditional with ternary operator.

The if-else block for setting n can be simplified.

-        if n > 0:
-            n = min(n, self.length - self.pos)
-        else:
-            n = self.length - self.pos
+        n = min(n, self.length - self.pos) if n > 0 else self.length - self.pos

101-118: Optimize string concatenation in readline method.

Building strings character by character with concatenation is inefficient. Consider using a list and joining at the end for better performance.

 def readline(self, n: int = -1) -> AnyStr:
     """
     Read a line of text.

     :param n: Number of bytes to read. If omitted, zero or negative,
         read until end of line.
     :returns: An 8-bit string.
     """
-    s: AnyStr = b"" if "b" in self.fh.mode else ""  # type: ignore[assignment]
+    chars: list[AnyStr] = []
     newline_character = b"\n" if "b" in self.fh.mode else "\n"
     while True:
         c = self.read(1)
         if not c:
             break
-        s = s + c
+        chars.append(c)
-        if c == newline_character or len(s) == n:
+        if c == newline_character or len(chars) == n:
             break
-    return s
+    return (b"" if "b" in self.fh.mode else "").join(chars)  # type: ignore[return-value]
venv/Lib/site-packages/PIL/IptcImagePlugin.py (3)

112-115: Simplify conditional with ternary operator.

The if-else block can be simplified.

-            if size:
-                tagdata = self.fp.read(size)
-            else:
-                tagdata = None
+            tagdata = self.fp.read(size) if size else None

127-130: Simplify conditional with ternary operator.

The id assignment can be simplified.

-        if (3, 65) in self.info:
-            id = self.info[(3, 65)][0] - 1
-        else:
-            id = 0
+        id = self.info[(3, 65)][0] - 1 if (3, 65) in self.info else 0

222-225: Use contextlib.suppress for cleaner exception handling.

Replace try-except-pass with contextlib.suppress.

+        import contextlib
         # get raw data from the IPTC/NAA tag (PhotoShop tags the data
         # as 4-byte integers, so we cannot use the get method...)
-        try:
+        with contextlib.suppress(KeyError):
             data = im.tag_v2._tagdata[TiffImagePlugin.IPTC_NAA_CHUNK]
-        except KeyError:
-            pass
venv/Lib/site-packages/PIL/AvifImagePlugin.py (1)

149-152: Simplify conditional assignment with ternary operator.

The if-else block can be simplified as suggested by static analysis.

-    if save_all:
-        append_images = list(info.get("append_images", []))
-    else:
-        append_images = []
+    append_images = list(info.get("append_images", [])) if save_all else []
venv/Lib/site-packages/PIL/DdsImagePlugin.py (1)

272-285: Consider documenting the backward compatibility layer.

The dynamic attribute creation provides backward compatibility but lacks documentation. Consider adding a comment explaining why this is needed.

 # Backward compatibility layer
+# Creates module-level constants like DDSD_CAPS, DDSCAPS_TEXTURE, etc.
+# for compatibility with older code that expects these as module attributes
 module = sys.modules[__name__]
venv/Lib/site-packages/PIL/ImageShow.py (1)

60-63: Simplify loop with any() function.

As suggested by static analysis, this can be more concise.

-    for viewer in _viewers:
-        if viewer.show(image, title=title, **options):
-            return True
-    return False
+    return any(viewer.show(image, title=title, **options) for viewer in _viewers)
venv/Lib/site-packages/PIL/Jpeg2KImagePlugin.py (3)

115-118: Simplify with ternary operator.

As suggested by static analysis.

-        if (ssiz[0] & 0x7F) + 1 > 8:
-            mode = "I;16"
-        else:
-            mode = "L"
+        mode = "I;16" if (ssiz[0] & 0x7F) + 1 > 8 else "L"

210-216: Rename unused loop variable.

Loop variable i is not used in the loop body.

-                for i in range(ne):
+                for _ in range(ne):
                     color: list[int] = []

370-373: Simplify with ternary operator.

As suggested by static analysis.

-    if filename.endswith(b".j2k") or info.get("no_jp2", False):
-        kind = "j2k"
-    else:
-        kind = "jp2"
+    kind = "j2k" if filename.endswith(b".j2k") or info.get("no_jp2", False) else "jp2"
venv/Lib/site-packages/PIL/BmpImagePlugin.py (1)

271-275: Simplify complex conditional expression.

The nested ternary operator is hard to read.

-                indices = (
-                    (0, 255)
-                    if file_info["colors"] == 2
-                    else list(range(file_info["colors"]))
-                )
+                if file_info["colors"] == 2:
+                    indices = (0, 255)
+                else:
+                    indices = list(range(file_info["colors"]))
venv/Lib/site-packages/PIL/ImageFont.py (2)

244-253: Simplify nested if statements and add stacklevel to warning

The nested if statements can be combined, and the warning should include stacklevel for better debugging.

-if freetype_version := features.version_module("freetype2"):
-    if parse_version(freetype_version) < parse_version("2.9.1"):
-        warnings.warn(
-            "Support for FreeType 2.9.0 is deprecated and will be removed "
-            "in Pillow 12 (2025-10-15). Please upgrade to FreeType 2.9.1 "
-            "or newer, preferably FreeType 2.10.4 which fixes "
-            "CVE-2020-15999.",
-            DeprecationWarning,
-        )
+if (freetype_version := features.version_module("freetype2")) and \
+   parse_version(freetype_version) < parse_version("2.9.1"):
+    warnings.warn(
+        "Support for FreeType 2.9.0 is deprecated and will be removed "
+        "in Pillow 12 (2025-10-15). Please upgrade to FreeType 2.9.1 "
+        "or newer, preferably FreeType 2.10.4 which fixes "
+        "CVE-2020-15999.",
+        DeprecationWarning,
+        stacklevel=2
+    )

920-922: Rename unused loop variable

The loop variable walkdir is not used within the loop body.

-for walkroot, walkdir, walkfilenames in os.walk(directory):
+for walkroot, _walkdir, walkfilenames in os.walk(directory):
venv/Lib/site-packages/PIL/ImageMorph.py (1)

121-124: Rename unused loop variable in rotation logic

The loop variable i is not used within the loop body.

-for i in range(4):
+for _ in range(4):
    patterns.append(
        (self._string_permute(patterns[-1][0], ROTATION_MATRIX), res)
    )
venv/Lib/site-packages/PIL/ImagePalette.py (1)

161-163: Combine nested if statements

These nested if statements can be combined for better readability.

-elif self.mode == "RGBA":
-    if len(color) == 3:
-        color += (255,)
+elif self.mode == "RGBA" and len(color) == 3:
+    color += (255,)
venv/Lib/site-packages/PIL/IcnsImagePlugin.py (2)

84-85: Rename unused loop variable

The loop variable i is not used within the loop body.

-for i in range(blocksize):
+for _ in range(blocksize):
    data.append(byte)

242-247: Use contextlib.suppress for cleaner exception handling

Replace try-except-pass with contextlib.suppress for cleaner code.

+from contextlib import suppress
+
 im = channels["RGB"].copy()
-try:
-    im.putalpha(channels["A"])
-except KeyError:
-    pass
+with suppress(KeyError):
+    im.putalpha(channels["A"])
venv/Lib/site-packages/PIL/ImageDraw.py (1)

600-640: Avoid star-arg unpacking after keyword arguments

The star-arg unpacking after keyword arguments at lines 620 and 636 is discouraged as it can be confusing and is flagged by static analysis.

Consider refactoring to avoid this pattern:

-                mask, offset = font.getmask2(  # type: ignore[union-attr,misc]
-                    text,
-                    mode,
-                    direction=direction,
-                    features=features,
-                    language=language,
-                    stroke_width=stroke_width,
-                    stroke_filled=True,
-                    anchor=anchor,
-                    ink=ink,
-                    start=start,
-                    *args,
-                    **kwargs,
-                )
+                # Pass args and kwargs separately to avoid star-arg after keyword
+                font_args = (text, mode) + args
+                font_kwargs = {
+                    'direction': direction,
+                    'features': features,
+                    'language': language,
+                    'stroke_width': stroke_width,
+                    'stroke_filled': True,
+                    'anchor': anchor,
+                    'ink': ink,
+                    'start': start,
+                    **kwargs
+                }
+                mask, offset = font.getmask2(*font_args, **font_kwargs)  # type: ignore[union-attr,misc]
venv/Lib/site-packages/PIL/IcoImagePlugin.py (2)

157-157: Rename unused loop variable

The loop variable i is not used within the loop body.

-        for i in range(self.nb_items):
+        for _ in range(self.nb_items):

356-358: Add explicit stacklevel to warning

The warning should include an explicit stacklevel to point to the caller's code.

-            warnings.warn("Image was not the expected size")
+            warnings.warn("Image was not the expected size", stacklevel=2)
venv/Lib/site-packages/PIL/JpegImagePlugin.py (1)

887-891: Add explicit stacklevel to warning

The warning should include stacklevel to point to the caller's code.

-        warnings.warn(
-            "Image appears to be a malformed MPO file, it will be "
-            "interpreted as a base JPEG file"
-        )
+        warnings.warn(
+            "Image appears to be a malformed MPO file, it will be "
+            "interpreted as a base JPEG file",
+            stacklevel=2
+        )
venv/Lib/site-packages/PIL/ImageFile.py (1)

449-456: Consider marking empty method as abstract or documenting it

The open method in StubHandler is empty but not marked as abstract, which could be confusing.

If this is meant to be optionally overridden with a default no-op behavior, consider adding a docstring to clarify:

 def open(self, im: StubImageFile) -> None:
+    """
+    Optional method to perform initialization when opening a stub image.
+    Default implementation does nothing.
+    """
     pass

Alternatively, if subclasses must implement this, mark it as abstract:

+@abc.abstractmethod
 def open(self, im: StubImageFile) -> None:
     pass
venv/Lib/site-packages/PIL/ImageCms.py (2)

255-265: Platform-specific encoding handling looks correct

The special handling for non-ASCII Windows paths is a necessary workaround. Consider adding a comment explaining why this is needed.

 if isinstance(profile, str):
     if sys.platform == "win32":
         profile_bytes_path = profile.encode()
         try:
             profile_bytes_path.decode("ascii")
         except UnicodeDecodeError:
+            # Windows file APIs may not handle non-ASCII paths correctly
+            # when passed as strings, so read the file manually
             with open(profile, "rb") as f:
                 self.profile = core.profile_frombytes(f.read())
             return

857-864: Consider simplifying the profile name logic

The multiple conditional returns make the logic harder to follow.

-if not (model or manufacturer):
-    return (profile.profile.profile_description or "") + "\n"
-if not manufacturer or (model and len(model) > 30):
-    return f"{model}\n"
-return f"{model} - {manufacturer}\n"
+# Fallback to description if no model/manufacturer
+if not (model or manufacturer):
+    return (profile.profile.profile_description or "") + "\n"
+
+# Use only model if it's long or manufacturer is missing
+if model and (not manufacturer or len(model) > 30):
+    return f"{model}\n"
+
+# Use both model and manufacturer
+return f"{model} - {manufacturer}\n"
venv/Lib/site-packages/PIL/ImageTransform.py (1)

26-26: Consider making the method attribute abstract.

The method attribute is declared but not initialized in the base class, which could lead to runtime errors if someone instantiates the base class directly.

Consider making this more explicit:

+from abc import ABC, abstractmethod

-class Transform(Image.ImageTransformHandler):
+class Transform(Image.ImageTransformHandler, ABC):
     """Base class for other transforms defined in :py:mod:`~PIL.ImageTransform`."""

-    method: Image.Transform
+    @property
+    @abstractmethod
+    def method(self) -> Image.Transform:
+        """The transformation method to be used."""
+        ...

Then update each subclass to use @property decorators for the method attribute.

Comment on lines +53 to +82
response = requests.post(
"https://api.dev.runwayml.com/v1/text_to_image",
json=payload,
headers=headers
)

if response.status_code != 200:
raise RuntimeError(f"API request failed with status {response.status_code}: {response.text}")

data = response.json()
image_url = data.get("image_url")
if not image_url:
raise RuntimeError("API response does not contain 'image_url'.")

# Step 2: Poll the image URL until image is ready or timeout is reached
start_time = time.time()
image_data = None

while time.time() - start_time < self.timeout:
img_response = requests.get(image_url)
if img_response.status_code == 200 and img_response.content:
image_data = img_response.content
break
time.sleep(1) # Polling interval

if not image_data:
raise RuntimeError("Failed to fetch image data within timeout period.")

# Step 3: Load image from bytes into PIL.Image
return Image.open(BytesIO(image_data))
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding request timeout and retry logic.

For production robustness, consider adding timeouts to the requests and basic retry logic for transient failures.

 response = requests.post(
     "https://api.dev.runwayml.com/v1/text_to_image",
     json=payload,
-    headers=headers
+    headers=headers,
+    timeout=30  # Add timeout for the request
 )

 while time.time() - start_time < self.timeout:
-    img_response = requests.get(image_url)
+    img_response = requests.get(image_url, timeout=30)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
response = requests.post(
"https://api.dev.runwayml.com/v1/text_to_image",
json=payload,
headers=headers
)
if response.status_code != 200:
raise RuntimeError(f"API request failed with status {response.status_code}: {response.text}")
data = response.json()
image_url = data.get("image_url")
if not image_url:
raise RuntimeError("API response does not contain 'image_url'.")
# Step 2: Poll the image URL until image is ready or timeout is reached
start_time = time.time()
image_data = None
while time.time() - start_time < self.timeout:
img_response = requests.get(image_url)
if img_response.status_code == 200 and img_response.content:
image_data = img_response.content
break
time.sleep(1) # Polling interval
if not image_data:
raise RuntimeError("Failed to fetch image data within timeout period.")
# Step 3: Load image from bytes into PIL.Image
return Image.open(BytesIO(image_data))
response = requests.post(
"https://api.dev.runwayml.com/v1/text_to_image",
json=payload,
headers=headers,
timeout=30 # Add timeout for the request
)
if response.status_code != 200:
raise RuntimeError(f"API request failed with status {response.status_code}: {response.text}")
data = response.json()
image_url = data.get("image_url")
if not image_url:
raise RuntimeError("API response does not contain 'image_url'.")
# Step 2: Poll the image URL until image is ready or timeout is reached
start_time = time.time()
image_data = None
while time.time() - start_time < self.timeout:
img_response = requests.get(image_url, timeout=30)
if img_response.status_code == 200 and img_response.content:
image_data = img_response.content
break
time.sleep(1) # Polling interval
if not image_data:
raise RuntimeError("Failed to fetch image data within timeout period.")
# Step 3: Load image from bytes into PIL.Image
return Image.open(BytesIO(image_data))
🤖 Prompt for AI Agents
In nodes/api_nodes/runway_text2img.py between lines 53 and 82, the HTTP requests
to the API and image URL lack timeout settings and retry logic, which can cause
hangs or failures on transient network issues. Add a timeout parameter to both
requests.post and requests.get calls to limit waiting time. Implement basic
retry logic with a limited number of retries and delays for transient failures
on both requests to improve robustness.

Comment on lines +119 to +132
def load(self) -> Image.core.PixelAccess | None:
if self.tile:
# We need to load the image data for this frame
data, timescale, pts_in_timescales, duration_in_timescales = (
self._decoder.get_frame(self.__frame)
)
self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale))
self.info["duration"] = round(1000 * (duration_in_timescales / timescale))

if self.fp and self._exclusive_fp:
self.fp.close()
self.fp = BytesIO(data)

return super().load()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add zero division protection for timescale.

The timescale value could potentially be zero, causing a division error.

         if self.tile:
             # We need to load the image data for this frame
             data, timescale, pts_in_timescales, duration_in_timescales = (
                 self._decoder.get_frame(self.__frame)
             )
-            self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale))
-            self.info["duration"] = round(1000 * (duration_in_timescales / timescale))
+            if timescale > 0:
+                self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale))
+                self.info["duration"] = round(1000 * (duration_in_timescales / timescale))
+            else:
+                self.info["timestamp"] = 0
+                self.info["duration"] = 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def load(self) -> Image.core.PixelAccess | None:
if self.tile:
# We need to load the image data for this frame
data, timescale, pts_in_timescales, duration_in_timescales = (
self._decoder.get_frame(self.__frame)
)
self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale))
self.info["duration"] = round(1000 * (duration_in_timescales / timescale))
if self.fp and self._exclusive_fp:
self.fp.close()
self.fp = BytesIO(data)
return super().load()
def load(self) -> Image.core.PixelAccess | None:
if self.tile:
# We need to load the image data for this frame
data, timescale, pts_in_timescales, duration_in_timescales = (
self._decoder.get_frame(self.__frame)
)
if timescale > 0:
self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale))
self.info["duration"] = round(1000 * (duration_in_timescales / timescale))
else:
self.info["timestamp"] = 0
self.info["duration"] = 0
if self.fp and self._exclusive_fp:
self.fp.close()
self.fp = BytesIO(data)
return super().load()
🤖 Prompt for AI Agents
In venv/Lib/site-packages/PIL/AvifImagePlugin.py around lines 119 to 132, the
code divides by timescale without checking if it is zero, which can cause a
division by zero error. Add a check to ensure timescale is not zero before
performing the division; if it is zero, handle it safely by either skipping the
division or assigning a default value to avoid the error.

Comment on lines +196 to +211
advanced = info.get("advanced")
if advanced is not None:
if isinstance(advanced, dict):
advanced = advanced.items()
try:
advanced = tuple(advanced)
except TypeError:
invalid = True
else:
invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced)
if invalid:
msg = (
"advanced codec options must be a dict of key-value string "
"pairs or a series of key-value two-tuples"
)
raise ValueError(msg)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve error handling for advanced options validation.

The bare except TypeError could mask other issues. Consider more specific error handling.

         try:
             advanced = tuple(advanced)
         except TypeError:
-            invalid = True
+            msg = "advanced codec options must be convertible to tuple"
+            raise ValueError(msg) from None
         else:
             invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced)
         if invalid:
             msg = (
                 "advanced codec options must be a dict of key-value string "
                 "pairs or a series of key-value two-tuples"
             )
             raise ValueError(msg)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
advanced = info.get("advanced")
if advanced is not None:
if isinstance(advanced, dict):
advanced = advanced.items()
try:
advanced = tuple(advanced)
except TypeError:
invalid = True
else:
invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced)
if invalid:
msg = (
"advanced codec options must be a dict of key-value string "
"pairs or a series of key-value two-tuples"
)
raise ValueError(msg)
advanced = info.get("advanced")
if advanced is not None:
if isinstance(advanced, dict):
advanced = advanced.items()
try:
advanced = tuple(advanced)
except TypeError:
msg = "advanced codec options must be convertible to tuple"
raise ValueError(msg) from None
else:
invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced)
if invalid:
msg = (
"advanced codec options must be a dict of key-value string "
"pairs or a series of key-value two-tuples"
)
raise ValueError(msg)
🤖 Prompt for AI Agents
In venv/Lib/site-packages/PIL/AvifImagePlugin.py around lines 196 to 211, the
code uses a bare except TypeError which can hide other unexpected errors. Modify
the error handling to catch only the specific TypeError that can occur during
the tuple conversion of advanced options. Avoid using a bare except and ensure
that only the intended exceptions related to invalid input types are caught,
allowing other exceptions to propagate normally.

Comment on lines +361 to +363
else:
msg = f"Unsupported BLP compression {repr(self._encoding)}"
raise BLPFormatError(msg)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix incorrect variable reference in error message

The error message incorrectly references self._encoding instead of self._compression.

         else:
-            msg = f"Unsupported BLP compression {repr(self._encoding)}"
+            msg = f"Unsupported BLP compression {repr(self._compression)}"
             raise BLPFormatError(msg)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
else:
msg = f"Unsupported BLP compression {repr(self._encoding)}"
raise BLPFormatError(msg)
else:
msg = f"Unsupported BLP compression {repr(self._compression)}"
raise BLPFormatError(msg)
🤖 Prompt for AI Agents
In venv/Lib/site-packages/PIL/BlpImagePlugin.py around lines 361 to 363, the
error message references the wrong variable `self._encoding`. Change this to
`self._compression` to correctly reflect the unsupported BLP compression type in
the error message.

Comment on lines +268 to +270
padding = file_info["palette_padding"]
palette = read(padding * file_info["colors"])
grayscale = True
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Validate palette size before reading.

The code reads padding * file_info["colors"] bytes without checking if that much data is available.

                 assert isinstance(file_info["palette_padding"], int)
                 padding = file_info["palette_padding"]
-                palette = read(padding * file_info["colors"])
+                palette_size = padding * file_info["colors"]
+                palette = read(palette_size)
+                if len(palette) < palette_size:
+                    msg = f"Incomplete palette: expected {palette_size} bytes, got {len(palette)}"
+                    raise OSError(msg)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
padding = file_info["palette_padding"]
palette = read(padding * file_info["colors"])
grayscale = True
assert isinstance(file_info["palette_padding"], int)
padding = file_info["palette_padding"]
palette_size = padding * file_info["colors"]
palette = read(palette_size)
if len(palette) < palette_size:
msg = f"Incomplete palette: expected {palette_size} bytes, got {len(palette)}"
raise OSError(msg)
grayscale = True
🤖 Prompt for AI Agents
In venv/Lib/site-packages/PIL/BmpImagePlugin.py around lines 268 to 270, before
reading the palette data using padding * file_info["colors"], add a check to
ensure the file has enough remaining bytes to read this amount. If not enough
data is available, handle the situation gracefully, such as by raising an error
or adjusting the read size to prevent reading beyond the file boundary.

Comment on lines +25 to +26
def _accept(prefix: bytes) -> bool:
return prefix.startswith(b"\x00\x00\x00\x00\x00\x00\x00\x04")
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify the magic number for McIdas format.

The magic number b"\x00\x00\x00\x00\x00\x00\x00\x04" (7 null bytes + 0x04) seems generic and could potentially match other file formats or cause false positives. Please verify this is the correct and unique identifier for McIdas area files.


🌐 Web query:

What is the correct file header/magic number for McIdas area image files?

💡 Result:

McIDAS AREA files do not have a traditional fixed "magic number" header like many image formats (e.g., PNG or GIF), but instead always start with a binary block called the Area Directory, which consists of the first 256 bytes (64 4-byte words) of the file[1][7][10]. There is no unique file signature or magic number value documented that immediately identifies an AREA file type solely by its first few bytes[3][6][7].

Essential details on the AREA header:

  • The first 256 bytes of every McIDAS AREA file are the Area Directory block[1][7][10].
  • This Area Directory contains metadata as a series of 32-bit integers, and its structure is well-documented, but the content will depend on how the AREA file was generated and does not include a fixed marker or ASCII identifier[7][1].
  • Detection of the format is achieved by validating the block structure and metadata layout, not a magic number[1][7].

Therefore, the correct file header for a McIDAS AREA image file is a 256-byte Area Directory block, but there is no fixed magic number or signature byte value typical in other file formats[1][7][10]. Validation of the file type is typically done via structure, not by matching magic bytes.

If you need to identify AREA files programmatically, you must parse the first 256 bytes according to the Area Directory specification and check for expected field values and consistency, rather than searching for a fixed byte sequence[7][1][10].

Citations:


McIDAS AREA detection must parse the full 256-byte Area Directory, not match a fixed “magic” prefix

The web research confirms that AREA files have no unique 8-byte signature—every file begins with a 256-byte directory block whose contents vary—so the current _accept check will both miss valid AREA files and invoke false positives.

Please update PIL/McIdasImagePlugin.py accordingly:

  • Remove or disable the hard-coded prefix.startswith(b"\x00\x00\x00\x00\x00\x00\x00\x04") test in _accept.
  • Instead, in _accept read at least the first 256 bytes and parse the Area Directory fields (e.g., verify record lengths, image dimensions, known directory word values).
  • Only return True if the directory structure matches expected McIDAS AREA metadata layout.

File to fix:

  • venv/Lib/site-packages/PIL/McIdasImagePlugin.py (the _accept function around lines 25–26)
🤖 Prompt for AI Agents
In venv/Lib/site-packages/PIL/McIdasImagePlugin.py around lines 25 to 26,
replace the current _accept function that checks a fixed 8-byte prefix with
logic that reads at least the first 256 bytes of the file and parses the Area
Directory structure. Implement validation of key directory fields such as record
lengths, image dimensions, and known directory word values to confirm the file
matches the McIDAS AREA format, returning True only if these checks pass;
otherwise, return False.

Comment on lines +46 to +47
self.area_descriptor_raw = s
self.area_descriptor = w = [0, *struct.unpack("!64i", s)]
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify array indexing bounds.

The code unpacks 64 integers and creates a 65-element array (with 0 prepended), but later accesses indices up to w[34]. Please verify that all accessed indices are within bounds and document the purpose of prepending 0.


🏁 Script executed:

#!/bin/bash
# Find all array access patterns to verify bounds
rg 'w\[(\d+)\]' --only-matching venv/Lib/site-packages/PIL/McIdasImagePlugin.py | sort -u

Length of output: 126


Array indexing is within bounds; add explanatory comment

All accessed indices (w[9], w[10], w[11], w[14], w[15], w[34]) are ≤ 64, and since w is constructed with 65 elements ([0, *struct.unpack("!64i", s)]), no out-of-bounds access occurs. The leading 0 is prepended to shift the unpacked values to 1-based positions (aligning with the McIDAS IDD field numbering).

Please add a brief comment above the unpack to explain this intent. For example:

-        self.area_descriptor_raw = s
-        self.area_descriptor = w = [0, *struct.unpack("!64i", s)]
+        # Prepend 0 so that unpacked values map to 1-based IDD field indices (w[1]…w[64])
+        self.area_descriptor_raw = s
+        self.area_descriptor = w = [0, *struct.unpack("!64i", s)]

This will clarify why the placeholder is needed.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.area_descriptor_raw = s
self.area_descriptor = w = [0, *struct.unpack("!64i", s)]
# Prepend 0 so that unpacked values map to 1-based IDD field indices (w[1]…w[64])
self.area_descriptor_raw = s
self.area_descriptor = w = [0, *struct.unpack("!64i", s)]
🤖 Prompt for AI Agents
In venv/Lib/site-packages/PIL/McIdasImagePlugin.py around lines 46 to 47, add a
brief comment above the line where w is assigned to explain that the leading 0
is prepended to the unpacked 64 integers to create a 1-based index array. This
clarifies that the array indexing aligns with McIDAS IDD field numbering and
ensures all accessed indices are within bounds.

Comment on lines +50 to +66
if w[11] == 1:
mode = rawmode = "L"
elif w[11] == 2:
mode = rawmode = "I;16B"
elif w[11] == 4:
# FIXME: add memory map support
mode = "I"
rawmode = "I;32B"
else:
msg = "unsupported McIdas format"
raise SyntaxError(msg)

self._mode = mode
self._size = w[10], w[9]

offset = w[34] + w[15]
stride = w[15] + w[10] * w[11] * w[14]
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add bounds checking for array access.

The code accesses multiple elements of the w array (w[9], w[10], w[11], w[14], w[15], w[34]) without bounds checking. Consider adding validation to prevent IndexError exceptions.

+        # Verify we have enough data in the descriptor
+        if len(w) < 35:  # Need at least index 34
+            msg = "invalid McIdas area descriptor"
+            raise SyntaxError(msg)
+        
         # get mode
         if w[11] == 1:
🤖 Prompt for AI Agents
In venv/Lib/site-packages/PIL/McIdasImagePlugin.py between lines 50 and 66, the
code accesses elements of the array w at indices 9, 10, 11, 14, 15, and 34
without verifying that these indices exist, which can cause IndexError. Add
validation before these accesses to check that the length of w is greater than
the highest index used (at least 35). If the check fails, raise a clear
exception or handle the error gracefully to prevent runtime crashes.

Comment on lines +147 to +149
runcount = runtype
img.write(row[idx : idx + runcount])
idx += runcount
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add bounds checking for slice operation

The slice operation row[idx : idx + runcount] could potentially read beyond the row buffer if the file is corrupted and runcount is larger than remaining bytes.

                    else:
                        runcount = runtype
+                       if idx + runcount > rowlen:
+                           msg = f"Invalid runcount {runcount} at position {idx} in row {x}"
+                           raise OSError(msg)
                        img.write(row[idx : idx + runcount])
                        idx += runcount
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
runcount = runtype
img.write(row[idx : idx + runcount])
idx += runcount
else:
runcount = runtype
if idx + runcount > rowlen:
msg = f"Invalid runcount {runcount} at position {idx} in row {x}"
raise OSError(msg)
img.write(row[idx : idx + runcount])
idx += runcount
🤖 Prompt for AI Agents
In venv/Lib/site-packages/PIL/MspImagePlugin.py around lines 147 to 149, the
slice operation row[idx : idx + runcount] may exceed the row buffer if runcount
is larger than the remaining bytes due to file corruption. Add bounds checking
to ensure idx + runcount does not go beyond the length of row by adjusting
runcount to the remaining bytes if necessary before slicing and writing.

Comment on lines +30 to +37
while True:
s = fp.readline()

if not s:
break
if s.startswith(b"#"):
continue
if len(s) > 100:
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Blank lines trigger ValueError – skip empty rows explicitly

fp.readline() may return b"\n"; after split() this becomes an empty list, so v = [int(x) for x in s.split()] raises ValueError, causing perfectly valid palette files with blank lines to fail.

@@
-            v = [int(x) for x in s.split()]
+            if not s.strip():          # skip empty lines
+                continue
+            v = [int(x) for x in s.split()]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while True:
s = fp.readline()
if not s:
break
if s.startswith(b"#"):
continue
if len(s) > 100:
if not s.strip(): # skip empty lines
continue
v = [int(x) for x in s.split()]
🤖 Prompt for AI Agents
In venv/Lib/site-packages/PIL/PaletteFile.py around lines 30 to 37, blank lines
read from the file cause a ValueError because after splitting, the list is empty
and integer conversion fails. To fix this, add an explicit check to skip empty
or whitespace-only lines before processing them, ensuring blank lines do not
cause errors.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review continued from previous batch...

Comment on lines +198 to +206
@staticmethod
def GRIDPOINTS(n: int) -> Flags:
"""
Fine-tune control over number of gridpoints

:param n: :py:class:`int` in range ``0 <= n <= 255``
"""
return Flags.NONE | ((n & 0xFF) << 16)

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add parameter validation for GRIDPOINTS

The docstring specifies that n should be in range 0-255, but there's no validation. Values outside this range will be silently masked.

 @staticmethod
 def GRIDPOINTS(n: int) -> Flags:
     """
     Fine-tune control over number of gridpoints
 
     :param n: :py:class:`int` in range ``0 <= n <= 255``
     """
+    if not 0 <= n <= 255:
+        raise ValueError(f"n must be in range 0-255, got {n}")
     return Flags.NONE | ((n & 0xFF) << 16)
🤖 Prompt for AI Agents
In venv/Lib/site-packages/PIL/ImageCms.py around lines 198 to 206, the
GRIDPOINTS method lacks validation for the parameter n, which should be between
0 and 255 as per the docstring. Add a check at the start of the method to raise
a ValueError if n is outside this range, ensuring only valid values are
processed and preventing silent masking of invalid inputs.

Comment on lines +815 to +821
if colorSpace == "LAB":
try:
colorTemp = float(colorTemp)
except (TypeError, ValueError) as e:
msg = f'Color temperature must be numeric, "{colorTemp}" not valid'
raise PyCMSError(msg) from e

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add validation for positive colorTemp in LAB profiles

The docstring states colorTemp should be positive for LAB profiles, but there's no validation.

 if colorSpace == "LAB":
     try:
         colorTemp = float(colorTemp)
     except (TypeError, ValueError) as e:
         msg = f'Color temperature must be numeric, "{colorTemp}" not valid'
         raise PyCMSError(msg) from e
+    if colorTemp <= 0:
+        msg = f"Color temperature must be positive, got {colorTemp}"
+        raise PyCMSError(msg)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if colorSpace == "LAB":
try:
colorTemp = float(colorTemp)
except (TypeError, ValueError) as e:
msg = f'Color temperature must be numeric, "{colorTemp}" not valid'
raise PyCMSError(msg) from e
if colorSpace == "LAB":
try:
colorTemp = float(colorTemp)
except (TypeError, ValueError) as e:
msg = f'Color temperature must be numeric, "{colorTemp}" not valid'
raise PyCMSError(msg) from e
if colorTemp <= 0:
msg = f"Color temperature must be positive, got {colorTemp}"
raise PyCMSError(msg)
🤖 Prompt for AI Agents
In venv/Lib/site-packages/PIL/ImageCms.py around lines 815 to 821, the code
converts colorTemp to float for LAB profiles but does not check if it is
positive as required by the docstring. Add a validation step after converting
colorTemp to float to verify it is greater than zero. If not, raise a PyCMSError
with a clear message indicating that colorTemp must be positive for LAB
profiles.

Comment on lines +86 to +94
def close(self) -> None:
self.__fp.close()
self.ole.close()
super().close()

def __exit__(self, *args: object) -> None:
self.__fp.close()
self.ole.close()
super().__exit__()
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Potential resource leak in exception scenarios.

The close() and __exit__() methods don't handle exceptions that might occur during cleanup. If self.__fp.close() or self.ole.close() raises an exception, the subsequent cleanup calls won't execute.

Consider implementing more robust cleanup:

 def close(self) -> None:
-    self.__fp.close()
-    self.ole.close()
+    try:
+        self.__fp.close()
+    finally:
+        try:
+            self.ole.close()
+        finally:
+            super().close()
-    super().close()

 def __exit__(self, *args: object) -> None:
-    self.__fp.close()
-    self.ole.close()
-    super().__exit__()
+    try:
+        self.__fp.close()
+    finally:
+        try:
+            self.ole.close()
+        finally:
+            super().__exit__(*args)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def close(self) -> None:
self.__fp.close()
self.ole.close()
super().close()
def __exit__(self, *args: object) -> None:
self.__fp.close()
self.ole.close()
super().__exit__()
def close(self) -> None:
try:
self.__fp.close()
finally:
try:
self.ole.close()
finally:
super().close()
def __exit__(self, *args: object) -> None:
try:
self.__fp.close()
finally:
try:
self.ole.close()
finally:
super().__exit__(*args)
🤖 Prompt for AI Agents
In venv/Lib/site-packages/PIL/MicImagePlugin.py around lines 86 to 94, the
close() and __exit__() methods do not handle exceptions during resource cleanup,
risking incomplete cleanup if an exception occurs. Modify both methods to catch
and handle exceptions individually for self.__fp.close() and self.ole.close(),
ensuring that all cleanup calls execute regardless of errors. Use try-except
blocks around each close call and ensure super().close() and super().__exit__()
are always called after attempting to close resources.

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.

1 participant