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
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()