Merge pull request #674 from mastodon/fix-665-image-downscaling
Fix no downscaling for raw image from camera issue
This commit is contained in:
commit
0d01e2ad23
@ -209,6 +209,7 @@ extension ComposeViewController {
|
|||||||
api: viewModel.context.apiService,
|
api: viewModel.context.apiService,
|
||||||
authContext: viewModel.authContext,
|
authContext: viewModel.authContext,
|
||||||
input: .image(image),
|
input: .image(image),
|
||||||
|
sizeLimit: composeContentViewModel.sizeLimit,
|
||||||
delegate: composeContentViewModel
|
delegate: composeContentViewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -8,11 +8,12 @@
|
|||||||
import os.log
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
import AVKit
|
import AVKit
|
||||||
import SessionExporter
|
|
||||||
import MastodonCore
|
import MastodonCore
|
||||||
|
import SessionExporter
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
extension AttachmentViewModel {
|
extension AttachmentViewModel {
|
||||||
func comporessVideo(url: URL) async throws -> URL {
|
func compressVideo(url: URL) async throws -> URL {
|
||||||
let urlAsset = AVURLAsset(url: url)
|
let urlAsset = AVURLAsset(url: url)
|
||||||
let exporter = NextLevelSessionExporter(withAsset: urlAsset)
|
let exporter = NextLevelSessionExporter(withAsset: urlAsset)
|
||||||
exporter.outputFileType = .mp4
|
exporter.outputFileType = .mp4
|
||||||
@ -92,3 +93,57 @@ extension AttachmentViewModel {
|
|||||||
}
|
}
|
||||||
} // end func
|
} // end func
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension AttachmentViewModel {
|
||||||
|
@AttachmentViewModelActor
|
||||||
|
func compressImage(data: Data, sizeLimit: SizeLimit) throws -> Output {
|
||||||
|
let maxPayloadSizeInBytes = max((sizeLimit.image ?? 10 * 1024 * 1024), 1 * 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()
|
||||||
|
}
|
||||||
|
@ -16,7 +16,7 @@ extension AttachmentViewModel {
|
|||||||
func load(input: Input) async throws -> Output {
|
func load(input: Input) async throws -> Output {
|
||||||
switch input {
|
switch input {
|
||||||
case .image(let image):
|
case .image(let image):
|
||||||
guard let data = image.pngData() else {
|
guard let data = image.normalized()?.pngData() else {
|
||||||
throw AttachmentError.invalidAttachmentType
|
throw AttachmentError.invalidAttachmentType
|
||||||
}
|
}
|
||||||
return .image(data, imageKind: .png)
|
return .image(data, imageKind: .png)
|
||||||
|
@ -48,8 +48,8 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable
|
|||||||
public let api: APIService
|
public let api: APIService
|
||||||
public let authContext: AuthContext
|
public let authContext: AuthContext
|
||||||
public let input: Input
|
public let input: Input
|
||||||
|
public let sizeLimit: SizeLimit
|
||||||
@Published var caption = ""
|
@Published var caption = ""
|
||||||
// @Published var sizeLimit = SizeLimit()
|
|
||||||
|
|
||||||
// output
|
// output
|
||||||
@Published public private(set) var output: Output?
|
@Published public private(set) var output: Output?
|
||||||
@ -77,11 +77,13 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable
|
|||||||
api: APIService,
|
api: APIService,
|
||||||
authContext: AuthContext,
|
authContext: AuthContext,
|
||||||
input: Input,
|
input: Input,
|
||||||
|
sizeLimit: SizeLimit,
|
||||||
delegate: AttachmentViewModelDelegate
|
delegate: AttachmentViewModelDelegate
|
||||||
) {
|
) {
|
||||||
self.api = api
|
self.api = api
|
||||||
self.authContext = authContext
|
self.authContext = authContext
|
||||||
self.input = input
|
self.input = input
|
||||||
|
self.sizeLimit = sizeLimit
|
||||||
self.delegate = delegate
|
self.delegate = delegate
|
||||||
super.init()
|
super.init()
|
||||||
// end init
|
// end init
|
||||||
@ -137,14 +139,17 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable
|
|||||||
var output = try await load(input: input)
|
var output = try await load(input: input)
|
||||||
|
|
||||||
switch output {
|
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):
|
case .video(let fileURL, let mimeType):
|
||||||
self.output = output
|
self.output = output
|
||||||
self.update(uploadState: .compressing)
|
self.update(uploadState: .compressing)
|
||||||
let compressedFileURL = try await comporessVideo(url: fileURL)
|
let compressedFileURL = try await compressVideo(url: fileURL)
|
||||||
output = .video(compressedFileURL, mimeType: mimeType)
|
output = .video(compressedFileURL, mimeType: mimeType)
|
||||||
try? FileManager.default.removeItem(at: fileURL) // remove old file
|
try? FileManager.default.removeItem(at: fileURL) // remove old file
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.outputSizeInByte = output.asAttachment.sizeInByte.flatMap { Int64($0) } ?? 0
|
self.outputSizeInByte = output.asAttachment.sizeInByte.flatMap { Int64($0) } ?? 0
|
||||||
@ -262,19 +267,15 @@ extension AttachmentViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// not in using
|
|
||||||
public struct SizeLimit {
|
public struct SizeLimit {
|
||||||
public let image: Int
|
public let image: Int?
|
||||||
public let gif: Int
|
public let video: Int?
|
||||||
public let video: Int
|
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
image: Int = 10 * 1024 * 1024, // 10 MiB
|
image: Int?,
|
||||||
gif: Int = 40 * 1024 * 1024, // 40 MiB
|
video: Int?
|
||||||
video: Int = 40 * 1024 * 1024 // 40 MiB
|
|
||||||
) {
|
) {
|
||||||
self.image = image
|
self.image = image
|
||||||
self.gif = gif
|
|
||||||
self.video = video
|
self.video = video
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -435,6 +435,7 @@ extension ComposeContentViewController: PHPickerViewControllerDelegate {
|
|||||||
api: viewModel.context.apiService,
|
api: viewModel.context.apiService,
|
||||||
authContext: viewModel.authContext,
|
authContext: viewModel.authContext,
|
||||||
input: .pickerResult(result),
|
input: .pickerResult(result),
|
||||||
|
sizeLimit: viewModel.sizeLimit,
|
||||||
delegate: viewModel
|
delegate: viewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -453,6 +454,7 @@ extension ComposeContentViewController: UIImagePickerControllerDelegate & UINavi
|
|||||||
api: viewModel.context.apiService,
|
api: viewModel.context.apiService,
|
||||||
authContext: viewModel.authContext,
|
authContext: viewModel.authContext,
|
||||||
input: .image(image),
|
input: .image(image),
|
||||||
|
sizeLimit: viewModel.sizeLimit,
|
||||||
delegate: viewModel
|
delegate: viewModel
|
||||||
)
|
)
|
||||||
viewModel.attachmentViewModels += [attachmentViewModel]
|
viewModel.attachmentViewModels += [attachmentViewModel]
|
||||||
@ -473,6 +475,7 @@ extension ComposeContentViewController: UIDocumentPickerDelegate {
|
|||||||
api: viewModel.context.apiService,
|
api: viewModel.context.apiService,
|
||||||
authContext: viewModel.authContext,
|
authContext: viewModel.authContext,
|
||||||
input: .url(url),
|
input: .url(url),
|
||||||
|
sizeLimit: viewModel.sizeLimit,
|
||||||
delegate: viewModel
|
delegate: viewModel
|
||||||
)
|
)
|
||||||
viewModel.attachmentViewModels += [attachmentViewModel]
|
viewModel.attachmentViewModels += [attachmentViewModel]
|
||||||
|
@ -88,7 +88,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||||||
// attachment
|
// attachment
|
||||||
@Published public var attachmentViewModels: [AttachmentViewModel] = []
|
@Published public var attachmentViewModels: [AttachmentViewModel] = []
|
||||||
@Published public var maxMediaAttachmentLimit = 4
|
@Published public var maxMediaAttachmentLimit = 4
|
||||||
// @Published public internal(set) var isMediaValid = true
|
@Published public internal(set) var maxImageMediaSizeLimitInByte = 10 * 1024 * 1024 // 10 MiB
|
||||||
|
|
||||||
// poll
|
// poll
|
||||||
@Published public var isPollActive = false
|
@Published public var isPollActive = false
|
||||||
@ -126,6 +126,14 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||||||
@Published var isPollButtonEnabled = false
|
@Published var isPollButtonEnabled = false
|
||||||
|
|
||||||
@Published public private(set) var shouldDismiss = true
|
@Published public private(set) var shouldDismiss = true
|
||||||
|
|
||||||
|
// size limit
|
||||||
|
public var sizeLimit: AttachmentViewModel.SizeLimit {
|
||||||
|
AttachmentViewModel.SizeLimit(
|
||||||
|
image: maxImageMediaSizeLimitInByte,
|
||||||
|
video: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
context: AppContext,
|
context: AppContext,
|
||||||
@ -252,6 +260,10 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||||||
if let maxOptions = configuration.polls?.maxOptions {
|
if let maxOptions = configuration.polls?.maxOptions {
|
||||||
maxPollOptionLimit = maxOptions
|
maxPollOptionLimit = maxOptions
|
||||||
}
|
}
|
||||||
|
// set photo attachment limit
|
||||||
|
if let imageSizeLimit = configuration.mediaAttachments?.imageSizeLimit {
|
||||||
|
maxImageMediaSizeLimitInByte = imageSizeLimit
|
||||||
|
}
|
||||||
// TODO: more limit
|
// TODO: more limit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,6 +276,7 @@ extension ShareViewController {
|
|||||||
api: context.apiService,
|
api: context.apiService,
|
||||||
authContext: authContext,
|
authContext: authContext,
|
||||||
input: .itemProvider(movieProvider),
|
input: .itemProvider(movieProvider),
|
||||||
|
sizeLimit: .init(image: nil, video: nil),
|
||||||
delegate: composeContentViewModel
|
delegate: composeContentViewModel
|
||||||
)
|
)
|
||||||
composeContentViewModel.attachmentViewModels.append(attachmentViewModel)
|
composeContentViewModel.attachmentViewModels.append(attachmentViewModel)
|
||||||
@ -285,6 +286,7 @@ extension ShareViewController {
|
|||||||
api: context.apiService,
|
api: context.apiService,
|
||||||
authContext: authContext,
|
authContext: authContext,
|
||||||
input: .itemProvider(provider),
|
input: .itemProvider(provider),
|
||||||
|
sizeLimit: .init(image: nil, video: nil),
|
||||||
delegate: composeContentViewModel
|
delegate: composeContentViewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user