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)" } } }