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()
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
}
)
)
// 裁剪框 init(onImageSelected: @escaping (UIImage) -> Void, shouldProcessImage: Bool = false, targetSize: CGSize? = nil) {
CropOverlay() self.onImageSelected = onImageSelected
} self.shouldProcessImage = shouldProcessImage
.frame(height: geometry.size.width) // 保持正方形 self.targetSize = targetSize
.clipped() }
Spacer() 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) {}
Text("拖动和缩放来选择圆形Logo区域")
.foregroundColor(.white)
.font(.caption)
.padding(.bottom, 20)
}
}
}
.navigationTitle("裁剪圆形Logo")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("取消") {
dismiss()
}
.foregroundColor(.white)
}
ToolbarItem(placement: .navigationBarTrailing) { func makeCoordinator() -> Coordinator {
Button("完成") { Coordinator(self)
let croppedImage = cropImage() }
onCropComplete(croppedImage)
dismiss() class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
} let parent: ImagePicker
.foregroundColor(.white)
.font(.system(size: 16, weight: .semibold)) 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)
} }
}
private func cropImage() -> UIImage { func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 80, height: 80)) picker.dismiss(animated: true)
}
return renderer.image { context in
// 计算裁剪区域 // 处理图片为正方形并缩放到指定大小,同时压缩到5KB以下
let imageSize = image.size private func processImageToSquare(image: UIImage, targetSize: CGSize) -> UIImage {
let viewSize = CGSize(width: 80, height: 80) 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 cropX = (originalSize.width - squareSize) / 2
let scaledImageOffset: CGPoint 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) guard let cgImage = image.cgImage?.cropping(to: cropRect) else {
scaledImageOffset = CGPoint(x: (viewSize.width - scaledImageSize.width) / 2, y: 0) return image
} 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)
} }
}
}
// MARK: - 裁剪覆盖层 // 通过缩小尺寸来压缩图片
struct CropOverlay: View { private func compressImageByReducingSize(_ image: UIImage, targetSizeInBytes: Int64) -> UIImage {
var body: some View { let originalSize = image.size
GeometryReader { geometry in let originalWidth = originalSize.width
ZStack { let originalHeight = originalSize.height
// 半透明遮罩
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() let currentMemorySize = Int64(originalWidth * originalHeight * 4) // 假设RGBA格式
.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 let scaleFactor = sqrt(Double(targetSizeInBytes) / Double(currentMemorySize))
Circle() let newWidth = max(originalWidth * CGFloat(scaleFactor), 40) // 最小40像素
.fill(Color.white) let newHeight = max(originalHeight * CGFloat(scaleFactor), 40)
.frame(width: 6, height: 6)
.offset( let newSize = CGSize(width: newWidth, height: newHeight)
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 // 重新渲染到新尺寸
) 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
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