Swift为UIImage添加描边边框时图片旋转、厚度不符问题
问题根因
1. 图片异常旋转
这个问题来自UIImage的朝向机制:UIImage 本身的imageOrientation属性是记录在元数据里的渲染朝向,底层cgImage的像素存储方向和UIImage.size的逻辑尺寸不一定匹配,尤其是相机拍摄的大尺寸照片,EXIF信息里会带旋转标记。原代码直接拿cgImage往上下文里绘制,完全没有处理朝向校正,绘制出来自然会出现方向偏转。
2. 描边厚度不匹配
两个原因共同导致这个问题:
- 第一是和旋转问题同源:没有校正朝向的情况下,
UIImage.size的逻辑宽高和底层cgImage的实际像素宽高不匹配,基于size计算的thickness、绘制矩形、画布尺寸全都是错的。 - 第二是坐标变换逻辑有漏洞:原代码在对上下文做了y轴翻转(
scaleBy(x:1.0, y:-1.0))之后,直接用原始计算的旋转向量做平移,y方向的偏移量没有适配翻转后的坐标系,上下方向的描边偏移量不对,加上反复拼接变换矩阵带来的累计误差、绘制矩形强制取整的问题,最终出来的边框厚度不均匀,和传入的参数值对不上。
修复方案
首先需要给UIImage加一个朝向校正的方法,把所有图片都校正为up朝向,保证底层cgImage的尺寸和UIImage的逻辑size完全一致,再做描边渲染。其次调整描边绘制时的坐标计算逻辑,用上下文状态存读的方式替代反复拼接变换矩阵的写法,避免累计误差,保证各个方向的偏移量准确。
修正后的完整代码如下:
public extension UIImage { /// 校正图片朝向,统一为up方向,消除EXIF旋转标记的影响 func fixOrientation() -> UIImage { if imageOrientation == .up { return self } UIGraphicsBeginImageContextWithOptions(size, false, scale) defer { UIGraphicsEndImageContext() } draw(in: CGRect(origin: .zero, size: size)) return UIGraphicsGetImageFromCurrentImageContext() ?? self } /** 返回纯色填充的图片版本,出错时返回原图 - Parameters: - color: 填充颜色,默认白色 - Returns: 纯色填充后的图片 */ func colorized(with color: UIColor = .white) -> UIImage { // 先校正朝向再做填充 let fixedImage = self.fixOrientation() UIGraphicsBeginImageContextWithOptions(fixedImage.size, false, fixedImage.scale) defer { UIGraphicsEndImageContext() } guard let context = UIGraphicsGetCurrentContext(), let cgImage = fixedImage.cgImage else { return self } let rect = CGRect(x: 0, y: 0, width: fixedImage.size.width, height: fixedImage.size.height) color.setFill() context.translateBy(x: 0, y: fixedImage.size.height) context.scaleBy(x: 1.0, y: -1.0) context.clip(to: rect, mask: cgImage) context.fill(rect) return UIGraphicsGetImageFromCurrentImageContext() ?? self } /** 为透明图片添加外描边,出错时返回原图 - Parameters: - color: 描边颜色,默认红色 - thickness: 描边厚度,默认2pt - quality: 描边渲染精度,单位为角度,值越小描边越平滑但渲染速度越慢,默认10度 - Returns: 添加描边后的图片 */ func stroked(with color: UIColor = .red, thickness: CGFloat = 2, quality: CGFloat = 10) -> UIImage { // 第一步先校正原图朝向 let fixedOriginal = self.fixOrientation() guard let cgImage = fixedOriginal.cgImage else { return self } // 生成纯色描边基底 let strokeImage = fixedOriginal.colorized(with: color) guard let strokeCGImage = strokeImage.cgImage else { return self } let step = quality == 0 ? 10 : abs(quality) // 所有尺寸基于校正后的图片计算 let newSize = CGSize(width: fixedOriginal.size.width + 2 * thickness, height: fixedOriginal.size.height + 2 * thickness) let originalDrawRect = CGRect(x: thickness, y: thickness, width: fixedOriginal.size.width, height: fixedOriginal.size.height) let offsetRadius = thickness UIGraphicsBeginImageContextWithOptions(newSize, false, fixedOriginal.scale) guard let context = UIGraphicsGetCurrentContext() else { return self } defer { UIGraphicsEndImageContext() } // 配置上下文参数 context.interpolationQuality = .high context.translateBy(x: 0, y: newSize.height) context.scaleBy(x: 1.0, y: -1.0) // 循环绘制各个角度的描边层 for angle in stride(from: 0, to: 360, by: step) { let radian = angle * .pi / 180 // 直接基于弧度计算当前角度的偏移量,适配翻转后的坐标系 let offsetX = offsetRadius * cos(radian) let offsetY = offsetRadius * sin(radian) // 保存上下文状态,绘制完直接恢复,避免反复拼接transform带来的累计误差 context.saveGState() context.translateBy(x: offsetX, y: offsetY) context.draw(strokeCGImage, in: originalDrawRect) context.restoreGState() } // 最后把原图绘制在最上层 context.draw(cgImage, in: originalDrawRect) return UIGraphicsGetImageFromCurrentImageContext() ?? self } }
额外说明
- 去掉了原代码中对绘制矩形调用
integral取整的逻辑,非整数坐标不会影响渲染精度,强制取整反而会导致偏移误差。 - 所有尺寸、缩放比的取值都基于校正朝向之后的图片,不管是小图还是大尺寸的相机照片,都不会再出现方向偏转、厚度不准的问题。
- 如果需要更平滑的描边,可以把
quality参数调小,比如设为1,对应的渲染速度会相应变慢。
内容的提问来源于stack exchange,提问作者Евгений Чепелев
相关产品推荐
相关产品推荐

