Files
awsr 82886ba9a3 Update Pillow support to 12.2.0
- Update "constraints.txt" and "requirements.txt".
- Migrate removed `ImageCms.FLAGS` to `ImageCms.Flags` enum.
- Update typing.
- Simplify getting path in `apply_srgb`.
2026-05-13 18:37:12 -07:00

173 lines
5.5 KiB
Python

from __future__ import annotations
"""Sharpfin color management (ICC profile handling).
Vendored from https://github.com/drhead/sharpfin (Apache 2.0)
"""
from io import BytesIO
from typing import TYPE_CHECKING, Any, cast
from warnings import warn
import numpy as np
from PIL import Image
from PIL.ImageCms import (
Direction, Intent, ImageCmsProfile, PyCMSError,
createProfile, getDefaultIntent, isIntentSupported, profileToProfile
)
from PIL.ImageCms import Flags as cms_flags
from PIL.ImageOps import exif_transpose
if TYPE_CHECKING:
from PIL import ImageFile
from torch import Tensor
Image.MAX_IMAGE_PIXELS = None
_SRGB = createProfile(colorSpace='sRGB')
_INTENT_FLAGS = {
Intent.PERCEPTUAL: cms_flags.HIGHRESPRECALC,
Intent.RELATIVE_COLORIMETRIC: (
cms_flags.HIGHRESPRECALC |
cms_flags.BLACKPOINTCOMPENSATION
),
Intent.ABSOLUTE_COLORIMETRIC: cms_flags.HIGHRESPRECALC
}
class CMSWarning(UserWarning):
def __init__(
self,
message: str,
*,
path: str | None = None,
cms_info: dict[str, Any] | None = None,
cause: Exception | None = None,
):
super().__init__(message)
self.__cause__ = cause
self.path = path
self.cms_info = cms_info
def _coalesce_intent(intent: Intent | int) -> Intent:
if isinstance(intent, Intent):
return intent
match intent:
case 0:
return Intent.PERCEPTUAL
case 1:
return Intent.RELATIVE_COLORIMETRIC
case 2:
return Intent.SATURATION
case 3:
return Intent.ABSOLUTE_COLORIMETRIC
case _:
raise ValueError("invalid intent")
def _add_info(info: dict[str, Any], source: object, key: str) -> None:
try:
if (value := getattr(source, key, None)) is not None:
info[key] = value
except Exception:
pass
def apply_srgb(img: Image.Image | ImageFile.ImageFile) -> Image.Image:
path = getattr(img, "filename", "")
try:
img.load()
try:
exif_transpose(img, in_place=True)
except Exception:
pass # corrupt EXIF metadata is fine
if (icc_raw := img.info.get("icc_profile")) is not None:
cms_info: dict[str, Any] = {
"native_mode": img.mode,
"transparency": img.has_transparency_data,
}
try:
profile = ImageCmsProfile(BytesIO(icc_raw))
_add_info(cms_info, profile.profile, "profile_description")
_add_info(cms_info, profile.profile, "target")
_add_info(cms_info, profile.profile, "xcolor_space")
_add_info(cms_info, profile.profile, "connection_space")
_add_info(cms_info, profile.profile, "colorimetric_intent")
_add_info(cms_info, profile.profile, "rendering_intent")
working_mode = img.mode
if img.mode.startswith(("RGB", "BGR", "P")):
working_mode = "RGBA" if img.has_transparency_data else "RGB"
elif img.mode.startswith(("L", "I", "F")) or img.mode == "1":
working_mode = "LA" if img.has_transparency_data else "L"
if img.mode != working_mode:
cms_info["working_mode"] = working_mode
img = img.convert(working_mode)
mode = "RGBA" if img.has_transparency_data else "RGB"
intent = Intent.RELATIVE_COLORIMETRIC
if isIntentSupported(profile, intent, Direction.INPUT) != 1:
intent = _coalesce_intent(getDefaultIntent(profile))
cms_info["conversion_intent"] = intent
if (flags := _INTENT_FLAGS.get(intent)) is not None:
if img.mode == mode:
profileToProfile(
img,
profile,
_SRGB,
renderingIntent=intent,
inPlace=True,
flags=flags
)
else:
img = cast('Image.Image', profileToProfile(
img,
profile,
_SRGB,
renderingIntent=intent,
outputMode=mode,
flags=flags
))
else:
warn(CMSWarning(
f"unsupported intent on {path} assuming sRGB: {cms_info}",
path=path,
cms_info=cms_info
), stacklevel=2)
except PyCMSError as ex:
warn(CMSWarning(
f"{ex} on {path}, assuming sRGB: {cms_info}",
path=path,
cms_info=cms_info,
cause=ex,
), stacklevel=2)
except Exception as ex:
print(f"{ex} on {path}")
if img.has_transparency_data:
if img.mode != "RGBA":
try:
img = img.convert("RGBA")
except ValueError:
img = img.convert("RGBa").convert("RGBA")
elif img.mode != "RGB":
img = img.convert("RGB")
return img
def put_srgb(img: Image.Image, tensor: Tensor) -> None:
if img.mode not in ("RGB", "RGBA", "RGBa"):
raise ValueError(f"Image has non-RGB mode {img.mode}.")
np.copyto(tensor.numpy(), np.asarray(img)[:, :, :3], casting="no")