Source code for colour_hdri.process.dng

"""
Adobe DNG SDK Conversion Process
================================

Define various objects implementing raw conversion based on *Adobe DNG SDK*
and *dcraw*:

-   :func:`colour_hdri.convert_raw_files_to_dng_files`
-   :func:`colour_hdri.convert_dng_files_to_intermediate_files`
-   :func:`colour_hdri.read_dng_files_exif_tags`
"""

from __future__ import annotations

import logging
import os
import platform
import re
import shlex
import subprocess
import typing

import numpy as np

if typing.TYPE_CHECKING:
    from colour.hints import Callable, List, Mapping, Sequence, Tuple

from colour.utilities import CanonicalMapping, optional
from colour.utilities.documentation import (
    DocstringText,
    is_documentation_building,
)

from colour_hdri.utilities import (
    EXIFTag,
    parse_exif_array,
    parse_exif_fraction,
    parse_exif_number,
    parse_exif_string,
    path_exists,
    read_exif_tags,
)

__author__ = "Colour Developers"
__copyright__ = "Copyright 2015 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"

__all__ = [
    "RAW_CONVERTER",
    "RAW_CONVERTER_ARGUMENTS_BAYER_CFA",
    "RAW_CONVERTER_ARGUMENTS_DEMOSAICING",
    "DNG_CONVERTER",
    "DNG_CONVERTER_ARGUMENTS",
    "DNG_EXIF_TAGS_BINDING",
    "convert_raw_files_to_dng_files",
    "convert_dng_files_to_intermediate_files",
    "read_dng_files_exif_tags",
]

LOGGER = logging.getLogger(__name__)

_IS_LINUX_PLATFORM: bool = platform.system() == "Linux"
"""Whether the current platform is *Linux*."""

_IS_MACOS_PLATFORM: bool = platform.system() == "Darwin"
"""Whether the current platform is *macOS*."""

_IS_WINDOWS_PLATFORM: bool = platform.system() in ("Windows", "Microsoft")
"""Whether the current platform is *Windows*."""

RAW_CONVERTER: str = "dcraw"
if is_documentation_building():  # pragma: no cover
    RAW_CONVERTER = DocstringText(RAW_CONVERTER)
    RAW_CONVERTER.__doc__ = """
Command line raw conversion application, typically Dave Coffin's *dcraw*.
"""

RAW_CONVERTER_ARGUMENTS_BAYER_CFA: str = '-t 0 -D -W -4 -T "{raw_file}"'
if _IS_WINDOWS_PLATFORM:
    RAW_CONVERTER_ARGUMENTS_BAYER_CFA = RAW_CONVERTER_ARGUMENTS_BAYER_CFA.replace(
        '"', ""
    )
if is_documentation_building():  # pragma: no cover
    RAW_CONVERTER_ARGUMENTS_BAYER_CFA = DocstringText(RAW_CONVERTER_ARGUMENTS_BAYER_CFA)
    RAW_CONVERTER_ARGUMENTS_BAYER_CFA.__doc__ = """
Arguments for the command line raw conversion application for non
demosaiced linear *tiff* file format output.
"""

RAW_CONVERTER_ARGUMENTS_DEMOSAICING: str = (
    '-t 0 -H 1 -r 1 1 1 1 -4 -q 3 -o 0 -T "{raw_file}"'
)
if _IS_WINDOWS_PLATFORM:
    RAW_CONVERTER_ARGUMENTS_DEMOSAICING = RAW_CONVERTER_ARGUMENTS_DEMOSAICING.replace(
        '"', ""
    )
if is_documentation_building():  # pragma: no cover
    RAW_CONVERTER_ARGUMENTS_DEMOSAICING = DocstringText(
        RAW_CONVERTER_ARGUMENTS_DEMOSAICING
    )
    RAW_CONVERTER_ARGUMENTS_DEMOSAICING.__doc__ = """
Arguments for the command line raw conversion application for demosaiced
linear *tiff* file format output.
"""

if _IS_MACOS_PLATFORM:
    DNG_CONVERTER: str = (
        "/Applications/Adobe DNG Converter.app/Contents/MacOS/Adobe DNG Converter"
    )
elif _IS_WINDOWS_PLATFORM:
    DNG_CONVERTER: str = "Adobe DNG Converter"
else:
    DNG_CONVERTER: str = "dnglab"

if is_documentation_building():  # pragma: no cover
    DNG_CONVERTER = DocstringText(DNG_CONVERTER)
    DNG_CONVERTER.__doc__ = """
Command line *DNG* conversion application, typically *Adobe DNG Converter*.
"""

DNG_CONVERTER_ARGUMENTS: str = '-cr7.1 -l -d "{output_directory}" "{raw_file}"'
if _IS_WINDOWS_PLATFORM:
    DNG_CONVERTER_ARGUMENTS = DNG_CONVERTER_ARGUMENTS.replace('"', "")
elif _IS_LINUX_PLATFORM:
    DNG_CONVERTER_ARGUMENTS = 'convert "{raw_file}" "{output_directory}"'
if is_documentation_building():  # pragma: no cover
    DNG_CONVERTER_ARGUMENTS = DocstringText(DNG_CONVERTER_ARGUMENTS)
    DNG_CONVERTER_ARGUMENTS.__doc__ = """
Arguments for the command line *DNG* conversion application.
"""

DNG_EXIF_TAGS_BINDING: CanonicalMapping = CanonicalMapping(
    {
        "EXIF": CanonicalMapping(
            {
                "Make": (parse_exif_string, None),
                "Camera Model Name": (parse_exif_string, None),
                "Camera Serial Number": (parse_exif_string, None),
                "Lens Model": (parse_exif_string, None),
                "DNG Lens Info": (parse_exif_string, None),
                "Focal Length": (parse_exif_number, None),
                "Exposure Time": (parse_exif_fraction, None),
                "F Number": (parse_exif_number, None),
                "ISO": (parse_exif_number, None),
                "CFA Pattern 2": (
                    lambda x: parse_exif_array(x, np.int64),
                    None,
                ),
                "CFA Plane Color": (
                    lambda x: parse_exif_array(x, np.int64),
                    None,
                ),
                "Black Level Repeat Dim": (
                    lambda x: parse_exif_array(x, np.int64),
                    None,
                ),
                "Black Level": (lambda x: parse_exif_array(x, np.int64), None),
                "White Level": (lambda x: parse_exif_array(x, np.int64), None),
                "Samples Per Pixel": (
                    lambda x: parse_exif_number(x, np.int64),
                    None,
                ),
                "Active Area": (lambda x: parse_exif_array(x, np.int64), None),
                "Orientation": (lambda x: parse_exif_number(x, np.int64), None),
                "Camera Calibration Sig": (parse_exif_string, None),
                "Profile Calibration Sig": (parse_exif_string, None),
                "Calibration Illuminant 1": (
                    lambda x: parse_exif_number(x, np.int64),
                    17,
                ),
                "Calibration Illuminant 2": (
                    lambda x: parse_exif_number(x, np.int_),
                    21,
                ),
                "Color Matrix 1": (
                    lambda x: parse_exif_array(x, np.float64, (3, 3)),
                    "1 0 0 0 1 0 0 0 1",
                ),
                "Color Matrix 2": (
                    lambda x: parse_exif_array(x, np.float64, (3, 3)),
                    "1 0 0 0 1 0 0 0 1",
                ),
                "Camera Calibration 1": (
                    lambda x: parse_exif_array(x, np.float64, (3, 3)),
                    "1 0 0 0 1 0 0 0 1",
                ),
                "Camera Calibration 2": (
                    lambda x: parse_exif_array(x, np.float64, (3, 3)),
                    "1 0 0 0 1 0 0 0 1",
                ),
                "Analog Balance": (
                    lambda x: parse_exif_array(x, np.float64),
                    "1 1 1",
                ),
                "Reduction Matrix 1": (
                    lambda x: parse_exif_array(x, np.float64, (3, 3)),
                    "1 0 0 0 1 0 0 0 1",
                ),
                "Reduction Matrix 2": (
                    lambda x: parse_exif_array(x, np.float64, (3, 3)),
                    "1 0 0 0 1 0 0 0 1",
                ),
                "Forward Matrix 1": (
                    lambda x: parse_exif_array(x, np.float64, (3, 3)),
                    "1 0 0 0 1 0 0 0 1",
                ),
                "Forward Matrix 2": (
                    lambda x: parse_exif_array(x, np.float64, (3, 3)),
                    "1 0 0 0 1 0 0 0 1",
                ),
                "As Shot Neutral": (
                    lambda x: parse_exif_array(x, np.float64),
                    "1 1 1",
                ),
                "Baseline Exposure": (
                    lambda x: parse_exif_number(x, np.float64),
                    None,
                ),
                "Baseline Noise": (
                    lambda x: parse_exif_number(x, np.float64),
                    None,
                ),
            }
        )
    }
)
DNG_EXIF_TAGS_BINDING.__doc__ = """
Exif tags binding for a *dng* file.
"""


[docs] def convert_raw_files_to_dng_files( raw_files: Sequence[str], output_directory: str, dng_converter: str | None = None, dng_converter_arguments: str | None = None, ) -> List[str]: """ Convert given raw files to *dng* files using given output directory. Parameters ---------- raw_files Raw files to convert to *dng* files. output_directory Output directory. dng_converter Command line *DNG* conversion application, typically *Adobe DNG Converter*. dng_converter_arguments Arguments for the command line *DNG* conversion application. Returns ------- :class:`list` *dng* files. Raises ------ RuntimeError If the *DNG* converter is not available. """ dng_converter = optional(dng_converter, DNG_CONVERTER) dng_converter_arguments = optional(dng_converter_arguments, DNG_CONVERTER_ARGUMENTS) dng_files = [] for raw_file in raw_files: raw_file_extension = os.path.splitext(raw_file)[1] dng_file = os.path.join( output_directory, os.path.basename(re.sub(f"{raw_file_extension}$", ".dng", raw_file)), ) if path_exists(dng_file): os.remove(dng_file) LOGGER.info( 'Converting "%s" file to "%s" file.', raw_file, dng_file, ) command = [ dng_converter, *shlex.split( dng_converter_arguments.format( output_directory=output_directory, raw_file=raw_file ), posix=not _IS_WINDOWS_PLATFORM, ), ] subprocess.call(command, shell=_IS_WINDOWS_PLATFORM) # noqa: S603 dng_files.append(dng_file) return dng_files
[docs] def convert_dng_files_to_intermediate_files( dng_files: Sequence[str], output_directory: str, raw_converter: str | None = None, raw_converter_arguments: str | None = None, ) -> List[str]: """ Convert given *dng* files to intermediate *tiff* files using given output directory. Parameters ---------- dng_files *dng* files to convert to intermediate *tiff* files. output_directory Output directory. raw_converter Command line raw conversion application, typically Dave Coffin's *dcraw*. raw_converter_arguments Arguments for the command line raw conversion application. Returns ------- :class:`list` Intermediate *tiff* files. """ raw_converter = optional(raw_converter, RAW_CONVERTER) raw_converter_arguments = optional( raw_converter_arguments, RAW_CONVERTER_ARGUMENTS_DEMOSAICING ) intermediate_files = [] for dng_file in dng_files: intermediate_file = re.sub("\\.dng$", ".tiff", dng_file) if path_exists(intermediate_file): os.remove(intermediate_file) LOGGER.info( 'Converting "%s" file to "%s" file.', dng_file, intermediate_file, ) command = [ raw_converter, *shlex.split( raw_converter_arguments.format( output_directory=output_directory, raw_file=dng_file ), posix=not _IS_WINDOWS_PLATFORM, ), ] subprocess.call(command, shell=_IS_WINDOWS_PLATFORM) # noqa: S603 tiff_file = os.path.join(output_directory, os.path.basename(intermediate_file)) if tiff_file != intermediate_file: if path_exists(tiff_file): os.remove(tiff_file) os.rename(intermediate_file, tiff_file) intermediate_files.append(tiff_file) return intermediate_files
[docs] def read_dng_files_exif_tags( dng_files: Sequence[str], exif_tags_binding: Mapping[ str, Mapping[str, Tuple[Callable, str | None]] ] = DNG_EXIF_TAGS_BINDING, ) -> List[CanonicalMapping]: """ Read given *dng* files exif tags using given binding. Parameters ---------- dng_files *dng* files to read the exif tags from. exif_tags_binding Exif tags binding. Returns ------- :class:`list` *dng* files exif tags. """ dng_files_exif_tags = [] for dng_file in dng_files: exif_tags = read_exif_tags(dng_file, numeric=True) binding = CanonicalMapping() for group, tags in exif_tags_binding.items(): binding[group] = CanonicalMapping() for tag, (parser, default) in tags.items(): exif_tag = exif_tags[group].get(tag) if exif_tag is None: binding[group][tag] = ( default if default is None else parser(EXIFTag(value=default)) ) else: binding[group][tag] = parser(exif_tag[0]) dng_files_exif_tags.append(binding) return dng_files_exif_tags