Расширение для фотографий: невозможно сохранить изображения, не ориентированные вверх

TL; DR: Расширение для редактирования фотографий iOS не может сохранить изменения в фотографиях, если только они не были сняты с помощью устройства в горизонтальной левой ориентации.


Я пытаюсь разработать расширение для редактирования фотографий на iOS.

Я создал свой код на основе шаблона Xcode, пример кода Apple и несколько руководств, доступных в Интернете.

Я заметил, что некоторые фотографии не сохраняются после применения изменений; Я получаю предупреждение, которое гласит:

Не удалось сохранить изменения

Произошла ошибка при сохранении. Пожалуйста, повторите попытку позже.

ОК

Поиск в Интернете привел меня к двум следующим вопросам здесь, в Stack Overflow:

  1. расширение iOS для фотографий FinishContentEditingWithCompletionHandler: невозможно сохранить изменения (уже применено исправление, не работает в моем случае)
  2. IOS) Расширение для фотографий не может сохранить изменения (нет полезных ответов на вопрос)

Я попробовал несколько расширений для редактирования, чтобы убедиться, что с моим устройством все в порядке, и обнаружил, что проблема возникает с:

  1. Мое приложение,
  2. Собственный пример кода Apple,
  3. Некоторые сторонние приложения в AppStore (например, Litely), но не другие (например, BitCam. Я хотел бы связаться с разработчики этого приложения, чтобы попросить несколько советов...).

Я заметил, что для данного фоторесурса из библиотеки проблема возникает либо всегда, либо никогда. То есть это как бы зависит от какого-то свойства редактируемой фотографии (поэтому вся эта затея с "Попробовать позже" в данном случае бессмысленна).

Я решил установить точку останова внутри метода finishContentEditing(completionHandler:) (вызванного для сохранения измененного изображения по URL-адресу, указанному фреймворком), и проверить различные свойства объекта PHContentEditingInput, переданного в начале сеанса редактирования.

Я быстро понял, что проблема всегда возникает со снимками, сделанными на iPhone в ориентации Портрет, Портрет вверх ногами или Пейзаж вправо. только тогда. Фотографии, сделанные в Пейзаж слева (кнопка «Домой» справа), могут быть сохранены без проблем.

Что делает пример кода Apple:

  1. Создайте экземпляр CIIMage из свойства fullSizeImageURL экземпляра PHContentEditingInput.
  2. Создайте ориентированную копию изображения из точки №1, вызвав для нее applyingOrientation() и передав значение свойства fullSizeImageOrientation входных данных.
  3. Примените соответствующий фильтр CoreImage к полноразмерному ориентированному изображению из точки №2.
  4. Создайте CIContext.
  5. Используйте контекст для вызова 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 Photo Editing Extension:

введите здесь описание изображения

...применив фильтр «Сепия» и нажав «Готово»:

введите здесь описание изображения

...Я получаю ужасное предупреждение:

введите здесь описание изображения

... и после его отклонения предварительный просмотр изображения каким-то образом поворачивается в ориентацию относительно альбомной слева:

введите здесь описание изображения

(например, фотография, сделанная в режиме Пейзаж справа, повернута на 180 градусов, фотография, сделанная в режиме Портрет, повернута на 90 градусов и т. д.)

Нажатие «Готово» или «Отмена», а затем «Отменить изменения» завершает сеанс, и изображение восстанавливает свою правильную ориентацию:

введите здесь описание изображения

Очевидно, есть некоторая ловушка, о которой не знаю ни я, ни разработчики Litely, ни пример кода Apple 2016 года (но знают разработчики BitCam).

Что происходит?


Обходной путь?

Если я сделаю снимок с iPhone в портретной ориентации и попытаюсь отредактировать его, в отладчике fullSizeImageOrientation будет .right, и редактирование завершится ошибкой, как только что было описано.

Но если я поверну изображение один раз на 180 градусов, используя инструмент по умолчанию:

введите здесь описание изображения

...сохранение, редактирование снова и поворот еще на 180 градусов (или, как вариант, на 90 + 270 градусов, но всегда в два отдельных редактирования), возвращая его обратно в исходное >исходную ориентацию и затем попробуйте отредактировать с помощью расширения, теперь значение fullSizeImageOrientation равно .up, и сохранение выполнено успешно. Я полагаю, это потому, что этот инструмент фактически поворачивает пиксельные данные, а не просто изменяет метаданные ориентации (тот факт, что он может обрезать и поворачивать под произвольными углами, не просто кратные 90 градусам, я думаю, это выдает...)

Конечно, это потребует неудобного взаимодействия с пользователем, поэтому на самом деле это не обходной путь (хотя программный эквивалент будет).


Приложение:

Я использую Xcode 10.0, и вышеизложенное было подтверждено как на iPhone 8 под управлением iOS 12 GM, так и на iPhone 5s под управлением iOS 11.4.1).


person Nicolas Miari    schedule 18.09.2018    source источник
comment
Кажется, у меня нет этой проблемы с моим расширением фотографий, но у меня была проблема, пока (как вы говорите) я не разобрался с ориентацией... Я бы сказал вам, что, по моему мнению, вы можете делать неправильно, но вы этого не сделали. показать любой код.   -  person matt    schedule 19.09.2018
comment
@matt Спасибо. Прямо сейчас я в основном запускаю пример кода Apple без изменений (ссылка в вопросе).   -  person Nicolas Miari    schedule 19.09.2018
comment
Ну, это не очень полезно. Я в общих чертах описал, что делаю я, но вам придется выяснить, чем (если вообще) это отличается от того, что делаете вы.   -  person matt    schedule 19.09.2018
comment
@matt Извините, теперь я добавлю фактический код к моему вопросу. Это является, хотя, по сути, то, что описано после того, что делает пример кода Apple:... (список из 5 пунктов).   -  person Nicolas Miari    schedule 19.09.2018
comment
Ну, вы говорите orientedImage = inputImage (CIImage, взятый из растровых данных по URL-адресу), а я говорю var ci = CIImage(contentsOf: inurl, options: [.applyOrientationProperty:true])!.   -  person matt    schedule 19.09.2018
comment
Но другое большое отличие состоит в том, что вы совершенно забыли установить данные настройки. Это абсолютно необходимо. Я удивлен, что вы можете когда-либо успешно сохраниться с этим.   -  person matt    schedule 19.09.2018
comment
@matt Извините, я закомментировал часть .applyingOrientation(input.fullSizeImageOrientation), чтобы посмотреть, имеет ли это значение (нет), и забыл вернуть ее обратно.   -  person Nicolas Miari    schedule 19.09.2018
comment
Это не совсем одно и то же. Попробуйте мой способ, пожалуйста.   -  person matt    schedule 19.09.2018
comment
Я устанавливаю данные настройки во внешнем методе, который вызывает этот. Код Apple поддерживает Photos, Video и LivePhotos, поэтому finishContentEditing(completionHandler:) разветвляется на три отдельных метода. Но данные настройки устанавливаются до ветвления и, следовательно, не включены в опубликованный мной код.   -  person Nicolas Miari    schedule 19.09.2018
comment
Хорошо, просто проверяю! :)   -  person matt    schedule 19.09.2018
comment
@matt Я попробовал ваш код, но пока никаких изменений. Должно быть что-то еще, что отличается. Сложность кода Apple не помогает; Я начну с нуля, используя ваш код, и посмотрю оттуда.   -  person Nicolas Miari    schedule 19.09.2018
comment
Дайте мне знать, если мой ответ полностью вас не устроит, и я удалю его, чтобы никого не вводить в заблуждение!   -  person matt    schedule 19.09.2018


Ответы (1)


Я, конечно, видел, что сохранение не удалось, потому что ориентация была неправильной, но следующая архитектура в настоящее время работает для меня:

func startContentEditing(with contentEditingInput: PHContentEditingInput, placeholderImage: UIImage) {
    self.input = contentEditingInput
    if let im = self.input?.displaySizeImage {
        self.displayImage = CIImage(image:im, options: [.applyOrientationProperty:true])!
        // ... other stuff depending on what the adjustment data was ...
    }
    self.mtkview.setNeedsDisplay()
}
func finishContentEditing(completionHandler: @escaping ((PHContentEditingOutput?) -> Void)) {
    DispatchQueue.global(qos:.default).async {
        let inurl = self.input!.fullSizeImageURL!
        let output = PHContentEditingOutput(contentEditingInput:self.input!)
        let outurl = output.renderedContentURL
        var ci = CIImage(contentsOf: inurl, options: [.applyOrientationProperty:true])!
        let space = ci.colorSpace!
        // ... apply real filter to `ci` based on user edits ...
        try! CIContext().writeJPEGRepresentation(
            of: ci, to: outurl, colorSpace: space)
        let data = // whatever
        output.adjustmentData = PHAdjustmentData(
            formatIdentifier: self.myidentifier, formatVersion: "1.0", data: data)
        completionHandler(output)
    }
}
person matt    schedule 19.09.2018
comment
Я отказался от попыток настроить MTKView должным образом, но я делаю легкие вещи на неподвижных изображениях, поэтому просто использую обычный UIImageView для предварительного просмотра. - person Nicolas Miari; 19.09.2018
comment
Ну, мне тоже потребовалось время, чтобы понять это! Заставить рендеринг CIFilter появиться в нужном месте — большая проблема. См. stackoverflow.com/a/51753747/341994 для некоторых рабочих шаблонов. (Я бы подумал, что это будет пустяком для крупного OpenGL чувака вроде вас!) - person matt; 19.09.2018
comment
Спасибо миллион раз. Мне не удалось заставить ваш код работать в примере проекта Apple, но он работает в новом проекте. Мой OpenGL сейчас довольно ржавый (и, вероятно, останется таким...) - person Nicolas Miari; 19.09.2018
comment
Сукин сын, это работает, а? Это хорошо, потому что я ОЧЕНЬ много бился, прежде чем остановился на этом. :) - person matt; 19.09.2018
comment
Вывод заключается в том, что функция «Невозможно сохранить изменения» охватывает множество различных вещей, которые могут пойти не так, и, таким образом, не дает никакой помощи. В этом свете я полностью согласен с тем, что мой вопрос без фактического исходного кода был слишком широким. - person Nicolas Miari; 19.09.2018
comment
Интересно, имеет ли какое-то отношение к этому время, когда вы устанавливаете данные корректировки действительно? - person matt; 19.09.2018