Swiftで画像を扱う場合には、CIImageをよく使いますよね。
CIImageをファイルに書き出そうとしたとき、img.save("filepath")
のようにできるかと思いきや、思った以上に難易度が高かったので解説記事としてまとめました。
Image I/Oを使う方法
まずは王道であり汎用性の高い方法から。
CIImageをCGImageに変換し、Image I/Oの機能を使ってファイルに書き出します。
コメント多めのサンプルプログラムで解説します。
// レンダリング前の抽象的なイメージ情報を扱う
// またCIImageをレンダリング後してCGImageにする機能も有する
import CoreImage
// 画像のファイル読み書き機能を提供するフレームワーク
import ImageIO
// ファイルタイプやMIMEタイプが定義されているフレームワーク
// 書き出すファイル拡張子を指定するために必要
import UniformTypeIdentifiers
// CIImageを作成
let url = URL(filePath: "work/sample.jpeg", relativeTo: URL.homeDirectory)
let img = CIImage(contentsOf: url)!
// CIImageをレンダリングしCGImageを作成する
let context = CIContext()
let cgImg = context.createCGImage(img, from: img.extent)!
// 画像の書き出し先URL
let destUrl = URL(filePath: "work/sample-dest.jpeg", relativeTo: URL.homeDirectory)
// 画像を書き出すCGImageDestinationインスタンスを作成
// 第一パラメータ:URLインスタンスをCFURLにタイプキャストして渡す
// 補足:FoundationフレームワークのインスタンスはCore Foundationフレームワークのインスタンスと
// tall-free bridgingによりタイプキャストが可能
// 第二パラメータ:出力する画像ファイルのタイプを指定
// 実際の値は"public.jpeg"や"public.png"ではあるものの
// UniformTypeIdentifiers.UTType.jpeg.identifierなどから取得する
// StringからCFStringもtall-free bridgingによりタイプキャストできる
// 第三パラメータはファイルに追加する画像の枚数で、普通の画像ファイルなら1
// 第四パラメータは常にnil
let dest = CGImageDestinationCreateWithURL(destUrl as CFURL, UTType.jpeg.identifier as CFString, 1, nil)!
// CGImageDestinationインスタンスに画像情報を追加
CGImageDestinationAddImage(dest, cgImg, nil)
CGImageDestinationFinalize(dest)
CIImageをファイルに書き出すために、Core Graphicsフレームワーク、Image I/Oフレームワーク、Uniform Type Identifiersフレームワークを使う必要があります。
再利用を前提に最適化した結果だとは思うのですが、個人的な感想は「画像をファイルに書き出すだけにしては必要なコーディング量が多くてしんどい」です。
URLをas CFURL
としてキャストしたりStringをas CFString
としてキャストしたりしている部分については、tall-free bridgingの仕様によるものですが、これも初見殺しですよね。これについてはIntroduction to Core Foundation Design Conceptsにて「キャストできるよ」と書かれています。
Core Foundation also provides “toll-free bridging” between certain services and the Cocoa’s Foundation framework
細かな制御ができることまで考えての設計だとは思うけど画像をファイルに書き出すたびにこの面倒な記述をする必要があるかと思うと気が滅入ってしまいます。
UIKitフレームワークを使う方法
iOSの開発では、UIKitフレームワークのUIImageクラスを使ってjpegやpng形式のDataに書き出し、Data.write(to)メソッドを使ってファイルに書き出すことができます。
import CoreImage
import Foundation
import UIKit
# iOSのBundleにあるsample.jpegのURLを作成
let url = Bundle.main.url(forResource: "sample", withExtension: "jpeg")!
# この記事のテーマであるCIImageインスタンスを作成
let img = CIImage(contentsOf: url)!
# CIImageからUIImageインスタンスを作成
let uiImg = UIImage(ciImage: img)
# UIImageに備わっているjpegData(compressionQaulity)やpngData()を使うことでDataインスタンスに変換できる
let data = uiImg.jpegData(compressionQuality: 0.7)!
# ファイル書き出し先を決定
let urlDest = URL(filePath: "sample-dest.jpeg", relativeTo: URL.documentsDirectory)
# Data.write(to)によりファイル書き出しできる
try! data.write(to: urlDest)
なお、SwiftUIの開発で使用するImageには同様の機能は備わっていません。
AppKitフレームワークを使う方法
macOSアプリの開発では、AppKitフレームワークのNSBitmapImageRepクラスを使ってjpegやpng形式のDataインスタンスを作り、Data.write(to)メソッドを使ってファイルに書き出すことができます。
import AppKit
import CoreImage
import Foundation
# CIImageを作成
let url = URL(filePath: "work/sample.jpeg", relativeTo: URL.homeDirectory)
let img = CIImage(contentsOf: url)!
# CIImageからDataインスタンスを作成
let bitmapRep = NSBitmapImageRep(ciImage: img)
let data = bitmapRep.representation(using: .jpeg, properties: [:])!
# 保存先ファイルを指定して書き出し
let urlDest = URL(filePath: "work/sample-dest.jpeg", relativeTo: URL.homeDirectory)
try! data.write(to: urlDest)
AppKitフレームワークはmacOSアプリでのみインポート可能で、iOSアプリの場合はこの方法は使えません。
また、NSBitmapImageRepが全く直感的に使える気がしないのは僕だけでしょうか…?
CoreImageWriterを使う方法
ここまで、Image I/Oを使う王道の方法、AppKitフレームワークを使う方法、UIKitを使う方法、などについて解説してきました。
ぶっちゃけどの方法も、難しかったり使い分けが面倒だったりしませんか?
そこでCIImageを指定したURLに保存できるCoreImageWriterというライブラリを作りました。
こんな感じで使えます。
let writer = CoreImageWriter()
let url = URL(filePath: "work/sample-dest.jpg", relativeTo: URL.homeDirectory)
try! writer.write(image: img, to: url)
最初に解説したImage I/Oを使った王道の方法をCoreImageWriterというクラスでラップしただけのもので、ファイル拡張子.jpeg, .jpg, .pngしか対応しておらず、細かな制御もできません。
でも、大抵のケースではこのライブラリを使うだけで十分だと思うんですよね。
File > Package DependenciesにCoreImageWriterを追加して使用して下さい。