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
let onCropComplete: (UIImage) -> Void
@Environment(\.dismiss) private var dismiss
@State private var scale: CGFloat = 1.0
@State private var lastScale: CGFloat = 1.0
@State private var offset: CGSize = .zero
@State private var lastOffset: CGSize = .zero
var body: some View { // MARK: - 图片选择器
NavigationView { struct ImagePicker: UIViewControllerRepresentable {
GeometryReader { geometry in let onImageSelected: (UIImage) -> Void
ZStack { let shouldProcessImage: Bool
Color.black let targetSize: CGSize?
.ignoresSafeArea()
init(onImageSelected: @escaping (UIImage) -> Void, shouldProcessImage: Bool = false, targetSize: CGSize? = nil) {
VStack { self.onImageSelected = onImageSelected
// 裁剪区域 self.shouldProcessImage = shouldProcessImage
ZStack { self.targetSize = targetSize
// 背景图片
Image(uiImage: image)
.resizable()
.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
} }
)
)
// 裁剪框 func makeUIViewController(context: Context) -> UIImagePickerController {
CropOverlay() let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.sourceType = .photoLibrary
picker.allowsEditing = false
return picker
} }
.frame(height: geometry.size.width) // 保持正方形
.clipped()
Spacer() func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
// 提示文字 func makeCoordinator() -> Coordinator {
Text("拖动和缩放来选择圆形Logo区域") Coordinator(self)
.foregroundColor(.white)
.font(.caption)
.padding(.bottom, 20)
} }
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
} }
.navigationTitle("裁剪圆形Logo")
.navigationBarTitleDisplayMode(.inline) parent.onImageSelected(finalImage)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("取消") {
dismiss()
} }
.foregroundColor(.white) picker.dismiss(animated: true)
} }
ToolbarItem(placement: .navigationBarTrailing) { func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
Button("完成") { picker.dismiss(animated: true)
let croppedImage = cropImage()
onCropComplete(croppedImage)
dismiss()
}
.foregroundColor(.white)
.font(.system(size: 16, weight: .semibold))
} }
// 处理图片为正方形并缩放到指定大小,同时压缩到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 cropImage() -> UIImage { // 压缩图片到指定大小以下
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 80, height: 80)) private func compressImageToTargetSize(_ image: UIImage, targetSizeInKB: Double) -> UIImage {
let targetSizeInBytes = Int64(targetSizeInKB * 1024)
return renderer.image { context in // 尝试不同的压缩质量
// 计算裁剪区域 let compressionQualities: [CGFloat] = [0.8, 0.6, 0.4, 0.2, 0.1, 0.05]
let imageSize = image.size
let viewSize = CGSize(width: 80, height: 80)
// 计算图片在视图中的实际大小和位置 for quality in compressionQualities {
let imageAspectRatio = imageSize.width / imageSize.height if let imageData = image.jpegData(compressionQuality: quality) {
let viewAspectRatio = viewSize.width / viewSize.height let dataSize = Int64(imageData.count)
let scaledImageSize: CGSize if dataSize <= targetSizeInBytes {
let scaledImageOffset: CGPoint // 如果压缩后的数据大小符合要求,重新创建UIImage
if let compressedImage = UIImage(data: imageData) {
print("✅ 图片压缩成功: \(dataSize) bytes (质量: \(quality))")
return compressedImage
}
}
}
}
if imageAspectRatio > viewAspectRatio { // 如果JPEG压缩仍然太大,尝试进一步缩小尺寸
// 图片更宽,以高度为准 print("⚠️ JPEG压缩后仍超过目标大小,尝试缩小尺寸")
scaledImageSize = CGSize(width: viewSize.height * imageAspectRatio, height: viewSize.height) return compressImageByReducingSize(image, targetSizeInBytes: targetSizeInBytes)
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)
} }
// 应用缩放和偏移 // 通过缩小尺寸来压缩图片
let finalImageSize = CGSize( private func compressImageByReducingSize(_ image: UIImage, targetSizeInBytes: Int64) -> UIImage {
width: scaledImageSize.width * scale, let originalSize = image.size
height: scaledImageSize.height * scale let originalWidth = originalSize.width
) let originalHeight = originalSize.height
let finalImageOffset = CGPoint( // 计算当前图片的内存大小
x: scaledImageOffset.x + offset.width, let currentMemorySize = Int64(originalWidth * originalHeight * 4) // 假设RGBA格式
y: scaledImageOffset.y + offset.height
)
// 计算裁剪区域 // 计算需要的缩放比例
let cropRect = CGRect( let scaleFactor = sqrt(Double(targetSizeInBytes) / Double(currentMemorySize))
x: -finalImageOffset.x, let newWidth = max(originalWidth * CGFloat(scaleFactor), 40) // 最小40像素
y: -finalImageOffset.y, let newHeight = max(originalHeight * CGFloat(scaleFactor), 40)
width: finalImageSize.width,
height: finalImageSize.height
)
// 创建圆形裁剪路径 - 半径和正方形宽度一样 let newSize = CGSize(width: newWidth, height: newHeight)
let circlePath = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 80, height: 80))
circlePath.addClip()
// 绘制裁剪后的图片 // 重新渲染到新尺寸
image.draw(in: cropRect) let renderer = UIGraphicsImageRenderer(size: newSize)
} let resizedImage = renderer.image { context in
} image.draw(in: CGRect(origin: .zero, size: newSize))
} }
// MARK: - 裁剪覆盖层 // 再次尝试JPEG压缩
struct CropOverlay: View { if let imageData = resizedImage.jpegData(compressionQuality: 0.3) {
var body: some View { let finalSize = Int64(imageData.count)
GeometryReader { geometry in print("✅ 通过缩小尺寸压缩成功: \(finalSize) bytes (新尺寸: \(newWidth) x \(newHeight))")
ZStack {
// 半透明遮罩
Color.black.opacity(0.5)
.mask(
Rectangle()
.overlay(
Circle()
.frame(
width: min(geometry.size.width, geometry.size.height) * 0.8,
height: min(geometry.size.width, geometry.size.height) * 0.8
)
.blendMode(.destinationOut)
)
)
// 圆形裁剪框边框 if let finalImage = UIImage(data: imageData) {
Circle() return finalImage
.stroke(Color.white, lineWidth: 2) }
.frame( }
width: min(geometry.size.width, geometry.size.height) * 0.8,
height: min(geometry.size.width, geometry.size.height) * 0.8
)
// 圆形裁剪框四角标记 // 如果还是太大,返回最小尺寸的图片
ForEach(0..<4) { corner in print("⚠️ 无法压缩到目标大小,返回最小尺寸图片")
Circle() let minSize = CGSize(width: 40, height: 40)
.fill(Color.white) let rendererMin = UIGraphicsImageRenderer(size: minSize)
.frame(width: 6, height: 6) return rendererMin.image { context in
.offset( image.draw(in: CGRect(origin: .zero, size: minSize))
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
)
} }
// 计算图片的内存大小
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