From f784df912d1bd1eb859bce47ace5ced2dbf3a7fa Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 22 Nov 2022 15:58:31 +0800 Subject: [PATCH] fix: no downscaling for raw image from camera issue --- .../Scene/Compose/ComposeViewController.swift | 1 + .../MastodonUI/Extension/UIImage.swift | 11 ++++ .../AttachmentViewModel+Compress.swift | 59 ++++++++++++++++++- .../Attachment/AttachmentViewModel+Load.swift | 2 +- .../Attachment/AttachmentViewModel.swift | 25 ++++---- .../ComposeContentViewController.swift | 3 + .../ComposeContentViewModel.swift | 14 ++++- .../Scene/ShareViewController.swift | 2 + 8 files changed, 101 insertions(+), 16 deletions(-) diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index ca33487ab..fbdbc7d12 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -209,6 +209,7 @@ extension ComposeViewController { api: viewModel.context.apiService, authContext: viewModel.authContext, input: .image(image), + sizeLimit: composeContentViewModel.sizeLimit, delegate: composeContentViewModel ) } diff --git a/MastodonSDK/Sources/MastodonUI/Extension/UIImage.swift b/MastodonSDK/Sources/MastodonUI/Extension/UIImage.swift index 141b723bc..5a83e1d61 100644 --- a/MastodonSDK/Sources/MastodonUI/Extension/UIImage.swift +++ b/MastodonSDK/Sources/MastodonUI/Extension/UIImage.swift @@ -16,3 +16,14 @@ extension UIImage { } } + +extension UIImage { + public func normalized() -> UIImage? { + if imageOrientation == .up { return self } + UIGraphicsBeginImageContext(size) + draw(in: CGRect(origin: CGPoint.zero, size: size)) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image + } + } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift index e5d6702ad..6df22d324 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift @@ -8,11 +8,12 @@ import os.log import UIKit import AVKit -import SessionExporter import MastodonCore +import SessionExporter +import Kingfisher extension AttachmentViewModel { - func comporessVideo(url: URL) async throws -> URL { + func compressVideo(url: URL) async throws -> URL { let urlAsset = AVURLAsset(url: url) let exporter = NextLevelSessionExporter(withAsset: urlAsset) exporter.outputFileType = .mp4 @@ -92,3 +93,57 @@ extension AttachmentViewModel { } } // end func } + +extension AttachmentViewModel { + @AttachmentViewModelActor + func compressImage(data: Data, sizeLimit: SizeLimit) throws -> Output { + let maxPayloadSizeInBytes = sizeLimit.image ?? 10 * 1024 * 1024 + + guard let image = KFCrossPlatformImage(data: data)?.kf.normalized, + var imageData = image.kf.pngRepresentation() + else { + throw AttachmentError.invalidAttachmentType + } + + repeat { + guard let image = KFCrossPlatformImage(data: imageData) else { + throw AttachmentError.invalidAttachmentType + } + + if imageData.kf.imageFormat == .PNG { + // A. png image + if imageData.count > maxPayloadSizeInBytes { + guard let compressedJpegData = image.jpegData(compressionQuality: 0.8) else { + throw AttachmentError.invalidAttachmentType + } + os_log("%{public}s[%{public}ld], %{public}s: compress png %.2fMiB -> jpeg %.2fMiB", ((#file as NSString).lastPathComponent), #line, #function, Double(imageData.count) / 1024 / 1024, Double(compressedJpegData.count) / 1024 / 1024) + imageData = compressedJpegData + } else { + os_log("%{public}s[%{public}ld], %{public}s: png %.2fMiB", ((#file as NSString).lastPathComponent), #line, #function, Double(imageData.count) / 1024 / 1024) + break + } + } else { + // B. other image + if imageData.count > maxPayloadSizeInBytes { + let targetSize = CGSize(width: image.size.width * 0.8, height: image.size.height * 0.8) + let scaledImage = image.kf.resize(to: targetSize) + guard let compressedJpegData = scaledImage.jpegData(compressionQuality: 0.8) else { + throw AttachmentError.invalidAttachmentType + } + os_log("%{public}s[%{public}ld], %{public}s: compress jpeg %.2fMiB -> jpeg %.2fMiB", ((#file as NSString).lastPathComponent), #line, #function, Double(imageData.count) / 1024 / 1024, Double(compressedJpegData.count) / 1024 / 1024) + imageData = compressedJpegData + } else { + os_log("%{public}s[%{public}ld], %{public}s: jpeg %.2fMiB", ((#file as NSString).lastPathComponent), #line, #function, Double(imageData.count) / 1024 / 1024) + break + } + } + } while (imageData.count > maxPayloadSizeInBytes) + + + return .image(imageData, imageKind: imageData.kf.imageFormat == .PNG ? .png : .jpg) + } +} + +@globalActor actor AttachmentViewModelActor { + static var shared = AttachmentViewModelActor() +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Load.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Load.swift index a259485f1..7cfd51eb5 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Load.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Load.swift @@ -16,7 +16,7 @@ extension AttachmentViewModel { func load(input: Input) async throws -> Output { switch input { case .image(let image): - guard let data = image.pngData() else { + guard let data = image.normalized()?.pngData() else { throw AttachmentError.invalidAttachmentType } return .image(data, imageKind: .png) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift index 18da157c5..e420d9ad1 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift @@ -48,8 +48,8 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable public let api: APIService public let authContext: AuthContext public let input: Input + public let sizeLimit: SizeLimit @Published var caption = "" - // @Published var sizeLimit = SizeLimit() // output @Published public private(set) var output: Output? @@ -77,11 +77,13 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable api: APIService, authContext: AuthContext, input: Input, + sizeLimit: SizeLimit, delegate: AttachmentViewModelDelegate ) { self.api = api self.authContext = authContext self.input = input + self.sizeLimit = sizeLimit self.delegate = delegate super.init() // end init @@ -137,14 +139,17 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable var output = try await load(input: input) switch output { + case .image(let data, _): + self.output = output + self.update(uploadState: .compressing) + let compressedOutput = try await compressImage(data: data, sizeLimit: sizeLimit) + output = compressedOutput case .video(let fileURL, let mimeType): self.output = output self.update(uploadState: .compressing) - let compressedFileURL = try await comporessVideo(url: fileURL) + let compressedFileURL = try await compressVideo(url: fileURL) output = .video(compressedFileURL, mimeType: mimeType) try? FileManager.default.removeItem(at: fileURL) // remove old file - default: - break } self.outputSizeInByte = output.asAttachment.sizeInByte.flatMap { Int64($0) } ?? 0 @@ -262,19 +267,15 @@ extension AttachmentViewModel { } } - // not in using public struct SizeLimit { - public let image: Int - public let gif: Int - public let video: Int + public let image: Int? + public let video: Int? public init( - image: Int = 10 * 1024 * 1024, // 10 MiB - gif: Int = 40 * 1024 * 1024, // 40 MiB - video: Int = 40 * 1024 * 1024 // 40 MiB + image: Int?, + video: Int? ) { self.image = image - self.gif = gif self.video = video } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift index 811da063a..319804fb1 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -435,6 +435,7 @@ extension ComposeContentViewController: PHPickerViewControllerDelegate { api: viewModel.context.apiService, authContext: viewModel.authContext, input: .pickerResult(result), + sizeLimit: viewModel.sizeLimit, delegate: viewModel ) } @@ -453,6 +454,7 @@ extension ComposeContentViewController: UIImagePickerControllerDelegate & UINavi api: viewModel.context.apiService, authContext: viewModel.authContext, input: .image(image), + sizeLimit: viewModel.sizeLimit, delegate: viewModel ) viewModel.attachmentViewModels += [attachmentViewModel] @@ -473,6 +475,7 @@ extension ComposeContentViewController: UIDocumentPickerDelegate { api: viewModel.context.apiService, authContext: viewModel.authContext, input: .url(url), + sizeLimit: viewModel.sizeLimit, delegate: viewModel ) viewModel.attachmentViewModels += [attachmentViewModel] diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 3321aeab7..a1ddb6101 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -88,7 +88,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { // attachment @Published public var attachmentViewModels: [AttachmentViewModel] = [] @Published public var maxMediaAttachmentLimit = 4 - // @Published public internal(set) var isMediaValid = true + @Published public internal(set) var maxImageMediaSizeLimitInByte = 10 * 1024 * 1024 // 10 MiB // poll @Published public var isPollActive = false @@ -126,6 +126,14 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { @Published var isPollButtonEnabled = false @Published public private(set) var shouldDismiss = true + + // size limit + public var sizeLimit: AttachmentViewModel.SizeLimit { + AttachmentViewModel.SizeLimit( + image: maxImageMediaSizeLimitInByte, + video: nil + ) + } public init( context: AppContext, @@ -252,6 +260,10 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { if let maxOptions = configuration.polls?.maxOptions { maxPollOptionLimit = maxOptions } + // set photo attachment limit + if let imageSizeLimit = configuration.mediaAttachments?.imageSizeLimit { + maxImageMediaSizeLimitInByte = imageSizeLimit + } // TODO: more limit } diff --git a/ShareActionExtension/Scene/ShareViewController.swift b/ShareActionExtension/Scene/ShareViewController.swift index 372ce5876..4a093becd 100644 --- a/ShareActionExtension/Scene/ShareViewController.swift +++ b/ShareActionExtension/Scene/ShareViewController.swift @@ -276,6 +276,7 @@ extension ShareViewController { api: context.apiService, authContext: authContext, input: .itemProvider(movieProvider), + sizeLimit: .init(image: nil, video: nil), delegate: composeContentViewModel ) composeContentViewModel.attachmentViewModels.append(attachmentViewModel) @@ -285,6 +286,7 @@ extension ShareViewController { api: context.apiService, authContext: authContext, input: .itemProvider(provider), + sizeLimit: .init(image: nil, video: nil), delegate: composeContentViewModel ) }