Dati EXIF: analisi tecnica dello strumento per la loro rimozione | SolutionCAFE IT

Dati EXIF: analisi tecnica dello strumento per la loro rimozione

Dati EXIF: analisi tecnica dello strumento per la loro rimozione

1920 1280 Nicola Montemurro

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.

Nicola Montemurro

Nicola Montemurro un Consulente IT specializzato in sicurezza e resilienza infrastrutturale, Windows, Linux, VMWare, CCNA

Tutte le storie di:Nicola Montemurro

Nicola Montemurro

Nicola Montemurro un Consulente IT specializzato in sicurezza e resilienza infrastrutturale, Windows, Linux, VMWare, CCNA

Tutte le storie di:Nicola Montemurro

    Preferenze Privacy

    Quando visiti il nostro sito web, possono essere memorizzate alcune informazioni, di servizi specifici, tramite il tuo browser, di solito sotto forma di cookie. Qui puoi modificare le tue preferenze sulla privacy. Il blocco di alcuni cookie può influire sulla tua esperienza sul nostro sito Web e sui servizi che offriamo.

    Click to enable/disable Google Analytics tracking code.
    Click to enable/disable Google Fonts.
    Click to enable/disable Google Maps.
    Click to enable/disable video embeds.
    Il nostro sito web utilizza cookie, principalmente di terze parti. Personalizza le preferenze sulla privacy e/o acconsenti all'utilizzo dei cookie.