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 do_work(self): backup_arguments = pickle.loads(self.content) self.path = backup_arguments.path self.device_name = backup_arguments.device_name self.uri = get_uri(path=self.path) self.fdo_cache_normal = FdoCacheNormal() self.fdo_cache_large = FdoCacheLarge() while True: worker_id, directive, content = self.receiver.recv_multipart() self.device_id = int(worker_id) self.check_for_command(directive, content) data = pickle.loads(content) # type: BackupFileData if data.message == BackupStatus.backup_started: self.reset_problems() elif data.message == BackupStatus.backup_completed: self.send_problems() else: self.amount_downloaded = 0 self.init_copy_progress() self.do_backup(data=data)
def run(self): """ Set up process and then process thumbnail requests one by one """ # Always set use_thumbnail_cache to True, because this is a daemon # process that runs for the lifetime of the program. User can # change the program preferences. # Whether to actually use it will be determined at the time the # thumbnail is sought, using the user's preference at that moment. thumbnail_caches = GetThumbnailFromCache(use_thumbnail_cache=True) self.frontend = self.context.socket(zmq.PUSH) directive, content = self.receiver.recv_multipart() self.check_for_command(directive, content) data = pickle.loads(content) # type: ThumbnailDaemonData assert data.frontend_port is not None self.frontend.connect("tcp://localhost:{}".format(data.frontend_port)) # handle freedesktop.org cache files directly fdo_cache_large = FdoCacheLarge() fdo_cache_normal = FdoCacheNormal() while True: directive, content = self.receiver.recv_multipart() self.check_for_command(directive, content) data = pickle.loads(content) # type: ThumbnailDaemonData rpd_file = data.rpd_file if data.backup_full_file_names is not None: # File has been backed up, and an extractor has already generated a FDO # thumbnail for it. Copy and modify the existing FDO thumbnail # MD5 name of the existing FDO thumbnail md5_name = data.fdo_name assert md5_name for backup_full_file_name in data.backup_full_file_names: # Check to see if existing thumbnail in FDO cache can be # modified and renamed to reflect new URI try: mtime = os.path.getmtime(backup_full_file_name) except OSError: logging.debug("Backup file does not exist: %s", backup_full_file_name) else: logging.debug( "Copying and modifying existing FDO 128 thumbnail for %s", backup_full_file_name, ) fdo_cache_normal.modify_existing_thumbnail_and_save_copy( existing_cache_thumbnail=md5_name, full_file_name=backup_full_file_name, size=rpd_file.size, modification_time=mtime, error_on_missing_thumbnail=True, ) logging.debug( "Copying and modifying existing FDO 256 thumbnail for %s", backup_full_file_name, ) fdo_cache_large.modify_existing_thumbnail_and_save_copy( existing_cache_thumbnail=md5_name, full_file_name=backup_full_file_name, size=rpd_file.size, modification_time=mtime, error_on_missing_thumbnail=False, ) else: # file has just been downloaded and renamed rpd_file.modified_via_daemon_process = True try: # Check the download source to see if it's in the caches, not the # file we've just downloaded use_thumbnail_cache = data.use_thumbnail_cache and not ( data.write_fdo_thumbnail and rpd_file.should_write_fdo()) cache_search = thumbnail_caches.get_from_cache( rpd_file=rpd_file, use_thumbnail_cache=use_thumbnail_cache) ( task, thumbnail_bytes, full_file_name_to_work_on, origin, ) = cache_search processing = set() # type: Set[ExtractionProcessing] if task == ExtractionTask.undetermined: # Thumbnail was not found in any cache: extract it task = preprocess_thumbnail_from_disk( rpd_file=rpd_file, processing=processing) if task != ExtractionTask.bypass: if rpd_file.thm_full_name is not None: full_file_name_to_work_on = ( rpd_file.download_thm_full_name) else: full_file_name_to_work_on = ( rpd_file.download_full_file_name) if task == ExtractionTask.bypass: self.content = pickle.dumps( GenerateThumbnailsResults( rpd_file=rpd_file, thumbnail_bytes=thumbnail_bytes), pickle.HIGHEST_PROTOCOL, ) self.send_message_to_sink() elif task != ExtractionTask.undetermined: # Send data to load balancer, which will send to one of its # workers self.content = pickle.dumps( ThumbnailExtractorArgument( rpd_file=rpd_file, task=task, processing=processing, full_file_name_to_work_on= full_file_name_to_work_on, secondary_full_file_name="", exif_buffer=None, thumbnail_bytes=thumbnail_bytes, use_thumbnail_cache=data.use_thumbnail_cache, file_to_work_on_is_temporary=False, write_fdo_thumbnail=data.write_fdo_thumbnail, send_thumb_to_main=True, force_exiftool=data.force_exiftool, ), pickle.HIGHEST_PROTOCOL, ) self.frontend.send_multipart([b"data", self.content]) except SystemExit as e: sys.exit(e) except: logging.error("Exception working on file %s", rpd_file.full_file_name) logging.exception("Traceback:")
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()