Files
suixinkan_uikit/suixinkan/Features/TravelAlbum/Services/TravelAlbumAIEditResultStore.swift
2026-07-31 16:48:28 +08:00

309 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// TravelAlbumAIEditResultStore.swift
// suixinkan
//
import CoreImage
import UIKit
/// AI
@MainActor
final class TravelAlbumAIEditResultStore {
///
struct Record {
var state: TravelAlbumAIEditResultState
var editedImage: UIImage?
var atmosphereImage: UIImage?
var coverImage: UIImage?
}
static let shared = TravelAlbumAIEditResultStore()
static let didChangeNotification = Notification.Name("TravelAlbumAIEditResultStore.didChange")
static let materialIDUserInfoKey = "materialID"
private var recordsByMaterialID: [Int: Record] = [:]
private init() {}
///
func state(for materialID: Int) -> TravelAlbumAIEditResultState {
recordsByMaterialID[materialID]?.state ?? TravelAlbumAIEditResultState()
}
///
func editedImage(for materialID: Int) -> UIImage? {
recordsByMaterialID[materialID]?.editedImage
}
///
func atmosphereImage(for materialID: Int) -> UIImage? {
recordsByMaterialID[materialID]?.atmosphereImage
}
///
func coverImage(for materialID: Int) -> UIImage? {
recordsByMaterialID[materialID]?.coverImage
}
///
func startProcessing(materialIDs: [Int]) {
materialIDs.forEach { materialID in
var record = recordsByMaterialID[materialID]
?? Record(
state: TravelAlbumAIEditResultState(),
editedImage: nil,
atmosphereImage: nil,
coverImage: nil
)
record.state.startProcessing()
recordsByMaterialID[materialID] = record
notifyChange(materialID: materialID)
}
}
///
func complete(
materialID: Int,
presetID: String,
atmosphereOptionID: String? = nil,
coverTemplateID: String? = nil,
editedImage: UIImage,
atmosphereImage: UIImage? = nil,
coverImage: UIImage? = nil
) {
var record = recordsByMaterialID[materialID]
?? Record(
state: TravelAlbumAIEditResultState(),
editedImage: nil,
atmosphereImage: nil,
coverImage: nil
)
record.state.complete(
presetID: presetID,
atmosphereOptionID: atmosphereOptionID,
coverTemplateID: coverTemplateID
)
record.editedImage = editedImage
record.atmosphereImage = atmosphereImage
record.coverImage = coverImage
recordsByMaterialID[materialID] = record
notifyChange(materialID: materialID)
}
///
func failProcessing(materialID: Int) {
guard var record = recordsByMaterialID[materialID] else { return }
record.state.failProcessing()
recordsByMaterialID[materialID] = record
notifyChange(materialID: materialID)
}
///
func restoreOriginal(materialID: Int) {
guard var record = recordsByMaterialID[materialID] else { return }
record.state.restoreOriginal()
record.editedImage = nil
record.atmosphereImage = nil
record.coverImage = nil
recordsByMaterialID[materialID] = record
notifyChange(materialID: materialID)
}
///
func selectDisplayMode(_ mode: TravelAlbumAIEditResultState.DisplayMode, materialID: Int) {
guard var record = recordsByMaterialID[materialID] else { return }
record.state.selectDisplayMode(mode)
recordsByMaterialID[materialID] = record
notifyChange(materialID: materialID)
}
///
func remove(materialID: Int) {
recordsByMaterialID.removeValue(forKey: materialID)
notifyChange(materialID: materialID)
}
private func notifyChange(materialID: Int) {
NotificationCenter.default.post(
name: Self.didChangeNotification,
object: self,
userInfo: [Self.materialIDUserInfoKey: materialID]
)
}
}
/// AI
@MainActor
enum TravelAlbumAIEditImageProcessor {
private static let context = CIContext()
///
static func render(
effect: TravelAlbumEditPreset.Effect,
source: UIImage
) -> UIImage {
guard effect != .original, let ciImage = CIImage(image: source) else { return source }
let output: CIImage
switch effect {
case .original:
output = ciImage
case .portrait:
output = ciImage.applyingFilter("CIColorControls", parameters: [
kCIInputSaturationKey: 0.86,
kCIInputBrightnessKey: 0.07,
kCIInputContrastKey: 1.08,
])
case .vintage:
output = ciImage.applyingFilter("CISepiaTone", parameters: [kCIInputIntensityKey: 0.45])
case .brocade:
output = ciImage.applyingFilter("CIColorControls", parameters: [
kCIInputSaturationKey: 1.22,
kCIInputBrightnessKey: 0.04,
kCIInputContrastKey: 1.1,
])
case .distantMountain:
output = ciImage.applyingFilter("CIColorControls", parameters: [
kCIInputSaturationKey: 0.72,
kCIInputBrightnessKey: 0.1,
kCIInputContrastKey: 0.9,
])
case .mist:
output = ciImage.applyingFilter("CIColorControls", parameters: [
kCIInputSaturationKey: 0.7,
kCIInputBrightnessKey: 0.14,
kCIInputContrastKey: 0.82,
])
case .summer:
output = ciImage.applyingFilter("CISepiaTone", parameters: [kCIInputIntensityKey: 0.24])
case .rich:
output = ciImage.applyingFilter("CIColorControls", parameters: [
kCIInputSaturationKey: 1.38,
kCIInputBrightnessKey: -0.02,
kCIInputContrastKey: 1.2,
])
}
guard let cgImage = context.createCGImage(output, from: output.extent) else { return source }
return UIImage(cgImage: cgImage, scale: source.scale, orientation: source.imageOrientation)
}
}
///
@MainActor
enum TravelAlbumCoverTemplateImageProcessor {
/// 使
static func render(
template: TravelAlbumCoverTemplate,
source: UIImage?,
targetSize: CGSize = CGSize(width: 1200, height: 900)
) -> UIImage {
UIGraphicsImageRenderer(size: targetSize).image { context in
let bounds = CGRect(origin: .zero, size: targetSize)
UIColor(hex: 0xCAD8E8).setFill()
context.cgContext.fill(bounds)
if let source {
source.draw(in: aspectFillRect(imageSize: source.size, bounds: bounds))
}
let unit = min(targetSize.width / 232, targetSize.height / 208)
switch template.previewStyle {
case .travelMemoir:
UIColor.black.withAlphaComponent(0.24).setFill()
context.cgContext.fill(bounds)
UIColor.white.setStroke()
let oval = UIBezierPath(ovalIn: CGRect(
x: bounds.midX - 50 * unit,
y: 28 * unit,
width: 100 * unit,
height: 100 * unit
))
oval.lineWidth = 9 * unit
oval.stroke()
drawCaption(
"TRAVEL\nMEMOIR",
in: CGRect(x: 30 * unit, y: bounds.height - 62 * unit, width: bounds.width - 60 * unit, height: 46 * unit),
alignment: .center,
fontSize: 17 * unit
)
case .minimal:
UIColor.white.withAlphaComponent(0.9).setFill()
context.cgContext.fill(CGRect(x: bounds.width * 0.55, y: 0, width: bounds.width * 0.45, height: bounds.height))
drawCaption(
"LESS\nIS MORE",
in: CGRect(x: bounds.width * 0.62, y: bounds.midY - 27 * unit, width: bounds.width * 0.3, height: 54 * unit),
alignment: .left,
color: .black,
fontSize: 17 * unit
)
case .scenicStory:
UIColor.black.withAlphaComponent(0.2).setFill()
context.cgContext.fill(bounds)
UIColor.white.setStroke()
let inset = 20 * unit
let frame = UIBezierPath(rect: bounds.insetBy(dx: inset, dy: inset))
frame.lineWidth = 4 * unit
frame.stroke()
drawCaption(
"SCENIC STORY",
in: CGRect(x: 34 * unit, y: bounds.height - 58 * unit, width: bounds.width - 68 * unit, height: 24 * unit),
alignment: .center,
fontSize: 17 * unit
)
case .film:
let stripHeight = 24 * unit
UIColor.black.withAlphaComponent(0.72).setFill()
context.cgContext.fill(CGRect(x: 0, y: 0, width: bounds.width, height: stripHeight))
context.cgContext.fill(CGRect(x: 0, y: bounds.height - stripHeight, width: bounds.width, height: stripHeight))
UIColor.white.withAlphaComponent(0.9).setFill()
var x = 10 * unit
while x <= bounds.width - 16 * unit {
context.cgContext.fill(CGRect(x: x, y: 6 * unit, width: 14 * unit, height: 10 * unit))
context.cgContext.fill(CGRect(
x: x,
y: bounds.height - 16 * unit,
width: 14 * unit,
height: 10 * unit
))
x += 30 * unit
}
drawCaption(
"THE JOURNEY",
in: CGRect(x: 28 * unit, y: bounds.height - 60 * unit, width: bounds.width - 56 * unit, height: 24 * unit),
alignment: .center,
fontSize: 17 * unit
)
}
}
}
private static func aspectFillRect(imageSize: CGSize, bounds: CGRect) -> CGRect {
guard imageSize.width > 0, imageSize.height > 0 else { return bounds }
let scale = max(bounds.width / imageSize.width, bounds.height / imageSize.height)
let drawSize = CGSize(width: imageSize.width * scale, height: imageSize.height * scale)
return CGRect(
x: bounds.midX - drawSize.width / 2,
y: bounds.midY - drawSize.height / 2,
width: drawSize.width,
height: drawSize.height
)
}
private static func drawCaption(
_ text: String,
in rect: CGRect,
alignment: NSTextAlignment,
color: UIColor = .white,
fontSize: CGFloat
) {
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = alignment
text.draw(
in: rect,
withAttributes: [
.font: UIFont.systemFont(ofSize: fontSize, weight: .bold),
.foregroundColor: color,
.paragraphStyle: paragraph,
]
)
}
}