1. 程式人生 > >《iOS Drawing Practical UIKit Solutions》讀書筆記(三) —— Drawing Images

《iOS Drawing Practical UIKit Solutions》讀書筆記(三) —— Drawing Images

UIKit Images

UIKit提供了許多函式可以讓我們操作Image,甚至我們可以僅通過程式碼的方式,獲取一個UIImage。

UIImage *SwatchWithColor(UIColor *color, CGFloat side) {
    UIGraphicsBeginImageContextWithOptions(CGSizeMake(side, side), YES, 0.0);
    [color setFill];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return
image; }

NOTO:

UIImage suports TIFF, JPEG, GIF, PNG, DIB(that is, BMP), ICO, CUR and XBM formats. You can load additional formats (like RAW) by using the ImageIO framework)

Image 的縮圖

製作Image的縮圖的程式碼很簡單,核心程式碼是UIImage的方法drawRect:

UIImage *image = [UIImage imageNamed:@"myImage"];
[image drawInRect:destinationRect];
UIImage
*thumbnail = UIGraphicsGetImageFromCurrentImageContext();

而這裡的難點在於,如何確定destinationRect。如果我們不做任何調整,直接使用目標rect的話,影象的比例就會失真。如果下圖所示,圖片的尺寸為高寬 : 2833 畫素 1933畫素, 當我們將其draw到一個矩形rect後,其高度就會壓縮來適應:

這裡寫圖片描述

乍看之下,似乎並不影響什麼。但如果我們是在現實人臉等有長寬特徵的圖片是,就會感覺很奇怪。

要解決這種問題,可以使用我們在上一篇中提到的Fitting和Filling模式。下面是具體的程式碼:

UIImage
*BuildThumbnail(UIImage *sourceImage, CGSize targetSize, BOOL useFitting){ UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0.0); CGRect targetRect = SizeMakeRect(targetSize); CGRect naturalRect = CGSizeMake(.size = sourceImage.size); // RectByFittingRect 和 RectByFillingRect的定義見上一篇部落格 CGRect destinationRect = useFitting? RectByFittingRect(nartualRect, targetRect): RectByFillingRect(naturalRect, targetRect); [sourceImage drawInRect:destinationRect]; UIImage *thumbinal = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return thumbinal; } CGRect SizeMakeRect(CGSize size) { return (CGRect){.size = size}; }

得出的結果如下所示:
這裡寫圖片描述

圖片裁剪

不同於縮圖,縮圖會將原始圖的data壓縮,而裁剪圖片則會使用原始的解析度,但僅是擷取原始圖片的一部分。
如下圖所示,當我們擷取雪貂的頭部放大時,圖片會變得模糊,因為其解析度是一定的。
這裡寫圖片描述

我們需要使用Quartz提供的

CGImageCreateWithImageInRect()

方法來裁剪圖片。

注意,當我使用Core Graphics方法來裁剪時,可以利用方法CGRectIntegral()來調整裁剪圖片的範圍(以畫素為單位),使得裁剪範圍落在原始圖片範圍內。

//CGRectIntegral 用法
    /*
     將origin值向下調整到最近整數,size向上調整到最近整數,使生成的CGRect可以完全包含原來的CGRect.
     */
    CGRect integralRect = CGRectIntegral(originalRect);
    NSLog(@"integralRect = %@",NSStringFromCGRect(integralRect));

下面分別給出Quartz和UIKit兩個版本的裁剪圖片方法,注意,Quartz是以畫素為單位的,而UIKit則是以邏輯單位點為單位。
Quartz :

UIImage *ExtractRectFromImage(UIImage *sourceImage, CGRect subRect) {
    CGImageRef imageRef = CGImageCreateWithImageInRect(sourceImage.CGImage, subRect);
    if (imageRef != NULL) {
        UIImage *output = [UIImage imageWithCGImage:imageRef];
        CGImageRelease(imageRef);
        return output;
    }
    return nil;
}

UIKit:

UIImage *ExtractSubimageFromRect(UIImage *sourceImage, CGRect rect) {
    UIGraphicsBeginImageContextWithOptions(rect.size, NO, 1);
    CGRect destRect = CGRectMake(-rect.origin.x, -rect.origin.y, sourceImage.size.width, sourceImage.size.height);
    [sourceImage drawInRect:destRect];
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return newImage;
}

灰度圖

我們可以將圖片的顏色資訊完全抹去,僅留下灰度資訊。稱之為灰度圖。

灰度圖每一個畫素佔用一個位元組(8 bits),沒有透明度資訊。

當我們需要建立一個灰度圖時,需要先建立一個grayscale的color space。在這個color space中,你所新增的任何顏色,都會別Quartz自動轉換為灰度強弱資訊,而不會顯示原始的顏色。建立灰度圖的程式碼如下:

UIImage *GrayscaleVersionOfImage(UIImage *sourceImage) {
    // 建立灰度顏色空間
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
    if (colorSpace == NULL) {
        return nil;
    }

    int width = sourceImage.size.width;
    int height = sourceImage.size.height;
    // 建立 context:每一個畫素 8 bits,沒有透明度
    CGContextRef context = CGBitmapContextCreate(NULL,
                                                 width,
                                                 height,
                                                 8,
                                                 width, colorSpace,
                                                 (CGBitmapInfo)kCGImageAlphaNone);
    if (context == NULL) {
        return nil;
    }

    // 在灰度空間中,繪製圖片
    CGRect rect = SizeMakeRect(sourceImage.size);
    CGContextDrawImage(context, rect, sourceImage.CGImage);
    CGImageRef imageRef = CGBitmapContextCreateImage(context);// 等同於UICraphicsGetImageFromCurrentImageContext
    CGContextRelease(context);

    // 返回灰度圖片
    UIImage *output = [UIImage imageWithCGImage:imageRef];
    CFRelease(imageRef);
    return output;
}

影象水印

水印會損壞原始的影象資訊,因此,在加了水印的圖片中去除水印,是十分困難的。

下面是一個水印的例子:
這裡寫圖片描述

這只是一個簡單的水印效果,主要是blend了string和圖片。

CGSize targetSize = CGSizeMake(1,2);
    UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0.0);
    CGContextRef context = UIGraphicsGetCurrentContext();

    // 將原始圖片draw到當前的context中
    CGRect targetRect = SizeMakeRect(targetSize);
    UIImage *sourceImage = [UIImage imageNamed:@"pronghorn.jpg"];
    CGRect imgRect = RectByFillingRect(SizeMakeRect(sourceImage.size), targetRect);
    [sourceImage drawInRect:imgRect];

    // Rotate context,使得水印文字傾斜45°
    CGPoint center = RectGetCenter(targetRect);
    CGContextTranslateCTM(context, center.x, center.y);
    CGContextRotateCTM(context, M_PI_4);
    CGContextTranslateCTM(context, -center.x, -center.y);

    // 建立水印內容
    NSString *watermark = @"watermark";
    UIFont *font = [UIFont fontWithName:@"HelveticaNeue" size:48];
    CGSize size = [watermark sizeWithAttributes:@{NSFontAttributeName:font}];
    CGRect stringRect = RectCenteredInRect(SizeMakeRect(size), targetRect);

    // Draw 水印, 同時使用bleng將水印突出顯示
    CGContextSetBlendMode(context, kCGBlendModeDifference);
    [watermark drawInRect:stringRect withAttributes:@{NSFontAttributeName:font, NSForegroundColorAttributeName:[UIColor whiteColor]}];

    // 生成圖片
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;

Retrieving Image Data

儘管我們可以通過函式

UIImagePNGRepresentation()
UIImageJPEGRepresentation()

來獲取NSData型別的資訊,但是這些資訊中包含了檔案頭,壓縮資訊等。
而當我們需要對一張圖片進行處理時,我們需要單純的byte-by-byte的圖片資訊。這裡,我們展示瞭如何獲取影象的位元組資訊並存儲為NSData型別的。

轉換的主要思路為:
1. 將圖片draw到context中
2. 呼叫CGBitmapContextGetData()獲取影象的bytes資訊

NOTE: 我們對於CGBitmapContextCreate函式已經不再陌生,其函式原型為:
CGBitmapContextCreate(data, width, height, bitsPerComponent, bytesPerRow, space, bitmapInfo)
其中第一個引數data,表示我們為Context所分配的記憶體。我們有兩種選擇,當傳入NULL的時候,Quartz會自動管理Context記憶體,而不需要手動dealloc。如果我們傳入了data值,則需要我們手動刪除記憶體。
其實,當我們呼叫CGBitmapContextGetData()的時候,就是獲取建立Context時所設定的data。

NSData *BytesFromRGBImage(UIImage *sourceImage) {
    if (!sourceImage) {
        return nil;
    }

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    if (colorSpace == NULL) {
        return nil;
    }

    // 建立Context
    int width = sourceImage.size.width;
    int height = sourceImage.size.height;
    CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, (CGBitmapInfo)kCGImageAlphaPremultipliedFirst);
    CGColorSpaceRelease(colorSpace);
    if (context == NULL) {
        return nil;
    }

    // 將圖片draw到當前context中
    CGRect rect = (CGRect){.size = sourceImage.size};
    CGContextDrawImage(const, rect, sourceImage.CGImage);

    // 由bytes獲取NSData
    NSData *data = [NSData dataWithBytes:CGBitmapContextGetData(const) length:(width * height * 4)];
    CGContextRelease(context);
    return data;
}

由Bytes獲取Images

由Bytes轉換為UIImage通用是藉助於

CGBitmapContextCreate

這裡我們將第一個引數傳入,即圖片的data。這就告訴Quartz,不要在自動為我們分配記憶體,而是使用我們指定的記憶體。

現在,我們可以在bytes和image直接相互轉換了,也就可以使得image可以修改了。

UIImage *ImageFromBytes(NSData *data, CGSize targetSize) {
    int width = targetSize.width;
    int height = targetSize.height;
    if (data.length < (width * height * 4)) {
        NSLog(@"Error: Got %d bytes. Expected %d bytes",
              data.length, width * height * 4);
        return nil;
    }

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    if (colorSpace == NULL) {
        return nil;
    }

    // 為bitmap建立context
    Byte *bytes = (Byte *)data.bytes;
    CGContextRef context = CGBitmapContextCreate(bytes,
                                                 width, height,
                                                 8,
                                                 width * 4,
                                                 colorSpace,
                                                 (CGBitmapInfo)kCGImageAlphaPremultipliedFirst);
    CGColorSpaceRelease(colorSpace);
    if (context == NULL) {
        return nil;
    }

    // 將data轉換為image
    CGImageRef imageRef = CGBitmapContextCreateImage(context);
    UIImage *image = [UIImage imageWithCGImage:imageRef];

    // clean up
    CGContextRelease(context);
    CFRelease(imageRef);

    return image;

}

Image的autolayout

我們在iOS中,通過Auto Layout來對齊各個View。

但有一個我們大家經常忽略的事實:Auto Layout並不是通過View的Frame來對齊的,而是通過Alignment rectangle

通常,我們不必在意這個細節,因為大多數情況下,Frame和Alignment rectangle是相等的。

但是當我們的Image中包含陰影,高光等裝飾性元素時,當我們想要將Image載入到ImageView中並利用Auto Layout對齊時,情況往往變得不盡如人意。(注意這裡說的陰影的效果不是指我們通過layer程式碼新增的元素,而是指圖片自帶的內容)

例如,我們想居中對齊一個綠色矩形,帶有黑色陰影的圖片, 預設的Auto Layout居中效果如下:
這裡寫圖片描述

可以明顯的看到,綠色矩形並沒有居中。這是為什麼呢?我們繼續看。

Debugging Alignment Rectangles

由於Auto Layout是根據Alignment Rectangle排列View的,因此我們可以檢視Alignment Rectangle 來debug約束。
具體做法為在XCode中,選擇Edit scheme,並在launch argument中輸入

-UIViewShowAlignmentRects YES
這裡寫圖片描述

可以看到,Alignment rectangle被黃線框了出來。可以看到,Alignment rectangle是和imageView的frame相同的,又因為陰影和綠色矩形在同一張圖片,因此,綠色矩形就沒有顯示到正中央啦。

這裡寫圖片描述

為了修正這種錯誤,我們需要將Alignment rectangle僅圍繞綠色矩形。我們可以利用UIImage的方法

- (UIImage *)imageWithAlignmentRectInsets:(UIEdgeInsets)alignmentInsets 

來約定UIImage的Alignment Rectangle。這裡我首先看一下矩形和陰影的inset:
這裡寫圖片描述

知道了這個間隙,我們就通過函式

- (UIImage *)imageWithAlignmentRectInsets:(UIEdgeInsets)alignmentInsets 

以原始圖片為藍本,建立一個修正了Alignment Rectangle的新image:

UIImage *newImage = [image imageWithAlignmentRectInsets:UIEdgeInsetsMake(0, 0, 30, 30)];

將newImage載入如UIImageView中,在debug,則看到黃色的框現在是緊緊包裹著綠色矩形啦,這樣,綠色矩形得到了居中。
這裡寫圖片描述

示例程式碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    // 調整圖片的Alignment Rectangle
    UIImage *image = [self createImage];

    // 居中imageView
    UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
    imageView.contentMode = UIViewContentModeScaleAspectFit; 
    imageView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:imageView];
    [imageView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor].active = YES;
    [imageView.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor].active = YES;
}

- (UIImage *)createImage {
    UIGraphicsBeginImageContext(CGSizeMake(250, 70));
    UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(50, 20, 200, 50)];
    [[UIColor blackColor] setFill];
    [path fill];
    path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 200, 50)];
    [[UIColor greenColor] setFill];
    [path fill];

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIImage *newImage = [image imageWithAlignmentRectInsets:UIEdgeInsetsMake(0, 0, 20, 50)];
    UIGraphicsEndImageContext();
    return newImage;
}

可拉伸的圖片

當我們使用圖片時,如果要顯示的區域和原始圖片尺寸不符時,為了填充顯示區域,我們需要拉伸圖片。如下圖所示

這裡寫圖片描述

這時,我們可以通過UIImage的方法:

- (UIImage *)resizableImageWithCapInsets:(UIEdgeInsets)capInsets resizingMode:(UIImageResizingMode)resizingMode

來指定圖片可以拉伸的部位。
具體可以參考資料:
resizableImageWithCapInsets:方法的探析

View的背景圖片

一般的,我們可以設定UIView的background color,卻不能夠設定背景圖片(UIImageView除外)。在這裡,介紹兩種方法來設定普通View的背景圖片:

  • UIColor的 colorWithPatternImage方法
self.view.backgroundColor=[UIColor colorWithPatternImage:[UIImage imageNamed:@"a"]]; 
  • UILayer的contents屬性
self.view.layer.contents = (id)image.CGImage;

這裡推薦使用第二種方法,因為第一種方法會佔用大量的記憶體。

PS:
imageNamed: 這個方法用一個指定的名字在系統快取中查詢並返回一個圖片物件如果它存在的話。如果快取中沒有找到相應的圖片,這個方法從指定的文件中載入然後快取並返回這個物件。因此imageNamed的優點是當載入時會快取圖片。所以當圖片會頻繁的使用時,那麼用imageNamed的方法會比較好。例如:你需要在 一個TableView裡的TableViewCell裡都載入同樣一個圖示,那麼用imageNamed載入影象效率很高。系統會把那個圖示Cache到記憶體,在TableViewCell裡每次利用那個圖 像的時候,只會把圖片指標指向同一塊記憶體。正是因此使用imageNamed會快取圖片,即將圖片的資料放在記憶體中,iOS的記憶體非常珍貴並且在記憶體消耗過大時,會強制釋放記憶體,即會遇到memory warnings。而在iOS系統裡面釋放影象的記憶體是一件比較麻煩的事情,有可能會造成記憶體洩漏。例如:當一 個UIView物件的animationImages是一個裝有UIImage物件動態陣列NSMutableArray,並進行逐幀動畫。當使用imageNamed的方式載入影象到一個動態陣列NSMutableArray,這將會很有可能造成記憶體洩露。原因很顯然的。

imageWithContentsOfFile:僅載入圖片,影象資料不會快取。因此對於較大的圖片以及使用情況較少時,那就可以用該方法,降低記憶體消耗。