Obiettivi e Necessità
La gestione delle immagini digitali, per coloro che ne condividono di continuo sui social, siti web, blog, etc. richiede una considerazione attenta dei metadati, in particolare dei dati EXIF (Exchangeable Image File Format). Questi metadati, che possono includere informazioni sensibili come posizione GPS, nome dell’utente, data di scatto e dettagli della fotocamera, possono compromettere la privacy e la sicurezza.
La gestione delle immagini digitali, per coloro che ne condividono di continuo sui social, siti web, blog, etc. richiede una considerazione attenta dei metadati, in particolare dei dati EXIF (Exchangeable Image File Format). Questi metadati, che possono includere informazioni sensibili come posizione GPS, nome dell’utente, data di scatto e dettagli della fotocamera, possono compromettere la privacy e la sicurezza.
Questo articolo analizza un’applicazione Python progettata per rimuovere i dati EXIF in modo automatizzato e sistematico, evidenziando la struttura logica e le funzionalità principali del codice.
Struttura e Logica del Codice
Il codice analizzato è suddiviso in moduli e funzioni ben definiti, ciascuno con un compito specifico. Questa organizzazione ne migliora la leggibilità, consente la facile manutenzione ed eventuale estensione.
Funzione per la trasformazione del formato file size
La funzione parse_human_readable_size
consente di definire la dimensione dei file di log in un formato intelligibile dall’utente.
def parse_human_readable_size(size_str): size_pattern = re.compile(r'(\d+)([KMG]B?)', re.IGNORECASE) match = size_pattern.fullmatch(size_str.strip()) if match: size = int(match.group(1)) unit = match.group(2).upper() if unit in ['KB', 'K']: return size * 1024 elif unit in ['MB', 'M']: return size * 1024**2 elif unit in ['GB', 'G']: return size * 1024**3 raise ValueError(f"Invalid size format: {size_str}")
2. Configurazione del Logging
La funzione setup_logging
gestisce la configurazione del sistema di logging, consentendo di definire percorso e nome del file di log qualora nell’argomento sia specificato l’intero filepath, altrimenti, qualora nell’argomento sia esplicitata solo la directory, assumerà come nome file lo stesso basename dello script in esecuzione. inoltre al raggiungimento di una data dimensione, si occuperà di effettuare la rotazione del file.
def setup_logging(log_file_path, max_log_size): log_dir = log_file_path.parent log_dir.mkdir(parents=True, exist_ok=True) if log_file_path.is_dir(): log_file_path = log_dir / f"{Path(__file__).stem}.log" handler = RotatingFileHandler(log_file_path, maxBytes=max_log_size, backupCount=5) formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) logger = logging.getLogger() logger.setLevel(logging.DEBUG) logger.addHandler(handler)
3. Creazione del percorso di Backup delle immagini soggette a modifica
La funzione generate_unique_backup_file
assicura che ogni backup abbia un nome unico, evitando conflitti.; qualora vengano processati più file con lo stesso nome, per evitare di sovrascrivere il backup del file precedentemente processato, la funzione si occuperà di generare un nome file con suffisso esadecimale a tre cifre (da 000 a FFF) da assegnare al nuovo file;
def generate_unique_backup_file(original_file_path, backup_dir): base_name = Path(original_file_path).name backup_path = Path(backup_dir) / base_name index = 0 while backup_path.exists(): index += 1 suffix = f"{index:03x}" backup_path = Path(backup_dir) / f"{Path(base_name).stem}_{suffix}{Path(base_name).suffix}" return backup_path
4. Backup dei File Originali
La funzione backup_file
gestisce il backup delle immagini originali prima della rimozione dei dati EXIF.
def backup_file(original_file_path, backup_dir): backup_path = generate_unique_backup_file(original_file_path, backup_dir) backup_path.parent.mkdir(parents=True, exist_ok=True) try: shutil.copy2(original_file_path, backup_path) logging.info(f"Backup created for: {original_file_path} at {backup_path}") except PermissionError: logging.error(f"Permission denied when backing up {original_file_path}.") except Exception as e: logging.error(f"Error backing up {original_file_path}: {e}")
5. Rimozione dei Dati EXIF
La funzione remove_exif_data
rimuove i dati EXIF dalle immagini.
def remove_exif_data(image_path, backup_dir=None): try: with Image.open(image_path) as image: exif_data = getattr(image, 'getexif', lambda: None)() if exif_data: if backup_dir: backup_file(image_path, backup_dir) image.save(image_path, quality=100) logging.info(f"EXIF data removed from: {image_path}") return True else: logging.debug(f"No EXIF data found in: {image_path}") return False except FileNotFoundError: logging.error(f"File not found: {image_path}") return False except PermissionError: logging.error(f"Permission denied for file: {image_path}") return False except OSError as e: logging.error(f"Error processing {image_path}: {e}") return False
6. Ricerca e Rimozione Dati EXIF
La funzione search_and_remove_exif
esplora le directory e rimuove i dati EXIF, richiamando la funzione remove_exif_data
.
def search_and_remove_exif(root_dir, backup_dir=None, exclude_exts=None, exclude_dirs=None, verbose=False): total_files_processed = 0 total_files_modified = 0 for root, dirs, files in os.walk(root_dir): if exclude_dirs and any(exclude in root for exclude in exclude_dirs): logging.debug(f"Excluded directory: {root}") continue for file in files: file_ext = Path(file).suffix.lower() if exclude_exts and file_ext in exclude_exts: logging.debug(f"Excluded file: {file}") continue if file_ext in ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'): image_path = Path(root) / file total_files_processed += 1 if remove_exif_data(image_path, backup_dir): total_files_modified += 1 if verbose: print(f"Removed EXIF from: {image_path}") else: if verbose: print(f"No EXIF data in: {image_path}") logging.info(f"Total files processed: {total_files_processed}") logging.info(f"Total files modified: {total_files_modified}")
7. Funzione Main
La funzione main
gestisce l’interfaccia a riga di comando e coordina l’esecuzione.
def main(): import argparse parser = argparse.ArgumentParser(description="Remove EXIF data from media files.") parser.add_argument('path', type=str, help="Path to the directory to search") parser.add_argument('--log-path', type=str, help="Path to the log file or directory", default=None) parser.add_argument('--backup-path', type=str, help="Path to the backup directory", default=None) parser.add_argument('--exclude-exts', type=str, nargs='*', help="File extensions to exclude", default=None) parser.add_argument('--exclude-dirs', type=str, nargs='*', help="Directories to exclude", default=None) parser.add_argument('--max-log-size', type=str, help="Max log file size (e.g. 5MB, 1GB)", default="5MB") parser.add_argument('--verbose', action='store_true', help="Enable verbose output") args = parser.parse_args() max_log_size = parse_human_readable_size(args.max_log_size) if args.log_path: log_file_path = Path(args.log_path) if log_file_path.is_dir(): log_file_path = log_file_path / f"{Path(__file__).stem}.log" else: log_file_path = Path(__file__).parent / f"{Path(__file__).stem}.log" setup_logging(log_file_path, max_log_size) start_time = time.time() search_and_remove_exif(args.path, args.backup_path, args.exclude_exts, args.exclude_dirs, args.verbose) elapsed_time = time.time() - start_time logging.info(f"Process completed in {elapsed_time:.2f} seconds.")
Codice completo
# ---------------------------------------------------------------------------------- # - Script file name : cleanexif.py # - Author : NM # - DNS administrator : NM # - Create : 22.08.2024 # - Last Update : 22.08.2024 # - Description : # - Position : /usr/local/script # - note : NON modificare senza AUTORIZZAZIONE dell'AMMINISTRATORE # ----------------------------------------------------------------------------------- import os import logging import shutil from logging.handlers import RotatingFileHandler from PIL import Image from pathlib import Path import time import re def parse_human_readable_size(size_str): """Converte una dimensione in formato human-readable in byte.""" size_pattern = re.compile(r'(\d+)([KMG]B?)', re.IGNORECASE) match = size_pattern.fullmatch(size_str.strip()) if match: size = int(match.group(1)) unit = match.group(2).upper() if unit in ['KB', 'K']: return size * 1024 elif unit in ['MB', 'M']: return size * 1024**2 elif unit in ['GB', 'G']: return size * 1024**3 raise ValueError(f"Invalid size format: {size_str}") def setup_logging(log_file_path, max_log_size): """Configura il logging con rotazione basata sulla dimensione.""" # Crea la directory se non esiste log_dir = log_file_path.parent log_dir.mkdir(parents=True, exist_ok=True) # Assicurati che il percorso sia un file, non una directory if log_file_path.is_dir(): log_file_path = log_dir / f"{Path(__file__).stem}.log" # Configura il gestore di log handler = RotatingFileHandler(log_file_path, maxBytes=max_log_size, backupCount=5) formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) logger = logging.getLogger() logger.setLevel(logging.DEBUG) logger.addHandler(handler) def generate_unique_backup_file(original_file_path, backup_dir): """Genera un percorso di backup unico aggiungendo un suffisso esadecimale se necessario.""" base_name = Path(original_file_path).name backup_path = Path(backup_dir) / base_name index = 0 while backup_path.exists(): index += 1 suffix = f"{index:03x}" backup_path = Path(backup_dir) / f"{Path(base_name).stem}_{suffix}{Path(base_name).suffix}" return backup_path def backup_file(original_file_path, backup_dir): """Copia il file originale nel percorso di backup, mantenendo la struttura.""" backup_path = generate_unique_backup_file(original_file_path, backup_dir) backup_path.parent.mkdir(parents=True, exist_ok=True) try: shutil.copy2(original_file_path, backup_path) logging.info(f"Backup created for: {original_file_path} at {backup_path}") except PermissionError: logging.error(f"Permission denied when backing up {original_file_path}.") except Exception as e: logging.error(f"Error backing up {original_file_path}: {e}") def remove_exif_data(image_path, backup_dir=None): """Rimuove i dati EXIF da un'immagine, creando un backup se richiesto.""" try: with Image.open(image_path) as image: exif_data = getattr(image, 'getexif', lambda: None)() if exif_data: if backup_dir: backup_file(image_path, backup_dir) image.save(image_path, quality=100) logging.info(f"EXIF data removed from: {image_path}") return True else: logging.debug(f"No EXIF data found in: {image_path}") return False except FileNotFoundError: logging.error(f"File not found: {image_path}") return False except PermissionError: logging.error(f"Permission denied for file: {image_path}") return False except OSError as e: logging.error(f"Error processing {image_path}: {e}") return False def search_and_remove_exif(root_dir, backup_dir=None, exclude_exts=None, exclude_dirs=None, verbose=False): """Cerca e rimuove i dati EXIF da tutti i file di immagine in una directory.""" total_files_processed = 0 total_files_modified = 0 for root, dirs, files in os.walk(root_dir): if exclude_dirs and any(exclude in root for exclude in exclude_dirs): logging.debug(f"Excluded directory: {root}") continue for file in files: file_ext = Path(file).suffix.lower() if exclude_exts and file_ext in exclude_exts: logging.debug(f"Excluded file: {file}") continue if file_ext in ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'): image_path = Path(root) / file total_files_processed += 1 if remove_exif_data(image_path, backup_dir): total_files_modified += 1 if verbose: print(f"Removed EXIF from: {image_path}") else: if verbose: print(f"No EXIF data in: {image_path}") logging.info(f"Total files processed: {total_files_processed}") logging.info(f"Total files modified: {total_files_modified}") def main(): import argparse parser = argparse.ArgumentParser(description="Remove EXIF data from media files.") parser.add_argument('path', type=str, help="Path to the directory to search") parser.add_argument('--log-path', type=str, help="Path to the log file or directory", default=None) parser.add_argument('--backup-path', type=str, help="Path to the backup directory", default=None) parser.add_argument('--exclude-exts', type=str, nargs='*', help="File extensions to exclude", default=None) parser.add_argument('--exclude-dirs', type=str, nargs='*', help="Directories to exclude", default=None) parser.add_argument('--max-log-size', type=str, help="Max log file size (e.g. 5MB, 1GB)", default="5MB") parser.add_argument('--verbose', action='store_true', help="Enable verbose output") args = parser.parse_args() max_log_size = parse_human_readable_size(args.max_log_size) # Imposta il percorso del file di log if args.log_path: log_file_path = Path(args.log_path) if log_file_path.is_dir(): log_file_path = log_file_path / f"{Path(__file__).stem}.log" else: log_file_path = Path(__file__).parent / f"{Path(__file__).stem}.log" setup_logging(log_file_path, max_log_size) start_time = time.time() search_and_remove_exif(args.path, args.backup_path, args.exclude_exts, args.exclude_dirs, args.verbose) elapsed_time = time.time() - start_time logging.info(f"Process completed in {elapsed_time:.2f} seconds.") if __name__ == "__main__": main() logging.shutdown()
Considerazioni finali
L’analisi dettagliata delle funzioni del codice pone l’accento sulla volontà di ottenere la progettazione accurata e la logica ben definita sinergiche all’obiettivo di rimuovere i dati EXIF dalle immagini, garantendo una maggiore protezione dei dati e nel contempo una gestione semplice e organizzata dei file.