automatic/modules/sharpfin/cms.py

175 lines
5.5 KiB
Python

"""Sharpfin color management (ICC profile handling).
Vendored from https://github.com/drhead/sharpfin (Apache 2.0)
"""
from io import BytesIO
from typing import Any, cast
from warnings import warn
import numpy as np
from torch import Tensor
import PIL.Image as image
import PIL.ImageCms as image_cms
from PIL.Image import Image
from PIL.ImageCms import (
Direction, Intent, ImageCmsProfile, PyCMSError,
createProfile, getDefaultIntent, isIntentSupported, profileToProfile
)
from PIL.ImageOps import exif_transpose
image.MAX_IMAGE_PIXELS = None
_SRGB = createProfile(colorSpace='sRGB')
_INTENT_FLAGS = {
Intent.PERCEPTUAL: image_cms.FLAGS["HIGHRESPRECALC"],
Intent.RELATIVE_COLORIMETRIC: (
image_cms.FLAGS["HIGHRESPRECALC"] |
image_cms.FLAGS["BLACKPOINTCOMPENSATION"]
),
Intent.ABSOLUTE_COLORIMETRIC: image_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:
if hasattr(img, 'filename'):
path = img.filename
else:
path = ""
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, 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
))
except PyCMSError as ex:
warn(CMSWarning(
f"{ex} on {path}, assuming sRGB: {cms_info}",
path=path,
cms_info=cms_info,
cause=ex,
))
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, 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")