Demo
前言
起初是看到项目中多张PNG图片组成的Loading动画(是给到MJRefresh
用的)觉得挺心爱的,所以用代码弄成一个GIF,打算用来作为微信的表情包:
但是看着好像缺了点东西… 哦,影子!原图是带影子的:
为什么会没了影子呢?经调研发现,本来这部分影子是带有通明度的,而制作的GIF是不能带有半通明的部分(至少通过代码生成的GIF是这样,假如能做到求奉告)。
没有影子其实也啥所谓,但我这个人有点强迫症,非得要把影子加回去,所以才有下面做法:
PS:以下都是通过CGContext
的办法进行制作的,这部分制作的代码比较多并且都很常见,所以就不放这儿了,详细可看Demo。
完结计划
计划一:先填充背景色,再制作图画
直接给出定论,是能够的:
但这并不是我想要的作用,我是希望能像微信的表情包那样只要图画内容,不想要这一大块背景,应该像这样:
计划二:先制作具有图画概括的色彩块,再制作图画
同样直接给出定论,也是能够的,并且确实不会有一大块背景了:
我这儿的做法是先制作白色的图画概括图片,再制作原图画盖上去。
制作白色的图画概括图片的详细做法:
#pragma mark 转换成白色概括的图片(镂空图片区域:通明->白色+不通明->通明)
+ (UIImage *)convertWhiteImage:(UIImage *)image {
if (!image) return nil;
CGRect rect = (CGRect){CGPointZero, image.size};
UIGraphicsBeginImageContextWithOptions(image.size, NO, 0);
[UIColor.whiteColor setFill];
UIRectFill(rect);
// 设置混色形式
[image drawInRect:rect blendMode:kCGBlendModeDestinationOut alpha:1.0];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
但这还不完全是我想要的作用,仔细跟微信的表情包比照,仍是缺了一样东西,那就是白色描边:
这些白色描边有什么用?我猜是为了能完好展现图画,防止本来的描边色跟背景色重叠,相当于加强了原图的抗锯齿作用吧。
怎么增加描边呢?扩展白色的图画概括图片?假如图画内容只要一个那倒可行,但假如是多个不相连的内容呢?例如:
中点扩展的话,其他字母的描边就对不准了,这种情况根本就没有准确的锚点。
通过一系列的调研,OpenCV
能够做到,但很可惜,自己水平实属低下,不会用… 去学吧,这但是很绵长的进程,难道CGContext
就没有简略粗犷的办法吗?
又通过一系列的调研发现,应该说以我的水平,发现确实办不到。
但是!能够以另一种办法完结:找到图画中的每一个非通明像素点的坐标点,然后对每一个坐标点都“扩展”,再填充(白色)。
举个例子说明:为方针像素点,为扩展的填充点,假定以像素点为中点向外扩展2点,那么该烘托范围则是这样:
有了思路就当即开干… 越过进程,直接看结果:
得出定论:还行!
PS:由所以白色的描边,而网站背景色也是白色的,为了更好地展现作用,特意加了黑色背景。
再对Supreme
试一下看看作用:
还行还行。
怎么完结
总结起来就一句:首先是遍历图片每个像素的色彩值,也就是RGBA,然后判断其间的Alpha值,只要非0,就扩展该像素点进行色彩填充,遍历填充完,再把图片制作盖上去即可。
核心办法是获取每个像素的色彩值,自己查阅材料测验后,为了能敷衍不同类型的图片也能增加描边的处理,最终参阅了SDWebImage
的做法:
SDWebImage
有个SDGetColorFromRGBA
的函数,能够通过坐标来获取方针像素的色彩值。值得注意的是,色彩通道的排列办法会有不同顺序,例如RGBA和ARBG,这些都会根据当时是大端仍是小端然后有不同的排布顺序,不仅如此,色彩通道的数量也各不相同,例如灰白图片的色彩通道就两个。总而言之,该函数很好的进行了各种判定,最终能获取正确的色彩值(PS:这儿真的踩了很多坑,要不是发现了这个办法,我估计会气晕,不过因此了解了这些色彩通道的排布办法也是挺乐的)。
我是几乎照搬该函数,并且做了一些定制化的修正,例如我改成了只获取alpha值,代码如下:
/// 对图片进行描边制作
/// - outlineStrokeWidth: 描边巨细
/// - outlineStrokeColor: 描边色彩
static void JPDrawOutlineStroke(CGImageRef imageRef, CGContextRef context, size_t outlineStrokeWidth, CGColorRef outlineStrokeColor, CGFloat diffX, CGFloat diffY) {
if (!imageRef || !context) {
return;
}
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
if (width == 0 || height == 0) return;
// 每一行的总字节数
size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);
if (bytesPerRow == 0) return;
// 每个像素包括的色彩通道数 = 一个像素的总位数 / 每个色彩通道运用的位数
// 在32位像素格式下,每个色彩通道固定占8位,所以算出的「每个像素包括的色彩通道数」相当于「每个像素占用的字节数」
size_t components = CGImageGetBitsPerPixel(imageRef) / CGImageGetBitsPerComponent(imageRef);
// greyscale有2个,RGB有3个,RGBA有4个,其他则是无法识别的色彩空间了
if (components != 2 && components != 3 && components != 4) return;
// 获取指向图画方针的字节数据的指针
CGDataProviderRef dataProvider = CGImageGetDataProvider(imageRef);
if (!dataProvider) return;
CFDataRef data = CGDataProviderCopyData(dataProvider);
if (!data) return;
const UInt8 *bytePtr = CFDataGetBytePtr(data);
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
// 获取通明信息
CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask;
// 获取字节排序(大端or小端)
CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask;
BOOL byteOrderNormal = NO;
switch (byteOrderInfo) {
case kCGBitmapByteOrderDefault:
case kCGBitmapByteOrder32Big:
byteOrderNormal = YES;
break;
default:
break;
}
// 烘托一个像素的巨细:以像素点为中点,线宽为外边距,向外扩展
CGFloat fillWH = (CGFloat)outlineStrokeWidth + 1 + (CGFloat)outlineStrokeWidth;
/**
* 其间为像素点,为`outlineStrokeWidth`,
* 假定`outlineStrokeWidth = 2`,那么烘托的巨细为:
*/
// 烘托一切非通明的像素点
CGContextSetFillColorWithColor(context, outlineStrokeColor);
for (size_t x = 0; x < width; x++) {
for (size_t y = 0; y < height; y++) {
// 像素下标 = 第几行 * 每一行的总字节数 + 第几列 * 每个像素占用的字节数
size_t byteIndex = y * bytesPerRow + x * components;
// 获取通明度
CGFloat alpha = 255.0;
if (components == 2) { // greyscale
alpha = JPGetAlphaFromGrayscaleAtPixel(bytePtr, byteIndex, byteOrderNormal, alphaInfo);
} else if (components == 3 || components == 4) { // RGB || RGBA
alpha = JPGetAlphaFromRGBAAtPixel(bytePtr, byteIndex, byteOrderNormal, alphaInfo);
}
alpha /= 255.0;
// 非通明的地方就涂色(注意:这儿的xy是根据图画的坐标,需求适配成context的坐标进行填充)
if (alpha > 0.1) { // 通明度0.1以下人眼【几乎】看不见,直接忽略吧
CGFloat fillX = (CGFloat)x - (CGFloat)outlineStrokeWidth;
CGFloat fillY = (CGFloat)y - (CGFloat)outlineStrokeWidth;
// 此处的y轴跟UIKit的上下颠倒,y = h - maxY
CGFloat fillMaxY = fillY + fillWH;
fillY = (CGFloat)height - fillMaxY;
fillX += diffX;
fillY += diffY;
// 填充色彩
CGContextFillRect(context, CGRectMake(fillX, fillY, fillWH, fillWH));
}
}
}
// 开释内存
CFRelease(data);
}
/// 获取方针像素的Alpha值(参阅SDWebImage)
static CGFloat JPGetAlphaFromRGBAAtPixel(const UInt8 *bytePtr, size_t byteIndex, BOOL byteOrderNormal, CGImageAlphaInfo alphaInfo) {
CGFloat a = 255.0;
switch (alphaInfo) {
case kCGImageAlphaPremultipliedFirst:
case kCGImageAlphaFirst:
{
if (byteOrderNormal) {
// ARGB
a = (CGFloat)bytePtr[byteIndex];
} else {
// BGRA
a = (CGFloat)bytePtr[byteIndex + 3];
}
break;
}
case kCGImageAlphaPremultipliedLast:
case kCGImageAlphaLast:
{
if (byteOrderNormal) {
// RGBA
a = (CGFloat)bytePtr[byteIndex + 3];
} else {
// ABGR
a = (CGFloat)bytePtr[byteIndex];
}
break;
}
case kCGImageAlphaNone:
case kCGImageAlphaNoneSkipLast:
case kCGImageAlphaNoneSkipFirst:
break;
case kCGImageAlphaOnly:
{
// A
a = (CGFloat)bytePtr[byteIndex];
break;
}
default:
break;
}
return a;
}
/// 这部分代码跟`JPGetAlphaFromRGBAAtPixel`迥然不同,只是换成了双通道办法获取,这儿就不详细展现了,想看能够参阅Demo
static CGFloat JPGetAlphaFromGrayscaleAtPixel(const UInt8 *bytePtr, size_t byteIndex, BOOL byteOrderNormal, CGImageAlphaInfo alphaInfo) {
......
}
运用(单张图片的处理):
UIImage *image = XXX;
CGFloat outlineStrokeWidth = 3;
UIColor *outlineStrokeColor = UIColor.whiteColor;
CGImageRef imageRef = image.CGImage;
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
if (width == 0 || height == 0) return;
// 内容或许有贴边的情况,为了让边缘的描边能完好展现,额定增加描边巨细的内边距
UIEdgeInsets padding = UIEdgeInsetsMake(outlineStrokeWidth, outlineStrokeWidth, outlineStrokeWidth, outlineStrokeWidth);
size_t renderWidth = padding.left + width + padding.right;
size_t renderHeight = padding.top + height + padding.bottom;
CGContextRef context = CGBitmapContextCreate(NULL,
renderWidth,
renderHeight,
CGImageGetBitsPerComponent(imageRef),
0, // 这儿不能用CGImageGetBytesPerRow(imageRef),
// →→ 由于`renderWidth`跟`width`或许不一样,
// →→ 要么重新核算`(renderWidth * 4)`、要么传0交给系统自动核算。
CGImageGetColorSpace(imageRef),
CGImageGetBitmapInfo(imageRef));
if (!context) return;
CGFloat diffX = padding.left;
CGFloat diffY = padding.bottom; // 此处的y轴跟UIKit的上下颠倒,所以是bottom
// 1.给图画内容增加概括描边(填充非通明部分)
JPDrawOutlineStroke(imageRef, context, outlineStrokeWidth, outlineStrokeColor.CGColor, diffX, diffY);
// 2.制作原图画(盖在概括描边上)
CGContextDrawImage(context, CGRectMake(diffX, diffY, width, height), imageRef);
// 3.取出新图画
CGImageRef newImageRef = CGBitmapContextCreateImage(context);
// 4.开释内存
CGContextRelease(context);
if (!newImageRef) {
return;
}
// 完结!
UIImage *newImage = [UIImage imageWithCGImage:newImageRef];
GIF的应用作用
有了上面的办法,只要对GIF的每一张图片增加描边就能够完结GIF描边了,看看在微信上的作用:
还行还行,但这种办法增加的描边实际上很粗糙,所以也只能应用在GIF上。
最后
以上办法和完结我都一同打包放到我的JPImageresizerView中,除了给GIF增加描边,还额定扩展了背景色、圆角、边框、内边距的增加,另外还有可持续获取图片方针像素的色彩值(图片测色器),有爱好能够去看看。