"""
EXIF Data Manipulation
======================
EXIF data manipulation routines based on *exiftool*:
- :func:`colour_hdri.parse_exif_data`
- :func:`colour_hdri.read_exif_tags`
- :func:`colour_hdri.copy_exif_tags`
- :func:`colour_hdri.update_exif_tags`
- :func:`colour_hdri.delete_exif_tags`
- :func:`colour_hdri.read_exif_tag`
- :func:`colour_hdri.write_exif_tag`
"""
from __future__ import annotations
import logging
import platform
import re
import subprocess
import typing
if typing.TYPE_CHECKING:
from collections import defaultdict
from dataclasses import dataclass, field
from fractions import Fraction
import numpy as np
from colour.constants import DTYPE_FLOAT_DEFAULT
if typing.TYPE_CHECKING:
from colour.hints import (
DTypeFloat,
DTypeReal,
List,
NDArray,
Real,
Sequence,
SupportsIndex,
Type,
)
from colour.utilities import as_array, as_float_scalar, optional
from colour.utilities.documentation import (
DocstringText,
is_documentation_building,
)
from colour_hdri.utilities import vivification
__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__ = [
"EXIF_EXECUTABLE",
"EXIFTag",
"parse_exif_string",
"parse_exif_number",
"parse_exif_fraction",
"parse_exif_array",
"parse_exif_data",
"read_exif_tags",
"copy_exif_tags",
"update_exif_tags",
"delete_exif_tags",
"read_exif_tag",
"write_exif_tag",
]
LOGGER = logging.getLogger(__name__)
_IS_WINDOWS_PLATFORM: bool = platform.system() in ("Windows", "Microsoft")
"""Whether the current platform is *Windows*."""
EXIF_EXECUTABLE: str = "exiftool"
if is_documentation_building(): # pragma: no cover
EXIF_EXECUTABLE = DocstringText(EXIF_EXECUTABLE)
EXIF_EXECUTABLE.__doc__ = """
Command line EXIF manipulation application, usually Phil Harvey's *ExifTool*.
"""
[docs]
@dataclass
class EXIFTag:
"""
EXIF tag data.
Parameters
----------
group
EXIF tag group name.
name
EXIF tag name.
value
EXIF tag value.
identifier
EXIF tag identifier.
"""
group: str | None = field(default_factory=lambda: None)
name: str | None = field(default_factory=lambda: None)
value: str | None = field(default_factory=lambda: None)
identifier: str | None = field(default_factory=lambda: None)
[docs]
def parse_exif_string(exif_tag: EXIFTag) -> str:
"""
Parse given EXIF tag assuming it is a string and return its value.
Parameters
----------
exif_tag
EXIF tag to parse.
Returns
-------
:class:`str`
Parsed EXIF tag value.
"""
return str(exif_tag.value)
[docs]
def parse_exif_number(exif_tag: EXIFTag, dtype: Type[DTypeReal] | None = None) -> Real:
"""
Parse given EXIF tag assuming it is a number type and return its value.
Parameters
----------
exif_tag
EXIF tag to parse.
dtype
Return value data type.
Returns
-------
:class:`numpy.floating` or :class:`numpy.integer`
Parsed EXIF tag value.
"""
dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
return dtype(exif_tag.value) # pyright: ignore
[docs]
def parse_exif_fraction(
exif_tag: EXIFTag, dtype: Type[DTypeFloat] | None = None
) -> float:
"""
Parse given EXIF tag assuming it is a fraction and return its value.
Parameters
----------
exif_tag
EXIF tag to parse.
dtype
Return value data type.
Returns
-------
:class:`numpy.floating`
Parsed EXIF tag value.
"""
dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
value = (
exif_tag.value if exif_tag.value is None else float(Fraction(exif_tag.value))
)
return as_float_scalar(value, dtype) # pyright: ignore
[docs]
def parse_exif_array(
exif_tag: EXIFTag,
dtype: Type[DTypeReal] | None = None,
shape: SupportsIndex | Sequence[SupportsIndex] | None = None,
) -> NDArray:
"""
Parse given EXIF tag assuming it is an array and return its value.
Parameters
----------
exif_tag
EXIF tag to parse.
dtype
Return value data type.
shape
Shape of the array to be returned.
Returns
-------
:class:`numpy.ndarray`
Parsed EXIF tag value.
"""
dtype = optional(dtype, DTYPE_FLOAT_DEFAULT)
value = exif_tag.value if exif_tag.value is None else exif_tag.value.split()
array = as_array(value, dtype) # pyright: ignore
if shape is not None:
array = np.reshape(array, shape)
return array
[docs]
def parse_exif_data(data: str) -> List:
"""
Parse given EXIF data output from *exiftool*.
Parameters
----------
data
EXIF data output.
Returns
-------
:class:`list`
Parsed EXIF data output.
Raises
------
ValueError
If the EXIF data output cannot be parsed.
"""
search = re.search(
r"\[(?P<group>\w+)\]\s*(?P<id>(\d+|-))?(?P<tag>.*?):(?P<value>.*$)",
data,
)
if search is not None:
return [
group.strip() if group is not None else group
for group in (
search.group("group"),
search.group("id"),
search.group("tag"),
search.group("value"),
)
]
exception = "The EXIF data output cannot be parsed!"
raise ValueError(exception)
# TODO: Find a better name.
[docs]
def read_exif_tag(image: str, tag: str, numeric: bool = False) -> str:
"""
Return given image EXIF tag value.
Parameters
----------
image
Image file to read the EXIF tag value of.
tag
Tag to read the value of.
Returns
-------
:class:`str`
Tag value.
"""
args = [f"-{tag}"]
if numeric:
args.append("-n")
value = (
str(
subprocess.check_output( # noqa: S603
[EXIF_EXECUTABLE, *args, image],
shell=_IS_WINDOWS_PLATFORM,
),
"utf-8",
"ignore",
)
.split(":")
.pop()
.strip()
)
LOGGER.info(
'Reading "%s" image "%s" EXIF tag value: "%s"',
image,
tag,
value,
)
return value
[docs]
def write_exif_tag(image: str, tag: str, value: str) -> bool:
"""
Set given image EXIF tag value.
Parameters
----------
image
Image file to set the EXIF tag value of.
tag
Tag to set the value of.
value
Value to set.
Returns
-------
:class:`bool`
Definition success.
"""
LOGGER.info(
'Writing "%s" image "%s" EXIF tag with "%s" value.',
image,
tag,
value,
)
arguments = [EXIF_EXECUTABLE, "-overwrite_original"]
arguments += [f"-{tag}={value}", image]
subprocess.check_output( # noqa: S603
arguments,
shell=_IS_WINDOWS_PLATFORM,
)
return True