You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,提问作者Евгений Чепелев

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 15:24:21