def get_exif_extract_from_jpeg(self, folder: str, file_name: str) -> bytearray: """ Extract strictly the app1 (exif) section of a jpeg. Uses libgphoto2 to extract the exif header. Assumes jpeg on camera is straight from the camera, i.e. not modified by an exif altering program off the camera. :param folder: directory on the camera where the jpeg is stored :param file_name: name of the jpeg :return: first section of jpeg such that it can be read by exiv2 or similar """ camera_file = self._get_file(folder, file_name, None, gp.GP_FILE_TYPE_EXIF) try: exif_data = gp.check_result( gp.gp_file_get_data_and_size(camera_file)) except gp.GPhoto2Error as ex: logging.error( "Error getting exif info for %s from camera %s: %s", os.path.join(folder, file_name), self.display_name, gphoto2_named_error(ex.code), ) raise CameraProblemEx(code=CameraErrorCode.read, gp_exception=ex) return bytearray(exif_data)
def get_exif_extract(self, folder: str, file_name: str, size_in_bytes: int = 200) -> bytearray: """ " Attempt to read only the exif portion of the file. Assumes exif is located at the beginning of the file. Use the result like this: metadata = GExiv2.Metadata() metadata.open_buf(buf) :param folder: directory on the camera the file is stored :param file_name: the photo's file name :param size_in_bytes: how much of the photo to read, starting from the front of the file """ buffer = bytearray(size_in_bytes) try: self.camera.file_read(folder, file_name, gp.GP_FILE_TYPE_NORMAL, 0, buffer, self.context) except gp.GPhoto2Error as e: logging.error( "Unable to extract portion of file from camera %s: %s", self.display_name, gphoto2_named_error(e.code), ) raise CameraProblemEx(code=CameraErrorCode.read, gp_exception=e) else: return buffer
def _get_file( self, dir_name: str, file_name: str, dest_full_filename: Optional[str] = None, file_type: int = gp.GP_FILE_TYPE_NORMAL, ) -> gp.CameraFile: try: camera_file = gp.check_result( gp.gp_camera_file_get(self.camera, dir_name, file_name, file_type, self.context)) except gp.GPhoto2Error as ex: logging.error( "Error reading %s from camera %s: %s", os.path.join(dir_name, file_name), self.display_name, gphoto2_named_error(ex.code), ) raise CameraProblemEx(code=CameraErrorCode.read, gp_exception=ex) if dest_full_filename is not None: try: gp.check_result( gp.gp_file_save(camera_file, dest_full_filename)) except gp.GPhoto2Error as ex: logging.error( "Error saving %s from camera %s: %s", os.path.join(dir_name, file_name), self.display_name, gphoto2_named_error(ex.code), ) raise CameraProblemEx(code=CameraErrorCode.write, gp_exception=ex) return camera_file
def save_file_chunk( self, dir_name: str, file_name: str, chunk_size_in_bytes: int, dest_full_filename: str, mtime: int = None, ) -> None: """ Save the file from the camera to a local destination. :param dir_name: directory on the camera :param file_name: the photo or video :param chunk_size_in_bytes: how much of the file to read, starting from the front of the file :param dest_full_filename: full path including filename where the file will be saved. :param mtime: if specified, set the file modification time to this value """ # get_exif_extract() can raise CameraProblemEx(code=CameraErrorCode.read): buffer = self.get_exif_extract(dir_name, file_name, chunk_size_in_bytes) view = memoryview(buffer) dest_file = None try: dest_file = io.open(dest_full_filename, "wb") src_bytes = view.tobytes() dest_file.write(src_bytes) dest_file.close() if mtime is not None: os.utime(dest_full_filename, times=(mtime, mtime)) except (OSError, PermissionError) as ex: logging.error( "Error saving file %s from camera %s: %s", os.path.join(dir_name, file_name), self.display_name, gphoto2_named_error(ex.errno), ) if dest_file is not None: dest_file.close() raise CameraProblemEx(code=CameraErrorCode.write, py_exception=ex)
def get_thumbnail( self, dir_name: str, file_name: str, ignore_embedded_thumbnail=False, cache_full_filename: Optional[str] = None, ) -> Optional[bytes]: """ :param dir_name: directory on the camera :param file_name: the photo or video :param ignore_embedded_thumbnail: if True, do not retrieve the embedded thumbnail :param cache_full_filename: full path including filename where the thumbnail will be saved. If none, will not save it. :return: thumbnail in bytes format, which will be full resolution if the embedded thumbnail is not selected """ if self.can_fetch_thumbnails and not ignore_embedded_thumbnail: get_file_type = gp.GP_FILE_TYPE_PREVIEW else: get_file_type = gp.GP_FILE_TYPE_NORMAL camera_file = self._get_file(dir_name, file_name, cache_full_filename, get_file_type) try: thumbnail_data = gp.check_result( gp.gp_file_get_data_and_size(camera_file)) except gp.GPhoto2Error as ex: logging.error( "Error getting image %s from camera %s: %s", os.path.join(dir_name, file_name), self.display_name, gphoto2_named_error(ex.code), ) raise CameraProblemEx(code=CameraErrorCode.read, gp_exception=ex) if thumbnail_data: data = memoryview(thumbnail_data) return data.tobytes()
def get_THM_file(self, full_THM_name: str) -> Optional[bytes]: """ Get THM thumbnail from camera :param full_THM_name: path and file name of the THM file :return: THM in raw bytes """ dir_name, file_name = os.path.split(full_THM_name) camera_file = self._get_file(dir_name, file_name) try: thumbnail_data = gp.check_result( gp.gp_file_get_data_and_size(camera_file)) except gp.GPhoto2Error as ex: logging.error( "Error getting THM file %s from camera %s: %s", os.path.join(dir_name, file_name), self.display_name, gphoto2_named_error(ex.code), ) raise CameraProblemEx(code=CameraErrorCode.read, gp_exception=ex) if thumbnail_data: data = memoryview(thumbnail_data) return data.tobytes()
def save_file_by_chunks( self, dir_name: str, file_name: str, size: int, dest_full_filename: str, progress_callback, check_for_command, return_file_bytes=False, chunk_size=1048576, ) -> Optional[bytes]: """ :param dir_name: directory on the camera :param file_name: the photo or video :param size: the size of the file in bytes :param dest_full_filename: full path including filename where the file will be saved :param progress_callback: a function with which to update copy progress :param check_for_command: a function with which to check to see if the execution should pause, resume or stop :param return_file_bytes: if True, return a copy of the file's bytes, else make that part of the return value None :param chunk_size: the size of the chunks to copy. The default is 1MB. :return: True if the file was successfully saved, else False, and the bytes that were copied """ src_bytes = None view = memoryview(bytearray(size)) amount_downloaded = 0 for offset in range(0, size, chunk_size): check_for_command() stop = min(offset + chunk_size, size) try: bytes_read = gp.check_result( self.camera.file_read( dir_name, file_name, gp.GP_FILE_TYPE_NORMAL, offset, view[offset:stop], self.context, )) amount_downloaded += bytes_read if progress_callback is not None: progress_callback(amount_downloaded, size) except gp.GPhoto2Error as ex: logging.error( "Error copying file %s from camera %s: %s", os.path.join(dir_name, file_name), self.display_name, gphoto2_named_error(ex.code), ) if progress_callback is not None: progress_callback(size, size) raise CameraProblemEx(code=CameraErrorCode.read, gp_exception=ex) dest_file = None try: dest_file = io.open(dest_full_filename, "wb") src_bytes = view.tobytes() dest_file.write(src_bytes) dest_file.close() except (OSError, PermissionError) as ex: logging.error( "Error saving file %s from camera %s. Error %s: %s", os.path.join(dir_name, file_name), self.display_name, ex.errno, ex.strerror, ) if dest_file is not None: dest_file.close() raise CameraProblemEx(code=CameraErrorCode.write, py_exception=ex) if return_file_bytes: return src_bytes
def __init__( self, model: str, port: str, is_mtp_device: bool, get_folders: bool = True, raise_errors: bool = False, context: gp.Context = None, specific_folders: Optional[List[str]] = None, ) -> None: """ Initialize a camera via libgphoto2. :param model: camera model, as returned by camera_autodetect() or gp_camera_autodetect() :param port: camera port, as returned by camera_autodetect() :param get_folders: whether to detect the DCIM folders on the camera :param raise_errors: if True, if necessary free camera, and raise error that occurs during initialization :param specific_folders: folders such as DCIM, PRIVATE, and MP_ROOT that are searched for if get_folders is True. If None, the root level folders are returned -- one for each storage slot. """ self.model = model self.port = port self.is_mtp_device = is_mtp_device # class method _concise_model_name discusses why a display name is # needed self.display_name = model self.camera_config = None if context is None: self.context = gp.Context() else: self.context = context self._select_camera(model, port) self.specific_folders = None # type: Optional[List[str]] self.specific_folder_located = False self._dual_slots_active = False self.storage_info = [] self.camera_initialized = False try: self.camera.init(self.context) self.camera_initialized = True except gp.GPhoto2Error as e: if e.code == gp.GP_ERROR_IO_USB_CLAIM: error_code = CameraErrorCode.inaccessible logging.error("{} is already mounted".format(model)) elif e.code == gp.GP_ERROR: logging.error( "An error occurred initializing the camera using libgphoto2" ) error_code = CameraErrorCode.inaccessible else: logging.error("Unable to access camera: %s", gphoto2_named_error(e.code)) error_code = CameraErrorCode.locked if raise_errors: raise CameraProblemEx(error_code, gp_exception=e) return concise_model_name = self._concise_model_name() if concise_model_name: self.display_name = concise_model_name if get_folders: try: self.specific_folders = self._locate_specific_folders( path="/", specific_folders=specific_folders) self.specific_folder_located = len(self.specific_folders) > 0 logging.debug( "Folders located on %s: %s", self.display_name, ", ".join(", ".join(map(str, sl)) for sl in self.specific_folders), ) except gp.GPhoto2Error as e: logging.error( "Unable to access camera %s: %s. Is it locked?", self.display_name, gphoto2_named_error(e.code), ) if raise_errors: self.free_camera() raise CameraProblemEx(CameraErrorCode.locked, gp_exception=e) self.folders_and_files = [] self.audio_files = {} self.video_thumbnails = [] abilities = self.camera.get_abilities() self.can_fetch_thumbnails = ( abilities.file_operations & gp.GP_FILE_OPERATION_PREVIEW != 0)