def init_logging(name, level=logging.INFO):
    """Logging config

    Set the level and create a more detailed formatter for debug mode.

    """
    logger = logging.getLogger(name)
    logger.setLevel(level)

    try:
        if os.isatty(sys.stdout.fileno()) and not sys.platform.startswith('win'):
            formatter = ColoredFormatter()
        elif level == logging.DEBUG:
            formatter = Formatter('%(levelname)s - %(message)s')
        else:
            formatter = Formatter('%(message)s')
    except Exception:
        # This fails when running tests with click (test_build)
        formatter = Formatter('%(message)s')

    handler = logging.StreamHandler()
    handler.setFormatter(formatter)
    logger.addHandler(handler) class Image(Media):
    """Gather all informations on an image file."""

    type = 'image'

    def __init__(self, filename, path, settings):
        super().__init__(filename, path, settings)
        imgformat = settings.get('img_format')

        if imgformat and PILImage.EXTENSION[self.src_ext] != imgformat.upper():
            # Find the extension that should match img_format
            extensions = {v: k for k, v in PILImage.EXTENSION.items()}
            ext = extensions[imgformat.upper()]
            self.dst_filename = self.basename + ext
            self.thumb_name = get_thumb(self.settings, self.dst_filename)

    @cached_property
    def date(self):
        """The date from the EXIF DateTimeOriginal metadata if available, or
        from the file date."""
        return self.exif and self.exif.get('dateobj', None) or self._get_file_date()

    @cached_property
    def exif(self):
        """If not `None` contains a dict with the most common tags. For more
        information, see :ref:`simple-exif-data`.
        """
        datetime_format = self.settings['datetime_format']
        return (
            get_exif_tags(self.raw_exif, datetime_format=datetime_format)
            if self.raw_exif and self.src_ext in ('.jpg', '.jpeg')
            else None
        )

    @cached_property
    def file_metadata(self):
        """Image file metadata (Exif and IPTC)"""
        return get_image_metadata(self.src_path)

    def _get_markdown_metadata(self):
        """Get metadata from filename.md."""
        meta = super()._get_markdown_metadata()

        # If a title or description hasn't been obtained by other means, look
        #  for the information in IPTC fields
        if not meta['title']:
            meta['title'] = self.file_metadata['iptc'].get('title', '')
        if not meta['description']:
            meta['description'] = self.file_metadata['iptc'].get('description', '')

        return meta

    @cached_property
    def raw_exif(self):
        """If not `None`, contains the raw EXIF tags."""
        if self.src_ext in ('.jpg', '.jpeg'):
            return self.file_metadata['exif']

    @cached_property
    def size(self):
        """The dimensions of the resized image."""
        return get_size(self.dst_path)

    @cached_property
    def input_size(self):
        """The dimensions of the input image."""
        return get_size(self.src_path)

    @cached_property
    def thumb_size(self):
        """The dimensions of the thumbnail image."""
        return get_size(self.thumb_path)

    def has_location(self):
        """True if location information is available for EXIF GPSInfo."""
        return self.exif is not None and 'gps' in self.exif def generate_image(source, outname, settings, options=None):
    """Image processor, rotate and resize the image.

    :param source: path to an image
    :param outname: output filename
    :param settings: settings dict
    :param options: dict with PIL options (quality, optimize, progressive)

    """

    logger = logging.getLogger(__name__)

    if settings['use_orig'] or source.endswith('.gif'):
        utils.copy(source, outname, symlink=settings['orig_link'])
        return

    img = _read_image(source)
    original_format = img.format

    if settings['copy_exif_data'] and settings['autorotate_images']:
        logger.warning(
            "The 'autorotate_images' and 'copy_exif_data' settings "
            "are not compatible because Sigal can't save the "
            "modified Orientation tag."
        )

    # Preserve EXIF data
    if settings['copy_exif_data'] and _has_exif_tags(img):
        if options is not None:
            options = deepcopy(options)
        else:
            options = {}
        options['exif'] = img.info['exif']

    # Rotate the img, and catch IOError when PIL fails to read EXIF
    if settings['autorotate_images']:
        try:
            img = Transpose().process(img)
        except (OSError, IndexError):
            pass

    # Resize the image
    if settings['img_processor']:
        try:
            logger.debug('Processor: %s', settings['img_processor'])
            processor_cls = getattr(pilkit.processors, settings['img_processor'])
        except AttributeError:
            logger.error('Wrong processor name: %s', settings['img_processor'])
            sys.exit()

        width, height = settings['img_size']

        if img.size[0] < img.size[1]:
            # swap target size if image is in portrait mode
            height, width = width, height

        processor = processor_cls(width, height, upscale=False)
        img = processor.process(img)

    # signal.send() does not work here as plugins can modify the image, so we
    # iterate other the receivers to call them with the image.
    for receiver in signals.img_resized.receivers_for(img):
        img = receiver(img, settings=settings)

    # first, use hard-coded output format, or PIL format, or original image
    # format, or fall back to JPEG
    outformat = settings.get('img_format') or img.format or original_format or 'JPEG'

    logger.debug('Save resized image to %s (%s)', outname, outformat)
    save_image(img, outname, outformat, options=options, autoconvert=True) def generate_thumbnail(
    source, outname, box, fit=True, options=None, thumb_fit_centering=(0.5, 0.5)
):
    """Create a thumbnail image."""

    logger = logging.getLogger(__name__)
    img = _read_image(source)
    img = Transpose().process(img)
    original_format = img.format

    if fit:
        img = ImageOps.fit(img, box, PILImage.ANTIALIAS, centering=thumb_fit_centering)
    else:
        img.thumbnail(box, PILImage.ANTIALIAS)

    outformat = img.format or original_format or 'JPEG'
    logger.debug('Save thumnail image: %s (%s)', outname, outformat)
    save_image(img, outname, outformat, options=options, autoconvert=True) def get_exif_data(filename):
    """Return a dict with the raw EXIF data."""

    logger = logging.getLogger(__name__)

    img = _read_image(filename)

    try:
        with warnings.catch_warnings(record=True) as caught_warnings:
            exif = img._getexif() or {}
    except ZeroDivisionError:
        logger.warning('Failed to read EXIF data.')
        return None

    for w in caught_warnings:
        fname = filename.filename if isinstance(filename, PILImage.Image) else filename
        logger.warning(
            f'PILImage reported a warning for file {fname}\n{w.category}: {w.message}'
        )

    data = {TAGS.get(tag, tag): value for tag, value in exif.items()}

    if 'GPSInfo' in data:
        try:
            data['GPSInfo'] = {
                GPSTAGS.get(tag, tag): value for tag, value in data['GPSInfo'].items()
            }
        except AttributeError:
            logger.info('Failed to get GPS Info')
            del data['GPSInfo']
    return data def get_exif_tags(data, datetime_format='%c'):
    """Make a simplified version with common tags from raw EXIF data."""

    logger = logging.getLogger(__name__)
    simple = {}

    for tag in ('Model', 'Make', 'LensModel'):
        if tag in data:
            val = data[tag][0] if isinstance(data[tag], tuple) else data[tag]
            simple[tag] = str(val).strip()

    if 'FNumber' in data:
        fnumber = data['FNumber']
        try:
            if IFDRational and isinstance(fnumber, IFDRational):
                simple['fstop'] = float(fnumber)
            else:
                simple['fstop'] = float(fnumber[0]) / fnumber[1]
        except Exception:
            logger.debug('Skipped invalid FNumber: %r', fnumber, exc_info=True)

    if 'FocalLength' in data:
        focal = data['FocalLength']
        try:
            if IFDRational and isinstance(focal, IFDRational):
                simple['focal'] = round(float(focal))
            else:
                simple['focal'] = round(float(focal[0]) / focal[1])
        except Exception:
            logger.debug('Skipped invalid FocalLength: %r', focal, exc_info=True)

    if 'ExposureTime' in data:
        exptime = data['ExposureTime']
        if IFDRational and isinstance(exptime, IFDRational):
            simple['exposure'] = f'{exptime.numerator}/{exptime.denominator}'
        elif isinstance(exptime, tuple):
            try:
                simple['exposure'] = str(fractions.Fraction(exptime[0], exptime[1]))
            except ZeroDivisionError:
                logger.info('Invalid ExposureTime: %r', exptime)
        elif isinstance(exptime, int):
            simple['exposure'] = str(exptime)
        else:
            logger.info('Unknown format for ExposureTime: %r', exptime)

    if data.get('ISOSpeedRatings'):
        simple['iso'] = data['ISOSpeedRatings']

    if 'DateTimeOriginal' in data:
        # Remove null bytes at the end if necessary
        date = data['DateTimeOriginal'].rsplit('\x00')[0]

        try:
            simple['dateobj'] = datetime.strptime(date, '%Y:%m:%d %H:%M:%S')
            simple['datetime'] = simple['dateobj'].strftime(datetime_format)
        except (ValueError, TypeError) as e:
            logger.info('Could not parse DateTimeOriginal: %s', e)

    if 'GPSInfo' in data:
        info = data['GPSInfo']
        lat_info = info.get('GPSLatitude')
        lon_info = info.get('GPSLongitude')
        lat_ref_info = info.get('GPSLatitudeRef')
        lon_ref_info = info.get('GPSLongitudeRef')

        if lat_info and lon_info and lat_ref_info and lon_ref_info:
            try:
                lat = dms_to_degrees(lat_info)
                lon = dms_to_degrees(lon_info)
            except (ZeroDivisionError, ValueError, TypeError):
                logger.info('Failed to read GPS info')
            else:
                simple['gps'] = {
                    'lat': -lat if lat_ref_info != 'N' else lat,
                    'lon': -lon if lon_ref_info != 'E' else lon,
                }

    return simple def get_iptc_data(filename):
    """Return a dict with the raw IPTC data."""

    logger = logging.getLogger(__name__)

    iptc_data = {}
    raw_iptc = {}

    # PILs IptcImagePlugin issues a SyntaxError in certain circumstances
    # with malformed metadata, see PIL/IptcImagePlugin.py", line 71.
    # ( https://github.com/python-pillow/Pillow/blob/
    # 9dd0348be2751beb2c617e32ff9985aa2f92ae5f/src/PIL/IptcImagePlugin.py#L71 )
    try:
        img = _read_image(filename)
        raw_iptc = IptcImagePlugin.getiptcinfo(img)
    except SyntaxError:
        logger.info('IPTC Error in %s', filename)

    # IPTC fields are catalogued in:
    # https://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata
    # 2:05 is the IPTC title property
    if raw_iptc and (2, 5) in raw_iptc:
        iptc_data["title"] = raw_iptc[(2, 5)].decode('utf-8', errors='replace')

    # 2:120 is the IPTC description property
    if raw_iptc and (2, 120) in raw_iptc:
        iptc_data["description"] = raw_iptc[(2, 120)].decode('utf-8', errors='replace')

    # 2:105 is the IPTC headline property
    if raw_iptc and (2, 105) in raw_iptc:
        iptc_data["headline"] = raw_iptc[(2, 105)].decode('utf-8', errors='replace')

    return iptc_data def get_image_metadata(filename):
    logger = logging.getLogger(__name__)
    exif, iptc, size = {}, {}, {}

    try:
        img = _read_image(filename)
    except Exception as e:
        logger.error('Could not open image %s metadata: %s', filename, e)
    else:
        try:
            if os.path.splitext(filename)[1].lower() in ('.jpg', '.jpeg'):
                exif = get_exif_data(img)
        except Exception as e:
            logger.warning('Could not read EXIF data from %s: %s', filename, e)

        try:
            iptc = get_iptc_data(img)
        except Exception as e:
            logger.warning('Could not read IPTC data from %s: %s', filename, e)

        try:
            size = get_size(img)
        except Exception as e:
            logger.warning('Could not read size from %s: %s', filename, e)

    return {'exif': exif, 'iptc': iptc, 'size': size} def get_size(file_path):
    """Return image size (width and height)."""
    try:
        im = _read_image(file_path)
    except (OSError, IndexError, TypeError, AttributeError) as e:
        logger = logging.getLogger(__name__)
        logger.error("Could not read size of %s due to %r", file_path, e)
    else:
        width, height = im.size
        return {'width': width, 'height': height} def process_image(media):
    """Process one image: resize, create thumbnail."""

    logger = logging.getLogger(__name__)
    logger.info('Processing %s', media.src_path)

    if media.src_ext in ('.jpg', '.jpeg', '.JPG', '.JPEG'):
        options = media.settings['jpg_options']
    elif media.src_ext == '.png':
        options = {'optimize': True}
    else:
        options = {}

    try:
        generate_image(media.src_path, media.dst_path, media.settings, options=options)

        if media.settings['make_thumbs']:
            generate_thumbnail(
                media.dst_path,
                media.thumb_path,
                media.settings['thumb_size'],
                fit=media.settings['thumb_fit'],
                options=options,
                thumb_fit_centering=media.settings["thumb_fit_centering"],
            )
    except Exception as e:
        logger.info('Failed to process: %r', e)
        if logger.getEffectiveLevel() == logging.DEBUG:
            raise
        else:
            return Status.FAILURE

    return Status.SUCCESS class Status:
    SUCCESS = 0
    FAILURE = 1 def create_settings(**kwargs):
    """Create a new default setting copy and initialize it with kwargs."""
    settings = _DEFAULT_CONFIG.copy()
    settings.update(kwargs)
    return settings
import os
import pytest
from unittest.mock import patch
from PIL import Image as PILImage
from sigal import init_logging
from sigal.gallery import Image
from sigal.image import (
    generate_image,
    generate_thumbnail,
    get_exif_data,
    get_exif_tags,
    get_iptc_data,
    get_image_metadata,
    get_size,
    process_image,
)
from sigal.settings import Status, create_settings



CURRENT_DIR = os.path.dirname(__file__)
SRCDIR = os.path.join(CURRENT_DIR, 'sample', 'pictures')
TEST_IMAGE = 'KeckObservatory20071020.jpg'
SRCFILE = os.path.join(SRCDIR, 'dir2', TEST_IMAGE)

TEST_GIF_IMAGE = 'example.gif'
SRC_GIF_FILE = os.path.join(SRCDIR, 'dir1', 'test1', TEST_GIF_IMAGE)


def test_process_image(tmpdir):
    "Test the process_image function."


    status = process_image(Image('foo.txt', 'bar', create_settings()))