Demo

前言

起初是看到项目中多张PNG图片组成的Loading动画(是给到MJRefresh用的)觉得挺心爱的,所以用代码弄成一个GIF,打算用来作为微信的表情包:

【iOS】日常笔记:运用CGContext给GIF增加白色描边

但是看着好像缺了点东西… 哦,影子!原图是带影子的:

【iOS】日常笔记:运用CGContext给GIF增加白色描边

为什么会没了影子呢?经调研发现,本来这部分影子是带有通明度的,而制作的GIF是不能带有半通明的部分(至少通过代码生成的GIF是这样,假如能做到求奉告)。

没有影子其实也啥所谓,但我这个人有点强迫症,非得要把影子加回去,所以才有下面做法:

PS:以下都是通过CGContext的办法进行制作的,这部分制作的代码比较多并且都很常见,所以就不放这儿了,详细可看Demo

完结计划

计划一:先填充背景色,再制作图画

直接给出定论,是能够的:

【iOS】日常笔记:运用CGContext给GIF增加白色描边

但这并不是我想要的作用,我是希望能像微信的表情包那样只要图画内容,不想要这一大块背景,应该像这样:

【iOS】日常笔记:运用CGContext给GIF增加白色描边

计划二:先制作具有图画概括的色彩块,再制作图画

同样直接给出定论,也是能够的,并且确实不会有一大块背景了:

【iOS】日常笔记:运用CGContext给GIF增加白色描边

我这儿的做法是先制作白色的图画概括图片,再制作原图画盖上去。

制作白色的图画概括图片的详细做法:

#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;
}

但这还不完全是我想要的作用,仔细跟微信的表情包比照,仍是缺了一样东西,那就是白色描边

【iOS】日常笔记:运用CGContext给GIF增加白色描边

这些白色描边有什么用?我猜是为了能完好展现图画,防止本来的描边色跟背景色重叠,相当于加强了原图的抗锯齿作用吧。

怎么增加描边呢?扩展白色的图画概括图片?假如图画内容只要一个那倒可行,但假如是多个不相连的内容呢?例如:

【iOS】日常笔记:运用CGContext给GIF增加白色描边

中点扩展的话,其他字母的描边就对不准了,这种情况根本就没有准确的锚点。

通过一系列的调研,OpenCV能够做到,但很可惜,自己水平实属低下,不会用… 去学吧,这但是很绵长的进程,难道CGContext就没有简略粗犷的办法吗?

又通过一系列的调研发现,应该说以我的水平,发现确实办不到

但是!能够以另一种办法完结:找到图画中的每一个非通明像素点的坐标点,然后对每一个坐标点都“扩展”,再填充(白色)

举个例子说明:为方针像素点,为扩展的填充点,假定以像素点为中点向外扩展2点,那么该烘托范围则是这样:

【iOS】日常笔记:运用CGContext给GIF增加白色描边

有了思路就当即开干… 越过进程,直接看结果:

【iOS】日常笔记:运用CGContext给GIF增加白色描边

得出定论:还行!

PS:由所以白色的描边,而网站背景色也是白色的,为了更好地展现作用,特意加了黑色背景。

再对Supreme试一下看看作用:

【iOS】日常笔记:运用CGContext给GIF增加白色描边

还行还行。

怎么完结

总结起来就一句:首先是遍历图片每个像素的色彩值,也就是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描边了,看看在微信上的作用:

【iOS】日常笔记:运用CGContext给GIF增加白色描边

还行还行,但这种办法增加的描边实际上很粗糙,所以也只能应用在GIF上

最后

以上办法和完结我都一同打包放到我的JPImageresizerView中,除了给GIF增加描边,还额定扩展了背景色、圆角、边框、内边距的增加,另外还有可持续获取图片方针像素的色彩值(图片测色器),有爱好能够去看看。