1 Star 1 Fork 3

冰奇 / QFImageMaskDemo

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README

iOS 隐形水印之 LSB 实现

技术背景

       首先,让我们看下本文实现的两个技术点

水印

       水印是经常出现在图片 & 视频等多媒体文件上是一种很常见的元素,它一般以单一的图片或者文字的形式显示在一个角落。它主要用于声明版权 & 防盗,以及查看来源等。而我们千帆直播在主播直播后,我们的后端会为生成的 点播 & 回放 打上「千帆直播」水印,如下图的右上角:

compare

       这种水印是可见的,添加水印的方式可以是直接将水印图片的原数据 memcpy 到载体图片原数据上(这里只是简单描述啦,对齐这些各位自己了解下)。还有可以使用第三方库,如 ffmpeg等。

LSB

       英文 least significant bit,中文义最低有效位。

       用个例子来描述下,我们知道一个数字,如11,把它转换为二进制的话就是 0000 1011,取最右边的 1 即为最低位。而本文是针对图片的,图片的原数据构成是 RBG,那我们的操作就是

(255, 255, 255) -取255-> 1111 1111 -> 把最后一位(1 => 0) -> 1111 1110 -放回-> (254, 255, 255)

       这也是本文实现的核心。而上面的操作会让原本的白色在 R值 上变小,然后再转换回颜色上,人眼是很难看出修改之后与之前的差别。因此我们这样的修改对原本的影响是很小的。这就是我们通过获取 LSB,修改它来实现本次技术点。

数字水印

       结合我们上面说的两个技术点,就是我们的实现了。

       这里介绍数字水印,一种不易被发现的水印,也可以叫它,隐形水印。顾名思义,它不会像一般的水印一样显示在图片 & 视频帧上,而是通过其他技术原理将信息附带在载体上,最后通过反计算来获取信息内容。

       数字水印的实现其实不止 LSB,还有其他改进的 LSB,或者其他算法,本文使用 标准LSB 来实现。但算法的大致都是建立在对原数据的修改不会在视觉上被察觉,将信息加入到原数据里面,有点像藏头诗。

       通过以上大概了解原理 & 技术背景之后,本篇将着重使用 标准的LSB 来为 iOS 的截图上添加隐形水印 & 获取隐形水印内容。

应用场景

       在 Demo App 上通过系统的截图后,将已经加入隐形水印的 App 截图展示,通过 直接分享到QQ 或者 保存到相册,再由相册分享到QQ,然后客服获取到图片之后将图片保存或者直接复制,转发给处理人员。然后通过相应的工具或者 App 将图片的隐形水印信息读取出来。

       至于信息内容,可以是当时 用户操作 App 的点击行为,或者部分接口的响应结果,从而来协助定位、追踪问题,等等。

QFImageMaskMan

这里的是整个 Demo & Demo 的操作展示,我在 iPhone 截图上将一首诗加入进去,然后分享出去或者保存本地。

QFImageMaskDemo

Demo

实现

static let markBin: [QFLSBMarkBin] = [.key, .lengthBit, .length, .info]

读者看完 Demo 之后尝试添加 version 记录

加密头

字段 说明 内容
key 标识 本库使用 "QianFan"
lengthBit 记录使用的 length(扩展来变更 length 所占的字节) 1 byte
length 全部信息的总长度,如果减去固定的信息头长度就是加密的信息长度 2 bytes

加密体

加密头 加密内容
key.cout + 1 + 2 length - (key.cout + 1 + 2)

encode & decode

       都是通过 CGContext 获取位图数据,encode3 使用了 CVPixelBuffer,方便后续应用到视频帧,然后对 data[n] 二进制化的尾数进行比较 & 操作。

class func baseEncode(data: UnsafeMutablePointer<UInt8>, infoData: Data, width w: Int, height h: Int) {
    let kQHKeyData = kQFkey.data(using: .utf8)!
    let kQHKeyDataCount = kQHKeyData.count
    let kLenghtCount = kLengthBit
    
    let markCount = kQHKeyDataCount + kLengthBitCount + kLenghtCount + infoData.count
    
    var bStop = false
    var markIndex = -1
    var markIndexText: Int = 0
    var markBin: QFLSBMarkBin?
    var markLengthBitCount = 0
    
    // 1、读取原数据
    for y in 0..<h {
        if bStop {
            break;
        }
        for x in 0..<w {
            let index = (y * w + x)
            
            let markIndexTemp = index / 8
            if markIndexTemp >= markCount {
                bStop = true
                break
            }

            // 2、水印信息的计算
            if markIndex != markIndexTemp {
                markIndex = markIndexTemp
                
                if markIndex < kQHKeyDataCount {
                    markBin = .key
                    markIndexText = Int(kQHKeyData[markIndex])
                }
                else if markIndex < kQHKeyDataCount + kLengthBitCount {
                    markBin = .lengthBit
                    markIndexText = kLengthBit
                }
                else if markIndex < kQHKeyDataCount + kLengthBitCount + kLenghtCount {
                    markLengthBitCount += 1
                    markBin = .length
                    if markLengthBitCount == 1 {
                        markIndexText = markCount%256
                    }
                    else if markLengthBitCount == 2 {
                        markIndexText = Int(markCount>>8)
                    }
                    else {
                        bStop = true
                        break
                    }
                }
                else if markIndex < markCount {
                    markBin = .info
                    markIndexText = Int(infoData[markIndex - (kQHKeyDataCount + kLengthBitCount + kLenghtCount)])
                }
            }
            
            if markBin == nil {
                bStop = true
                break
            }
            
            // 3、读取原数据 & 获取最低有效位
            let offset = 4 * index
            let red = data[offset+1]
            let redBinary = red % 2
            
            let markIndexTextBinaryIndex = index % 8
            let markIndexTextBinary = Int((markIndexText / (1<<Int(markIndexTextBinaryIndex))) % 2)
            
            // 4、最低有效位记录
            if redBinary != markIndexTextBinary {
                if markIndexTextBinary == 0 {
                    if red == 255 {
                        data[offset+1] = 254
                    }
                    else {
                        data[offset+1] = red + 1
                    }
                }
                else {
                    if red == 0 {
                        data[offset+1] = 1
                    }
                    else {
                        data[offset+1] = red - 1
                    }
                }
            }
        }
    }
}

// 逻辑上与 baseEncode 相同,读取原数据后记录到对应的水印数据信息上。
class func baseDecode(data: UnsafeMutablePointer<UInt8>, width w: Int, height h: Int) -> (result: Bool, info: String) {

	// ......
	
    return (false, "解密");
}

       具体可以看:QFLSBMan.swift(代码没有注释,也比较乱,抱歉了哈)。

补充

       1、这里需要注意在截图类的逻辑那部分,有如下的函数 & 执行(加密后的图片再执行)

       将加密后将图片先缓存本地,再获从缓存本地的图片获取出来进行分享和保存相册,这样加密的数才不会由于保存相册或者直接分享而导致数据会丢失。目前测试分享 QQ 和 钉钉 数据解码正常,而微信会丢失的。

compare

       以上图片都是不使用 toPng() 操作后,图片的加密像素部分,都发生变化,所以已无法解密出原来的信息。

       而缓存本地的作用就是为了解决该情况(再考察其他办法解决方案)。

private func toPng() -> Bool

if !toPng() {
	print("showScreenshot toPng 失败")
	return
}

       2、还有每张截图都加入 QFScreenshot 的普通水印,可简单区分 系统截图 还是 加密截图。

let text = "QFScreenshot" as NSString
let p = CGPoint(x: 20, y: 160)
text.draw(at: p, withAttributes: [NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16), NSAttributedString.Key.foregroundColor: UIColor(white: 0.5, alpha: 1)])

总结

       LSB 的抗干扰比较差,原因在于假如使用的 第三方 分享,其在传送过程对图片进行二次处理或者优化,很可能会导致信息的丢失。当然目前的 Demo 实践上,只要用户使用截图后展示的截图图片通过保存到相册,再 QQ 分享 或者 直接QQ分享 都能保证信息的完整性。

       应该会有人问为什么不直接上传到自家的服务器,并且直接将信息一并传过去,无须这样加密,确实也有道理。但其实加隐形水印这种方式也很直接,让用户直接将截图分享与 QQ 的客服或者支撑人员,然后进行沟通。并且本文只是隐形水印在 iOS 的实现,其用途的扩展就由各位脑补。

其他用途

       1、将信息重复布满整张图片(可以使用重复文案,如 QianFan),加入在类似 抽奖 或者 大转盘 等具有敏感信息的结果截图上,以防止有可能的 PS 做假。

       2、用在视频帧中,在视频里面传输指令或者信息(帧丢失的话偶就不管了哈),等等。

改进的隐形水印

       1、该实现的原理是通过计算像素指定通道(例如 R通道)的 所有 bit 的 0 / 1 ,通过奇偶数 来替换标准的LSB 的最低位 进行比较,这样做更具有隐蔽性。

       2、加密的信息可以是二维码,可以另一张小图片(具体看图片是灰度图还是彩色图)。

空文件

简介

暂无描述 展开 收起
Swift
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
Swift
1
https://gitee.com/chenqihui/QFImageMaskDemo.git
git@gitee.com:chenqihui/QFImageMaskDemo.git
chenqihui
QFImageMaskDemo
QFImageMaskDemo
master

搜索帮助