Проверяем обрезку изображений iOS: от выделения до границ пикселей
Вы помещаете изображение размером 4000×3000 пикселей в область обрезки 300×300 пунктов, выделяете центральную часть, но после экспорта человек в кадре оказывается смещён вверх. Не спешите менять обработку жестов: при вписывании с сохранением пропорций сверху и снизу остаются поля по 37.5 пункта. Если пересчитывать координаты относительно всей области просмотра, неверными будут и начало выделения, и его высота. Сначала проверьте координаты на облачном Mac с помощью чистого Swift-кода, а затем подключайте интерфейс — так ошибки вычислений можно отделить от ошибок отрисовки.
Фиксируем правила для трёх систем координат
Нужно чётко различать три объекта: прямоугольник исходного изображения в пикселях, фактический прямоугольник отображения изображения внутри представления и область, выделенную пользователем перетаскиванием. Последние два объекта должны находиться в одной системе координат. Нельзя задавать один относительно родительского представления, а другой — относительно контейнера изображения.
В статье рассматривается только вписывание с сохранением пропорций — aspect fit. Заполнение с обрезкой, поворот и перспективные преобразования не рассматриваются. Начало координат находится в левом верхнем углу, горизонтальная ось направлена вправо, вертикальная — вниз. Предполагается, что ориентация входного изображения уже нормализована для отображения, а ширина и высота заданы в реальных пикселях, а не в пунктах объекта изображения.
Масштаб экрана — не то же самое, что масштаб изображения. Умножение координат выделения на масштаб экрана даёт верный результат лишь при случайном совпадении размеров и не подходит в качестве универсальной формулы обрезки.
Если интерфейс использует вложенные представления, сначала переведите выделение в систему координат контейнера изображения через API преобразования координат, а уже затем вызывайте функцию ниже. Если поддерживается масштабирование жестами, предварительно нужно также отменить преобразования переноса и масштабирования. Подставлять преобразованный прямоугольник напрямую в статическую формулу нельзя.
Вычисляем прямоугольник отображения и переводим координаты в пиксели
Сохраните следующий код в файл CropCheck.swift. Если доступен полный набор инструментов Xcode, выполните в терминале xcrun swift CropCheck.swift. Код зависит только от системного Foundation, не обращается к сети и не требует симулятора.
import Foundation
func cropPixels(
source: CGSize,
viewport: CGSize,
selection: CGRect
) -> CGRect? {
let values = [
source.width, source.height,
viewport.width, viewport.height,
selection.origin.x, selection.origin.y,
selection.width, selection.height
]
guard values.allSatisfy({ $0.isFinite }),
source.width > 0, source.height > 0,
viewport.width > 0, viewport.height > 0,
source.width.rounded(.down) == source.width,
source.height.rounded(.down) == source.height,
selection.width > 0, selection.height > 0
else {
return nil
}
let scale = min(
viewport.width / source.width,
viewport.height / source.height
)
guard scale.isFinite, scale > 0 else {
return nil
}
let display = CGRect(
x: (viewport.width - source.width * scale) / 2,
y: (viewport.height - source.height * scale) / 2,
width: source.width * scale,
height: source.height * scale
)
let clipped = selection.intersection(display)
guard !clipped.isNull, !clipped.isEmpty else {
return nil
}
let left = max(
0, floor((clipped.minX - display.minX) / scale)
)
let top = max(
0, floor((clipped.minY - display.minY) / scale)
)
let right = min(
source.width,
ceil((clipped.maxX - display.minX) / scale)
)
let bottom = min(
source.height,
ceil((clipped.maxY - display.minY) / scale)
)
guard right > left, bottom > top else {
return nil
}
return CGRect(
x: left, y: top,
width: right - left, height: bottom - top
)
}
Порядок действий менять нельзя: сначала находим пересечение выделения с областью отображения, затем вычитаем смещение изображения и только после этого делим на коэффициент масштаба. Если сначала округлить координаты в представлении, погрешность увеличится пропорционально отношению размеров исходного изображения и области просмотра.
Границы округляются наружу, чтобы результат охватывал все пиксели, которых касается выделение. Это не гарантирует точного сохранения математических размеров выделенной области. Если продукту нужен фиксированный размер результата, масштабирование при экспорте следует выполнять отдельным последующим шагом.
Предотвращаем смещения с помощью проверяемых примеров
Добавьте следующие проверки в тот же файл. При успешном выполнении вывода не будет; если хотя бы одна проверка не пройдёт, процесс завершится с ошибкой. Не запускайте этот проверочный скрипт с оптимизациями, которые удаляют вызовы assert.
let source = CGSize(width: 4000, height: 3000)
let viewport = CGSize(width: 300, height: 300)
func mapped(_ rectangle: CGRect) -> CGRect? {
cropPixels(
source: source,
viewport: viewport,
selection: rectangle
)
}
assert(
mapped(CGRect(x: 75, y: 75, width: 150, height: 150))
== CGRect(x: 1000, y: 500, width: 2000, height: 2000)
)
assert(
mapped(CGRect(x: 0, y: 0, width: 300, height: 300))
== CGRect(x: 0, y: 0, width: 4000, height: 3000)
)
assert(
mapped(CGRect(x: 10, y: 0, width: 20, height: 20))
== nil
)
assert(
mapped(CGRect(x: 0, y: 0, width: 0, height: 20))
== nil
)
assert(
mapped(CGRect(x: 225, y: 225, width: 100, height: 100))
== CGRect(x: 3000, y: 2500, width: 1000, height: 500)
)
Пример с центральным выделением легко проверить вручную: масштаб отображения равен 0.075. После вычитания верхнего поля вертикальная координата начала выделения составляет 37.5 пункта, что соответствует координате 500 пикселей в исходном изображении. Последний пример выходит одновременно за правую и нижнюю границы. Он подтверждает, что функция действительно отсекает выступающую часть, а не просто ограничивает координаты начала.
Дополните проверки портретным изображением, непрямоугольным в смысле соотношения сторон контейнером — то есть не квадратным — и выделением с дробными координатами. Для любого корректного результата проверяйте, что ширина и высота положительны, границы не выходят за пределы исходного изображения, а координаты всех четырёх сторон целые. Входные значения с отрицательной шириной или высотой здесь сразу отклоняются; направление перетаскивания должно быть заранее нормализовано на уровне интерфейса.
Отдельно проверяем вычисления и фактический экспорт
Когда математические тесты пройдут, подготовьте тестовое изображение с фиксированными пиксельными размерами: разными цветами в четырёх углах и крестом в центре. Сначала экспортируйте его в формат без потерь, чтобы сжатие с потерями не вносило шум в проверки цветов.
Проверьте три условия: ширина и высота экспортированного изображения совпадают со значениями, возвращёнными функцией; при обрезке правого нижнего угла в результат не попадает метка из левого верхнего; при выделении всего изображения не появляется лишняя прозрачная кайма. Не ограничивайтесь побайтовым сравнением файлов: изменения метаданных, добавленных кодировщиком, не обязательно означают изменение пикселей.
Если обрезка выполняется непосредственно по нормализованному пиксельному изображению, передавайте пиксельный прямоугольник, полученный функцией из статьи. Если изображение перерисовывается через графический контекст, отдельно проверьте направление осей относительно начала координат и параметры масштаба вывода. Самая частая ошибка на этом этапе — повторное умножение на масштаб отображения в коде экспорта, хотя функция преобразования уже вернула координаты в пикселях.
Нужно проверить и асинхронную загрузку изображений. Если после выделения пользователем области загружается другой исходник с иными размерами, необходимо пересчитать координаты или сбросить прежнее выделение. Нельзя использовать масштаб старого изображения вместе с пиксельными размерами нового.
Превращаем записи об ошибках в воспроизводимые входные данные
При каждом сбое сохраняйте ширину и высоту исходного изображения в пикселях, размеры контейнера в пунктах, прямоугольник выделения, фактический режим отображения и результат вычислений. Достаточно записать числовые значения — загружать фотографии пользователя не требуется. Для воспроизведения используйте тестовое изображение тех же размеров.
Перед слиянием изменений проверьте как минимум шесть типов входных данных: альбомное изображение, портретное изображение, выделение всего изображения, выделение только пустого поля, выход за границы и дробные координаты границ. После изменения компоновки необходимо заново получить размеры контейнера, а не использовать значения, сохранённые до поворота экрана.
Итоговая проверка состоит из двух уровней: тесты чистой функции подтверждают правильность соотношений координат, а тесты экспорта — ориентацию и размеры пиксельного результата. Только после успешного прохождения обоих уровней проверяйте удобство перетаскивания через удалённый рабочий стол. Тогда при изменении интерфейса, замене исходного изображения или доработке экспорта будет понятно, на каком именно этапе возникла ошибка.
Часто задаваемые вопросы
Нужно ли умножать координаты обрезки на масштаб экрана?
Не напрямую. Рассчитайте отношение размеров исходника в пикселях к фактическому прямоугольнику показа изображения. Масштаб экрана описывает другое отношение.
Зачем округлять границы наружу?
Округление нижних границ вниз, а верхних вверх включает все затронутые выделением пиксели. Затем ограничьте результат размерами исходного изображения.
Запускайте проекты на выделенном физическом Mac
Собирайте и тестируйте проекты, а также работайте с удалённым рабочим столом на облачном Mac. Выберите конфигурацию и период оплаты под свои задачи. Фактическая доступность отображается в консоли в реальном времени.