TL;DR: Расширение редактирования фотографий iOS не может сохранить изменения в фотографиях, если они не были сделаны устройством с альбомной ориентацией влево .
Я пытаюсь разработать расширение для редактирования фотографийна iOS.
Я основал свой код на шаблоне Xcode, Образец кода Apple и несколько учебных пособий, доступных в Интернете.
Я заметил, что некоторые фотографии не удается сохранитьпосле применения изменений;Я получаю предупреждение , которое гласит:
Невозможно сохранить изменения
Произошла ошибка при сохранении.Пожалуйста, повторите попытку позже.
OK
Поиск в Интернете привел меня к двум следующим вопросам здесь, о переполнении стека:
- Расширение фотографии для iOSНет полезных ответов на вопрос)
Я попробовал несколько расширений для редактирования, просто чтобы убедиться, что с моим устройством что-то не так, и обнаружил, что проблема возникает с:
- Мое приложение,
- Пример кода Apple ,
- Некоторые сторонних приложений в AppStore (например, * 1053)* Little ), но не другие (например, BitCam - хотелось бы связаться с разработчиками этого приложения и попросить несколько советов ...).
Я заметил, что для данного фоторепортажа из библиотеки проблема всегда возникает или никогда не возникает.То есть, похоже, что это зависит от некоторого свойства редактируемой фотографии (поэтому весь бизнес «Попробуй позже» в этом случае не имеет смысла).
Я решил установить точку останова внутри метода finishContentEditing(completionHandler:)
(вызываемого для сохранения измененного изображения по URL-адресу, указанному в платформе) и проверить различные свойства объекта PHContentEditingInput
, переданного в началесеанса редактирования.
Я быстро понял, что проблема всегда возникает с изображениями, снятыми на iPhone в Портрет , Портрет вверх ногами или Пейзаж вправо ориентации, и только тогда.Фотографии, сделанные в Пейзаж влево (кнопка «Домой» справа), могут быть сохранены без проблем.
Пример кода Apple:
- Создание
CIIMage
экземпляр из свойства fullSizeImageURL
экземпляра PHContentEditingInput
. - Создайте ориентированную копию изображения из точки # 1, вызвав на нем
applyingOrientation()
, передав значениеfullSizeImageOrientation
свойство ввода. - Примените соответствующий фильтр CoreImage к полноразмерному ориентированному изображению из точки # 2.
- Создайте
CIContext
. - Используйте контекст для вызова
writeJPEGRepresentation(of:to:colorSpace:)
передачи модифицированного CIImage, полученного в # 3, renderedContentURL
из PHContentEditingOutput
и цветового пространства оригинала CIImage
.
Фактический код:
DispatchQueue.global(qos: .userInitiated).async {
// Load full-size image to process from input.
guard let url = input.fullSizeImageURL
else { fatalError("missing input image url") }
guard let inputImage = CIImage(contentsOf: url)
else { fatalError("can't load input image to apply edit") }
// Define output image with Core Image edits.
let orientedImage = inputImage//.applyingOrientation(input.fullSizeImageOrientation)
let outputImage: CIImage
switch selectedFilterName {
case .some(wwdcFilter):
outputImage = orientedImage.applyingWWDCDemoEffect()
case .some(let filterName):
outputImage = orientedImage.applyingFilter(filterName, parameters: [:])
default:
outputImage = orientedImage
}
// Usually you want to create a CIContext early and reuse it, but
// this extension uses one (explicitly) only on exit.
let context = CIContext()
// Render the filtered image to the expected output URL.
if #available(OSXApplicationExtension 10.12, iOSApplicationExtension 10.0, *) {
// Use Core Image convenience method to write JPEG where supported.
do {
try context.writeJPEGRepresentation(of: outputImage, to: output.renderedContentURL, colorSpace: inputImage.colorSpace!)
completionHandler(output)
} catch let error {
NSLog("can't write image: \(error)")
completionHandler(nil)
}
} else {
// Use CGImageDestination to write JPEG in older OS.
guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent)
else { fatalError("can't create CGImage") }
guard let destination = CGImageDestinationCreateWithURL(output.renderedContentURL as CFURL, kUTTypeJPEG, 1, nil)
else { fatalError("can't create CGImageDestination") }
CGImageDestinationAddImage(destination, cgImage, nil)
let success = CGImageDestinationFinalize(destination)
if success {
completionHandler(output)
} else {
completionHandler(nil)
}
}
}
(слегка реорганизовано для публикации здесь. Блюк кода выше находится в отдельном методе, вызываемом из блока очереди отправки)
Когда я пытаюсь отредактировать фотографию, снятую устройством, в (скажем) Пейзаж вправо ориентация:
... выбор образца кода Apple Расширение редактирования фотографий:
... применяя фильтр "Сепия" и нажимая "Готово":
... Я получаю ужасalert:
... и после его отклонения предварительный просмотр изображения каким-то образом поворачивается к ориентации относительно ландшафта влево:
(т. Е. Фотография, сделанная в Пейзаж вправо , повернута на 180 градусов, фотография, сделанная в Портрет , являетсяповернут на 90 градусов и т. д.)
Нажатие «Готово» или «Отмена», а затем «Отмена изменений» завершает сеанс, и изображение восстанавливается в правильной ориентации:
Очевидно, есть некоторая ловушка, о которой не знают ни я, ни разработчики Little , ни образец кода Apple 2016 (но разработчики BitCam знают).
Что происходит?
Обход
Если я сделаю снимок с iPhone в книжной ориентации и попытаюсь отредактировать его, на отладчике fullSizeImageOrientation
будет .right
, и редактирование завершится неудачно, как только что описано.
Но если я поверну изображение один раз на 180 градусов, используя инструмент по умолчанию:
... сохранение, редактирование снова и вращение еще на 180 градусов (или, альтернативно, 90 + 270 градусов, но всегда в двух отдельных правках ), возвращая его обратно в исходная ориентация и , затем попытаться редактировать с использованием расширения, теперь значение fullSizeImageOrientation
равно .up
, и сохранение завершено, Я считаю, что это потому, что этот инструмент фактически вращает данные пикселей вместо того, чтобы просто изменять ориентацию метаданные (тот факт, что он может обрезать и поворачивать под произвольные углы , не просто кратные 90 градусов, я думаю, что это отдает ...)
Конечно, это потребует неудобного взаимодействия с пользователем, так что это не обходной путь (хотя программный эквивалент будет).
Добавление:
Я использую Xcode 10.0, и это подтверждается как на iPhone 8 под управлением iOS 12 GM, так и на iPhone 5 под управлением iOS 11.4.1).