def __init__(self) -> None:
        self.thumbnailSizeNeeded = QSize(ThumbnailSize.width, ThumbnailSize.height)
        self.thumbnail_cache = ThumbnailCacheSql(create_table_if_not_exists=False)
        self.fdo_cache_large = FdoCacheLarge()
        self.fdo_cache_normal = FdoCacheNormal()

        super().__init__('Thumbnail Extractor')
    def __init__(self, use_thumbnail_cache: bool) -> None:

        if use_thumbnail_cache:
            self.thumbnail_cache = ThumbnailCacheSql(
                create_table_if_not_exists=False)
        else:
            self.thumbnail_cache = None

        # Access large size Freedesktop.org thumbnail cache
        self.fdo_cache_large = FdoCacheLarge()

        self.thumbnail_size_needed = QSize(ThumbnailSize.width,
                                           ThumbnailSize.height)
class GetThumbnailFromCache:
    """
    Try to get thumbnail from Rapid Photo Downloader's thumbnail cache
    or from the FreeDesktop.org cache.
    """
    def __init__(self, use_thumbnail_cache: bool) -> None:

        if use_thumbnail_cache:
            self.thumbnail_cache = ThumbnailCacheSql(
                create_table_if_not_exists=False)
        else:
            self.thumbnail_cache = None

        # Access large size Freedesktop.org thumbnail cache
        self.fdo_cache_large = FdoCacheLarge()

        self.thumbnail_size_needed = QSize(ThumbnailSize.width,
                                           ThumbnailSize.height)

    def image_large_enough(self, size: QSize) -> bool:
        """Check if image is equal or bigger than thumbnail size."""
        return (size.width() >= self.thumbnail_size_needed.width()
                or size.height() >= self.thumbnail_size_needed.height())

    def get_from_cache(
        self,
        rpd_file: RPDFile,
        use_thumbnail_cache: bool = True
    ) -> Tuple[ExtractionTask, bytes, str, ThumbnailCacheOrigin]:
        """
        Attempt to get a thumbnail for the file from the Rapid Photo Downloader
        thumbnail cache or from the FreeDesktop.org 256x256 thumbnail cache.

        :param rpd_file:
        :param use_thumbnail_cache: whether to use the
        :return:
        """

        task = ExtractionTask.undetermined
        thumbnail_bytes = None
        full_file_name_to_work_on = ""
        origin = None  # type: Optional[ThumbnailCacheOrigin]

        # Attempt to get thumbnail from Thumbnail Cache
        # (see cache.py for definitions of various caches)
        if self.thumbnail_cache is not None and use_thumbnail_cache:
            get_thumbnail = self.thumbnail_cache.get_thumbnail_path(
                full_file_name=rpd_file.full_file_name,
                mtime=rpd_file.modification_time,
                size=rpd_file.size,
                camera_model=rpd_file.camera_model,
            )
            rpd_file.thumbnail_cache_status = get_thumbnail.disk_status
            if get_thumbnail.disk_status != ThumbnailCacheDiskStatus.not_found:
                origin = ThumbnailCacheOrigin.thumbnail_cache
                task = ExtractionTask.bypass
                if get_thumbnail.disk_status == ThumbnailCacheDiskStatus.failure:
                    rpd_file.thumbnail_status = ThumbnailCacheStatus.generation_failed
                    rpd_file.thumbnail_cache_status = ThumbnailCacheDiskStatus.failure
                elif get_thumbnail.disk_status == ThumbnailCacheDiskStatus.found:
                    rpd_file.thumbnail_cache_status = ThumbnailCacheDiskStatus.found
                    if get_thumbnail.orientation_unknown:
                        rpd_file.thumbnail_status = (
                            ThumbnailCacheStatus.orientation_unknown)
                    else:
                        rpd_file.thumbnail_status = ThumbnailCacheStatus.ready
                    with open(get_thumbnail.path, "rb") as thumbnail:
                        thumbnail_bytes = thumbnail.read()

        # Attempt to get thumbnail from large FDO Cache if not found in Thumbnail Cache
        # and it's not being downloaded directly from a camera (if it's from a camera,
        # it's not going to be in the FDO cache)

        if task == ExtractionTask.undetermined and not rpd_file.from_camera:
            get_thumbnail = self.fdo_cache_large.get_thumbnail(
                full_file_name=rpd_file.full_file_name,
                modification_time=rpd_file.modification_time,
                size=rpd_file.size,
                camera_model=rpd_file.camera_model,
            )
            if get_thumbnail.disk_status == ThumbnailCacheDiskStatus.found:
                rpd_file.fdo_thumbnail_256_name = get_thumbnail.path
                thumb = get_thumbnail.thumbnail  # type: QImage
                if thumb is not None:
                    if self.image_large_enough(thumb.size()):
                        task = ExtractionTask.load_file_directly
                        full_file_name_to_work_on = get_thumbnail.path
                        origin = ThumbnailCacheOrigin.fdo_cache
                        rpd_file.thumbnail_status = ThumbnailCacheStatus.fdo_256_ready

        return task, thumbnail_bytes, full_file_name_to_work_on, origin
Example #4
0
    def closeEvent(self, QCloseEvent):
        if self.received != len(self.rpd_files):
            print("WARNING: Didn't receive correct amount of thumbnails. Missing {}".format(
                len(self.rpd_files) - self.received))
        self.thumbnailer.stop()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-d', '--data', dest='data', type=str)
    parser.add_argument('-p', '--profile', dest='profile', action="store_true")
    parser.add_argument("--reset", action="store_true", dest="reset",
                 help="reset all thumbnail caches and exit")
    args = parser.parse_args()
    if args.reset:
        cache = ThumbnailCacheSql(create_table_if_not_exists=False)
        cache.purge_cache()
        print("Thumbnail cache reset")
        cache = os.path.join(BaseDirectory.xdg_cache_home, 'thumbnails')
        folders = [os.path.join(cache, subdir) for subdir in ('normal', 'large')]
        i = 0
        for folder in folders:
            for the_file in os.listdir(folder):
                file_path = os.path.join(folder, the_file)
                try:
                    if os.path.isfile(file_path):
                        i += 1
                        os.remove(file_path)
                except OSError as e:
                    print(e)
        print('Removed {} XDG thumbnails'.format(i))
class ThumbnailExtractor(LoadBalancerWorker):

    # Exif rotation constants
    rotate_0 = '1'
    rotate_90 = '6'
    rotate_180 = '3'
    rotate_270 = '8'

    maxStandardSize = QSize(
        max(ThumbnailSize.width, ThumbnailSize.height),
        max(ThumbnailSize.width, ThumbnailSize.height)
    )

    def __init__(self) -> None:
        self.thumbnailSizeNeeded = QSize(ThumbnailSize.width, ThumbnailSize.height)
        self.thumbnail_cache = ThumbnailCacheSql(create_table_if_not_exists=False)
        self.fdo_cache_large = FdoCacheLarge()
        self.fdo_cache_normal = FdoCacheNormal()

        super().__init__('Thumbnail Extractor')

    def rotate_thumb(self, thumbnail: QImage, orientation: str) -> QImage:
        """
        If required return a rotated copy the thumbnail
        :param thumbnail: thumbnail to rotate
        :param orientation: EXIF orientation tag
        :return: possibly rotated thumbnail
        """

        if orientation == self.rotate_90:
            thumbnail = thumbnail.transformed(QTransform().rotate(90))
        elif orientation == self.rotate_270:
            thumbnail = thumbnail.transformed(QTransform().rotate(270))
        elif orientation == self.rotate_180:
            thumbnail = thumbnail.transformed(QTransform().rotate(180))
        return thumbnail

    def image_large_enough(self, size: QSize) -> bool:
        """Check if image is equal or bigger than thumbnail size."""

        return (
            size.width() >= self.thumbnailSizeNeeded.width() or
            size.height() >= self.thumbnailSizeNeeded.height()
        )

    def _extract_256_thumb(self, rpd_file: RPDFile,
                          processing: Set[ExtractionProcessing],
                          orientation: Optional[str]) -> PhotoDetails:

        thumbnail = None
        data = rpd_file.metadata.get_preview_256()
        if isinstance(data, bytes):
            thumbnail = QImage.fromData(data)
            if thumbnail.isNull():
                thumbnail = None
            else:
                if thumbnail.width() > 160 or thumbnail.height() > 120:
                    processing.add(ExtractionProcessing.resize)

        return PhotoDetails(thumbnail, orientation)

    def _extract_metadata(self, rpd_file: RPDFile,
                          processing: Set[ExtractionProcessing]) -> PhotoDetails:

        thumbnail = orientation = None
        try:
            orientation = rpd_file.metadata.orientation()
        except Exception:
            pass

        rpd_file.mdatatime = rpd_file.metadata.timestamp(missing=0.0)

        # Not all files have an exif preview, but some do
        # (typically CR2, ARW, PEF, RW2).
        # If they exist, they are (almost!) always 160x120

        # TODO how about thumbnail_cache_status?
        if self.write_fdo_thumbnail and rpd_file.fdo_thumbnail_256 is None:
            photo_details = self._extract_256_thumb(
                rpd_file=rpd_file, processing=processing, orientation=orientation
            )
            if photo_details.thumbnail is not None:
                return photo_details
            # if no valid preview found, fall back to the code below and make do with the best
            # we can get

        preview = rpd_file.metadata.get_small_thumbnail_or_first_indexed_preview()
        if preview:
            thumbnail = QImage.fromData(preview)
            if thumbnail.isNull():
                thumbnail = None
            else:
                # logging.critical("%s, %sx%s", orientation, thumbnail.width(), thumbnail.height())
                if thumbnail.width() < thumbnail.height() and \
                        orientation in (self.rotate_270, self.rotate_90):
                    # The orientation has already been applied to the thumbnail
                    logging.debug("Already rotated: %s", rpd_file.get_current_full_file_name())
                    orientation = self.rotate_0

                if max(thumbnail.width(), thumbnail.height()) > 160:
                    logging.debug("Resizing: %s", rpd_file.get_current_full_file_name())
                    processing.add(ExtractionProcessing.resize)
                elif not rpd_file.is_jpeg():
                    processing.add(ExtractionProcessing.strip_bars_photo)

        return PhotoDetails(thumbnail, orientation)

    def get_disk_photo_thumb(self, rpd_file: Photo,
                             full_file_name: str,
                             processing: Set[ExtractionProcessing],
                             force_exiftool: bool) -> PhotoDetails:
        """
        Get the photo's thumbnail from a file that is on disk.

        Sets rpd_file's mdatatime.

        :param rpd_file: file details
        :param full_file_name: full name of the file from which to get the metadata
        :param processing: processing extraction tasks to complete,
        :param force_exiftool: whether to force the use of ExifTool to load the metadata
        :return: thumbnail and its orientation
        """

        orientation = None
        thumbnail = None
        photo_details = PhotoDetails(thumbnail, orientation)
        if rpd_file.load_metadata(full_file_name=full_file_name, et_process=self.exiftool_process,
                                  force_exiftool=force_exiftool):

            photo_details = self._extract_metadata(rpd_file, processing)
            thumbnail = photo_details.thumbnail

        if thumbnail is not None:
            return photo_details
        elif rpd_file.is_raw() and have_functioning_rawkit:
            try:
                with rawkit.raw.Raw(filename=full_file_name) as raw:
                    raw.options.white_balance = rawkit.options.WhiteBalance(camera=True, auto=False)
                    if rpd_file.cache_full_file_name and not rpd_file.download_full_file_name:
                        temp_file = '{}.tiff'.format(os.path.splitext(full_file_name)[0])
                        cache_dir = os.path.dirname(rpd_file.cache_full_file_name)
                        if os.path.isdir(cache_dir):
                            temp_file = os.path.join(cache_dir, temp_file)
                            temp_dir = None
                        else:
                            temp_dir = tempfile.mkdtemp(prefix="rpd-tmp-")
                            temp_file = os.path.join(temp_dir, temp_file)
                    else:
                        temp_dir = tempfile.mkdtemp(prefix="rpd-tmp-")
                        name = os.path.basename(full_file_name)
                        temp_file = '{}.tiff'.format(os.path.splitext(name)[0])
                        temp_file = os.path.join(temp_dir, temp_file)
                    try:
                        logging.debug("Saving temporary rawkit render to %s", temp_file)
                        raw.save(filename=temp_file)
                    except Exception:
                        logging.exception(
                            "Rendering %s failed. Exception:", rpd_file.full_file_name
                        )
                    else:
                        thumbnail = QImage(temp_file)
                        os.remove(temp_file)
                        if thumbnail.isNull():
                            logging.debug("Qt failed to load rendered %s", rpd_file.full_file_name)
                            thumbnail = None
                        else:
                            logging.debug("Rendered %s using libraw", rpd_file.full_file_name)
                            processing.add(ExtractionProcessing.resize)

                            # libraw already correctly oriented the thumbnail
                            processing.remove(ExtractionProcessing.orient)
                            orientation = '1'
                if temp_dir:
                    os.rmdir(temp_dir)
            except ImportError as e:
                logging.warning(
                    'Cannot use rawkit to render thumbnail for %s', rpd_file.full_file_name
                )
            except Exception as e:
                logging.exception(
                    "Rendering thumbnail for %s not supported. Exception:", rpd_file.full_file_name
                )

        if thumbnail is None and rpd_file.is_loadable():
            thumbnail = QImage(full_file_name)
            processing.add(ExtractionProcessing.resize)
            if not rpd_file.from_camera:
                processing.remove(ExtractionProcessing.orient)
            if thumbnail.isNull():
                thumbnail = None
                logging.warning(
                    "Unable to create a thumbnail out of the file: {}".format(full_file_name)
                )

        return PhotoDetails(thumbnail, orientation)

    def get_from_buffer(self, rpd_file: Photo,
                        raw_bytes: bytearray,
                        processing: Set[ExtractionProcessing]) -> PhotoDetails:
        if not rpd_file.load_metadata(raw_bytes=raw_bytes, et_process=self.exiftool_process):
            # logging.warning("Extractor failed to load metadata from extract of %s", rpd_file.name)
            return PhotoDetails(None, None)
        else:
            return self._extract_metadata(rpd_file, processing)

    def get_photo_orientation(self, rpd_file: Photo,
                              force_exiftool: bool,
                              full_file_name: Optional[str]=None,
                              raw_bytes: Optional[bytearray]=None) -> Optional[str]:

        if rpd_file.metadata is None:
            self.load_photo_metadata(
                rpd_file=rpd_file, full_file_name=full_file_name, raw_bytes=raw_bytes,
                force_exiftool=force_exiftool
            )

        if rpd_file.metadata is not None:
            try:
                return rpd_file.metadata.orientation()
            except Exception:
                pass
        return None

    def assign_mdatatime(self, rpd_file: Union[Photo, Video],
                         force_exiftool: bool,
                         full_file_name: Optional[str]=None,
                         raw_bytes: Optional[bytearray]=None) -> None:
        """
        Load the file's metadata and assign the metadata time to the rpd file
        """

        if rpd_file.file_type == FileType.photo:
            self.assign_photo_mdatatime(
                rpd_file=rpd_file, full_file_name=full_file_name, raw_bytes=raw_bytes,
                force_exiftool=force_exiftool
            )
        else:
            self.assign_video_mdatatime(rpd_file=rpd_file, full_file_name=full_file_name)

    def assign_photo_mdatatime(self, rpd_file: Photo, force_exiftool: bool,
                               full_file_name: Optional[str]=None,
                               raw_bytes: Optional[bytearray]=None) -> None:
        """
        Load the photo's metadata and assign the metadata time to the rpd file
        """

        self.load_photo_metadata(
            rpd_file=rpd_file, full_file_name=full_file_name, raw_bytes=raw_bytes,
            force_exiftool=force_exiftool
        )
        if rpd_file.metadata is not None and rpd_file.date_time() is None:
            rpd_file.mdatatime = 0.0

    def load_photo_metadata(self, rpd_file: Photo, force_exiftool: bool,
                        full_file_name: Optional[str]=None,
                        raw_bytes: Optional[bytearray]=None) -> None:
        """
        Load the photo's metadata into the rpd file
        """

        if raw_bytes is not None:
            if rpd_file.is_jpeg_type():
                rpd_file.load_metadata(app1_segment=raw_bytes, et_process=self.exiftool_process)
            else:
                rpd_file.load_metadata(raw_bytes=raw_bytes, et_process=self.exiftool_process)
        else:
            rpd_file.load_metadata(
                full_file_name=full_file_name, et_process=self.exiftool_process,
                force_exiftool=force_exiftool
            )

    def assign_video_mdatatime(self, rpd_file: Video, full_file_name: str) -> None:
        """
        Load the video's metadata and assign the metadata time to the rpd file
        """

        if rpd_file.metadata is None:
            rpd_file.load_metadata(full_file_name=full_file_name, et_process=self.exiftool_process)
        if rpd_file.date_time() is None:
            rpd_file.mdatatime = 0.0

    def get_video_rotation(self, rpd_file: Video, full_file_name: str) -> Optional[str]:
        """
        Some videos have a rotation tag. If this video does, return it.
        """

        if rpd_file.metadata is None:
            rpd_file.load_metadata(full_file_name=full_file_name, et_process=self.exiftool_process)
        orientation = rpd_file.metadata.rotation(missing=None)
        if orientation == 180:
            return self.rotate_180
        elif orientation == 90:
            return self.rotate_90
        elif orientation == 270:
            return self.rotate_270
        return None

    def check_for_stop(self, directive: bytes, content: bytes):
        if directive == b'cmd':
            assert content == b'STOP'
            return True
        return False

    def extract_thumbnail(self, task: ExtractionTask,
                          rpd_file: Union[Photo, Video],
                          processing: Set[ExtractionProcessing],
                          data: ThumbnailExtractorArgument
                          ) -> Tuple[Optional[QImage], Optional[str]]:
        """
        Extract the thumbnail using one of a variety of methods,
        depending on the file

        :param task: extraction task to perform
        :param rpd_file: rpd_file to work on
        :param processing: processing tasks
        :param data: some other processing arguments passed to this process
        :return: thumbnail and its orientation, if found
        """

        orientation = None

        if task == ExtractionTask.load_from_exif:
            thumbnail_details = self.get_disk_photo_thumb(
                rpd_file, data.full_file_name_to_work_on, processing, data.force_exiftool
            )
            thumbnail = thumbnail_details.thumbnail
            if thumbnail is not None:
                orientation = thumbnail_details.orientation

        elif task in (ExtractionTask.load_file_directly,
                      ExtractionTask.load_file_and_exif_directly,
                      ExtractionTask.load_file_directly_metadata_from_secondary):
            thumbnail = QImage(data.full_file_name_to_work_on)

            if task == ExtractionTask.load_file_and_exif_directly:
                self.assign_photo_mdatatime(
                    rpd_file=rpd_file, full_file_name=data.full_file_name_to_work_on,
                    force_exiftool=data.force_exiftool
                )
            elif task == ExtractionTask.load_file_directly_metadata_from_secondary:
                self.assign_mdatatime(
                    rpd_file=rpd_file, full_file_name=data.secondary_full_file_name,
                    force_exiftool=data.force_exiftool
                )

            if ExtractionProcessing.orient in processing:
                orientation = self.get_photo_orientation(
                    rpd_file=rpd_file, full_file_name=data.full_file_name_to_work_on,
                    force_exiftool=data.force_exiftool
                )

        elif task in (ExtractionTask.load_from_bytes,
                      ExtractionTask.load_from_bytes_metadata_from_temp_extract):
            try:
                assert data.thumbnail_bytes is not None
            except AssertionError:
                logging.error(
                    "Thumbnail bytes not extracted for %s (value is None)",
                    rpd_file.get_current_full_file_name()
                )
            thumbnail = QImage.fromData(data.thumbnail_bytes)
            if thumbnail.width() > self.thumbnailSizeNeeded.width() or thumbnail.height()\
                    > self.thumbnailSizeNeeded.height():
                processing.add(ExtractionProcessing.resize)
                processing.remove(ExtractionProcessing.strip_bars_photo)
            if data.exif_buffer and ExtractionProcessing.orient in processing:
                orientation = self.get_photo_orientation(
                    rpd_file=rpd_file, raw_bytes=data.exif_buffer,
                    force_exiftool=data.force_exiftool
                )
            if task == ExtractionTask.load_from_bytes_metadata_from_temp_extract:
                self.assign_mdatatime(
                    rpd_file=rpd_file, full_file_name=data.secondary_full_file_name,
                    force_exiftool=data.force_exiftool
                )
                orientation = rpd_file.metadata.orientation()
                os.remove(data.secondary_full_file_name)
                rpd_file.temp_cache_full_file_chunk = ''

        elif task == ExtractionTask.load_from_exif_buffer:
            thumbnail_details = self.get_from_buffer(rpd_file, data.exif_buffer, processing)
            thumbnail = thumbnail_details.thumbnail
            if thumbnail is not None:
                orientation = thumbnail_details.orientation

        elif task in (ExtractionTask.load_heif_directly,
                      ExtractionTask.load_heif_and_exif_directly):
            assert have_heif_module
            thumbnail = load_heif(
                data.full_file_name_to_work_on, process_name=self.identity.decode()
            )

            if task == ExtractionTask.load_heif_and_exif_directly:
                self.assign_photo_mdatatime(
                    rpd_file=rpd_file, full_file_name=data.full_file_name_to_work_on,
                    force_exiftool=data.force_exiftool
                )
            if ExtractionProcessing.orient in processing:
                orientation = self.get_photo_orientation(
                    rpd_file=rpd_file, full_file_name=data.full_file_name_to_work_on,
                    force_exiftool=data.force_exiftool
                )

        else:
            assert task in (
                ExtractionTask.extract_from_file, ExtractionTask.extract_from_file_and_load_metadata
            )
            if rpd_file.file_type == FileType.photo:
                self.assign_photo_mdatatime(
                    rpd_file=rpd_file, full_file_name=data.full_file_name_to_work_on,
                    force_exiftool=data.force_exiftool
                )
                thumbnail_bytes = rpd_file.metadata.get_small_thumbnail_or_first_indexed_preview()
                if thumbnail_bytes:
                    thumbnail = QImage.fromData(thumbnail_bytes)
                    orientation = rpd_file.metadata.orientation()
            else:
                assert rpd_file.file_type == FileType.video

                if ExtractionTask.extract_from_file_and_load_metadata:
                    self.assign_video_mdatatime(
                        rpd_file=rpd_file, full_file_name=data.full_file_name_to_work_on
                    )
                if not have_gst:
                    thumbnail = None
                else:
                    png = get_video_frame(data.full_file_name_to_work_on, 1.0)
                    if not png:
                        thumbnail = None
                        logging.warning(
                            "Could not extract video thumbnail from %s",
                            data.rpd_file.get_display_full_name()
                        )
                    else:
                        thumbnail = QImage.fromData(png)
                        if thumbnail.isNull():
                            thumbnail = None
                        else:
                            processing.add(ExtractionProcessing.add_film_strip)
                            orientation = self.get_video_rotation(
                                rpd_file, data.full_file_name_to_work_on
                            )
                            if orientation is not None:
                                processing.add(ExtractionProcessing.orient)
                            processing.add(ExtractionProcessing.resize)

        return thumbnail, orientation

    def process_files(self):
        """
        Loop continuously processing photo and video thumbnails
        """

        logging.debug("{} worker started".format(self.requester.identity.decode()))

        while True:
            directive, content = self.requester.recv_multipart()
            if self.check_for_stop(directive, content):
                break

            data = pickle.loads(content)  # type: ThumbnailExtractorArgument

            thumbnail_256 = png_data = None
            task = data.task
            processing = data.processing
            rpd_file = data.rpd_file

            logging.debug(
                "Working on task %s for %s", task.name, rpd_file.download_name or rpd_file.name
            )

            self.write_fdo_thumbnail = data.write_fdo_thumbnail

            try:
                if rpd_file.fdo_thumbnail_256 is not None and data.write_fdo_thumbnail:
                    if rpd_file.thumbnail_status != ThumbnailCacheStatus.fdo_256_ready:
                        logging.error(
                            "Unexpected thumbnail cache status for %s: %s",
                            rpd_file.full_file_name, rpd_file.thumbnail_status.name
                        )
                    thumbnail = thumbnail_256 = QImage.fromData(rpd_file.fdo_thumbnail_256)
                    orientation_unknown = False
                else:
                    thumbnail, orientation = self.extract_thumbnail(
                        task, rpd_file, processing, data
                    )
                    if data.file_to_work_on_is_temporary:
                        os.remove(data.full_file_name_to_work_on)
                        rpd_file.temp_cache_full_file_chunk = ''

                    if thumbnail is not None:
                        if ExtractionProcessing.strip_bars_photo in processing:
                            thumbnail = crop_160x120_thumbnail(thumbnail)
                        elif ExtractionProcessing.strip_bars_video in processing:
                            thumbnail = crop_160x120_thumbnail(thumbnail, 15)
                        if ExtractionProcessing.resize in processing:
                            # Resize the thumbnail before rotating
                            if ((orientation == '1' or orientation is None) and
                                    thumbnail.height() > thumbnail.width()):

                                # Special case: pictures from some cellphones have already
                                # been rotated
                                thumbnail = thumbnail.scaled(
                                    self.maxStandardSize,
                                    Qt.KeepAspectRatio,
                                    Qt.SmoothTransformation
                                )
                            else:
                                if rpd_file.should_write_fdo() and \
                                        image_large_enough_fdo(thumbnail.size()) \
                                        and max(thumbnail.height(), thumbnail.width()) > 256:
                                    thumbnail_256 = thumbnail.scaled(
                                        QSize(256, 256),
                                        Qt.KeepAspectRatio,
                                        Qt.SmoothTransformation
                                    )
                                    thumbnail = thumbnail_256
                                if data.send_thumb_to_main:
                                    # thumbnail = self.rotate_thumb(thumbnail, orientation)
                                    # orientation = None
                                    thumbnail = thumbnail.scaled(
                                        self.thumbnailSizeNeeded,
                                        Qt.KeepAspectRatio,
                                        Qt.SmoothTransformation
                                    )
                                else:
                                    thumbnail = None

                            if not thumbnail is None and thumbnail.isNull():
                                thumbnail = None

                    if orientation is not None:
                        if thumbnail is not None:
                            thumbnail = self.rotate_thumb(thumbnail, orientation)
                        if thumbnail_256 is not None:
                            thumbnail_256 = self.rotate_thumb(thumbnail_256, orientation)

                    if ExtractionProcessing.add_film_strip in processing:
                        if thumbnail is not None:
                            thumbnail = add_filmstrip(thumbnail)
                        if thumbnail_256 is not None:
                            thumbnail = add_filmstrip(thumbnail_256)

                    if thumbnail is not None:
                        buffer = qimage_to_png_buffer(thumbnail)
                        png_data = buffer.data()

                    orientation_unknown = (
                        ExtractionProcessing.orient in processing and orientation is None
                    )

                    if data.send_thumb_to_main and data.use_thumbnail_cache and \
                            rpd_file.thumbnail_cache_status == ThumbnailCacheDiskStatus.not_found:
                        self.thumbnail_cache.save_thumbnail(
                            full_file_name=rpd_file.full_file_name,
                            size=rpd_file.size,
                            mtime=rpd_file.modification_time,
                            mdatatime=rpd_file.mdatatime,
                            generation_failed=thumbnail is None,
                            orientation_unknown=orientation_unknown,
                            thumbnail=thumbnail,
                            camera_model=rpd_file.camera_model
                        )

                if (thumbnail is not None or thumbnail_256 is not None) and \
                        rpd_file.should_write_fdo():
                    if self.write_fdo_thumbnail:
                        # The modification time of the file may have changed when the file was saved
                        # Ideally it shouldn't, but it does sometimes, e.g. on NTFS!
                        # So need to get the modification time from the saved file.
                        mtime = os.path.getmtime(rpd_file.download_full_file_name)

                        if thumbnail_256 is not None:
                            rpd_file.fdo_thumbnail_256_name = self.fdo_cache_large.save_thumbnail(
                                full_file_name=rpd_file.download_full_file_name,
                                size=rpd_file.size,
                                modification_time=mtime,
                                generation_failed=False,
                                thumbnail=thumbnail_256,
                                free_desktop_org=False
                            )
                            thumbnail_128 = thumbnail_256.scaled(
                                    QSize(128, 128),
                                    Qt.KeepAspectRatio,
                                    Qt.SmoothTransformation
                            )
                        else:
                            thumbnail_128 = thumbnail.scaled(
                                QSize(128, 128),
                                Qt.KeepAspectRatio,
                                Qt.SmoothTransformation
                            )
                        rpd_file.fdo_thumbnail_128_name = self.fdo_cache_normal.save_thumbnail(
                            full_file_name=rpd_file.download_full_file_name,
                            size=rpd_file.size,
                            modification_time=mtime,
                            generation_failed=False,
                            thumbnail=thumbnail_128,
                            free_desktop_org=False
                        )
                    elif thumbnail_256 is not None and rpd_file.fdo_thumbnail_256 is None:
                        rpd_file.fdo_thumbnail_256 = qimage_to_png_buffer(thumbnail).data()

                if thumbnail is not None:
                    if orientation_unknown:
                        rpd_file.thumbnail_status = ThumbnailCacheStatus.orientation_unknown
                    elif rpd_file.fdo_thumbnail_256 is not None:
                        rpd_file.thumbnail_status = ThumbnailCacheStatus.fdo_256_ready
                    else:
                        rpd_file.thumbnail_status = ThumbnailCacheStatus.ready

            except SystemExit as e:
                self.exiftool_process.terminate()
                sys.exit(e)
            except:
                logging.error("Exception working on file %s", rpd_file.full_file_name)
                logging.error("Task: %s", task)
                logging.error("Processing tasks: %s", processing)
                logging.exception("Traceback:")

            # Purge metadata, as it cannot be pickled
            if not data.send_thumb_to_main:
                png_data = None
            rpd_file.metadata = None
            self.sender.send_multipart(
                [
                    b'0', b'data',
                    pickle.dumps(
                        GenerateThumbnailsResults(rpd_file=rpd_file, thumbnail_bytes=png_data),
                        pickle.HIGHEST_PROTOCOL
                    )
                ]
            )
            self.requester.send_multipart([b'', b'', b'OK'])

    def do_work(self):
        if False:
            # exiv2 pumps out a LOT to stderr - use cautiously!
            context = show_errors()
            self.error_stream = sys.stderr
        else:
            # Redirect stderr, hiding error output from exiv2
            context = stdchannel_redirected(sys.stderr, os.devnull)
            self.error_stream = sys.stdout
        with context:
            # In some situations, using a context manager for exiftool can
            # result in exiftool processes not being terminated. So let's
            # handle starting and terminating it manually.
            self.exiftool_process = exiftool.ExifTool()
            self.exiftool_process.start()
            self.process_files()
            self.exit()

    def cleanup_pre_stop(self) -> None:
        logging.debug(
            "Terminating thumbnail extractor ExifTool process for %s", self.identity.decode()
        )
        self.exiftool_process.terminate()