PhotoKit框架詳細解析(三) —— 圖像的獲取、修改功茴、保存庐冯、編輯以及撤銷等簡單示例(二)

版本記錄

版本號 時間
V1.0 2020.10.09 星期五

前言

在我們開發(fā)中總有和系統(tǒng)相冊進行交互的時候,包含圖片和視頻的獲取坎穿,存儲展父,修改等操作返劲。這個模塊我們就一起來看下這個相關(guān)的框架PhotoKit。感興趣的可以看下面幾篇文章栖茉。
1. PhotoKit框架詳細解析(一) —— 基本概覽(一)
2. PhotoKit框架詳細解析(二) —— 圖像的獲取篮绿、修改、保存衡载、編輯以及撤銷等簡單示例(一)

源碼

1. Swift

首先我們看下工程組織結(jié)構(gòu)

接著看下sb中的內(nèi)容

下面就是源碼了

1. AlbumCollectionViewController.swift
import UIKit
import Photos

class AlbumCollectionViewController: UICollectionViewController {
  var sections: [AlbumCollectionSectionType] = [.all, .smartAlbums, .userCollections]
  var allPhotos = PHFetchResult<PHAsset>()
  var smartAlbums = PHFetchResult<PHAssetCollection>()
  var userCollections = PHFetchResult<PHAssetCollection>()

  override func viewDidLoad() {
    super.viewDidLoad()
    getPermissionIfNecessary { granted in
      guard granted else { return }
      self.fetchAssets()
      DispatchQueue.main.async {
        self.collectionView.reloadData()
      }
    }
    PHPhotoLibrary.shared().register(self)
  }

  deinit {
    PHPhotoLibrary.shared().unregisterChangeObserver(self)
  }

  @IBSegueAction func makePhotosCollectionViewController(_ coder: NSCoder) -> PhotosCollectionViewController? {
    guard
      let selectedIndexPath = collectionView.indexPathsForSelectedItems?.first
      else { return nil }

    let sectionType = sections[selectedIndexPath.section]
    let item = selectedIndexPath.item

    let assets: PHFetchResult<PHAsset>
    let title: String

    switch sectionType {
    case .all:
      assets = allPhotos
      title = AlbumCollectionSectionType.all.description
    case .smartAlbums, .userCollections:
      let album =
        sectionType == .smartAlbums ? smartAlbums[item] : userCollections[item]
      assets = PHAsset.fetchAssets(in: album, options: nil)
      title = album.localizedTitle ?? ""
    }

    return PhotosCollectionViewController(assets: assets, title: title, coder: coder)
  }

  override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
    if kind == UICollectionView.elementKindSectionHeader {
      guard let headerView = collectionView.dequeueReusableSupplementaryView(
        ofKind: kind,
        withReuseIdentifier: AlbumCollectionReusableView.reuseIdentifier,
        for: indexPath) as? AlbumCollectionReusableView
        else {
        fatalError("Unable to dequeue AlbumCollectionReusableView")
      }
      headerView.title.text = sections[indexPath.section].description
      return headerView
    }
    return UICollectionReusableView()
  }

  override func numberOfSections(in collectionView: UICollectionView) -> Int {
    return sections.count
  }

  override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    // 1
    guard let cell =
      collectionView.dequeueReusableCell(
        withReuseIdentifier: AlbumCollectionViewCell.reuseIdentifier,
        for: indexPath) as? AlbumCollectionViewCell
    else {
      fatalError("Unable to dequeue AlbumCollectionViewCell")
    }
    // 2
    var coverAsset: PHAsset?
    let sectionType = sections[indexPath.section]
    switch sectionType {
    // 3
    case .all:
      coverAsset = allPhotos.firstObject
      cell.update(title: sectionType.description, count: allPhotos.count)
    // 4
    case .smartAlbums, .userCollections:
      let collection = sectionType == .smartAlbums ?
        smartAlbums[indexPath.item] :
        userCollections[indexPath.item]
      let fetchedAssets = PHAsset.fetchAssets(in: collection, options: nil)
      coverAsset = fetchedAssets.firstObject
      cell.update(title: collection.localizedTitle, count: fetchedAssets.count)
    }
    // 5
    guard let asset = coverAsset else { return cell }
    cell.photoView.fetchImageAsset(asset, targetSize: cell.bounds.size) { success in
      cell.photoView.isHidden = !success
      cell.emptyView.isHidden = success
    }
    return cell
  }

  func getPermissionIfNecessary(completionHandler: @escaping (Bool) -> Void) {
    // 1
    guard PHPhotoLibrary.authorizationStatus() != .authorized else {
      completionHandler(true)
      return
    }
    // 2
    PHPhotoLibrary.requestAuthorization { status in
      completionHandler(status == .authorized ? true : false)
    }
  }

  func fetchAssets() {// 1
    let allPhotosOptions = PHFetchOptions()
    allPhotosOptions.sortDescriptors = [
      NSSortDescriptor(
        key: "creationDate",
        ascending: false)
    ]
    // 2
    allPhotos = PHAsset.fetchAssets(with: allPhotosOptions)
    // 3
    smartAlbums = PHAssetCollection.fetchAssetCollections(
      with: .smartAlbum,
      subtype: .albumRegular,
      options: nil)
    // 4
    userCollections = PHAssetCollection.fetchAssetCollections(
      with: .album,
      subtype: .albumRegular,
      options: nil)
  }

  override func collectionView(
    _ collectionView: UICollectionView,
    numberOfItemsInSection section: Int
  ) -> Int {
    switch sections[section] {
    case .all: return 1
    case .smartAlbums: return smartAlbums.count
    case .userCollections: return userCollections.count
    }
  }
}

extension AlbumCollectionViewController: PHPhotoLibraryChangeObserver {
  func photoLibraryDidChange(_ changeInstance: PHChange) {
    DispatchQueue.main.sync {
      // 1
      if let changeDetails = changeInstance.changeDetails(for: allPhotos) {
        allPhotos = changeDetails.fetchResultAfterChanges
      }
      // 2
      if let changeDetails = changeInstance.changeDetails(for: smartAlbums) {
        smartAlbums = changeDetails.fetchResultAfterChanges
      }
      if let changeDetails = changeInstance.changeDetails(for: userCollections) {
        userCollections = changeDetails.fetchResultAfterChanges
      }
      // 4
      collectionView.reloadData()
    }
  }
}
2. PhotosCollectionViewController.swift
import UIKit
import Photos

class PhotosCollectionViewController: UICollectionViewController {
  var assets: PHFetchResult<PHAsset>

  required init?(coder: NSCoder) {
    fatalError("init(coder:) not implemented.")
  }

  init?(assets: PHFetchResult<PHAsset>, title: String, coder: NSCoder) {
    self.assets = assets
    super.init(coder: coder)
    self.title = title
  }

  override func viewDidLoad() {
    super.viewDidLoad()
    PHPhotoLibrary.shared().register(self)
  }

  deinit {
    PHPhotoLibrary.shared().unregisterChangeObserver(self)
  }

  @IBSegueAction func makePhotoViewController(_ coder: NSCoder) -> PhotoViewController? {
    guard let selectedIndexPath = collectionView.indexPathsForSelectedItems?.first else { return nil }
    return PhotoViewController(asset: assets[selectedIndexPath.item], coder: coder)
  }

  override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return assets.count
  }

  override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    guard let cell = collectionView.dequeueReusableCell(
      withReuseIdentifier: PhotoCollectionViewCell.reuseIdentifier,
      for: indexPath) as? PhotoCollectionViewCell else {
        fatalError("Unable to dequeue PhotoCollectionViewCell")
    }
    let asset = assets[indexPath.item]
    cell.photoView.fetchImageAsset(asset, targetSize: cell.photoView.bounds.size, completionHandler: nil)
    return cell
  }
}

extension PhotosCollectionViewController: PHPhotoLibraryChangeObserver {
  func photoLibraryDidChange(_ changeInstance: PHChange) {
    // 1
    guard let change = changeInstance.changeDetails(for: assets) else {
      return
    }
    DispatchQueue.main.sync {
      // 2
      assets = change.fetchResultAfterChanges
      collectionView.reloadData()
    }
  }
}
3. PhotoViewController.swift
import UIKit
import Photos
import PhotosUI

class PhotoViewController: UIViewController {
  @IBOutlet weak var imageView: UIImageView!

  @IBOutlet weak var toolbar: UIToolbar!

  @IBOutlet weak var favoriteButton: UIBarButtonItem!
  @IBAction func favoriteTapped(_ sender: Any) { toggleFavorite() }

  @IBOutlet weak var saveButton: UIBarButtonItem!
  @IBAction func saveTapped(_ sender: Any) { saveImage() }

  @IBOutlet weak var undoButton: UIBarButtonItem!
  @IBAction func undoTapped(_ sender: Any) { undo() }

  @IBAction func applyFilterTapped(_ sender: Any) { applyFilter() }

  var asset: PHAsset
  var editingOutput: PHContentEditingOutput?

  required init?(coder: NSCoder) {
    fatalError("init(coder:) not implemented")
  }

  init?(asset: PHAsset, coder: NSCoder) {
    self.asset = asset
    super.init(coder: coder)
  }

  override func viewDidLoad() {
    super.viewDidLoad()
    getPhoto()
    updateFavoriteButton()
    updateUndoButton()
    saveButton.isEnabled = false
    PHPhotoLibrary.shared().register(self)
  }

  deinit {
    PHPhotoLibrary.shared().unregisterChangeObserver(self)
  }

  func updateFavoriteButton() {
    if asset.isFavorite {
      favoriteButton.image = UIImage(systemName: "heart.fill")
    } else {
      favoriteButton.image = UIImage(systemName: "heart")
    }
  }

  func updateUndoButton() {
    let adjustmentResources = PHAssetResource.assetResources(for: asset)
      .filter { $0.type == .adjustmentData }
    undoButton.isEnabled = !adjustmentResources.isEmpty
  }

  func toggleFavorite() {
    // 1
    let changeHandler: () -> Void = {
      let request = PHAssetChangeRequest(for: self.asset)
      request.isFavorite = !self.asset.isFavorite
    }
    // 2
    PHPhotoLibrary.shared().performChanges(changeHandler, completionHandler: nil)
  }

  func applyFilter() {
    // 1
    asset.requestContentEditingInput(with: nil) { input, _ in
      // 2
      guard let bundleID = Bundle.main.bundleIdentifier else {
        fatalError("Error: unable to get bundle identifier")
      }
      guard let input = input else {
        fatalError("Error: cannot get editing input")
      }
      guard let filterData = Filter.noir.data else {
        fatalError("Error: cannot get filter data")
      }
      // 3
      let adjustmentData = PHAdjustmentData(
        formatIdentifier: bundleID,
        formatVersion: "1.0",
        data: filterData)
      // 4
      self.editingOutput = PHContentEditingOutput(contentEditingInput: input)
      guard let editingOutput = self.editingOutput else { return }
      editingOutput.adjustmentData = adjustmentData
      // 5
      let fitleredImage = self.imageView.image?.applyFilter(.noir)
      self.imageView.image = fitleredImage
      // 6
      let jpegData = fitleredImage?.jpegData(compressionQuality: 1.0)
      do {
        try jpegData?.write(to: editingOutput.renderedContentURL)
      } catch {
        print(error.localizedDescription)
      }
      // 7
      DispatchQueue.main.async {
        self.saveButton.isEnabled = true
      }
    }
  }

  func saveImage() {
    // 1
    let changeRequest: () -> Void = {
      let changeRequest = PHAssetChangeRequest(for: self.asset)
      changeRequest.contentEditingOutput = self.editingOutput
    }
    // 2
    let completionHandler: (Bool, Error?) -> Void = { success, error in
      guard success else {
        print("Error: cannot edit asset: \(String(describing: error))")
        return
      }
      // 3
      self.editingOutput = nil
      DispatchQueue.main.async {
        self.saveButton.isEnabled = false
      }
    }
    // 4
    PHPhotoLibrary.shared().performChanges(
      changeRequest,
      completionHandler: completionHandler)
  }

  func undo() {
    // 1
    let changeRequest: () -> Void = {
      let request = PHAssetChangeRequest(for: self.asset)
      request.revertAssetContentToOriginal()
    }
    // 2
    let completionHandler: (Bool, Error?) -> Void = { success, error in
      guard success else {
        print("Error: can't revert the asset: \(String(describing: error))")
        return
      }
      DispatchQueue.main.async {
        self.undoButton.isEnabled = false
      }
    }
    // 3
    PHPhotoLibrary.shared().performChanges(
      changeRequest,
      completionHandler: completionHandler)
  }

  func getPhoto() {
    imageView.fetchImageAsset(asset, targetSize: view.bounds.size, completionHandler: nil)
  }
}

// 1
extension PhotoViewController: PHPhotoLibraryChangeObserver {
  func photoLibraryDidChange(_ changeInstance: PHChange) {
    // 2
    guard
      let change = changeInstance.changeDetails(for: asset),
      let updatedAsset = change.objectAfterChanges
      else { return }
    // 3
    DispatchQueue.main.sync {
      // 4
      asset = updatedAsset
      imageView.fetchImageAsset(
        asset,
        targetSize: view.bounds.size
      ) { [weak self] _ in
        guard let self = self else { return }
        // 5
        self.updateFavoriteButton()
        self.updateUndoButton()
      }
    }
  }
}
4. UIImageView+Extension.swift
import UIKit
import Photos

extension UIImageView {
  func fetchImageAsset(_ asset: PHAsset?, targetSize size: CGSize, contentMode: PHImageContentMode = .aspectFill, options: PHImageRequestOptions? = nil, completionHandler: ((Bool) -> Void)?) {
    // 1
    guard let asset = asset else {
      completionHandler?(false)
      return
    }
    // 2
    let resultHandler: (UIImage?, [AnyHashable: Any]?) -> Void = { image, info in
      self.image = image
      completionHandler?(true)
    }
    // 3
    PHImageManager.default().requestImage(
      for: asset,
      targetSize: size,
      contentMode: contentMode,
      options: options,
      resultHandler: resultHandler)
  }
}
5. AlbumCollectionReusableView.swift
import UIKit

class AlbumCollectionReusableView: UICollectionReusableView {
  static let reuseIdentifier = "headerView"
  @IBOutlet weak var title: UILabel!
}
6. AlbumCollectionViewCell.swift
import UIKit

class AlbumCollectionViewCell: UICollectionViewCell {
  static let reuseIdentifier = "albumCell"
  @IBOutlet weak var emptyView: UIImageView!
  @IBOutlet weak var photoView: UIImageView!
  @IBOutlet weak var albumTitle: UILabel!
  @IBOutlet weak var albumCount: UILabel!

  override func prepareForReuse() {
    super.prepareForReuse()
    albumTitle.text = "Untitled"
    albumCount.text = "0 photos"
    photoView.image = nil
    photoView.isHidden = true
    emptyView.isHidden = false
  }

  func update(title: String?, count: Int) {
    albumTitle.text = title ?? "Untitled"
    albumCount.text = "\(count.description) \(count == 1 ? "photo" : "photos")"
  }
}
7. AlbumSectionType.swift
enum AlbumCollectionSectionType: Int, CustomStringConvertible {
  case all, smartAlbums, userCollections

  var description: String {
    switch self {
    case .all: return "All Photos"
    case .smartAlbums: return "Smart Albums"
    case .userCollections: return "User Collections"
    }
  }
}
8. AlbumCollectionViewController+Extensions.swift
import UIKit

extension AlbumCollectionViewController: UICollectionViewDelegateFlowLayout {
  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CollectionViewFlowLayoutType(.album, frame: view.frame).sizeForItem
  }

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
    return CollectionViewFlowLayoutType(.album, frame: view.frame).sectionInsets
  }

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
    return CollectionViewFlowLayoutType(.album, frame: view.frame).sectionInsets.left
  }
}
9. CollectionViewFlowLayout.swift
import UIKit

struct CollectionViewFlowLayoutType {
  enum ViewType { case album, photos }

  private var viewType: ViewType = .album
  private var viewFrame: CGRect = .zero
  var itemsPerRow: CGFloat {
    switch viewType {
    case .album: return 2
    case .photos: return 3
    }
  }
  var sectionInsets: UIEdgeInsets {
    switch viewType {
    case .album: return UIEdgeInsets(top: 4.0, left: 8.0, bottom: 4.0, right: 8.0)
    case .photos: return UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)
    }
  }
  var sizeForItem: CGSize {
    let paddingSpace = sectionInsets.left * (itemsPerRow + 1)
    let availableWidth = viewFrame.width - paddingSpace
    let widthPerItem = availableWidth / itemsPerRow
    return CGSize(width: widthPerItem, height: widthPerItem)
  }

  init(_ type: ViewType, frame: CGRect) {
    viewType = type
    viewFrame = frame
  }
}
10. Filter.swift 
import Foundation

enum Filter: String {
  case noir = "CIPhotoEffectNoir"

  var data: Data? {
    return self.rawValue.data(using: .utf8)
  }
}
11. PhotoCollectionViewCell.swift
import UIKit

class PhotoCollectionViewCell: UICollectionViewCell {
  static let reuseIdentifier = "photoCell"
  @IBOutlet weak var photoView: UIImageView!
  @IBOutlet weak var livePhotoIndicator: UIImageView!

  override func prepareForReuse() {
    super.prepareForReuse()
    photoView.image = nil
    livePhotoIndicator.isHidden = true
  }
}
12. PhotosCollectionViewController+Extensions.swift
import UIKit

extension PhotosCollectionViewController: UICollectionViewDelegateFlowLayout {
  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CollectionViewFlowLayoutType(.photos, frame: view.frame).sizeForItem
  }

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
    return CollectionViewFlowLayoutType(.photos, frame: view.frame).sectionInsets
  }

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
    return CollectionViewFlowLayoutType(.photos, frame: view.frame).sectionInsets.left
  }
}
13. UIImage+Extensions.swift
import UIKit

extension UIImage {
  func applyFilter(_ filter: Filter) -> UIImage? {
    let filter = CIFilter(name: filter.rawValue)
    let inputImage = CIImage(image: self)
    filter?.setValue(inputImage, forKey: "inputImage")
    guard let finalImage = filter?.outputImage else { return nil }
    guard let cgImage = CIContext().createCGImage(finalImage, from: finalImage.extent) else { return nil }
    return UIImage(cgImage: cgImage)
  }
}

后記

本篇主要講述了圖像的獲取搔耕、修改、保存痰娱、編輯以及撤銷等簡單示例弃榨,感興趣的給個贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市梨睁,隨后出現(xiàn)的幾起案子鲸睛,更是在濱河造成了極大的恐慌,老刑警劉巖坡贺,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件官辈,死亡現(xiàn)場離奇詭異,居然都是意外死亡遍坟,警方通過查閱死者的電腦和手機拳亿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來愿伴,“玉大人肺魁,你說我怎么就攤上這事「艚冢” “怎么了鹅经?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長怎诫。 經(jīng)常有香客問我瘾晃,道長,這世上最難降的妖魔是什么幻妓? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任蹦误,我火速辦了婚禮,結(jié)果婚禮上肉津,老公的妹妹穿的比我還像新娘胖缤。我一直安慰自己,他們只是感情好阀圾,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布哪廓。 她就那樣靜靜地躺著,像睡著了一般初烘。 火紅的嫁衣襯著肌膚如雪涡真。 梳的紋絲不亂的頭發(fā)上分俯,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機與錄音哆料,去河邊找鬼缸剪。 笑死,一個胖子當著我的面吹牛东亦,可吹牛的內(nèi)容都是我干的杏节。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼典阵,長吁一口氣:“原來是場噩夢啊……” “哼奋渔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起壮啊,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤嫉鲸,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后歹啼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體玄渗,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年狸眼,在試婚紗的時候發(fā)現(xiàn)自己被綠了藤树。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡拓萌,死狀恐怖也榄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情司志,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布降宅,位于F島的核電站骂远,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏腰根。R本人自食惡果不足惜激才,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望额嘿。 院中可真熱鬧瘸恼,春花似錦、人聲如沸册养。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽球拦。三九已至靠闭,卻和暖如春帐我,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背愧膀。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工拦键, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人檩淋。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓芬为,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蟀悦。 傳聞我的和親對象是個殘疾皇子媚朦,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353