Refactor QRCodeColor enum in QRCodeStyleModels to remove gradient color cases, simplifying color options for QR code customization. Update ScannerView to enhance image picker functionality with new parameters for image processing. Revise QRCodeStyleView to streamline image selection and processing, including automatic resizing and compression of selected images, improving overall user experience in logo customization.

main
v504 1 year ago
parent 9bd7effd4c
commit 3d48275c62

@ -24,20 +24,6 @@ enum QRCodeColor: String, CaseIterable, Hashable {
case olive = "olive" case olive = "olive"
case silver = "silver" case silver = "silver"
// 渐变色
case gradientRed = "gradientRed"
case gradientBlue = "gradientBlue"
case gradientGreen = "gradientGreen"
case gradientPurple = "gradientPurple"
case gradientOrange = "gradientOrange"
case gradientPink = "gradientPink"
case gradientYellow = "gradientYellow"
case gradientCyan = "gradientCyan"
case gradientMagenta = "gradientMagenta"
case gradientTeal = "gradientTeal"
case gradientIndigo = "gradientIndigo"
case gradientLime = "gradientLime"
var color: Color { var color: Color {
switch self { switch self {
case .black: return Color(red: 0, green: 0, blue: 0) case .black: return Color(red: 0, green: 0, blue: 0)
@ -60,18 +46,6 @@ enum QRCodeColor: String, CaseIterable, Hashable {
case .maroon: return Color(red: 0.5, green: 0, blue: 0) case .maroon: return Color(red: 0.5, green: 0, blue: 0)
case .olive: return Color(red: 0.5, green: 0.5, blue: 0) case .olive: return Color(red: 0.5, green: 0.5, blue: 0)
case .silver: return Color(red: 0.75, green: 0.75, blue: 0.75) case .silver: return Color(red: 0.75, green: 0.75, blue: 0.75)
case .gradientRed: return Color(red: 1, green: 0, blue: 0)
case .gradientBlue: return Color(red: 0, green: 0, blue: 1)
case .gradientGreen: return Color(red: 0, green: 1, blue: 0)
case .gradientPurple: return Color(red: 0.5, green: 0, blue: 0.5)
case .gradientOrange: return Color(red: 1, green: 0.5, blue: 0)
case .gradientPink: return Color(red: 1, green: 0.75, blue: 0.8)
case .gradientYellow: return Color(red: 1, green: 1, blue: 0)
case .gradientCyan: return Color(red: 0, green: 1, blue: 1)
case .gradientMagenta: return Color(red: 1, green: 0, blue: 1)
case .gradientTeal: return Color(red: 0, green: 0.5, blue: 0.5)
case .gradientIndigo: return Color(red: 0.3, green: 0, blue: 0.5)
case .gradientLime: return Color(red: 0.5, green: 1, blue: 0)
} }
} }
@ -80,7 +54,7 @@ enum QRCodeColor: String, CaseIterable, Hashable {
} }
static var foregroundColors: [QRCodeColor] { static var foregroundColors: [QRCodeColor] {
return [.black, .red, .blue, .green, .purple, .orange, .pink, .brown, .navy, .teal, .indigo, .maroon, .olive, .gradientRed, .gradientBlue, .gradientGreen, .gradientPurple, .gradientOrange, .gradientPink, .gradientYellow, .gradientCyan, .gradientMagenta, .gradientTeal, .gradientIndigo, .gradientLime] return [.black, .red, .blue, .green, .purple, .orange, .pink, .brown, .navy, .teal, .indigo, .maroon, .olive]
} }
static var backgroundColors: [QRCodeColor] { static var backgroundColors: [QRCodeColor] {

@ -81,7 +81,10 @@ struct ScannerView: View {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(false) .navigationBarBackButtonHidden(false)
.sheet(isPresented: $showImagePicker) { .sheet(isPresented: $showImagePicker) {
ImagePicker(onImageSelected: handleImageDecodeResult) ImagePicker(
onImageSelected: handleImageDecodeResult,
shouldProcessImage: false
)
} }
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
@ -515,46 +518,7 @@ struct ScannerView: View {
} }
} }
// MARK: - 图片选择器(兼容iOS 15)
struct ImagePicker: UIViewControllerRepresentable {
let onImageSelected: (UIImage) -> Void
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.sourceType = .photoLibrary
picker.allowsEditing = true
picker.videoQuality = .typeHigh
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 editedImage = info[.editedImage] as? UIImage {
parent.onImageSelected(editedImage)
} else if let image = info[.originalImage] as? UIImage {
parent.onImageSelected(image)
}
picker.dismiss(animated: true)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
}
}
}
// MARK: - 解码失败提示覆盖层 // MARK: - 解码失败提示覆盖层
struct DecodeFailureOverlay: View { struct DecodeFailureOverlay: View {

@ -51,11 +51,8 @@ struct QRCodeStyleView: View {
// Logo选择 // Logo选择
@State private var selectedLogo: QRCodeLogo? = nil @State private var selectedLogo: QRCodeLogo? = nil
@State private var customLogoImage: UIImage? = nil @State private var customLogoImage: UIImage? = nil
@State private var photoPickerItem: Any? = nil
@State private var photoLibraryAccessGranted = false @State private var photoLibraryAccessGranted = false
@State private var showingImagePicker = false @State private var showingImagePicker = false
@State private var showingImageCropper = false
@State private var imageToCrop: UIImage? = nil
// 生成的二维码图片 // 生成的二维码图片
@State private var qrCodeImage: UIImage? @State private var qrCodeImage: UIImage?
@ -95,7 +92,7 @@ struct QRCodeStyleView: View {
let cgImage = customLogoImage.cgImage { let cgImage = customLogoImage.cgImage {
// 使用自定义Logo // 使用自定义Logo
print("应用自定义Logo,CGImage大小: \(cgImage.width) x \(cgImage.height)") print("应用自定义Logo,CGImage大小: \(cgImage.width) x \(cgImage.height)")
d.logoTemplate = QRCode.LogoTemplate.CircleCenter(image: cgImage) d.logoTemplate = QRCode.LogoTemplate.CircleCenter(image: cgImage, inset: 0)
} else if let selectedLogo = selectedLogo, } else if let selectedLogo = selectedLogo,
let logoImage = selectedLogo.image, let logoImage = selectedLogo.image,
let cgImage = logoImage.cgImage { let cgImage = logoImage.cgImage {
@ -131,19 +128,14 @@ struct QRCodeStyleView: View {
checkPhotoLibraryPermission() checkPhotoLibraryPermission()
} }
.sheet(isPresented: $showingImagePicker) { .sheet(isPresented: $showingImagePicker) {
ImagePicker { image in ImagePicker(
imageToCrop = image onImageSelected: { image in
showingImageCropper = true customLogoImage = image
}
}
.sheet(isPresented: $showingImageCropper) {
if let imageToCrop = imageToCrop {
ImageCropperView(image: imageToCrop) { croppedImage in
customLogoImage = croppedImage
selectedLogo = nil // 清除预设Logo选择 selectedLogo = nil // 清除预设Logo选择
self.imageToCrop = nil },
} shouldProcessImage: true,
} targetSize: CGSize(width: 80, height: 80)
)
} }
} }
@ -640,193 +632,187 @@ struct QRCodeStyleView: View {
} }
} }
// MARK: - 图片裁剪视图
struct ImageCropperView: View {
let image: UIImage // MARK: - 图片选择器
let onCropComplete: (UIImage) -> Void struct ImagePicker: UIViewControllerRepresentable {
let onImageSelected: (UIImage) -> Void
let shouldProcessImage: Bool
let targetSize: CGSize?
@Environment(\.dismiss) private var dismiss init(onImageSelected: @escaping (UIImage) -> Void, shouldProcessImage: Bool = false, targetSize: CGSize? = nil) {
@State private var scale: CGFloat = 1.0 self.onImageSelected = onImageSelected
@State private var lastScale: CGFloat = 1.0 self.shouldProcessImage = shouldProcessImage
@State private var offset: CGSize = .zero self.targetSize = targetSize
@State private var lastOffset: CGSize = .zero }
var body: some View { func makeUIViewController(context: Context) -> UIImagePickerController {
NavigationView { let picker = UIImagePickerController()
GeometryReader { geometry in picker.delegate = context.coordinator
ZStack { picker.sourceType = .photoLibrary
Color.black picker.allowsEditing = false
.ignoresSafeArea() 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)
VStack { // 打印最终图片的内存大小
// 裁剪区域 let memorySize = calculateImageMemorySize(image: finalImage)
ZStack { print("📊 最终自定义图片内存大小: \(memorySize)")
// 背景图片 } else {
Image(uiImage: image) // 不处理图片,直接使用原图
.resizable() finalImage = image
.aspectRatio(contentMode: .fit)
.scaleEffect(scale)
.offset(offset)
.gesture(
SimultaneousGesture(
MagnificationGesture()
.onChanged { value in
let delta = value / lastScale
lastScale = value
scale = min(max(scale * delta, 0.5), 3.0)
}
.onEnded { _ in
lastScale = 1.0
},
DragGesture()
.onChanged { value in
let delta = CGSize(
width: value.translation.width - lastOffset.width,
height: value.translation.height - lastOffset.height
)
lastOffset = value.translation
offset = CGSize(
width: offset.width + delta.width,
height: offset.height + delta.height
)
}
.onEnded { _ in
lastOffset = .zero
}
)
)
// 裁剪框
CropOverlay()
}
.frame(height: geometry.size.width) // 保持正方形
.clipped()
Spacer()
// 提示文字
Text("拖动和缩放来选择圆形Logo区域")
.foregroundColor(.white)
.font(.caption)
.padding(.bottom, 20)
}
}
}
.navigationTitle("裁剪圆形Logo")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("取消") {
dismiss()
}
.foregroundColor(.white)
} }
ToolbarItem(placement: .navigationBarTrailing) { parent.onImageSelected(finalImage)
Button("完成") {
let croppedImage = cropImage()
onCropComplete(croppedImage)
dismiss()
}
.foregroundColor(.white)
.font(.system(size: 16, weight: .semibold))
}
} }
picker.dismiss(animated: true)
} }
}
private func cropImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 80, height: 80))
return renderer.image { context in func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
// 计算裁剪区域 picker.dismiss(animated: true)
let imageSize = image.size }
let viewSize = CGSize(width: 80, height: 80)
// 处理图片为正方形并缩放到指定大小,同时压缩到5KB以下
private func processImageToSquare(image: UIImage, targetSize: CGSize) -> UIImage {
let originalSize = image.size
// 计算图片在视图中的实际大小和位置 // 计算正方形边长(取较小的边)
let imageAspectRatio = imageSize.width / imageSize.height let squareSize = min(originalSize.width, originalSize.height)
let viewAspectRatio = viewSize.width / viewSize.height
let scaledImageSize: CGSize // 计算裁剪区域(居中裁剪)
let scaledImageOffset: CGPoint let cropX = (originalSize.width - squareSize) / 2
let cropY = (originalSize.height - squareSize) / 2
let cropRect = CGRect(x: cropX, y: cropY, width: squareSize, height: squareSize)
if imageAspectRatio > viewAspectRatio { // 裁剪图片
// 图片更宽,以高度为准 guard let cgImage = image.cgImage?.cropping(to: cropRect) else {
scaledImageSize = CGSize(width: viewSize.height * imageAspectRatio, height: viewSize.height) return image
scaledImageOffset = CGPoint(x: (viewSize.width - scaledImageSize.width) / 2, y: 0)
} else {
// 图片更高,以宽度为准
scaledImageSize = CGSize(width: viewSize.width, height: viewSize.width / imageAspectRatio)
scaledImageOffset = CGPoint(x: 0, y: (viewSize.height - scaledImageSize.height) / 2)
} }
// 应用缩放和偏移 // 创建新的UIImage
let finalImageSize = CGSize( let croppedImage = UIImage(cgImage: cgImage)
width: scaledImageSize.width * scale,
height: scaledImageSize.height * scale
)
let finalImageOffset = CGPoint( // 缩放到目标大小
x: scaledImageOffset.x + offset.width, let renderer = UIGraphicsImageRenderer(size: targetSize)
y: scaledImageOffset.y + offset.height let scaledImage = renderer.image { context in
) croppedImage.draw(in: CGRect(origin: .zero, size: targetSize))
}
// 计算裁剪区域 // 压缩图片到5KB以下
let cropRect = CGRect( return compressImageToTargetSize(scaledImage, targetSizeInKB: 5.0)
x: -finalImageOffset.x, }
y: -finalImageOffset.y,
width: finalImageSize.width, // 压缩图片到指定大小以下
height: finalImageSize.height 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 {
let circlePath = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 80, height: 80)) if let imageData = image.jpegData(compressionQuality: quality) {
circlePath.addClip() let dataSize = Int64(imageData.count)
if dataSize <= targetSizeInBytes {
// 如果压缩后的数据大小符合要求,重新创建UIImage
if let compressedImage = UIImage(data: imageData) {
print("✅ 图片压缩成功: \(dataSize) bytes (质量: \(quality))")
return compressedImage
}
}
}
}
// 绘制裁剪后的图片 // 如果JPEG压缩仍然太大,尝试进一步缩小尺寸
image.draw(in: cropRect) print("⚠️ JPEG压缩后仍超过目标大小,尝试缩小尺寸")
return compressImageByReducingSize(image, targetSizeInBytes: targetSizeInBytes)
} }
}
} // 通过缩小尺寸来压缩图片
private func compressImageByReducingSize(_ image: UIImage, targetSizeInBytes: Int64) -> UIImage {
// MARK: - 裁剪覆盖层 let originalSize = image.size
struct CropOverlay: View { let originalWidth = originalSize.width
var body: some View { let originalHeight = originalSize.height
GeometryReader { geometry in
ZStack { // 计算当前图片的内存大小
// 半透明遮罩 let currentMemorySize = Int64(originalWidth * originalHeight * 4) // 假设RGBA格式
Color.black.opacity(0.5)
.mask( // 计算需要的缩放比例
Rectangle() let scaleFactor = sqrt(Double(targetSizeInBytes) / Double(currentMemorySize))
.overlay( let newWidth = max(originalWidth * CGFloat(scaleFactor), 40) // 最小40像素
Circle() let newHeight = max(originalHeight * CGFloat(scaleFactor), 40)
.frame(
width: min(geometry.size.width, geometry.size.height) * 0.8, let newSize = CGSize(width: newWidth, height: newHeight)
height: min(geometry.size.width, geometry.size.height) * 0.8
) // 重新渲染到新尺寸
.blendMode(.destinationOut) let renderer = UIGraphicsImageRenderer(size: newSize)
) let resizedImage = renderer.image { context in
) image.draw(in: CGRect(origin: .zero, size: newSize))
}
// 圆形裁剪框边框
Circle() // 再次尝试JPEG压缩
.stroke(Color.white, lineWidth: 2) if let imageData = resizedImage.jpegData(compressionQuality: 0.3) {
.frame( let finalSize = Int64(imageData.count)
width: min(geometry.size.width, geometry.size.height) * 0.8, print("✅ 通过缩小尺寸压缩成功: \(finalSize) bytes (新尺寸: \(newWidth) x \(newHeight))")
height: min(geometry.size.width, geometry.size.height) * 0.8
)
// 圆形裁剪框四角标记 if let finalImage = UIImage(data: imageData) {
ForEach(0..<4) { corner in return finalImage
Circle()
.fill(Color.white)
.frame(width: 6, height: 6)
.offset(
x: corner % 2 == 0 ? -min(geometry.size.width, geometry.size.height) * 0.4 : min(geometry.size.width, geometry.size.height) * 0.4,
y: corner < 2 ? -min(geometry.size.width, geometry.size.height) * 0.4 : min(geometry.size.width, geometry.size.height) * 0.4
)
} }
} }
// 如果还是太大,返回最小尺寸的图片
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
let colorSpace = 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)"
} }
} }
} }

Loading…
Cancel
Save