Encode a PIL Image to bytes in the given media type. Supports only the four Anthropic API image types. Raises ``ImageProcessingError`` for any other media_type so the caller can't silently mislabel bytes (e.g. asking for image/tiff and getting PNG bytes back tagged as image/tiff — w
(img: Any, media_type: str, quality: int | None = None)
| 182 | |
| 183 | |
| 184 | def _encode_image(img: Any, media_type: str, quality: int | None = None) -> bytes: |
| 185 | """Encode a PIL Image to bytes in the given media type. |
| 186 | |
| 187 | Supports only the four Anthropic API image types. Raises |
| 188 | ``ImageProcessingError`` for any other media_type so the caller can't |
| 189 | silently mislabel bytes (e.g. asking for image/tiff and getting PNG |
| 190 | bytes back tagged as image/tiff — which the API would reject). |
| 191 | """ |
| 192 | if media_type not in _SUPPORTED_ENCODE_TYPES: |
| 193 | raise ImageProcessingError( |
| 194 | f"Unsupported image encoding type: {media_type}. " |
| 195 | f"Supported: {sorted(_SUPPORTED_ENCODE_TYPES)}" |
| 196 | ) |
| 197 | buf = io.BytesIO() |
| 198 | if media_type == "image/png": |
| 199 | # Plain optimized PNG. Palette quantization happens in |
| 200 | # ``compress_image_to_byte_budget`` step 2 (where it's gated on |
| 201 | # the source being PNG); keeping it out of the default encode |
| 202 | # path preserves alpha for opaque-vs-transparent inputs alike. |
| 203 | save_kwargs: dict[str, Any] = {"format": "PNG", "optimize": True, "compress_level": 9} |
| 204 | img.save(buf, **save_kwargs) |
| 205 | elif media_type == "image/jpeg": |
| 206 | # JPEG requires RGB; convert if image has alpha/palette. |
| 207 | if img.mode not in ("RGB", "L"): |
| 208 | img = img.convert("RGB") |
| 209 | save_kwargs = {"format": "JPEG", "quality": quality or 80, "optimize": True} |
| 210 | img.save(buf, **save_kwargs) |
| 211 | elif media_type == "image/webp": |
| 212 | save_kwargs = {"format": "WEBP", "quality": quality or 80} |
| 213 | img.save(buf, **save_kwargs) |
| 214 | elif media_type == "image/gif": |
| 215 | # GIF re-encode preserves palette but loses transparency in some |
| 216 | # frames; acceptable per TS behavior. |
| 217 | img.save(buf, format="GIF") |
| 218 | return buf.getvalue() |
| 219 | |
| 220 | |
| 221 | def _resize_to_envelope(img: Any, max_w: int, max_h: int) -> tuple[Any, int, int]: |
no test coverage detected