很多 APP 都在敏感頁面有水印,主要為了應對輿情時可以追蹤圖片來源,一般在水印上都會有員工或使用者 ID 和暱稱。
水印的用途總結有亮點:
- 追蹤來源
- 威懾作用
威懾作用是指當使用者看到水印時,會自覺避免違法傳輿行為。
但是,當不需要威懾作用時,例如,為了保持應用或者圖片的美觀,顯形的水印似乎不是那麼必要,這時候可以考慮使用隱形水印。
最近在同事在知乎上看到一種水印。
如下圖,表面似乎沒有什麼水印
但通過 PS 的混色模式處理後,隱形水印就顯示出來了
具體處理方式是
- 在原圖上圖層新增全黑圖層
- 全黑圖層選擇『顏色加深』
到此為止,我對 PS 的演算法產生了好奇,混色模式是常用工具,但是以前沒有注意過公式。
顏色加深混色模式
PS 的混色模式,其實是底圖和混色層的每個畫素點,經過一系列計算後得到的結果層。
翻閱了一系列資料後我發現,現有的公式都是不正確的,有些熱門文章裡也不對。而 PS 官方文件只對幾種混色模式進行了介紹,而並沒有給出公式。
檢視每個通道中的顏色資訊,並通過增加二者之間的對比度使基色變暗以反映出混合色。與白色混合後不產生變化。
比較多的是這套公式:
結果色 = 基色-[(255-基色)×(255-混合色)]/混合色
公式中(255-基色)和(255-混合色)分別是基色和混合色的反相。
- 若混合色為0(黑色),(基色×混合色)為0,得到的數值為一相個負值,歸為0,所以不論基色為何值均為0。
- 當混合色的色階值是255(白色)時,混合色同基色。
基本查到的演算法公式都有一個致命問題,公式都標明瞭,任何顏色和黑色混色結果為黑色,這顯然與上文中 PS 處理結果不符合。如果按照這套理論,整個圖片都應該黑了。
最後我試出來一個接近的方案是,
- 結果色 = 基色 —(基色反相×混合色反相)/ 混合色
- 如混色層為黑色,則認為 RGB 為 (1, 1, 1),即非常深的灰色
這個公式可以基本實現 PS 中的顏色加深效果。可以將淺色變深,越淺越深。
隱形水印的實現
新增水印
首先介紹 iOS 中的基本影像處理方式:
- 獲取圖片的所有畫素點
- 改變指標指向的畫素資訊
+ (UIImage *)addWatermark:(UIImage *)image
text:(NSString *)text {
UIFont *font = [UIFont systemFontOfSize:32];
NSDictionary *attributes = @{NSFontAttributeName: font,
NSForegroundColorAttributeName: [UIColor colorWithRed:0
green:0
blue:0
alpha:0.01]};
UIImage *newImage = [image copy];
CGFloat x = 0.0;
CGFloat y = 0.0;
CGFloat idx0 = 0;
CGFloat idx1 = 0;
CGSize textSize = [text sizeWithAttributes:attributes];
while (y < image.size.height) {
y = (textSize.height * 2) * idx1;
while (x < image.size.width) {
@autoreleasepool {
x = (textSize.width * 2) * idx0;
newImage = [self addWatermark:newImage
text:text
textPoint:CGPointMake(x, y)
attributedString:attributes];
}
idx0 ++;
}
x = 0;
idx0 = 0;
idx1 ++;
}
return newImage;
}
+ (UIImage *)addWatermark:(UIImage *)image
text:(NSString *)text
textPoint:(CGPoint)point
attributedString:(NSDictionary *)attributes {
UIGraphicsBeginImageContext(image.size);
[image drawInRect:CGRectMake(0,0, image.size.width, image.size.height)];
CGSize textSize = [text sizeWithAttributes:attributes];
[text drawInRect:CGRectMake(point.x, point.y, textSize.width, textSize.height) withAttributes:attributes];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
複製程式碼
顯示水印
通過上文提到的公式,可以讓水印顯示。
+ (UIImage *)visibleWatermark:(UIImage *)image {
// 1. Get the raw pixels of the image
// 定義 32位整形指標 *inputPixels
UInt32 * inputPixels;
//轉換圖片為CGImageRef,獲取引數:長寬高,每個畫素的位元組數(4),每個R的位元數
CGImageRef inputCGImage = [image CGImage];
NSUInteger inputWidth = CGImageGetWidth(inputCGImage);
NSUInteger inputHeight = CGImageGetHeight(inputCGImage);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
NSUInteger bytesPerPixel = 4;
NSUInteger bitsPerComponent = 8;
// 每行位元組數
NSUInteger inputBytesPerRow = bytesPerPixel * inputWidth;
// 開闢記憶體區域,指向首畫素地址
inputPixels = (UInt32 *)calloc(inputHeight * inputWidth, sizeof(UInt32));
// 根據指標,前面的引數,建立畫素層
CGContextRef context = CGBitmapContextCreate(inputPixels, inputWidth, inputHeight,
bitsPerComponent, inputBytesPerRow, colorSpace,
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
//根據目前畫素在介面繪製影像
CGContextDrawImage(context, CGRectMake(0, 0, inputWidth, inputHeight), inputCGImage);
// 畫素處理
for (int j = 0; j < inputHeight; j++) {
for (int i = 0; i < inputWidth; i++) {
@autoreleasepool {
UInt32 *currentPixel = inputPixels + (j * inputWidth) + i;
UInt32 color = *currentPixel;
UInt32 thisR,thisG,thisB,thisA;
// 這裡直接移位獲得RBGA的值,以及輸出寫的非常好!
thisR = R(color);
thisG = G(color);
thisB = B(color);
thisA = A(color);
UInt32 newR,newG,newB;
newR = [self mixedCalculation:thisR];
newG = [self mixedCalculation:thisG];
newB = [self mixedCalculation:thisB];
*currentPixel = RGBAMake(newR,
newG,
newB,
thisA);
}
}
}
//建立新圖
// 4. Create a new UIImage
CGImageRef newCGImage = CGBitmapContextCreateImage(context);
UIImage * processedImage = [UIImage imageWithCGImage:newCGImage];
//釋放
// 5. Cleanup!
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
free(inputPixels);
return processedImage;
}
+ (int)mixedCalculation:(int)originValue {
// 結果色 = 基色 —(基色反相×混合色反相)/ 混合色
int mixValue = 1;
int resultValue = 0;
if (mixValue == 0) {
resultValue = 0;
} else {
resultValue = originValue - (255 - originValue) * (255 - mixValue) / mixValue;
}
if (resultValue < 0) {
resultValue = 0;
}
return resultValue;
}
複製程式碼
程式碼和開源庫
為了方便使用,寫了一個開源庫,封裝的很實用,附帶 DEMO