Одного разу зіткнувся з магічним багом, причину виникнення якого, не вдавалося відшукати майже протягом місяця. Так що там причину, його навіть відтворити ніяк не вдавалося. Замовник постійно скаржився що зображення обрізаються неправильно, та при тому ще непередбачувано повертаються ...
Суть проекту полягала в тому, що користувачі викладають фотографії, супроводжуючи їх коротенькими історіями, далі модератори перевіряють контент на відповідність тематиці ресурсу, обрізають фотографії по необхідному співвідношенню сторін і публікують ці історії.
Буквально з перших же днів виходу проекту в продакшн, почали надходити скарги від модераторів. Скаржилися на те, що фотографії обрізаються не так як це очікується, іноді з'являються чорні смужки, іноді фотографії раптово повертаються, іноді взагалі не відбувається жодних змін після обрізки.
Для обрізки зображень використовувалася бібліотека django-image-cropping. яка дозволяє в адмінці джанго обрізати зображення за допомогою jQuery плагіна Jcrop.
Оскільки помилку ніяк не вдавалося відтворити, стали вважати, що причина ховається десь в клієнтської частини. За два з половиною тижні пошуку помилки, були перепробувані трохи менше ніж всі комбінації різних браузерів і операційних систем, включаючи застарілі версії браузерів. Але відтворити помилку все ніяк не вдавалося.
Тим не менш, старанне тестування обрізки зображення в різних браузерах виявило супутню помилку. Від браузера, до речі, незалежну. Продакшен сервер був оптимізований під велику кількість відвідувачів. Сам проект вдавав із себе регенеріруемий, статичний сайт, кеш nginx му; невелике api для додавання і пошуку історій; і адміністративний інтерфейс для премодерації історій і перегенерации статичного контенту.
Так вийшло, що nginx дуже старанно справлявся з кешуванням, і досить часто після обрізки зображення в адмінці показував старе (не обрізати зображення), після чого модератор знову і знову обрізав картинку, а в результаті вихідне зображення різалося по неіснуючим координатами. Відповідно на виході виходило ізкромсаное режим із чорними смужками і коли кеш оновлювався то це ставало помітно.
Внаслідок чого, проблема відображення старого зображення після обрізки зникла. В адмінці виводився наступний html код відображення зображення:
Помилка знайдена і ліквідована. Більше повторяться не повинна. Можна святкувати перемогу.
Буквально через тиждень, після сабмита змін в продакшн, від модераторів знову надійшла скарга на некоректну обрізати зображення. На щастя, на цей раз вони явно вказали на якому саме зображенні відтворюється некоректна обрізка і переслали дане зображення по електронній пошті.
Відкривши лист, я відкрив зображення в новій вкладці і зберіг її. Почав тестувати в vagrant е - помилка не чути, зображення обрізається як потрібно, нічого не повертається і не з'являються темні смуги. Перевірив цю ж картинку на продакшн сервері - не чути. Хитрість виявилася в тому, що потрібно було картинку саме скачати. а не зберегти з листа. Отримавши вихідне зображення, помилка стала регулярно відтворюється.
У графічних зображеннях додаткову метаінформацію можна зберігати в форматі EXIF. Де серед іншого може міститися інформація про орієнтацію зображення. Вивівши exif даного файлу, відразу стало зрозуміло чому помилка відтворюється саме на ньому.
Exif Orientation тег описує орієнтацію зображення при відображенні, використовуючи значення від 1 до 8:
Наочно на прикладі фотокамери, можуть виникати 4 варіанти тега орієнтації:
При збереженні зображення за допомогою PIL всі метадані exif стираються, тому збережена картинка без метаінформації вже не повертається при відображенні, а залишається така як є. Тобто з точки зору користувача - непередбачувано повертаються.
Оскільки PIL стирає метаінформацію при збереженні зображення, то будемо повертати зображення при наявності тега орієнтації самостійно.
Ця функція перевіряє наявність інформації про поворот зображення, і якщо вона присутня, повертає картинку на відповідний кут. Зберігаючи зображення метаінформація стирається, і тому, якщо навіть exif orientation тег не була фотоапаратним (3, 6, 8), а відбитий (2, 4, 5, 7). Те картинка буде такою як і є в дійсності, і в разі необхідності, модератори зможуть її повернути як їм потрібно.
Ось такий простий функцією зважилася ця підступна помилка.
Для більшої наочності, наведемо приклад неправильної обрізки зображення через кешування, а так же такої різниці у поведінці зображення з exif орієнтацією і без. В якості вихідного зображення будемо використовувати знамениту Олену. але для більш помітних змін, які не її канонічний варіант 512x512, а прямокутне зображення. Для роботи з exif інформацією скористаємося консольної утилітою exiftool.
Візьмемо портретно орієнтоване зображення, і спробуємо його двічі обрізати на одному і тому ж превью зображенні.
Модератор обрізає зображення, потім після оновлення сторінки повторно завантажується теж-саме превью. Модератор обрізає зображення вдруге, думаючи при цьому, що зміни по якійсь причині не збереглися.
Але, насправді, зображення в перший раз обрізати коректно.
Отримавши повторні координати для обрізки зображення, відбувається наступне.
І так далі, при кожній наступній обрізки вихідне зображення буде продовжувати шматувати. Тому випадковий GET параметр дозволяє боротися з кешуванням і завантажувати необхідне превью для правильної обробки зображення.
Створимо вихідне зображення, з ландшафтним співвідношення сторін, без заданого тега орієнтації.
Відобразимо метаінформацію в даному зображенні.
Скопіюємо зображення і встановимо тег орієнтації 6.
Щоб побачити поворот відкрийте зображення в новій вкладці.Тепер подивимося яка метаінформація міститься в цьому зображенні.
Як видно, в тег орієнтації встановлено значення Rotate 90 CW.
Завантаживши дані зображення на сервіс - можна побачити різну поведінку:
Зображення без тега орієнтації не повертається і відображається як є. з ландшафтним співвідношенням сторін.
Зображення з тегом орієнтації при відображенні повертається в портретне співвідношення сторін.
Друге зображення виглядає як потрібно і модератор без сумнівів обрізає зображення.
Обрізаючи зображення як показано на малюнку зверху, модератор очікує отримати наступний результат.
Однак, оскільки координати задані для портретної орієнтації, а саме зображення має розміри альбомної орієнтації, результат виходить несподіваним. Картинка повертається, і виникає чорна смуга знизу зображення.
Після використання попереднього повороту зображення, і видалення метаінформації, дане несподіване поведінку зникає, зображення відображається таким яке воно є насправді. Завантажуючи картинку з тегом орієнтації, при збереженні на сервері вона відразу зберігається повертається.
Тобто в разі, якщо модератор обрізає портретне зображення.
Він отримує, правильний портретний результат.