diff --git a/Common.xcconfig b/Common.xcconfig index 54d715d..2a9693f 100644 --- a/Common.xcconfig +++ b/Common.xcconfig @@ -1,5 +1,5 @@ // Version information -MARKETING_VERSION = 2.0.0 +MARKETING_VERSION = 2.1.0 // Deployment targets IPHONEOS_DEPLOYMENT_TARGET = 9.0 diff --git a/Framework/Sources/UIColor+SRGAppearance.h b/Framework/Sources/UIColor+SRGAppearance.h index cbbf2d6..66d4651 100644 --- a/Framework/Sources/UIColor+SRGAppearance.h +++ b/Framework/Sources/UIColor+SRGAppearance.h @@ -27,11 +27,20 @@ OBJC_EXPORT NSValueTransformer *SRGHexadecimalColorTransformer(void); @property (class, nonatomic, readonly) UIColor *srg_blueColor; /** - * Return the color matching a hexadecimal string (with or without leading wildcard), `nil` if the string does - * not correspond to a color. + * Return the color matching a hexadecimal #rrggbbaa or #rrggbb string representation (the leading wildcard is optional), + * or `nil` if the string does not correspond to a color. + * + * @discussion Supports uppercase or lowercase digits. */ + (nullable UIColor *)srg_colorFromHexadecimalString:(NSString *)hexadecimalString; +/** + * Return the color as a hexadecimal #rrggbbaa (#rrggbb if the alpha channel is 1) string representation. + * + * @discussion Always return lowercase digits with a leading wildcard. + */ +@property (nonatomic, readonly, copy) NSString *srg_hexadecimalString; + @end NS_ASSUME_NONNULL_END diff --git a/Framework/Sources/UIColor+SRGAppearance.m b/Framework/Sources/UIColor+SRGAppearance.m index e80bf8c..f4b0aa2 100644 --- a/Framework/Sources/UIColor+SRGAppearance.m +++ b/Framework/Sources/UIColor+SRGAppearance.m @@ -12,6 +12,8 @@ @interface SRGHexadecimalColorValueTransformer : NSValueTransformer @implementation UIColor (SRGAppearance) +#pragma mark Class methods + + (UIColor *)srg_redColor { return [UIColor srg_colorFromHexadecimalString:@"#9d0018"]; @@ -27,6 +29,13 @@ + (UIColor *)srg_colorFromHexadecimalString:(NSString *)hexadecimalString return [SRGHexadecimalColorTransformer() transformedValue:hexadecimalString]; } +#pragma mark Getters and setters + +- (NSString *)srg_hexadecimalString +{ + return [SRGHexadecimalColorTransformer() reverseTransformedValue:self]; +} + @end @implementation SRGHexadecimalColorValueTransformer @@ -49,20 +58,30 @@ - (id)transformedValue:(id)value return nil; } - NSScanner *scanner = [NSScanner scannerWithString:value]; - if ([value hasPrefix:@"#"]) { - scanner.scanLocation = 1; + NSString *string = [value hasPrefix:@"#"] ? [value substringFromIndex:1] : value; + if (string.length != 6 && string.length != 8) { + return nil; } unsigned rgbValue = 0; + NSScanner *scanner = [NSScanner scannerWithString:string]; if (! [scanner scanHexInt:&rgbValue]) { return nil; } - CGFloat red = ((rgbValue & 0xFF0000) >> 16) / 255.f; - CGFloat green = ((rgbValue & 0x00FF00) >> 8) / 255.f; - CGFloat blue = (rgbValue & 0x0000FF) / 255.f; - return [UIColor colorWithRed:red green:green blue:blue alpha:1.f]; + if (string.length == 8) { + CGFloat red = ((rgbValue & 0xFF000000) >> 24) / 255.f; + CGFloat green = ((rgbValue & 0x00FF0000) >> 16) / 255.f; + CGFloat blue = ((rgbValue & 0x0000FF00) >> 8) / 255.f; + CGFloat alpha = (rgbValue & 0x000000FF) / 255.f; + return [UIColor colorWithRed:red green:green blue:blue alpha:alpha]; + } + else { + CGFloat red = ((rgbValue & 0xFF0000) >> 16) / 255.f; + CGFloat green = ((rgbValue & 0x00FF00) >> 8) / 255.f; + CGFloat blue = (rgbValue & 0x0000FF) / 255.f; + return [UIColor colorWithRed:red green:green blue:blue alpha:1.f]; + } } - (id)reverseTransformedValue:(id)value @@ -72,7 +91,18 @@ - (id)reverseTransformedValue:(id)value } const CGFloat *components = CGColorGetComponents([value CGColor]); - return [NSString stringWithFormat:@"#%02lx%02lx%02lx", lroundf(components[0] * 255), lroundf(components[1] * 255), lroundf(components[2] * 255)]; + + long red = lroundf(components[0] * 255); + long green = lroundf(components[1] * 255); + long blue = lroundf(components[2] * 255); + long alpha = lroundf(components[3] * 255); + + if (alpha == 255) { + return [NSString stringWithFormat:@"#%02lx%02lx%02lx", red, green, blue]; + } + else { + return [NSString stringWithFormat:@"#%02lx%02lx%02lx%02lx", red, green, blue, alpha]; + } } @end diff --git a/Framework/Sources/UIImage+SRGAppearance.h b/Framework/Sources/UIImage+SRGAppearance.h index 92837b1..7eb6158 100644 --- a/Framework/Sources/UIImage+SRGAppearance.h +++ b/Framework/Sources/UIImage+SRGAppearance.h @@ -11,27 +11,66 @@ NS_ASSUME_NONNULL_BEGIN @interface UIImage (SRGAppearance) /** - * Return an image generated from the vector image at the specified path. - * - * @param filePath The path of the vector image to use. - * @param size The size of the image to create. - * - * @return The generated image, `nil` if generation failed. +* Return an image generated from the vector image at the specified path. The image is fitted to the specified size +* and remaining space is filled with the provided color. +* +* @param filePath The path of the vector image to use. +* @param size The size of the image to create. Components set to 0 are calculated automatically based on the +* resource aspect ratio (or its intrinsic size if both are 0). +* @param fillColor The color to use for filling, transparent if `nil`. +* +* @return The generated image, `nil` if generation failed. +*/ ++ (nullable UIImage *)srg_vectorImageAtPath:(NSString *)filePath withSize:(CGSize)size fillColor:(nullable UIColor *)fillColor; + +/** + * Same as `-srg_vectorImageAtPath:withSize:fillColor:` without fill color. */ + (nullable UIImage *)srg_vectorImageAtPath:(NSString *)filePath withSize:(CGSize)size; /** - * Return the file URL of an image generated from the vector image at the specified path. - * - * @param filePath The path of the vector image to use. - * @param size The size of the image to create. - * - * @return The generated image, `nil` if generation failed. - * - * @discussion Images are stored in the `/Library/Caches` directory. + * Same as `-srg_vectorImageAtPath:withSize:fillColor:`, calculating the other dimension based on the image aspect ratio. + */ ++ (nullable UIImage *)srg_vectorImageAtPath:(NSString *)filePath withWidth:(CGFloat)width; ++ (nullable UIImage *)srg_vectorImageAtPath:(NSString *)filePath withHeight:(CGFloat)height; + +/** + * Same as `-srg_vectorImageAtPath:withSize:fillColor:`, returning the image with its intrinsic size. + */ ++ (nullable UIImage *)srg_vectorImageAtPath:(NSString *)filePath; + +/** +* Return the file URL of an image generated from the vector image at the specified path. The image is fitted to the +* specified size and remaining space is filled with the provided color. +* +* @param filePath The path of the vector image to use. +* @param size The size of the image to create. Components set to 0 are calculated automatically based on the +* resource aspect ratio (or its intrinsic size if both are 0). +* @param fillColor The color to use for filling, transparent if `nil`. +* +* @return The generated image, `nil` if generation failed. +* +* @discussion Images are stored in the `/Library/Caches` directory. +*/ ++ (nullable NSURL *)srg_URLForVectorImageAtPath:(NSString *)filePath withSize:(CGSize)size fillColor:(nullable UIColor *)fillColor; + +/** + * Same as `-srg_URLForVectorImageAtPath:withSize:fillColor:` without fill color. */ + (nullable NSURL *)srg_URLForVectorImageAtPath:(NSString *)filePath withSize:(CGSize)size; +/** + * Same as `-srg_URLForVectorImageAtPath:withSize:fillColor:`, calculating the other dimension based on the image + * aspect ratio. + */ ++ (nullable NSURL *)srg_URLForVectorImageAtPath:(NSString *)filePath withWidth:(CGFloat)width; ++ (nullable NSURL *)srg_URLForVectorImageAtPath:(NSString *)filePath withHeight:(CGFloat)height; + +/** + * Same as `-srg_URLForVectorImageAtPath:withSize:fillColor:`, returning the image with its intrinsic size. + */ ++ (nullable NSURL *)srg_URLForVectorImageAtPath:(NSString *)filePath; + /** * Clears the vector image cache. */ diff --git a/Framework/Sources/UIImage+SRGAppearance.m b/Framework/Sources/UIImage+SRGAppearance.m index 28f8e49..99eb65a 100644 --- a/Framework/Sources/UIImage+SRGAppearance.m +++ b/Framework/Sources/UIImage+SRGAppearance.m @@ -6,6 +6,10 @@ #import "UIImage+SRGAppearance.h" +#import "UIColor+SRGAppearance.h" + +// Implementation borrowed from https://github.com/erica/useful-things + static CGFloat SRGAppearanceImageAspectScaleFit(CGSize sourceSize, CGRect destRect) { CGSize destSize = destRect.size; @@ -27,7 +31,7 @@ static CGRect SRGAppearanceImageRectByFittingRect(CGRect sourceRect, CGRect dest return SRGAppearanceImageRectAroundCenter(center, targetSize); } -static void SRGAppearanceImageDrawPDFPageInRect(CGPDFPageRef pageRef, CGRect rect) +static void SRGAppearanceImageDrawPDFPageInRect(CGPDFPageRef pageRef, CGRect rect, CGColorRef fillColor) { CGContextRef context = UIGraphicsGetCurrentContext(); @@ -43,7 +47,12 @@ static void SRGAppearanceImageDrawPDFPageInRect(CGPDFPageRef pageRef, CGRect rec // Flip the rect, which remains in UIKit space CGRect d = CGRectApplyAffineTransform(rect, transform); - // Calculate a rectangle to draw to + if (fillColor) { + CGContextSetFillColorWithColor(context, fillColor); + CGContextFillRect(context, d); + } + + // Calculate the rectangle to draw to CGRect pageRect = CGPDFPageGetBoxRect(pageRef, kCGPDFCropBox); CGFloat drawingAspect = SRGAppearanceImageAspectScaleFit(pageRect.size, d); CGRect drawingRect = SRGAppearanceImageRectByFittingRect(pageRect, d); @@ -70,17 +79,36 @@ static void SRGAppearanceImageDrawPDFPageInRect(CGPDFPageRef pageRef, CGRect rec @implementation UIImage (SRGAppearance) -// Implementation borrowed from https://github.com/erica/useful-things -+ (UIImage *)srg_vectorImageAtPath:(NSString *)filePath withSize:(CGSize)size ++ (UIImage *)srg_vectorImageAtPath:(NSString *)filePath withSize:(CGSize)size fillColor:(UIColor *)fillColor { - NSURL *fileURL = [self srg_URLForVectorImageAtPath:filePath withSize:size]; + NSURL *fileURL = [self srg_URLForVectorImageAtPath:filePath withSize:size fillColor:fillColor]; return fileURL ? [UIImage imageWithContentsOfFile:fileURL.path] : nil; } -+ (NSURL *)srg_URLForVectorImageAtPath:(NSString *)filePath withSize:(CGSize)size ++ (UIImage *)srg_vectorImageAtPath:(NSString *)filePath withSize:(CGSize)size +{ + return [self srg_vectorImageAtPath:filePath withSize:size fillColor:nil]; +} + ++ (UIImage *)srg_vectorImageAtPath:(NSString *)filePath withWidth:(CGFloat)width +{ + return [self srg_vectorImageAtPath:filePath withSize:CGSizeMake(width, 0.f) fillColor:nil]; +} + ++ (UIImage *)srg_vectorImageAtPath:(NSString *)filePath withHeight:(CGFloat)height +{ + return [self srg_vectorImageAtPath:filePath withSize:CGSizeMake(0.f, height) fillColor:nil]; +} + ++ (UIImage *)srg_vectorImageAtPath:(NSString *)filePath +{ + return [self srg_vectorImageAtPath:filePath withSize:CGSizeZero fillColor:nil]; +} + ++ (NSURL *)srg_URLForVectorImageAtPath:(NSString *)filePath withSize:(CGSize)size fillColor:(UIColor *)fillColor { // Check cached image existence at the very beginning, and return it if available - NSString *cachedFileName = [NSString stringWithFormat:@"%@_%@_%@.png", @(filePath.hash), @(size.width), @(size.height)]; + NSString *cachedFileName = [NSString stringWithFormat:@"%@_%@_%@_%@.png", @(filePath.hash), @(size.width), @(size.height), fillColor.srg_hexadecimalString ?: @"none"]; NSString *cachesDirectory = SRGAppearanceVectorImageCachesDirectory(); NSString *cachedFilePath = [cachesDirectory stringByAppendingPathComponent:cachedFileName]; if ([[NSFileManager defaultManager] fileExistsAtPath:cachedFilePath]) { @@ -100,9 +128,21 @@ + (NSURL *)srg_URLForVectorImageAtPath:(NSString *)filePath withSize:(CGSize)siz return nil; } - UIGraphicsBeginImageContextWithOptions(size, NO, 1. /* Force scale to 1 (0 would use device scale) */); CGPDFPageRef pageRef = CGPDFDocumentGetPage(pdfDocumentRef, 1); - SRGAppearanceImageDrawPDFPageInRect(pageRef, CGRectMake(0.f, 0.f, size.width, size.height)); + CGRect pageRect = CGPDFPageGetBoxRect(pageRef, kCGPDFCropBox); + + if (CGSizeEqualToSize(size, CGSizeZero)) { + size = pageRect.size; + } + else if (size.width == 0.f) { + size.width = size.height * pageRect.size.width / pageRect.size.height; + } + else if (size.height == 0.f) { + size.height = size.width * pageRect.size.height / pageRect.size.width; + } + + UIGraphicsBeginImageContextWithOptions(size, NO, 1. /* Force scale to 1 (0 would use device scale) */); + SRGAppearanceImageDrawPDFPageInRect(pageRef, CGRectMake(0.f, 0.f, size.width, size.height), fillColor.CGColor); UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); @@ -120,6 +160,26 @@ + (NSURL *)srg_URLForVectorImageAtPath:(NSString *)filePath withSize:(CGSize)siz return [NSURL fileURLWithPath:cachedFilePath]; } ++ (NSURL *)srg_URLForVectorImageAtPath:(NSString *)filePath withSize:(CGSize)size +{ + return [self srg_URLForVectorImageAtPath:filePath withSize:size fillColor:nil]; +} + ++ (NSURL *)srg_URLForVectorImageAtPath:(NSString *)filePath withWidth:(CGFloat)width +{ + return [self srg_URLForVectorImageAtPath:filePath withSize:CGSizeMake(width, 0.f) fillColor:nil]; +} + ++ (NSURL *)srg_URLForVectorImageAtPath:(NSString *)filePath withHeight:(CGFloat)height +{ + return [self srg_URLForVectorImageAtPath:filePath withSize:CGSizeMake(0.f, height) fillColor:nil]; +} + ++ (NSURL *)srg_URLForVectorImageAtPath:(NSString *)filePath +{ + return [self srg_URLForVectorImageAtPath:filePath withSize:CGSizeZero fillColor:nil]; +} + + (void)srg_clearVectorImageCache { NSString *cachesDirectory = SRGAppearanceVectorImageCachesDirectory(); diff --git a/Tests/Sources/ColorTestCase.m b/Tests/Sources/ColorTestCase.m index 37b691d..4a84f33 100644 --- a/Tests/Sources/ColorTestCase.m +++ b/Tests/Sources/ColorTestCase.m @@ -19,22 +19,59 @@ - (void)testColorFromHexadecimalString XCTAssertEqualObjects([UIColor srg_colorFromHexadecimalString:@"#00ff00"], UIColor.greenColor); XCTAssertEqualObjects([UIColor srg_colorFromHexadecimalString:@"#0000ff"], UIColor.blueColor); + XCTAssertEqualObjects([UIColor srg_colorFromHexadecimalString:@"#ff000080"], [UIColor.redColor colorWithAlphaComponent:128.f / 255.f]); + XCTAssertEqualObjects([UIColor srg_colorFromHexadecimalString:@"#ff000000"], [UIColor.redColor colorWithAlphaComponent:0.f]); + XCTAssertEqualObjects([UIColor srg_colorFromHexadecimalString:@"#ff0000ff"], [UIColor.redColor colorWithAlphaComponent:1.f]); + + XCTAssertEqualObjects([UIColor srg_colorFromHexadecimalString:@"FF0000"], UIColor.redColor); XCTAssertEqualObjects([UIColor srg_colorFromHexadecimalString:@"ff0000"], UIColor.redColor); XCTAssertNil([UIColor srg_colorFromHexadecimalString:@"#zzzzzz"]); } +- (void)testHexadecimalString +{ + XCTAssertEqualObjects(UIColor.redColor.srg_hexadecimalString, @"#ff0000"); + XCTAssertEqualObjects(UIColor.greenColor.srg_hexadecimalString, @"#00ff00"); + XCTAssertEqualObjects(UIColor.blueColor.srg_hexadecimalString, @"#0000ff"); + + XCTAssertEqualObjects([UIColor.redColor colorWithAlphaComponent:128.f / 255.f].srg_hexadecimalString, @"#ff000080"); + XCTAssertEqualObjects([UIColor.redColor colorWithAlphaComponent:0.5f].srg_hexadecimalString, @"#ff000080"); + + XCTAssertEqualObjects([UIColor srg_colorFromHexadecimalString:@"FF0000FF"].srg_hexadecimalString, @"#ff0000"); +} + - (void)testHexadecimalColorTransformer { XCTAssertEqualObjects([SRGHexadecimalColorTransformer() transformedValue:@"#ff0000"], UIColor.redColor); XCTAssertEqualObjects([SRGHexadecimalColorTransformer() transformedValue:@"#00ff00"], UIColor.greenColor); XCTAssertEqualObjects([SRGHexadecimalColorTransformer() transformedValue:@"#0000ff"], UIColor.blueColor); + XCTAssertEqualObjects([SRGHexadecimalColorTransformer() transformedValue:@"#ff0000ff"], UIColor.redColor); + XCTAssertEqualObjects([SRGHexadecimalColorTransformer() transformedValue:@"#00ff00ff"], UIColor.greenColor); + XCTAssertEqualObjects([SRGHexadecimalColorTransformer() transformedValue:@"#0000ffff"], UIColor.blueColor); + + XCTAssertEqualObjects([SRGHexadecimalColorTransformer() transformedValue:@"#ff000080"], [UIColor.redColor colorWithAlphaComponent:128.f / 255.f]); + XCTAssertEqualObjects([SRGHexadecimalColorTransformer() transformedValue:@"#00ff0080"], [UIColor.greenColor colorWithAlphaComponent:128.f / 255.f]); + XCTAssertEqualObjects([SRGHexadecimalColorTransformer() transformedValue:@"#0000ff80"], [UIColor.blueColor colorWithAlphaComponent:128.f / 255.f]); + + XCTAssertEqualObjects([SRGHexadecimalColorTransformer() transformedValue:@"#ff000000"], [UIColor.redColor colorWithAlphaComponent:0.f]); + XCTAssertEqualObjects([SRGHexadecimalColorTransformer() transformedValue:@"#00ff0000"], [UIColor.greenColor colorWithAlphaComponent:0.f]); + XCTAssertEqualObjects([SRGHexadecimalColorTransformer() transformedValue:@"#0000ff00"], [UIColor.blueColor colorWithAlphaComponent:0.f]); + XCTAssertEqualObjects([SRGHexadecimalColorTransformer() transformedValue:@"ff0000"], UIColor.redColor); XCTAssertEqualObjects([SRGHexadecimalColorTransformer() reverseTransformedValue:UIColor.redColor], @"#ff0000"); XCTAssertEqualObjects([SRGHexadecimalColorTransformer() reverseTransformedValue:UIColor.greenColor], @"#00ff00"); XCTAssertEqualObjects([SRGHexadecimalColorTransformer() reverseTransformedValue:UIColor.blueColor], @"#0000ff"); + + XCTAssertEqualObjects([SRGHexadecimalColorTransformer() reverseTransformedValue:[UIColor.redColor colorWithAlphaComponent:128.f / 255.f]], @"#ff000080"); + XCTAssertEqualObjects([SRGHexadecimalColorTransformer() reverseTransformedValue:[UIColor.greenColor colorWithAlphaComponent:128.f / 255.f]], @"#00ff0080"); + XCTAssertEqualObjects([SRGHexadecimalColorTransformer() reverseTransformedValue:[UIColor.blueColor colorWithAlphaComponent:128.f / 255.f]], @"#0000ff80"); + + XCTAssertEqualObjects([SRGHexadecimalColorTransformer() reverseTransformedValue:[UIColor.redColor colorWithAlphaComponent:0.f]], @"#ff000000"); + XCTAssertEqualObjects([SRGHexadecimalColorTransformer() reverseTransformedValue:[UIColor.greenColor colorWithAlphaComponent:0.f]], @"#00ff0000"); + XCTAssertEqualObjects([SRGHexadecimalColorTransformer() reverseTransformedValue:[UIColor.blueColor colorWithAlphaComponent:0.f]], @"#0000ff00"); } @end