feat: maximize LDAP avatar quality (384px, progressive JPEG, 4:4:4)
- Увеличено разрешение: 120px -> 384px (+900% пикселей) - Повышено качество JPEG: 50-95 -> 75-98 - Добавлен progressive JPEG для лучшего сжатия - Включён subsampling 4:4:4 (без потери цветности) - Добавлена обработка EXIF ориентации - Добавлено сохранение пропорций (thumbnail вместо resize) - Обработка прозрачности PNG Улучшения в: - employees/ldap/utils/image_utils.py - employees/ldap/services/user_service.py - employees/ldap/sync_service.py Добавлены тестовые скрипты: - test_ldap_simple.py (быстрый тест) - test_avatar_simple.py (тест системного сжатия) - TEST_IMAGE_SCRIPTS.md (документация)
This commit is contained in:
parent
b4221386ff
commit
b5fbeca09f
84
backend/TEST_IMAGE_SCRIPTS.md
Normal file
84
backend/TEST_IMAGE_SCRIPTS.md
Normal file
@ -0,0 +1,84 @@
|
||||
# Тестовые скрипты для сжатия изображений
|
||||
|
||||
Скрипты для проверки функций сжатия аватаров с поддержкой EXIF-ориентации.
|
||||
|
||||
## Быстрые тесты (рекомендуется для Windows)
|
||||
|
||||
### 1. Тест LDAP-сжатия (384px, 100KB, HIGH QUALITY)
|
||||
```bash
|
||||
python test_ldap_simple.py path/to/photo.jpg
|
||||
```
|
||||
|
||||
### 2. Тест обычного сжатия аватаров (512px, 500KB)
|
||||
```bash
|
||||
python test_avatar_simple.py path/to/photo.jpg
|
||||
```
|
||||
|
||||
## Интерактивные тесты (с эмодзи, могут не работать в Windows cmd)
|
||||
|
||||
### 3. Полный тест LDAP
|
||||
```bash
|
||||
python test_ldap_image_compression.py
|
||||
```
|
||||
|
||||
### 4. Полный тест аватаров
|
||||
```bash
|
||||
python test_image_compression.py
|
||||
```
|
||||
|
||||
## Что проверяют скрипты
|
||||
|
||||
✅ **EXIF ориентация** - фото с телефонов не переворачиваются
|
||||
✅ **Сжатие размера** - процент уменьшения файла
|
||||
✅ **Разрешение** - размер в пикселях до и после
|
||||
✅ **Лимиты** - соответствие ограничениям (100KB для LDAP, 500KB для системы)
|
||||
|
||||
## Примеры вывода
|
||||
|
||||
### test_ldap_simple.py:
|
||||
```
|
||||
======================================================================
|
||||
LDAP Image Compression Test: IMG_1234.jpg
|
||||
======================================================================
|
||||
|
||||
BEFORE processing:
|
||||
Size: 2453.67 KB
|
||||
Resolution: 3024x4032
|
||||
Mode: RGB, Format: JPEG
|
||||
EXIF Orientation: Rotated 90 CW
|
||||
|
||||
Processing with normalize_avatar_to_jpeg(size_px=384, max_kb=100)...
|
||||
HIGH QUALITY: progressive JPEG, 4:4:4 subsampling, quality 75-98
|
||||
Saved to: IMG_1234_ldap.jpg
|
||||
|
||||
AFTER processing (LDAP format):
|
||||
Size: 68.45 KB
|
||||
Resolution: 288x384
|
||||
Mode: RGB, Format: JPEG
|
||||
EXIF Orientation: Normal
|
||||
|
||||
LDAP Compliance Check:
|
||||
[OK] File size: 68.45 KB (limit: 100 KB)
|
||||
|
||||
Statistics:
|
||||
Size reduction: 97.2%
|
||||
Before: 3024x4032 (2453.67 KB)
|
||||
After: 288x384 (68.45 KB)
|
||||
[OK] Orientation fixed automatically!
|
||||
Was: Rotated 90 CW
|
||||
Now: Normal (rotated in pixels)
|
||||
======================================================================
|
||||
```
|
||||
|
||||
## Требования
|
||||
|
||||
- Python 3.8+
|
||||
- Django (настройки проекта)
|
||||
- Pillow (PIL)
|
||||
|
||||
## Примечания
|
||||
|
||||
- Скрипты автоматически настраивают Django
|
||||
- Обработанные файлы сохраняются рядом с оригиналом
|
||||
- LDAP версия: суффикс `_ldap.jpg`
|
||||
- Обычная версия: суффикс `_compressed.jpg`
|
||||
@ -141,7 +141,10 @@ class UserService:
|
||||
if dto.group_cns:
|
||||
sync_user_groups_by_cns(conn, dn, set(dto.group_cns), do_write=True)
|
||||
if dto.avatar_bytes:
|
||||
avatar = normalize_avatar_to_jpeg(dto.avatar_bytes, max_kb=100)
|
||||
# Увеличен размер до 384px для максимального качества в LDAP
|
||||
avatar = normalize_avatar_to_jpeg(
|
||||
dto.avatar_bytes, size_px=384, max_kb=100
|
||||
)
|
||||
if avatar:
|
||||
modify_user_attrs(
|
||||
conn, dn, {"thumbnailPhoto": avatar}, do_write=True
|
||||
@ -636,7 +639,10 @@ class UserService:
|
||||
# 5) Аватар
|
||||
avatar_saved = False
|
||||
if avatar_bytes:
|
||||
avatar = normalize_avatar_to_jpeg(avatar_bytes, max_kb=100)
|
||||
# Увеличен размер до 384px для максимального качества в LDAP
|
||||
avatar = normalize_avatar_to_jpeg(
|
||||
avatar_bytes, size_px=384, max_kb=100
|
||||
)
|
||||
if avatar:
|
||||
modify_user_attrs(conn, dn, {"thumbnailPhoto": avatar}, do_write=True)
|
||||
avatar_saved = True # Флаг для сохранения в БД
|
||||
|
||||
@ -256,7 +256,10 @@ def export_users_update_profile(
|
||||
avatar_bytes = avatar_field.read()
|
||||
if avatar_bytes:
|
||||
try:
|
||||
avatar_bytes = normalize_avatar_to_jpeg(avatar_bytes)
|
||||
# Увеличен размер до 384px для максимального качества в LDAP
|
||||
avatar_bytes = normalize_avatar_to_jpeg(
|
||||
avatar_bytes, size_px=384, max_kb=100
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"[WARN] avatar: не удалось нормализовать для %s: %s",
|
||||
|
||||
@ -13,18 +13,26 @@ except ImportError:
|
||||
|
||||
|
||||
def normalize_avatar_to_jpeg(
|
||||
data: bytes, *, size_px: int = 120, max_kb: int = 100
|
||||
data: bytes, *, size_px: int = 384, max_kb: int = 100
|
||||
) -> bytes:
|
||||
"""Нормализует аватар в JPEG нужного размера и лимита.
|
||||
|
||||
ВАЖНО: Автоматически применяет EXIF-ориентацию перед обработкой.
|
||||
Это предотвращает "переворачивание" фотографий, сделанных на телефоны
|
||||
в портретной или перевёрнутой ориентации.
|
||||
|
||||
КАЧЕСТВО: Использует высокое разрешение (384px), progressive JPEG
|
||||
и 4:4:4 subsampling для максимального качества лиц в пределах 100KB.
|
||||
|
||||
Args:
|
||||
data (bytes): Входной образ (PNG/JPEG/...).
|
||||
size_px (int): Квадратный размер целевого изображения (по умолчанию 120).
|
||||
size_px (int): Максимальный размер изображения (по умолчанию 384).
|
||||
Пропорции сохраняются, итоговое изображение может быть меньше.
|
||||
max_kb (int): Максимальный размер файла в килобайтах
|
||||
(ограничение AD ~100 KB).
|
||||
|
||||
Returns:
|
||||
bytes: JPEG-данные, соответствующие ограничениям.
|
||||
bytes: JPEG-данные, соответствующие ограничениям с правильной ориентацией.
|
||||
|
||||
Raises:
|
||||
RuntimeError: Если Pillow недоступен или не удалось обработать
|
||||
@ -40,16 +48,62 @@ def normalize_avatar_to_jpeg(
|
||||
|
||||
try:
|
||||
img = Image.open(io.BytesIO(data))
|
||||
img = img.convert("RGB")
|
||||
img = img.resize((size_px, size_px))
|
||||
|
||||
# бинарный поиск по качеству для соблюдения max_kb
|
||||
lo, hi = 50, 95
|
||||
# ИСПРАВЛЕНИЕ: Применяем EXIF-ориентацию перед обработкой
|
||||
# Это предотвращает "поворот" фотографий с телефонов
|
||||
try:
|
||||
# Получаем EXIF данные
|
||||
exif = img.getexif()
|
||||
# Тег 274 = Orientation
|
||||
orientation = exif.get(0x0112) if exif else None
|
||||
|
||||
if orientation:
|
||||
# Применяем поворот согласно EXIF
|
||||
if orientation == 3:
|
||||
img = img.rotate(180, expand=True)
|
||||
elif orientation == 6:
|
||||
img = img.rotate(270, expand=True)
|
||||
elif orientation == 8:
|
||||
img = img.rotate(90, expand=True)
|
||||
except (AttributeError, KeyError, IndexError):
|
||||
# Если EXIF недоступен или повреждён - продолжаем без ротации
|
||||
pass
|
||||
|
||||
# Конвертируем в RGB (для JPEG)
|
||||
if img.mode in ("RGBA", "LA", "P"):
|
||||
# Создаем белый фон для прозрачных изображений
|
||||
background = Image.new("RGB", img.size, (255, 255, 255))
|
||||
if img.mode == "P":
|
||||
img = img.convert("RGBA")
|
||||
mask = img.split()[-1] if img.mode == "RGBA" else None
|
||||
background.paste(img, mask=mask)
|
||||
img = background
|
||||
elif img.mode != "RGB":
|
||||
img = img.convert("RGB")
|
||||
|
||||
# УЛУЧШЕНИЕ: Используем thumbnail для сохранения пропорций
|
||||
# вместо resize, который растягивает изображение
|
||||
img.thumbnail((size_px, size_px), Image.Resampling.LANCZOS)
|
||||
|
||||
# Бинарный поиск по качеству для соблюдения max_kb
|
||||
# КАЧЕСТВО: Повышен минимум до 75, используется progressive и 4:4:4
|
||||
lo, hi = 75, 98
|
||||
best = None
|
||||
while lo <= hi:
|
||||
mid = (lo + hi) // 2
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="JPEG", quality=mid, optimize=True)
|
||||
# МАКСИМАЛЬНОЕ КАЧЕСТВО:
|
||||
# - progressive: лучше сжатие, плавная загрузка
|
||||
# - subsampling=0: 4:4:4 (без потери цветности, лучше для лиц)
|
||||
# - optimize: оптимизация таблиц Хаффмана
|
||||
img.save(
|
||||
buf,
|
||||
format="JPEG",
|
||||
quality=mid,
|
||||
optimize=True,
|
||||
progressive=True,
|
||||
subsampling=0, # 4:4:4 - без потери цветности
|
||||
)
|
||||
kb = len(buf.getvalue()) // 1024
|
||||
if kb <= max_kb:
|
||||
best = buf.getvalue()
|
||||
@ -58,9 +112,16 @@ def normalize_avatar_to_jpeg(
|
||||
hi = mid - 1
|
||||
|
||||
if best is None:
|
||||
# минимально возможное
|
||||
# минимально возможное (с теми же настройками качества)
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="JPEG", quality=50, optimize=True)
|
||||
img.save(
|
||||
buf,
|
||||
format="JPEG",
|
||||
quality=75,
|
||||
optimize=True,
|
||||
progressive=True,
|
||||
subsampling=0,
|
||||
)
|
||||
best = buf.getvalue()
|
||||
|
||||
return best
|
||||
|
||||
168
backend/test_avatar_simple.py
Normal file
168
backend/test_avatar_simple.py
Normal file
@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Тестовый скрипт для проверки функции compress_avatar (система).
|
||||
Упрощённая версия без эмодзи для Windows консоли.
|
||||
|
||||
Использование:
|
||||
python test_avatar_simple.py <путь_к_файлу>
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Добавляем путь к модулям Django
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
# Настраиваем Django перед импортом
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
|
||||
import django
|
||||
django.setup()
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
print("ERROR: Pillow не установлен. Установите: pip install Pillow")
|
||||
sys.exit(1)
|
||||
|
||||
from common.image_utils import compress_avatar
|
||||
|
||||
|
||||
def get_image_info(image_path):
|
||||
"""Получить информацию об изображении."""
|
||||
try:
|
||||
img = Image.open(image_path)
|
||||
size = os.path.getsize(image_path)
|
||||
|
||||
# EXIF ориентация
|
||||
orientation_code = None
|
||||
orientation_name = "No EXIF"
|
||||
try:
|
||||
exif = img.getexif()
|
||||
if exif:
|
||||
orientation_code = exif.get(0x0112)
|
||||
orientation_names = {
|
||||
1: "Normal", 3: "Rotated 180",
|
||||
6: "Rotated 90 CW", 8: "Rotated 270 CW"
|
||||
}
|
||||
orientation_name = orientation_names.get(orientation_code, f"Code {orientation_code}")
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
'size': size,
|
||||
'size_kb': size / 1024,
|
||||
'dimensions': img.size,
|
||||
'mode': img.mode,
|
||||
'format': img.format,
|
||||
'orientation_code': orientation_code,
|
||||
'orientation_name': orientation_name
|
||||
}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
|
||||
def test_avatar_compression(input_path):
|
||||
"""Тестировать сжатие аватара."""
|
||||
print("\n" + "="*70)
|
||||
print(f"Avatar Compression Test: {os.path.basename(input_path)}")
|
||||
print("="*70)
|
||||
|
||||
# Информация ДО
|
||||
print("\nBEFORE processing:")
|
||||
info_before = get_image_info(input_path)
|
||||
if 'error' in info_before:
|
||||
print(f" ERROR: {info_before['error']}")
|
||||
return False
|
||||
|
||||
print(f" Size: {info_before['size_kb']:.2f} KB ({info_before['size']:,} bytes)")
|
||||
print(f" Resolution: {info_before['dimensions'][0]}x{info_before['dimensions'][1]}")
|
||||
print(f" Mode: {info_before['mode']}, Format: {info_before['format']}")
|
||||
print(f" EXIF Orientation: {info_before['orientation_name']}")
|
||||
|
||||
# Обработка
|
||||
try:
|
||||
with open(input_path, 'rb') as f:
|
||||
original_data = f.read()
|
||||
|
||||
print("\nProcessing with compress_avatar(max_width=512, max_kb=500)...")
|
||||
compressed_data = compress_avatar(original_data)
|
||||
|
||||
# Сохранение
|
||||
output_path = input_path.replace('.', '_compressed.')
|
||||
if not output_path.endswith('.jpg'):
|
||||
output_path = os.path.splitext(output_path)[0] + '.jpg'
|
||||
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(compressed_data)
|
||||
|
||||
print(f" Saved to: {output_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ERROR: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
# Информация ПОСЛЕ
|
||||
print("\nAFTER processing:")
|
||||
info_after = get_image_info(output_path)
|
||||
if 'error' in info_after:
|
||||
print(f" ERROR: {info_after['error']}")
|
||||
return False
|
||||
|
||||
print(f" Size: {info_after['size_kb']:.2f} KB ({info_after['size']:,} bytes)")
|
||||
print(f" Resolution: {info_after['dimensions'][0]}x{info_after['dimensions'][1]}")
|
||||
print(f" Mode: {info_after['mode']}, Format: {info_after['format']}")
|
||||
print(f" EXIF Orientation: {info_after['orientation_name']}")
|
||||
|
||||
# Статистика
|
||||
size_reduction = ((info_before['size'] - info_after['size']) / info_before['size']) * 100
|
||||
|
||||
print(f"\nStatistics:")
|
||||
print(f" Size reduction: {size_reduction:.1f}%")
|
||||
print(f" Before: {info_before['dimensions'][0]}x{info_before['dimensions'][1]} ({info_before['size_kb']:.2f} KB)")
|
||||
print(f" After: {info_after['dimensions'][0]}x{info_after['dimensions'][1]} ({info_after['size_kb']:.2f} KB)")
|
||||
|
||||
# Проверка ориентации
|
||||
if info_before['orientation_code'] and info_before['orientation_code'] != 1:
|
||||
if info_after['orientation_code'] is None or info_after['orientation_code'] == 1:
|
||||
print(f" [OK] Orientation fixed automatically!")
|
||||
print(f" Was: {info_before['orientation_name']}")
|
||||
print(f" Now: Normal (rotated in pixels)")
|
||||
else:
|
||||
print(f" [WARN] EXIF orientation preserved: {info_after['orientation_name']}")
|
||||
|
||||
print("="*70)
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python test_avatar_simple.py <image_file>")
|
||||
print("\nExample:")
|
||||
print(" python test_avatar_simple.py photo.jpg")
|
||||
sys.exit(1)
|
||||
|
||||
input_path = sys.argv[1]
|
||||
|
||||
if not os.path.exists(input_path):
|
||||
print(f"ERROR: File not found: {input_path}")
|
||||
sys.exit(1)
|
||||
|
||||
if not os.path.isfile(input_path):
|
||||
print(f"ERROR: Not a file: {input_path}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
success = test_avatar_compression(input_path)
|
||||
sys.exit(0 if success else 1)
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nInterrupted by user")
|
||||
sys.exit(130)
|
||||
except Exception as e:
|
||||
print(f"\nCRITICAL ERROR: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
176
backend/test_ldap_simple.py
Normal file
176
backend/test_ldap_simple.py
Normal file
@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Тестовый скрипт для проверки функции normalize_avatar_to_jpeg (LDAP).
|
||||
Упрощённая версия без эмодзи для Windows консоли.
|
||||
|
||||
Использование:
|
||||
python test_ldap_simple.py <путь_к_файлу>
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Добавляем путь к модулям Django
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
# Настраиваем Django перед импортом
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
|
||||
import django
|
||||
django.setup()
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
print("ERROR: Pillow не установлен. Установите: pip install Pillow")
|
||||
sys.exit(1)
|
||||
|
||||
from employees.ldap.utils.image_utils import normalize_avatar_to_jpeg
|
||||
|
||||
|
||||
def get_image_info(image_path):
|
||||
"""Получить информацию об изображении."""
|
||||
try:
|
||||
img = Image.open(image_path)
|
||||
size = os.path.getsize(image_path)
|
||||
|
||||
# EXIF ориентация
|
||||
orientation_code = None
|
||||
orientation_name = "No EXIF"
|
||||
try:
|
||||
exif = img.getexif()
|
||||
if exif:
|
||||
orientation_code = exif.get(0x0112)
|
||||
orientation_names = {
|
||||
1: "Normal", 3: "Rotated 180",
|
||||
6: "Rotated 90 CW", 8: "Rotated 270 CW"
|
||||
}
|
||||
orientation_name = orientation_names.get(orientation_code, f"Code {orientation_code}")
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
'size': size,
|
||||
'size_kb': size / 1024,
|
||||
'dimensions': img.size,
|
||||
'mode': img.mode,
|
||||
'format': img.format,
|
||||
'orientation_code': orientation_code,
|
||||
'orientation_name': orientation_name
|
||||
}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
|
||||
def test_ldap_compression(input_path):
|
||||
"""Тестировать LDAP-сжатие одного файла."""
|
||||
print("\n" + "="*70)
|
||||
print(f"LDAP Image Compression Test: {os.path.basename(input_path)}")
|
||||
print("="*70)
|
||||
|
||||
# Информация ДО
|
||||
print("\nBEFORE processing:")
|
||||
info_before = get_image_info(input_path)
|
||||
if 'error' in info_before:
|
||||
print(f" ERROR: {info_before['error']}")
|
||||
return False
|
||||
|
||||
print(f" Size: {info_before['size_kb']:.2f} KB")
|
||||
print(f" Resolution: {info_before['dimensions'][0]}x{info_before['dimensions'][1]}")
|
||||
print(f" Mode: {info_before['mode']}, Format: {info_before['format']}")
|
||||
print(f" EXIF Orientation: {info_before['orientation_name']}")
|
||||
|
||||
# Обработка
|
||||
try:
|
||||
with open(input_path, 'rb') as f:
|
||||
original_data = f.read()
|
||||
|
||||
print("\nProcessing with normalize_avatar_to_jpeg(size_px=384, max_kb=100)...")
|
||||
print(" HIGH QUALITY: progressive JPEG, 4:4:4 subsampling, quality 75-98")
|
||||
compressed_data = normalize_avatar_to_jpeg(
|
||||
original_data, size_px=384, max_kb=100
|
||||
)
|
||||
|
||||
# Сохранение
|
||||
output_path = input_path.replace('.', '_ldap.')
|
||||
if not output_path.endswith('.jpg'):
|
||||
output_path = os.path.splitext(output_path)[0] + '.jpg'
|
||||
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(compressed_data)
|
||||
|
||||
print(f" Saved to: {output_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ERROR: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
# Информация ПОСЛЕ
|
||||
print("\nAFTER processing (LDAP format):")
|
||||
info_after = get_image_info(output_path)
|
||||
if 'error' in info_after:
|
||||
print(f" ERROR: {info_after['error']}")
|
||||
return False
|
||||
|
||||
print(f" Size: {info_after['size_kb']:.2f} KB")
|
||||
print(f" Resolution: {info_after['dimensions'][0]}x{info_after['dimensions'][1]}")
|
||||
print(f" Mode: {info_after['mode']}, Format: {info_after['format']}")
|
||||
print(f" EXIF Orientation: {info_after['orientation_name']}")
|
||||
|
||||
# Проверка лимитов
|
||||
size_ok = info_after['size_kb'] <= 100
|
||||
print(f"\nLDAP Compliance Check:")
|
||||
print(f" [{'OK' if size_ok else 'FAIL'}] File size: {info_after['size_kb']:.2f} KB (limit: 100 KB)")
|
||||
|
||||
# Статистика
|
||||
size_reduction = ((info_before['size'] - info_after['size']) / info_before['size']) * 100
|
||||
|
||||
print(f"\nStatistics:")
|
||||
print(f" Size reduction: {size_reduction:.1f}%")
|
||||
print(f" Before: {info_before['dimensions'][0]}x{info_before['dimensions'][1]} ({info_before['size_kb']:.2f} KB)")
|
||||
print(f" After: {info_after['dimensions'][0]}x{info_after['dimensions'][1]} ({info_after['size_kb']:.2f} KB)")
|
||||
|
||||
# Проверка ориентации
|
||||
if info_before['orientation_code'] and info_before['orientation_code'] != 1:
|
||||
if info_after['orientation_code'] is None or info_after['orientation_code'] == 1:
|
||||
print(f" [OK] Orientation fixed automatically!")
|
||||
print(f" Was: {info_before['orientation_name']}")
|
||||
print(f" Now: Normal (rotated in pixels)")
|
||||
else:
|
||||
print(f" [WARN] EXIF orientation preserved: {info_after['orientation_name']}")
|
||||
|
||||
print("="*70)
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python test_ldap_simple.py <image_file>")
|
||||
print("\nExample:")
|
||||
print(" python test_ldap_simple.py photo.jpg")
|
||||
sys.exit(1)
|
||||
|
||||
input_path = sys.argv[1]
|
||||
|
||||
if not os.path.exists(input_path):
|
||||
print(f"ERROR: File not found: {input_path}")
|
||||
sys.exit(1)
|
||||
|
||||
if not os.path.isfile(input_path):
|
||||
print(f"ERROR: Not a file: {input_path}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
success = test_ldap_compression(input_path)
|
||||
sys.exit(0 if success else 1)
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nInterrupted by user")
|
||||
sys.exit(130)
|
||||
except Exception as e:
|
||||
print(f"\nCRITICAL ERROR: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
Loading…
x
Reference in New Issue
Block a user