如何在iPhone端用Swift压缩WhatsApp贴纸PNG至100KB以内?
Hey there! Let's tackle this WhatsApp sticker size issue together—since you need transparent PNGs under 100KB at 512x512, here are targeted fixes based on your code and use case:
First, Let's Diagnose Your Current Issues
- Your
normalize()method isn't actually creating PNG8:
The code usesCGColorSpaceCreateDeviceRGB()(a 32-bit RGBA color space) instead of an indexed color space required for PNG8. That's why your file got bigger—you're still saving a 32-bit PNG, plus the forced resampling degraded quality. UIGraphicsImageContextdefaults to uncompressed 32-bit RGBA:
The editing tools like yourerase()method generate full 32-bit images, and saving them with defaultUIImage.pngData()doesn't apply any compression optimizations.
Fix 1: Optimize 32-bit PNG Compression (Best Balance of Quality & Size)
Skip PNG8 unless you're willing to invest in proper color quantization (more on that later). Instead, use Apple's ImageIO framework to apply lossy compression to your 32-bit PNGs—this can easily get you under 100KB while keeping transparency and good quality.
Replace your default save logic with this:
func saveOptimizedTransparentPNG(_ image: UIImage, to outputURL: URL) -> Bool { guard let cgImage = image.cgImage else { return false } // Configure compression options (adjust quality to balance size/quality) let compressionOptions: [CFString: Any] = [ kCGImageDestinationLossyCompressionQuality: 0.7, // 0 = smallest, 1 = highest quality kCGImageDestinationBackgroundColor: UIColor.clear.cgColor // Explicitly preserve transparency ] // Create ImageIO destination and write the image guard let destination = CGImageDestinationCreateWithURL( outputURL as CFURL, kUTTypePNG, 1, nil ) else { return false } CGImageDestinationAddImage(destination, cgImage, compressionOptions as CFDictionary) return CGImageDestinationFinalize(destination) }
Fix 2: Ensure Your Final Image Is Exactly 512x512 (No Hidden Retina Resolution)
In your normalize() method, make sure you're returning an image with a scale of 1 (not the default screen scale, which could make your image 1024x1024 under the hood):
func normalize() -> UIImage { let size = CGSize(width: 512, height: 512) let genericColorSpace = CGColorSpaceCreateDeviceRGB() let context = CGContext( data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: 4 * Int(size.width), space: genericColorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue ) context?.interpolationQuality = .high // Use higher quality resampling let destRect = CGRect(origin: .zero, size: size) context?.draw(self.cgImage!, in: destRect) guard let tmpThumbImage = context?.makeImage() else { return self } // Explicitly set scale to 1 to avoid Retina resolution bloat let result = UIImage(cgImage: tmpThumbImage, scale: 1, orientation: .up) return result }
Fix 3: If You Still Want PNG8 (For Ultra-Small Sizes)
PNG8 requires an indexed color space (max 256 colors) with transparency support. Manual palette creation (like your initial attempt) leads to bad quality—use a color quantization library or algorithm to generate a palette from your image's actual colors.
Here's a simplified example using ImageIO to convert to indexed color space (for better results, consider a library like SDWebImage which has built-in quantization):
func convertToOptimizedPNG8(_ image: UIImage) -> UIImage? { guard let cgImage = image.cgImage else { return nil } // Create an sRGB color space for the palette guard let sRGBSpace = CGColorSpace(name: CGColorSpace.sRGB) else { return nil } // Generate a palette from the image's colors (simplified example) // For production, use a proper quantization algorithm guard let palette = CGColorPaletteCreateWithColors(sRGBSpace, nil, 0) else { return nil } guard let indexedSpace = CGColorSpaceCreateIndexed(sRGBSpace, 255, palette) else { return nil } // Draw image into indexed context guard let context = CGContext( data: nil, width: cgImage.width, height: cgImage.height, bitsPerComponent: 8, bytesPerRow: cgImage.width, space: indexedSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue ) else { return nil } context.draw(cgImage, in: CGRect(origin: .zero, size: CGSize(width: cgImage.width, height: cgImage.height))) guard let indexedCGImage = context.makeImage() else { return nil } return UIImage(cgImage: indexedCGImage, scale: 1, orientation: .up) }
Quick Fix for Your Eraser/Edit Tools
Your erase() method is mostly correct, but make sure you're using the optimized save function above instead of saving directly from UIGraphicsGetImageFromCurrentImageContext(). Also, avoid re-rendering layers unnecessarily—cache intermediate results where possible to reduce pixel data bloat.
内容的提问来源于stack exchange,提问作者Pit WARLORD

