"""
Raw camera image processor using rawpy (LibRaw).
The optional ``raw`` dependency group must be installed::
uv sync --extra raw
"""
from __future__ import annotations
import io
from collections.abc import Mapping
from typing import IO, Any
from madam.core import Asset, OperatorError, Processor, operator
_MIME_TYPE_TO_PIL_FORMAT: dict[str, str] = {
'image/jpeg': 'JPEG',
'image/png': 'PNG',
'image/tiff': 'TIFF',
}
[docs]
class RawImageProcessor(Processor):
"""
Represents a processor that handles raw camera image formats (DNG, CR2,
NEF, ARW, and any other format supported by LibRaw).
Reading and decoding require the `rawpy <https://letmaik.github.io/rawpy/>`_
package, which is a Python binding for LibRaw. Pillow is used to encode the
decoded image into the requested output format.
Install the optional ``raw`` extra to get both dependencies::
pip install madam[raw]
"""
[docs]
def __init__(self, config: Mapping[str, Any] | None = None) -> None:
"""
Initializes a new ``RawImageProcessor``.
:param config: Mapping with settings.
"""
super().__init__(config)
[docs]
def can_read(self, file: IO) -> bool:
try:
import rawpy
except ImportError:
return False
data = file.read()
file.seek(0)
try:
with rawpy.imread(io.BytesIO(data)):
return True
except rawpy.LibRawError:
return False
[docs]
def read(self, file: IO) -> Asset:
"""
Reads a raw camera image file and returns an :class:`~madam.core.Asset`.
The essence contains the original raw bytes unchanged. The ``width``
and ``height`` metadata attributes reflect the full sensor dimensions
before demosaicing.
:param file: Readable binary file-like object containing raw image data
:type file: IO
:return: Asset with ``mime_type='image/x-raw'``, ``width``, and ``height``
:rtype: Asset
"""
try:
import rawpy
except ImportError as e:
raise OperatorError('rawpy is required for reading raw images; install the raw extra') from e
raw_bytes = file.read()
with rawpy.imread(io.BytesIO(raw_bytes)) as raw:
width = raw.sizes.width
height = raw.sizes.height
return Asset._from_bytes(raw_bytes, mime_type='image/x-raw', width=width, height=height)
[docs]
@operator
def decode(self, asset: Asset, mime_type: str = 'image/png') -> Asset:
"""
Demosaics a raw camera image and returns the result as a standard
raster image asset.
Demosaicing is performed with LibRaw using automatic white balance.
The result is encoded into the format specified by *mime_type*.
:param asset: Raw image asset to demosaic
:type asset: Asset
:param mime_type: MIME type of the output image (``'image/png'``,
``'image/jpeg'``, or ``'image/tiff'``)
:type mime_type: str
:return: Decoded raster image asset
:rtype: Asset
:raises OperatorError: if *mime_type* is not supported
"""
try:
import rawpy
except ImportError as e:
raise OperatorError('rawpy is required for decoding raw images; install the raw extra') from e
import PIL.Image
pil_format = _MIME_TYPE_TO_PIL_FORMAT.get(mime_type)
if pil_format is None:
raise OperatorError(f'Unsupported MIME type for raw decode: {mime_type!r}')
with rawpy.imread(io.BytesIO(asset.essence.read())) as raw:
rgb_array = raw.postprocess(use_camera_wb=True, no_auto_bright=False)
pil_image = PIL.Image.fromarray(rgb_array)
width, height = pil_image.size
buf = io.BytesIO()
pil_image.save(buf, format=pil_format)
buf.seek(0)
return Asset(essence=buf, mime_type=mime_type, width=width, height=height)