From 3d48275c62863bc5207be8f20761e227215ea517 Mon Sep 17 00:00:00 2001 From: v504 Date: Tue, 26 Aug 2025 14:54:49 +0800 Subject: [PATCH] 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. --- MyQrCode/Models/QRCodeStyleModels.swift | 28 +- MyQrCode/ScannerView/ScannerView.swift | 46 +--- MyQrCode/Views/QRCodeStyleView.swift | 350 ++++++++++++------------ 3 files changed, 174 insertions(+), 250 deletions(-) diff --git a/MyQrCode/Models/QRCodeStyleModels.swift b/MyQrCode/Models/QRCodeStyleModels.swift index f40cfda..7796cd8 100644 --- a/MyQrCode/Models/QRCodeStyleModels.swift +++ b/MyQrCode/Models/QRCodeStyleModels.swift @@ -24,20 +24,6 @@ enum QRCodeColor: String, CaseIterable, Hashable { case olive = "olive" 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 { switch self { 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 .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 .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] { - 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] { diff --git a/MyQrCode/ScannerView/ScannerView.swift b/MyQrCode/ScannerView/ScannerView.swift index f06a0c1..0317482 100644 --- a/MyQrCode/ScannerView/ScannerView.swift +++ b/MyQrCode/ScannerView/ScannerView.swift @@ -81,7 +81,10 @@ struct ScannerView: View { .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(false) .sheet(isPresented: $showImagePicker) { - ImagePicker(onImageSelected: handleImageDecodeResult) + ImagePicker( + onImageSelected: handleImageDecodeResult, + shouldProcessImage: false + ) } .toolbar { 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: - 解码失败提示覆盖层 struct DecodeFailureOverlay: View { diff --git a/MyQrCode/Views/QRCodeStyleView.swift b/MyQrCode/Views/QRCodeStyleView.swift index 52303c3..6d05a8f 100644 --- a/MyQrCode/Views/QRCodeStyleView.swift +++ b/MyQrCode/Views/QRCodeStyleView.swift @@ -51,11 +51,8 @@ struct QRCodeStyleView: View { // Logo选择 @State private var selectedLogo: QRCodeLogo? = nil @State private var customLogoImage: UIImage? = nil - @State private var photoPickerItem: Any? = nil @State private var photoLibraryAccessGranted = false @State private var showingImagePicker = false - @State private var showingImageCropper = false - @State private var imageToCrop: UIImage? = nil // 生成的二维码图片 @State private var qrCodeImage: UIImage? @@ -95,7 +92,7 @@ struct QRCodeStyleView: View { let cgImage = customLogoImage.cgImage { // 使用自定义Logo 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, let logoImage = selectedLogo.image, let cgImage = logoImage.cgImage { @@ -131,19 +128,14 @@ struct QRCodeStyleView: View { checkPhotoLibraryPermission() } .sheet(isPresented: $showingImagePicker) { - ImagePicker { image in - imageToCrop = image - showingImageCropper = true - } - } - .sheet(isPresented: $showingImageCropper) { - if let imageToCrop = imageToCrop { - ImageCropperView(image: imageToCrop) { croppedImage in - customLogoImage = croppedImage + ImagePicker( + onImageSelected: { image in + customLogoImage = image 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 + + +// MARK: - 图片选择器 +struct ImagePicker: UIViewControllerRepresentable { + let onImageSelected: (UIImage) -> Void + let shouldProcessImage: Bool + let targetSize: CGSize? - @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 + init(onImageSelected: @escaping (UIImage) -> Void, shouldProcessImage: Bool = false, targetSize: CGSize? = nil) { + self.onImageSelected = onImageSelected + self.shouldProcessImage = shouldProcessImage + self.targetSize = targetSize + } - var body: some View { - NavigationView { - GeometryReader { geometry in - ZStack { - Color.black - .ignoresSafeArea() + 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) - VStack { - // 裁剪区域 - ZStack { - // 背景图片 - 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 - } - ) - ) - - // 裁剪框 - 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) + // 打印最终图片的内存大小 + let memorySize = calculateImageMemorySize(image: finalImage) + print("📊 最终自定义图片内存大小: \(memorySize)") + } else { + // 不处理图片,直接使用原图 + finalImage = image } - ToolbarItem(placement: .navigationBarTrailing) { - Button("完成") { - let croppedImage = cropImage() - onCropComplete(croppedImage) - dismiss() - } - .foregroundColor(.white) - .font(.system(size: 16, weight: .semibold)) - } + parent.onImageSelected(finalImage) } + picker.dismiss(animated: true) } - } - - private func cropImage() -> UIImage { - let renderer = UIGraphicsImageRenderer(size: CGSize(width: 80, height: 80)) - return renderer.image { context in - // 计算裁剪区域 - let imageSize = image.size - let viewSize = CGSize(width: 80, height: 80) + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true) + } + + // 处理图片为正方形并缩放到指定大小,同时压缩到5KB以下 + private func processImageToSquare(image: UIImage, targetSize: CGSize) -> UIImage { + let originalSize = image.size - // 计算图片在视图中的实际大小和位置 - let imageAspectRatio = imageSize.width / imageSize.height - let viewAspectRatio = viewSize.width / viewSize.height + // 计算正方形边长(取较小的边) + let squareSize = min(originalSize.width, originalSize.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 { - // 图片更宽,以高度为准 - scaledImageSize = CGSize(width: viewSize.height * imageAspectRatio, height: viewSize.height) - 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) + // 裁剪图片 + guard let cgImage = image.cgImage?.cropping(to: cropRect) else { + return image } - // 应用缩放和偏移 - let finalImageSize = CGSize( - width: scaledImageSize.width * scale, - height: scaledImageSize.height * scale - ) + // 创建新的UIImage + let croppedImage = UIImage(cgImage: cgImage) - let finalImageOffset = CGPoint( - x: scaledImageOffset.x + offset.width, - y: scaledImageOffset.y + offset.height - ) + // 缩放到目标大小 + let renderer = UIGraphicsImageRenderer(size: targetSize) + let scaledImage = renderer.image { context in + croppedImage.draw(in: CGRect(origin: .zero, size: targetSize)) + } - // 计算裁剪区域 - let cropRect = CGRect( - x: -finalImageOffset.x, - y: -finalImageOffset.y, - width: finalImageSize.width, - height: finalImageSize.height - ) + // 压缩图片到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] - // 创建圆形裁剪路径 - 半径和正方形宽度一样 - let circlePath = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 80, height: 80)) - circlePath.addClip() + 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 + } + } + } + } - // 绘制裁剪后的图片 - image.draw(in: cropRect) + // 如果JPEG压缩仍然太大,尝试进一步缩小尺寸 + print("⚠️ JPEG压缩后仍超过目标大小,尝试缩小尺寸") + return compressImageByReducingSize(image, targetSizeInBytes: targetSizeInBytes) } - } -} - -// MARK: - 裁剪覆盖层 -struct CropOverlay: View { - var body: some View { - GeometryReader { geometry in - 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) - ) - ) - - // 圆形裁剪框边框 - Circle() - .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 - ) + + // 通过缩小尺寸来压缩图片 + 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))") - // 圆形裁剪框四角标记 - ForEach(0..<4) { corner in - 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 - ) + 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 + 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)" } } }