You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1060 lines
44 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import SwiftUI
import QRCode
import CoreData
import Photos
import Combine
internal import SwiftImageReadWrite
#if canImport(PhotosUI)
import PhotosUI
#endif
// MARK: -
enum TabType: String, CaseIterable {
case colors = "colors"
case dots = "dots"
case eyes = "eyes"
case logos = "logos"
var displayName: String {
switch self {
case .colors: return "colors".localized
case .dots: return "dot_types".localized
case .eyes: return "eyes".localized
case .logos: return "logo".localized
}
}
var iconName: String {
switch self {
case .colors: return "paintpalette"
case .dots: return "circle.grid.3x3"
case .eyes: return "eye"
case .logos: return "photo"
}
}
}
// MARK: -
struct QRCodeStyleView: View {
let qrCodeContent: String
let qrCodeType: QRCodeType
let existingStyleData: QRCodeStyleData? //
let historyItem: HistoryItem? //
@Environment(\.dismiss) private var dismiss
@EnvironmentObject var coreDataManager: CoreDataManager
@EnvironmentObject var languageManager: LanguageManager
//
@State private var selectedForegroundColor: QRCodeColor = .black
@State private var selectedBackgroundColor: QRCodeColor = .white
//
@State private var selectedDotType: QRCodeDotType = .square
//
@State private var selectedEyeType: QRCodeEyeType = .square
// Logo
@State private var selectedLogo: QRCodeLogo? = nil
@State private var customLogoImage: UIImage? = nil
@State private var photoLibraryAccessGranted = false
@State private var showingImagePicker = false
//
@State private var isLoading = false
@State private var showingSavedView = false
//
@State private var selectedTabType: TabType = .colors
// QRCode
private func createQRCodeDocument() -> QRCode.Document {
let d = try! QRCode.Document(engine: QRCodeEngineExternal())
// 使
d.utf8String = qrCodeContent
//
d.design.backgroundColor(selectedBackgroundColor.cgColor)
//
d.design.style.eye = QRCode.FillStyle.Solid(selectedForegroundColor.cgColor)
d.design.style.eyeBackground = selectedBackgroundColor.cgColor
//
d.design.shape.onPixels = selectedDotType.pixelShape
d.design.style.onPixels = QRCode.FillStyle.Solid(selectedForegroundColor.cgColor)
d.design.style.onPixelsBackground = selectedBackgroundColor.cgColor
d.design.shape.offPixels = selectedDotType.pixelShape
d.design.style.offPixels = QRCode.FillStyle.Solid(selectedBackgroundColor.cgColor)
d.design.style.offPixelsBackground = selectedBackgroundColor.cgColor
//
d.design.shape.eye = selectedEyeType.eyeShape
// LogoLogo
if let customLogoImage = customLogoImage,
let cgImage = customLogoImage.cgImage {
// 使Logo
print("应用自定义LogoCGImage大小: \(cgImage.width) x \(cgImage.height)")
d.logoTemplate = QRCode.LogoTemplate.CircleCenter(image: cgImage, inset: 0)
} else if let selectedLogo = selectedLogo,
let logoImage = selectedLogo.image,
let cgImage = logoImage.cgImage {
// 使Logo
print("应用预设Logo: \(selectedLogo.displayName)")
d.logoTemplate = QRCode.LogoTemplate.CircleCenter(image: cgImage)
} else {
print("没有设置任何Logo")
}
return d
}
var body: some View {
VStack(spacing: 0) {
//
qrCodePreviewSection
//
styleSelectionSection
}
.navigationTitle("custom_style".localized)
.id(languageManager.refreshTrigger)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("save".localized) {
saveQRCode()
}
.id(languageManager.refreshTrigger)
.font(.system(size: 16, weight: .semibold))
}
}
.onAppear {
checkPhotoLibraryPermission()
initializeExistingStyle()
}
.sheet(isPresented: $showingImagePicker) {
ImagePicker(
onImageSelected: { image in
customLogoImage = image
selectedLogo = nil // Logo
},
shouldProcessImage: true,
targetSize: CGSize(width: 80, height: 80)
)
}
.background(
NavigationLink(
destination: QRCodeSavedView(
qrCodeImage: generateQRCodeImage(),
qrCodeContent: qrCodeContent,
qrCodeType: qrCodeType,
styleData: createStyleData(),
historyItem: historyItem
),
isActive: $showingSavedView
) {
EmptyView()
}
)
}
// MARK: -
private var qrCodePreviewSection: some View {
VStack(spacing: 16) {
QRCodeDocumentUIView(document: createQRCodeDocument())
.frame(width: 300, height: 300)
}
.padding()
.background(Color(.systemBackground))
}
// MARK: -
private var styleSelectionSection: some View {
VStack(spacing: 0) {
//
tabTypeSelection
//
contentArea
}
.background(Color(.systemGroupedBackground))
}
// MARK: -
private var tabTypeSelection: some View {
HStack(spacing: 0) {
ForEach(TabType.allCases, id: \.self) { tabType in
Button(action: {
selectedTabType = tabType
}) {
VStack(spacing: 4) {
Image(systemName: tabType.iconName)
.font(.system(size: 20))
.foregroundColor(selectedTabType == tabType ? .blue : .gray)
Text(tabType.displayName)
.font(.caption)
.foregroundColor(selectedTabType == tabType ? .blue : .gray)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(
Rectangle()
.fill(selectedTabType == tabType ? Color.blue.opacity(0.1) : Color.clear)
)
}
}
}
.background(Color(.systemBackground))
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(Color(.separator)),
alignment: .bottom
)
}
// MARK: -
private var contentArea: some View {
Group {
switch selectedTabType {
case .colors:
colorsContent
case .dots:
dotsContent
case .eyes:
eyesContent
case .logos:
logosContent
}
}
.frame(maxHeight: 400)
}
// MARK: -
private var colorsContent: some View {
ScrollView {
VStack(spacing: 24) {
//
colorSelectionSection(
title: "foreground_color".localized,
colors: QRCodeColor.foregroundColors,
selectedColor: $selectedForegroundColor
)
//
colorSelectionSection(
title: "background_color".localized,
colors: QRCodeColor.backgroundColors,
selectedColor: $selectedBackgroundColor
)
}
.padding()
}
}
// MARK: -
private var dotsContent: some View {
ScrollView {
VStack(spacing: 16) {
Text("select_dot_type".localized)
.font(.title2)
.fontWeight(.bold)
.padding(.top)
.id(languageManager.refreshTrigger)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 16) {
ForEach(QRCodeDotType.allCases, id: \.self) { dotType in
Button(action: {
selectedDotType = dotType
}) {
VStack(spacing: 8) {
if let image = loadImage(named: dotType.thumbnailName) {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.background(Color.white)
.cornerRadius(12)
} else {
RoundedRectangle(cornerRadius: 12)
.fill(Color.gray.opacity(0.3))
.frame(width: 60, height: 60)
.overlay(
Text("?")
.font(.title2)
.foregroundColor(.secondary)
)
}
Text(dotType.displayName)
.font(.caption)
.foregroundColor(.primary)
.multilineTextAlignment(.center)
}
.padding(12)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(selectedDotType == dotType ? Color.blue.opacity(0.1) : Color.clear)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(selectedDotType == dotType ? Color.blue : Color.clear, lineWidth: 3)
)
)
}
}
}
.padding(.horizontal)
}
}
}
// MARK: -
private var eyesContent: some View {
ScrollView {
VStack(spacing: 16) {
Text("select_eye_type".localized)
.font(.title2)
.fontWeight(.bold)
.padding(.top)
.id(languageManager.refreshTrigger)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 16) {
ForEach(QRCodeEyeType.allCases, id: \.self) { eyeType in
Button(action: {
selectedEyeType = eyeType
}) {
VStack(spacing: 8) {
if let image = loadImage(named: eyeType.thumbnailName) {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.background(Color.white)
.cornerRadius(12)
} else {
RoundedRectangle(cornerRadius: 12)
.fill(Color.gray.opacity(0.3))
.frame(width: 60, height: 60)
.overlay(
Text("?")
.font(.title2)
.foregroundColor(.secondary)
)
}
Text(eyeType.displayName)
.font(.caption)
.foregroundColor(.primary)
.multilineTextAlignment(.center)
}
.padding(12)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(selectedEyeType == eyeType ? Color.blue.opacity(0.1) : Color.clear)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(selectedEyeType == eyeType ? Color.blue : Color.clear, lineWidth: 3)
)
)
}
}
}
.padding(.horizontal)
}
}
}
// MARK: - Logo
private var logosContent: some View {
ScrollView {
VStack(spacing: 16) {
Text("select_logo".localized)
.font(.title2)
.fontWeight(.bold)
.padding(.top)
.id(languageManager.refreshTrigger)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 16) {
// Logo
Button(action: {
selectedLogo = nil
customLogoImage = nil
}) {
VStack(spacing: 8) {
RoundedRectangle(cornerRadius: 12)
.fill(Color.gray.opacity(0.3))
.frame(width: 60, height: 60)
.overlay(
Text("none".localized)
.font(.title2)
.foregroundColor(.secondary)
.id(languageManager.refreshTrigger)
)
Text("no_logo".localized)
.font(.caption)
.foregroundColor(.primary)
.multilineTextAlignment(.center)
.id(languageManager.refreshTrigger)
}
.padding(12)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(selectedLogo == nil && customLogoImage == nil ? Color.blue.opacity(0.1) : Color.clear)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(selectedLogo == nil && customLogoImage == nil ? Color.blue : Color.clear, lineWidth: 3)
)
)
}
// Logo
if photoLibraryAccessGranted {
Button(action: {
showingImagePicker = true
}) {
VStack(spacing: 8) {
if let customLogoImage = customLogoImage {
Image(uiImage: customLogoImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.background(Color.white)
.cornerRadius(12)
} else {
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue.opacity(0.2))
.frame(width: 60, height: 60)
.overlay(
Image(systemName: "photo.badge.plus")
.font(.title2)
.foregroundColor(.blue)
)
}
Text("custom".localized)
.font(.caption)
.foregroundColor(.primary)
.multilineTextAlignment(.center)
.id(languageManager.refreshTrigger)
}
.padding(12)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(customLogoImage != nil ? Color.blue.opacity(0.1) : Color.clear)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(customLogoImage != nil ? Color.blue : Color.clear, lineWidth: 3)
)
)
}
} else {
//
Button(action: {
//
if let settingsUrl = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(settingsUrl)
}
}) {
VStack(spacing: 8) {
RoundedRectangle(cornerRadius: 12)
.fill(Color.red.opacity(0.2))
.frame(width: 60, height: 60)
.overlay(
Image(systemName: "exclamationmark.triangle")
.font(.title2)
.foregroundColor(.red)
)
Text("permission_required".localized)
.font(.caption)
.foregroundColor(.red)
.multilineTextAlignment(.center)
.id(languageManager.refreshTrigger)
}
.padding(12)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color.clear)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color.red.opacity(0.3), lineWidth: 1)
)
)
}
}
// Logo
ForEach(QRCodeLogo.allCases, id: \.self) { logo in
Button(action: {
selectedLogo = logo
customLogoImage = nil // Logo
}) {
VStack(spacing: 8) {
if let image = loadImage(named: logo.thumbnailName) {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.background(Color.white)
.cornerRadius(12)
} else {
RoundedRectangle(cornerRadius: 12)
.fill(Color.gray.opacity(0.3))
.frame(width: 60, height: 60)
.overlay(
Text("?")
.font(.title2)
.foregroundColor(.secondary)
)
}
Text(logo.displayName)
.font(.caption)
.foregroundColor(.primary)
.multilineTextAlignment(.center)
}
.padding(12)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(selectedLogo == logo ? Color.blue.opacity(0.1) : Color.clear)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(selectedLogo == logo ? Color.blue : Color.clear, lineWidth: 3)
)
)
}
}
}
.padding(.horizontal)
}
}
}
// MARK: -
private func colorSelectionSection(
title: String,
colors: [QRCodeColor],
selectedColor: Binding<QRCodeColor>
) -> some View {
VStack(alignment: .leading, spacing: 12) {
Text(title)
.font(.headline)
.foregroundColor(.primary)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 6), spacing: 12) {
ForEach(colors, id: \.self) { color in
Button(action: {
selectedColor.wrappedValue = color
}) {
RoundedRectangle(cornerRadius: 8)
.fill(color.color)
.frame(height: 40)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(selectedColor.wrappedValue == color ? Color.blue : Color.clear, lineWidth: 3)
)
}
}
}
}
}
// MARK: -
private func initializeExistingStyle() {
guard let existingStyle = existingStyleData else { return }
//
if let foregroundColor = QRCodeColor(rawValue: existingStyle.foregroundColor) {
selectedForegroundColor = foregroundColor
}
if let backgroundColor = QRCodeColor(rawValue: existingStyle.backgroundColor) {
selectedBackgroundColor = backgroundColor
}
if let dotType = QRCodeDotType(rawValue: existingStyle.dotType) {
selectedDotType = dotType
}
if let eyeType = QRCodeEyeType(rawValue: existingStyle.eyeType) {
selectedEyeType = eyeType
}
// Logo
if existingStyle.hasCustomLogo {
// Logo
customLogoImage = existingStyle.customLogoImage
} else if let logoString = existingStyle.logo,
let logo = QRCodeLogo(rawValue: logoString) {
selectedLogo = logo
}
}
// MARK: -
private func saveQRCode() {
//
if historyItem != nil {
updateExistingHistory()
} else {
saveToHistory()
}
//
showingSavedView = true
}
// MARK: -
private func generateQRCodeImage() -> UIImage {
do {
let imageData = try createQRCodeDocument().pngData(dimension: 600)
return UIImage(data: imageData) ?? UIImage()
} catch {
print("生成二维码图片失败:\(error.localizedDescription)")
return UIImage()
}
}
// MARK: -
private func createStyleData() -> QRCodeStyleData {
var logoIdentifier: String? = nil
var hasCustomLogo = false
var customLogoFileName: String? = nil
if let customLogo = customLogoImage {
// Logo
let fileName = "custom_\(UUID().uuidString).png"
logoIdentifier = "custom_\(UUID().uuidString)"
hasCustomLogo = true
customLogoFileName = fileName
//
saveCustomLogoToFile(customLogo, fileName: fileName)
print("🖼️ 自定义Logo已保存到文件\(fileName)")
} else if let selectedLogo = selectedLogo {
// Logo
logoIdentifier = selectedLogo.rawValue
hasCustomLogo = false
print("🏷️ 使用预设Logo\(selectedLogo.rawValue)")
}
return QRCodeStyleData(
foregroundColor: selectedForegroundColor.rawValue,
backgroundColor: selectedBackgroundColor.rawValue,
dotType: selectedDotType.rawValue,
eyeType: selectedEyeType.rawValue,
logo: logoIdentifier,
hasCustomLogo: hasCustomLogo,
customLogoFileName: customLogoFileName
)
}
// MARK: -
private func saveToHistory() {
// 线Core Data
DispatchQueue.main.async {
do {
let context = self.coreDataManager.container.viewContext
let historyItem = HistoryItem(context: context)
historyItem.id = UUID()
historyItem.dataType = DataType.qrcode.rawValue
historyItem.dataSource = DataSource.created.rawValue
historyItem.createdAt = Date()
historyItem.isFavorite = false
historyItem.qrCodeType = self.qrCodeType.rawValue
historyItem.content = self.qrCodeContent
print("📝 创建历史记录项:\(self.qrCodeContent)")
//
let styleData = self.createStyleData()
print("🎨 样式数据创建成功:\(styleData.styleDescription)")
// JSON
do {
let jsonData = try JSONEncoder().encode(styleData)
let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
historyItem.qrCodeStyleData = jsonString
print("✅ 样式数据已转换为JSON并设置到历史记录项")
print("📄 JSON字符串长度\(jsonString.count)")
} catch {
print("❌ 样式数据JSON编码失败\(error)")
}
//
if let savedJsonString = historyItem.qrCodeStyleData {
print("✅ 样式数据验证成功JSON字符串长度 \(savedJsonString.count)")
} else {
print("❌ 样式数据验证失败:数据未正确设置")
}
// Core Data
try context.save()
print("✅ 自定义二维码保存成功:\(self.qrCodeContent)")
//
self.coreDataManager.objectWillChange.send()
} catch {
print("❌ Core Data保存失败\(error.localizedDescription)")
print("❌ 错误详情:\(error)")
// NSError
if let nsError = error as NSError? {
print("❌ 错误域:\(nsError.domain)")
print("❌ 错误代码:\(nsError.code)")
print("❌ 用户信息:\(nsError.userInfo)")
}
}
}
}
// MARK: -
private func updateExistingHistory() {
guard let existingHistoryItem = historyItem else { return }
// 线Core Data
DispatchQueue.main.async {
do {
let context = self.coreDataManager.container.viewContext
//
let styleData = self.createStyleData()
print("🎨 样式数据更新成功:\(styleData.styleDescription)")
// JSON
do {
let jsonData = try JSONEncoder().encode(styleData)
let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
existingHistoryItem.qrCodeStyleData = jsonString
print("✅ 样式数据已更新到历史记录项")
} catch {
print("❌ 样式数据JSON编码失败\(error)")
}
// Core Data
try context.save()
print("✅ 二维码样式更新成功")
//
self.coreDataManager.objectWillChange.send()
} catch {
print("❌ Core Data更新失败\(error.localizedDescription)")
print("❌ 错误详情:\(error)")
// NSError
if let nsError = error as NSError? {
print("❌ 错误域:\(nsError.domain)")
print("❌ 错误代码:\(nsError.code)")
print("❌ 用户信息:\(nsError.userInfo)")
}
}
}
}
// MARK: -
private func checkPhotoLibraryPermission() {
let status = PHPhotoLibrary.authorizationStatus()
print("相册权限状态: \(status.rawValue)")
switch status {
case .authorized, .limited:
photoLibraryAccessGranted = true
print("相册权限已授权")
case .denied, .restricted:
photoLibraryAccessGranted = false
print("相册权限被拒绝")
case .notDetermined:
print("相册权限未确定,正在请求...")
PHPhotoLibrary.requestAuthorization { newStatus in
DispatchQueue.main.async {
self.photoLibraryAccessGranted = (newStatus == .authorized || newStatus == .limited)
print("权限请求结果: \(newStatus.rawValue), 授权状态: \(self.photoLibraryAccessGranted)")
}
}
@unknown default:
photoLibraryAccessGranted = false
print("相册权限未知状态")
}
}
// MARK: - Logo
private func saveCustomLogoToFile(_ image: UIImage, fileName: String) {
//
let maxSize: CGFloat = 200 //
let resizedImage = resizeImage(image, to: CGSize(width: maxSize, height: maxSize))
guard let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
print("❌ 无法获取文档目录")
return
}
let customLogosPath = documentsPath.appendingPathComponent("CustomLogos")
// CustomLogos
do {
try FileManager.default.createDirectory(at: customLogosPath, withIntermediateDirectories: true, attributes: nil)
} catch {
print("❌ 创建CustomLogos目录失败\(error)")
return
}
//
let imagePath = customLogosPath.appendingPathComponent(fileName)
if let imageData = resizedImage.pngData() {
do {
try imageData.write(to: imagePath)
print("✅ 自定义Logo保存成功\(fileName)")
} catch {
print("❌ 保存自定义Logo失败\(error)")
}
} else {
print("❌ 转换图片数据失败")
}
}
// MARK: -
private func resizeImage(_ image: UIImage, to size: CGSize) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { context in
image.draw(in: CGRect(origin: .zero, size: size))
}
}
// MARK: -
private func loadImage(named name: String) -> UIImage? {
// 1: Bundle
if let image = UIImage(named: name) {
return image
}
// 2: Resources
let subdirectories = ["dots", "eyes", "logos"]
for subdirectory in subdirectories {
if let path = Bundle.main.path(forResource: name, ofType: "png", inDirectory: "Resources/\(subdirectory)") {
return UIImage(contentsOfFile: path)
}
}
// 3: BundleResources
if Bundle.main.path(forResource: "Resources", ofType: nil) != nil {
for subdirectory in subdirectories {
if let imagePath = Bundle.main.path(forResource: name, ofType: "png", inDirectory: subdirectory) {
return UIImage(contentsOfFile: imagePath)
}
}
}
// 4: Assets.xcassets
if let image = UIImage(named: name, in: Bundle.main, with: nil) {
return image
}
// 5: Bundle
if let path = Bundle.main.path(forResource: name, ofType: "png") {
return UIImage(contentsOfFile: path)
}
return nil
}
}
// MARK: -
struct ImagePicker: UIViewControllerRepresentable {
let onImageSelected: (UIImage) -> Void
let shouldProcessImage: Bool
let targetSize: CGSize?
init(onImageSelected: @escaping (UIImage) -> Void, shouldProcessImage: Bool = false, targetSize: CGSize? = nil) {
self.onImageSelected = onImageSelected
self.shouldProcessImage = shouldProcessImage
self.targetSize = targetSize
}
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.sourceType = .photoLibrary
picker.allowsEditing = false
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[.originalImage] as? UIImage {
let finalImage: UIImage
if parent.shouldProcessImage, let targetSize = parent.targetSize {
//
finalImage = processImageToSquare(image: image, targetSize: targetSize)
//
let memorySize = calculateImageMemorySize(image: finalImage)
print("📊 最终自定义图片内存大小: \(memorySize)")
} else {
// 使
finalImage = image
}
parent.onImageSelected(finalImage)
}
picker.dismiss(animated: true)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
}
// 5KB
private func processImageToSquare(image: UIImage, targetSize: CGSize) -> UIImage {
let originalSize = image.size
//
let squareSize = min(originalSize.width, originalSize.height)
//
let cropX = (originalSize.width - squareSize) / 2
let cropY = (originalSize.height - squareSize) / 2
let cropRect = CGRect(x: cropX, y: cropY, width: squareSize, height: squareSize)
//
guard let cgImage = image.cgImage?.cropping(to: cropRect) else {
return image
}
// UIImage
let croppedImage = UIImage(cgImage: cgImage)
//
let renderer = UIGraphicsImageRenderer(size: targetSize)
let scaledImage = renderer.image { context in
croppedImage.draw(in: CGRect(origin: .zero, size: targetSize))
}
// 5KB
return compressImageToTargetSize(scaledImage, targetSizeInKB: 5.0)
}
//
private func compressImageToTargetSize(_ image: UIImage, targetSizeInKB: Double) -> UIImage {
let targetSizeInBytes = Int64(targetSizeInKB * 1024)
//
let compressionQualities: [CGFloat] = [0.8, 0.6, 0.4, 0.2, 0.1, 0.05]
for quality in compressionQualities {
if let imageData = image.jpegData(compressionQuality: quality) {
let dataSize = Int64(imageData.count)
if dataSize <= targetSizeInBytes {
// UIImage
if let compressedImage = UIImage(data: imageData) {
print("✅ 图片压缩成功: \(dataSize) bytes (质量: \(quality))")
return compressedImage
}
}
}
}
// JPEG
print("⚠️ JPEG压缩后仍超过目标大小尝试缩小尺寸")
return compressImageByReducingSize(image, targetSizeInBytes: targetSizeInBytes)
}
//
private func compressImageByReducingSize(_ image: UIImage, targetSizeInBytes: Int64) -> UIImage {
let originalSize = image.size
let originalWidth = originalSize.width
let originalHeight = originalSize.height
//
let currentMemorySize = Int64(originalWidth * originalHeight * 4) // RGBA
//
let scaleFactor = sqrt(Double(targetSizeInBytes) / Double(currentMemorySize))
let newWidth = max(originalWidth * CGFloat(scaleFactor), 40) // 40
let newHeight = max(originalHeight * CGFloat(scaleFactor), 40)
let newSize = CGSize(width: newWidth, height: newHeight)
//
let renderer = UIGraphicsImageRenderer(size: newSize)
let resizedImage = renderer.image { context in
image.draw(in: CGRect(origin: .zero, size: newSize))
}
// JPEG
if let imageData = resizedImage.jpegData(compressionQuality: 0.3) {
let finalSize = Int64(imageData.count)
print("✅ 通过缩小尺寸压缩成功: \(finalSize) bytes (新尺寸: \(newWidth) x \(newHeight))")
if let finalImage = UIImage(data: imageData) {
return finalImage
}
}
//
print("⚠️ 无法压缩到目标大小,返回最小尺寸图片")
let minSize = CGSize(width: 40, height: 40)
let rendererMin = UIGraphicsImageRenderer(size: minSize)
return rendererMin.image { context in
image.draw(in: CGRect(origin: .zero, size: minSize))
}
}
//
private func calculateImageMemorySize(image: UIImage) -> String {
guard let cgImage = image.cgImage else {
return "无法计算"
}
let width = cgImage.width
let height = cgImage.height
let bitsPerComponent = cgImage.bitsPerComponent
let bytesPerRow = cgImage.bytesPerRow
_ = cgImage.colorSpace
//
let memorySizeInBytes = height * bytesPerRow
//
let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useKB, .useMB]
formatter.countStyle = .memory
let memorySizeString = formatter.string(fromByteCount: Int64(memorySizeInBytes))
//
return "\(memorySizeString) (\(width) x \(height), \(bitsPerComponent) bits/component, \(bytesPerRow) bytes/row)"
}
}
}
// MARK: -
#Preview {
QRCodeStyleView(qrCodeContent: "https://www.example.com", qrCodeType: .url, existingStyleData: nil, historyItem: nil)
.environmentObject(CoreDataManager.shared)
.environmentObject(LanguageManager.shared)
}