Существуют десятки утилит для очистки метаданных (от ExifTool до встроенных средств ОС), но у всех них есть общий минус — они требуют ручного действия.
В результате родился MetaPure — фоновый демон для Windows, который перехватывает файлы в буфере обмена в момент копирования и бесшовно выжигает из них метаданные, прежде чем вы нажмете Ctrl+V.
Windows хранит скопированные файлы, почему парсить XML напрямую иногда лучше, чем использовать готовые библиотеки, и какие грабли поджидают при работе с win32clipboard.
Архитектура: все случается до Ctrl+V
Логика работы максимально проста для пользователя, но чуть сложнее под капотом:
Пользователь копирует файл (или группу файлов).
Фоновый поток (
ClipboardMonitor) опрашивает буфер обмена с интервалом в 500 мс.Если обнаружен формат
CF_HDROP(список файлов), запускается валидация пути.Переданный путь отправляется в
MetadataScrubber.Файл на диске перезаписывается "чистой" версией.
Да, утилита перезаписывает оригинальный файл по месту. Это осознанное архитектурное решение: если мы попробуем создать копию, нам придется подменять данные прямо в буфере обмена, что в Windows API быстро превратится в ад с блокировками памяти.
Под капотом буфера обмена: охота на CF_HDROP
Работа с буфером обмена в Python обычно сводится к библиотеке pyperclip, но она умеет работать только с текстом. Чтобы получить список скопированных файлов, придется спуститься на уровень Win32 API через pywin32.
Файлы в буфере лежат не в виде списка строк, а в специфическом формате CF_HDROP (Drop File List).
И здесь начались первые грабли. Если посмотреть в логи разработки, можно увидеть классическую боль:
src.clipboard_monitor - ERROR - Ошибка при получении файлов из буфера обмена: module 'win32clipboard' has no attribute 'DragQueryFile' ... src.clipboard_monitor - ERROR - Ошибка при получении файлов из буфера обмена: function 'DragQueryFileW' not found
Дело в том, что в старых версиях pywin32 метод GetClipboardData(win32clipboard.CF_HDROP) возвращал сырой указатель на структуру DROPFILES, и для получения имен файлов нужно было вручную вызывать функцию DragQueryFileW из shell32.dll через ctypes. В новых версиях обертка умеет возвращать сразу список строк, но при сборке через PyInstaller или при несовпадении версий DLL это поведение ломается.
В финальной версии кода я использую встроенный парсинг обертки, но валидация полученных данных стала критически важной:
if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_HDROP): file_list = win32clipboard.GetClipboardData(win32clipboard.CF_HDROP)
Защита от symlink-атак
Поскольку скрипт работает в фоне и имеет доступ к записи файлов, нужно было исключить ситуации, когда кто-то скопирует ярлык, указывающий на C:\Windows\System32, или symlink.
В ClipboardMonitor реализован жесткий validate_path:
Проверка, что путь абсолютный.
Сравнение
inodeоригинального пути иresolved_pathдля блокировки симлинков и жестких ссылок.Проверка, что файл не лежит внутри директории
WINDIR.Лимит на количество файлов за одно копирование (защита от перегрузки системы при копировании папки с тысячами фото).
В логах есть забавный момент: приложение пыталось обработать папку C:\Users\denis\Desktop\MetaPure как файл. Защита от срабатывания на директориях (resolved_path.is_file()) оказалась не лишней.
Хирургия по удалению метаданных
MetadataScrubber поддерживает изображения (JPEG, PNG, TIFF, BMP), видео (контейнеры MP4, MKV, MOV — хотя метаданные там часто хранятся в специфических атомах, требующих ffmpeg, пока поддержка базовая) и документы (PDF, DOCX).
Очистка делится на 4 категории: геолокация, устройство (камера/телефон), ПО (обработчики) и персональная информация.
Изображения: паттерн "Temp -> Move"
Удалять EXIF из картинок "на лету" опасно. Если прервать процесс записи, файл будет поврежден. Я использую атомарную замену через временный файл:
# Открываем, берем только "чистые" пиксели with Image.open(file_path) as img: data = list(img.getdata()) image_without_exif = Image.new(img.mode, img.size) image_without_exif.putdata(data) # Пишем во временный файл temp_path = file_path.with_suffix(file_path.suffix + '.temp') image_without_exif.save(temp_path, format=format_name) # Атомарная замена оригинала shutil.move(str(temp_path), str(file_path))
Это гарантирует, что если пользователь в момент очистки вставит файл куда-то, он получит либо оригинал, либо уже полностью пересохраненный файл, но никогда не "половинку".
Word (.docx): грязный хак с XML
Формат DOCX — это обычный ZIP-архив с XML-файлами внутри. Библиотека python-docx отлично умеет менять свойства документа (автора, дату изменения), но она абсолютно беспомощна, когда дело доходит до Track Changes (истории изменений) и скрытых комментариев.
Чтобы вычистить ревизии, пришлось лезть прямо в _blob внутренних частей документа (parts):
xml = part._blob.decode('utf-8') if 'w:revision' in xml or 'w:ins' in xml or 'w:del' in xml: xml = xml.replace('w:revision', 'w:revision_removed')\ .replace('w:ins ', 'w:p ')\ .replace('w:del ', 'w:p ') part._blob = xml.encode('utf-8')
Да, прямая работа с сырым XML через replace — это грязный хак, который может сломаться на специфической разметке сложных таблиц. Но альтернативы в экосистеме Python просто нет. Этот подход выжигает теги правок на корневом уровне, заставляя Word воспринимать текст как финальный.
PDF: пересборка
С PDF всё проще. Используется pypdf: мы читаем все страницы и собираем их в новый объект PdfWriter, не перенося метаданные словаря.
writer = PdfWriter() for page in reader.pages: writer.add_page(page) writer.add_metadata({}) # Пустой словарь
Грабли с системным треем и Windows API
Для фонового приложения критически важен минималистичный интерфейс. Я решил отказаться от всплывающих уведомлений (баллонов), так как при активном копировании файлов они только отвлекают.
Но в логах остался след борьбы с системным треем:
src.notification - ERROR - Ошибка при показе уведомления: (-2147467259, 'Shell_NotifyIcon', 'Неопознанная ошибка') src.notification - ERROR - Ошибка при уничтожении окна уведомлений: (1400, 'DestroyWindow', 'Недопустимый дескриптор окна.')
Windows Shell_NotifyIcon (NIM_ADD / NIM_DELETE) требует строгого соблюдения жизненного цикла оконных дескрипторов (HWND). Если поток, создавший иконку, завершается без корректного вызова DestroyWindow, или если попытаться обновить иконку после закрытия окна Tkinter, COM-объекты накрываются медным тазом. В итоге модуль уведомлений был полностью вырезан в угоду стабильности фонового процесса.
Итоги
MetaPure получился именно тем инструментом, которым хочется пользоваться самому — запустил и забыл. Он не требует подписки, не шлет данные на сторонние серверы (вся обработка идет локально через PIL и парсеры), и защищает от классических утечек данных при пересылке файлов.
Что можно улучшить в будущем:
Поддержка HEIC (фотографии с iPhone требуют специфического парсинга EXIF).
Глубокая очистка видео (вырезание метаданных из атомов
moov/udtaчерезmutagenили обертку надffmpeg).Адаптация под Linux (через прослушку D-Bus интерфейса буфера обмена) и macOS (через
NSPasteboard).
Исходный код проекта доступен на GitHub: https://github.com/slimeopus/MetaPure/releases
P.S. Приложение использует polling буфера обмена раз в полсекунды, что дает идеальный баланс между моментальностью реакции и нагрузкой на процессор (0% CPU в простое).


