iOS 中为图片添加水印的实现分析

488 阅读5分钟

一、实现原理详解

1. 位图上下文与图像叠加

在 iOS 中,UIImage 的底层数据是以位图(Bitmap)形式存储的,每个像素点包含颜色信息。当需要对图片进行叠加、滤镜、裁剪等操作时,我们需要通过 位图图形上下文(Bitmap Graphics Context) 来实现。

  • 位图上下文 是一个画布,所有绘图操作(如绘制图片、文字)最终都会渲染到该上下文中。
  • UIGraphicsBeginImageContextWithOptions(size, opaque, scale) 是创建位图上下文的核心方法:
    • size:上下文的尺寸(通常与原图一致)。
    • opaque:是否不透明(设为 false 以支持透明度)。
    • scale:图像的缩放比例(通常使用原图的 scale 以避免模糊)。

2. 图像叠加流程

  1. 创建上下文:通过 UIGraphicsBeginImageContextWithOptions 创建一个新的位图上下文。
  2. 绘制原图:将原始图片绘制到上下文中。
  3. 绘制水印:在上下文中绘制水印图片或文字,通过设置透明度(alpha)和混合模式(blendMode)控制叠加效果。
  4. 生成新图片:从上下文中提取处理后的位图数据,封装为新的 UIImage
  5. 释放资源:调用 UIGraphicsEndImageContext() 释放上下文。

3. 颜色混合与透明度

  • 透明度alpha):通过 alpha 参数控制水印的透明度(0.0 为完全透明,1.0 为不透明)。
  • 混合模式blendMode):默认使用 .normal 模式(直接覆盖),其他模式如 .overlay 可实现更复杂的视觉效果。

二、代码实现步骤

1. 添加图片水印

核心逻辑

func addImageWatermark(waterMarkImage: UIImage, 
                       corner: WaterMarkCorner = .BottomRight,
                       margin: CGPoint = CGPoint(x: 10, y: 10),
                       alpha: CGFloat = 0.5) -> UIImage? {
    // 1. 创建位图上下文
    UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale)
    guard UIGraphicsGetCurrentContext() != nil else { return nil }
    
    // 2. 绘制原图
    self.draw(in: CGRect(origin: .zero, size: self.size))
    
    // 3. 计算水印位置
    var waterMarkRect = CGRect(origin: .zero, size: waterMarkImage.size)
    switch corner {
    case .TopLeft:
        waterMarkRect.origin = margin
    case .TopRight:
        waterMarkRect.origin = CGPoint(x: self.size.width - waterMarkImage.size.width - margin.x,
                                       y: margin.y)
    case .BottomLeft:
        waterMarkRect.origin = CGPoint(x: margin.x,
                                       y: self.size.height - waterMarkImage.size.height - margin.y)
    case .BottomRight:
        waterMarkRect.origin = CGPoint(x: self.size.width - waterMarkImage.size.width - margin.x,
                                       y: self.size.height - waterMarkImage.size.height - margin.y)
    }
    
    // 4. 绘制水印图片(叠加到原图上)
    waterMarkImage.draw(in: waterMarkRect, blendMode: .normal, alpha: alpha)
    
    // 5. 生成新图片
    let resultImage = UIGraphicsGetImageFromCurrentImageContext()
    
    // 6. 关闭上下文
    defer {
        UIGraphicsEndImageContext()
    }
    return resultImage
}

2. 添加文字水印

核心逻辑

func addTextWatermark(text: String,
                      font: UIFont = UIFont.systemFont(ofSize: 30),
                      color: UIColor = .white,
                      position: CGPoint = CGPoint(x: 20, y: 20),
                      alpha: CGFloat = 0.5) -> UIImage? {
    UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale)
    guard UIGraphicsGetCurrentContext() != nil else { return nil }
    
    // 1. 绘制原图
    self.draw(in: CGRect(origin: .zero, size: self.size))
    
    // 2. 设置文字属性
    let attrs: [NSAttributedString.Key: Any] = [
        .font: font,
        .foregroundColor: color,
        .backgroundColor: UIColor.clear
    ]
    
    // 3. 计算文字绘制区域
    let textSize = text.size(withAttributes: attrs)
    let textRect = CGRect(origin: position, size: textSize)
    
    // 4. 绘制文字
    text.draw(in: textRect, withAttributes: attrs)
    
    // 5. 生成新图片
    let resultImage = UIGraphicsGetImageFromCurrentImageContext()
    
    // 6. 关闭上下文
    defer {
        UIGraphicsEndImageContext()
    }
    return resultImage
}

三、完整示例代码

1. ViewController 实现

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var LOGOImage: UIImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 加载原始图片
        guard let originalImage = UIImage(named: "gogo") else {
            print("原图加载失败")
            return
        }
        
        // 缩放水印图片
        let scaleRatio = 0.3 // 缩小为原图的30%
        let scaledWaterMarkSize = CGSize(width: originalImage.size.width * scaleRatio, height: originalImage.size.height * scaleRatio)
        let waterMarkImage = UIGraphicsImageRenderer(size: scaledWaterMarkSize).image { _ in
            originalImage.draw(in: CGRect(origin: .zero, size: scaledWaterMarkSize))
        }
        
        // 添加图片水印
        guard let imageWithImageWatermark = originalImage.addImageWatermark(
            waterMarkImage: waterMarkImage,
            corner: .BottomRight,
            margin: CGPoint(x: 10, y: 10),
            alpha: 0.5
        ) else {
            print("图片水印添加失败")
            return
        }
        
        // 添加文字水印
        guard let finalImage = imageWithImageWatermark.addTextWatermark(
            text: "90 够晨仔出品",
            font: UIFont.systemFont(ofSize: 40),
            color: .red,
            alpha: 0.5
        ) else {
            print("文字水印添加失败")
            return
        }
        
        // 显示结果
        LOGOImage.image = finalImage
        LOGOImage.contentMode = .scaleAspectFit
    }
}

2. UIImage 扩展

extension UIImage {
    /// 添加图片水印
    func addImageWatermark(waterMarkImage: UIImage,
                           corner: WaterMarkCorner = .BottomRight,
                           margin: CGPoint = CGPoint(x: 10, y: 10),
                           alpha: CGFloat = 0.5) -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale)
        guard UIGraphicsGetCurrentContext() != nil else { return nil }
        
        // 绘制原图
        self.draw(in: CGRect(origin: .zero, size: self.size))
        
        // 计算水印位置
        var waterMarkRect = CGRect(origin: .zero, size: waterMarkImage.size)
        switch corner {
        case .TopLeft:
            waterMarkRect.origin = margin
        case .TopRight:
            waterMarkRect.origin = CGPoint(x: self.size.width - waterMarkImage.size.width - margin.x,
                                           y: margin.y)
        case .BottomLeft:
            waterMarkRect.origin = CGPoint(x: margin.x,
                                           y: self.size.height - waterMarkImage.size.height - margin.y)
        case .BottomRight:
            waterMarkRect.origin = CGPoint(x: self.size.width - waterMarkImage.size.width - margin.x,
                                           y: self.size.height - waterMarkImage.size.height - margin.y)
        }
        
        // 绘制水印
        waterMarkImage.draw(in: waterMarkRect, blendMode: .normal, alpha: alpha)
        
        // 生成新图片
        let resultImage = UIGraphicsGetImageFromCurrentImageContext()
        defer {
            UIGraphicsEndImageContext()
        }
        return resultImage
    }
    
    // 水印位置枚举
    enum WaterMarkCorner {
        case TopLeft, TopRight, BottomLeft, BottomRight
    }
    
    /// 添加文字水印
    func addTextWatermark(text: String,
                          font: UIFont = UIFont.systemFont(ofSize: 30),
                          color: UIColor = .white,
                          position: CGPoint = CGPoint(x: 20, y: 20),
                          alpha: CGFloat = 0.5) -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale)
        guard UIGraphicsGetCurrentContext() != nil else { return nil }
        
        // 绘制原图
        self.draw(in: CGRect(origin: .zero, size: self.size))
        
        // 设置文字属性
        let attrs: [NSAttributedString.Key: Any] = [
            .font: font,
            .foregroundColor: color,
            .backgroundColor: UIColor.clear
        ]
        
        // 计算文字绘制区域
        let textSize = text.size(withAttributes: attrs)
        let textRect = CGRect(origin: position, size: textSize)
        
        // 绘制文字
        text.draw(in: textRect, withAttributes: attrs)
        
        // 生成新图片
        let resultImage = UIGraphicsGetImageFromCurrentImageContext()
        defer {
            UIGraphicsEndImageContext()
        }
        return resultImage
    }
}

四、注意事项与优化建议

  1. 图片资源加载

    • 确保 Assets.xcassets 中存在名为 gogo 的图片。
    • 使用 print() 调试日志确认图片是否加载成功。
  2. 水印位置调整

    • 如果水印被裁剪,可调整 corner 参数或增加 margin
    • 水印图片过大时,可通过 scaleRatio 缩放尺寸。
  3. 性能优化

    • 避免在主线程处理大尺寸图片。
    • 使用 autoreleasepool 管理内存(尤其在批量处理时)。
  4. 透明度与视觉效果

    • 调整 alpha 值控制水印透明度(推荐 0.3~0.7)。
    • 尝试不同 blendMode 实现独特视觉效果(如 .overlay.multiply)。

五、总结

图像水印的转换流程图

graph TD
    A[原始图片] --> B[创建位图上下文]
    B --> C[绘制原图]
    C --> D{选择水印类型}
    D -->|图片水印| E[加载水印图片]
    D -->|文字水印| F[设置文字属性]
    E --> G[计算水印位置]
    F --> H[计算文字绘制区域]
    G --> I[绘制图片水印]
    H --> J[绘制文字水印]
    I --> K[生成带水印的新图片]
    J --> K
    K --> L[关闭上下文]
    L --> M[返回最终图像]

流程图说明

  1. 原始图片

    • 起点是通过 UIImage(named:) 加载的原始图片资源。
  2. 创建位图上下文

    • 使用 UIGraphicsBeginImageContextWithOptions(...) 创建画布,尺寸与原图一致,支持透明度。
  3. 绘制原图

    • 将原始图片绘制到位图上下文中,作为水印叠加的基础。
  4. 选择水印类型

    • 分支为 图片水印文字水印 两种处理逻辑。
  5. 图片水印处理

    • 加载水印图片:通过 UIImage(named:) 或缩放操作获取水印图片。
    • 计算水印位置:根据 WaterMarkCorner 枚举和 margin 参数确定水印绘制区域。
    • 绘制图片水印:通过 draw(in:blendMode:alpha:) 方法将水印叠加到原图上。
  6. 文字水印处理

    • 设置文字属性:定义字体、颜色、透明度等。
    • 计算文字绘制区域:根据文字内容和属性计算绘制位置。
    • 绘制文字水印:通过 draw(in:withAttributes:) 方法将文字叠加到原图上。
  7. 生成最终图像

    • 从位图上下文中提取处理后的图像数据,封装为新的 UIImage 对象。
    • 关闭上下文并释放资源。

水印叠加的视觉效果示例

graph LR
    A[原图] --> B[水印图片]
    A --> C[水印文字]
    B --> D[叠加后的图片]
    C --> D

关键参数对结果的影响

参数作用说明
alpha控制水印透明度(0.0~1.0),值越小越透明。
corner水印位置(左上、右上、左下、右下),决定水印在图片上的锚点。
margin水印与图片边缘的距离,避免水印超出可视区域。
blendMode混合模式(如 .normal.overlay),影响水印与原图的颜色混合效果。