在使用知乎,微博的时候,我们经常可以看到自己上传的图片被加上了文字水印,在实际的应用开发过程中,很多客户端都需要开发者自己编写 Canvs 绘制图形水印的方法,今天我想在这里介绍一个轻量级的开源 Android 图片水印框架来避免开发过程中编写复杂的绘图代码:AndroidWM,并且剖析它的实现过程和特色用法。
这个框架最大程度上简化了图片水印绘制的问题,暴露了灵活的接口给用户使用。在绘制水印的时候,只需要创建一个水印对象,一个水印创建者对象,然后把定制的水印对象传给水印创建者即可。
实现水印第一步:引用类库 androidWM
如果你使用 Android Studio 进行项目的开发,只需要在应用 app 的 build.gradle
里面添加上这么一行代码:
dependencies {
...
implementation 'com.huangyz0918:androidwm:0.2.3'
...
}
然后点击同步 gradle,让其自动下载并且安装好依赖即可。
实现水印第二步:创建一个水印,完工!
androidWM 给我们提供了四种不同的水印:
- 图形水印
- 文字水印
- 隐形图形水印
- 隐形文字水印
我们可以创建不同的水印对象来实现对水印风格的定制化,比如我想创建一个文字水印,我就实例化一个WatermarkText
,并且设置它的属性:
WatermarkText watermarkText = new WatermarkText(“Hello World”)
.setPositionX(0.5) // 横坐标
.setPositionY(0.5) // 纵坐标
.setTextAlpha(100) // 透明度
.setTextColor(Color.WHITE) // 文字水印文字颜色
.setTextFont(R.font.champagne) // 字体
.setTextShadow(0.1f, 5, 5, Color.BLUE); // 字体的阴影
这样我就拿到了一个文字水印 watermarkText
,接下来,我还需要一个画师帮助我把水印画到我想要的背景底图上面,于是我们需要实例化一个WatermarkBuilder
:
WatermarkBuilder.create(this, backgroundBitmap) // 加载背景底图
.loadWatermarkText(watermarkText) // 加载水印对象
.getWatermark() // 绘制带有水印的图片
.setToImageView(backgroundView); // 设置结果到 ImageView 里
这里一气呵成,首先传入一个上下文 context
,然后传入一张你想要绘制的背景底图,这个背景图片可以来自于一个 ImageView
, 也可以来自于系统的资源(如: R.drawable.image
),你要是想直接传入一个 Bitmap
也是可以的。最后,在调用完方法 .getWatermark()
以后,androidWM 可以帮助你直接将带有水印的图片设置到一个ImageView
里面,或者使用方法 .getOutputImage()
获得输出的 Bitmap
。
同理,你也可以通过实例化一个 WatermarkImage
来创建一个图像水印:
WatermarkImage watermarkImage = new WatermarkImage(watermarkBitmap)
.setImageAlpha(80)
.setPositionX(0.5)
.setPositionY(0.5)
.setRotation(15)
.setSize(0.3);
WatermarkBuilder
.create(this, backgroundBitmap)
.loadWatermarkImage(watermarkImage)
.getWatermark()
.setToImageView(backgroundView);
Boom ! 画出来的水印长这样:
创建隐形水印
隐形水印是框架 androidWM 的一个特色,它支持两种不同方式的隐形水印:
- LSB 空域隐形水印
- 频域水印
创建隐形水印的话,设置水印的位置和大小颜色就没有意义了,水印将被编码到肉眼不可见的空间,只有使用框架的某些方法才能够成功地检测出来。
创建一个隐形的水印也是很简单的,我们同样可以通过 WatermarkBuilder
来绘制一个隐形的文字或者是图形水印:
WatermarkBuilder
.create(this, backgroundBitmap)
.loadWatermarkImage(watermarkBitmap)
.setInvisibleWMListener(true, new BuildFinishListener() {
@Override
public void onSuccess(Bitmap object) {
if (object != null) {
// do something...
}
}
@Override
public void onFailure(String message) {
// do something...
}
});
在方法 .setInvisibleWMListener()
中第一个参数是用于选择水印模式的参数:
- true 使用空域 LSB 方法绘制隐形水印
- false 使用频域隐形水印
同样,对于隐形水印的检测,我们可以初始化一个水印检测器 WatermarkDetector
:
WatermarkDetector
.create(inputBitmap, true)
.detect(false, new DetectFinishListener() {
@Override
public void onSuccess(DetectionReturnValue returnValue) {
Bitmap watermarkImage = returnValue.getWatermarkBitmap();
String watermarkString = returnValue.getWatermarkString();
}
@Override
public void onFailure(String message) {
// do something...
}
});
在函数创建的时候,也需要选择一种检测模式,如果是 LSB 空域水印的检测,在第二个布尔参数的位置传进 true,反正是频域隐形水印的话就传 false。在检测方法 detect()
的第一个参数也是类似,如果需要检测文字类的水印传入 true,图片类的水印传入 false。
LSB 实现隐形水印的原理
讲完了框架的使用,我们来说一下关于 androidWM 实现图片隐形水印的原理,首先要说 LSB 空域水印。LSB 的全称是 Least Significant Bits,即通过图形中最不重要的一些信息位来储存我们隐藏信息(水印)。LSB 方法是最简单的嵌入水印的方法,事实上,任何一幅图片都具备一定的容噪性,这表现在像素数据的最低有效位 (Least Significant Bit,LSB) 对人眼的视觉影响很小,秘密信息就隐藏在图像每一个像素的最低位或次低位,实现其不可见性。
为了实现我们的隐形水印,androidWM 将一幅图片的所有像素提取出来,其中每个像素都可以分解为 ARGB 四位信息(分别是 alpha, red, green, blue),作为标准的 ARGB 编码方式,每一位都由一个从 0 到 255 的整形数字表示,其中作为人眼来说,最后一位色阶的细小变化是根本不会被观察到的,我们将水印的文字(或是图片)信息编码为一串二进制的字符,将每个 ARGB 的值的最后一位清零,用二进制字符替换,达到了利用最后一位色阶来储存隐藏的信息的作用。
在实际的工程中,我们将水印信息编码成二进制以后,加上了非二进制的前后缀用于识别程序检测,同时,为了让整张图片都被水印给覆盖,我们的算法将水印信息循环地铺设在背景图片中,只要图片之中含有一个有效的水印信息,就能够成功地检测到。
对于 LSB 空域数字水印来说,其有如下优点:
- 支持的水印信息量大
- 对原图影响小
- 算法简单
但是其也有不可忽视地巨大缺点:
- 在空间域上绘制数字水印,水印会随着图片变化而破坏
- 稳定性差,不能抵抗图像的裁剪、缩放和 jpg 压缩