| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // cc makeicns2.m -o makeicns2 -fobjc-arc -framework Foundation -framework |
| // CoreGraphics |
| |
| // Note that this doesn't handle @2x icons. If those are ever needed, add an |
| // "int scale" parameter to parallel the "int size" one. |
| |
| #include <CoreGraphics/CoreGraphics.h> |
| #import <Foundation/Foundation.h> |
| #include <assert.h> |
| #include <libkern/OSByteOrder.h> |
| #include <stdint.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| |
| typedef struct { |
| FourCharCode type; |
| uint32_t length; |
| } BlockHeader; |
| |
| BlockHeader MakeBlockHeader(FourCharCode type, uint32_t length) { |
| BlockHeader header = {type, length}; |
| header.type = OSSwapHostToBigInt32(header.type); |
| header.length = OSSwapHostToBigInt32(header.length); |
| |
| return header; |
| } |
| |
| typedef struct { |
| FourCharCode image_type; |
| FourCharCode mask_type; |
| } ImageAndMaskType; |
| |
| ImageAndMaskType ImageAndMaskTypesFromSize(int size) { |
| switch (size) { |
| case 16: { |
| ImageAndMaskType types = {'is32', 's8mk'}; |
| return types; |
| } |
| case 32: { |
| ImageAndMaskType types = {'il32', 'l8mk'}; |
| return types; |
| } |
| case 48: { |
| ImageAndMaskType types = {'ih32', 'h8mk'}; |
| return types; |
| } |
| case 128: { |
| ImageAndMaskType types = {'it32', 't8mk'}; |
| return types; |
| } |
| default: |
| assert(false); |
| } |
| } |
| |
| FourCharCode PNGIconTypeFromSize(int size) { |
| switch (size) { |
| case 16: |
| return 'icp4'; |
| case 32: |
| return 'icp5'; |
| case 64: |
| return 'icp6'; |
| case 128: |
| return 'ic07'; |
| case 256: |
| return 'ic08'; |
| case 512: |
| return 'ic09'; |
| default: |
| assert(false); |
| } |
| } |
| |
| bool CGImageHasAlphaData(CGImageRef image) { |
| // If the image specifies that it has no alpha, early-out. |
| CGImageAlphaInfo alpha_info = CGImageGetAlphaInfo(image); |
| if (alpha_info == kCGImageAlphaNone || |
| alpha_info == kCGImageAlphaNoneSkipFirst || |
| alpha_info == kCGImageAlphaNoneSkipLast) { |
| return false; |
| } |
| |
| // Otherwise, the image might specify that it is a full RGBA image but not |
| // actually contain alpha data. Extract the alpha channel and examine it. |
| |
| size_t image_width = CGImageGetWidth(image); |
| size_t image_height = CGImageGetHeight(image); |
| |
| CGContextRef context = |
| CGBitmapContextCreate(/*data=*/NULL, image_width, image_height, |
| /*bitsPerComponent=*/8, /*bytesPerRow=*/image_width, |
| /*space=*/NULL, (CGBitmapInfo)kCGImageAlphaOnly); |
| CGContextDrawImage(context, CGRectMake(0, 0, image_width, image_height), |
| image); |
| unsigned char* context_data = CGBitmapContextGetData(context); |
| |
| bool alpha_observed = false; |
| for (size_t i = 0; i < image_width * image_height; ++i) { |
| if (context_data[i] == 0) { |
| alpha_observed = true; |
| break; |
| } |
| } |
| |
| CGContextRelease(context); |
| |
| return alpha_observed; |
| } |
| |
| void CreateDataOrImageFromPNG(NSString* iconset, |
| int size, |
| NSData** out_png_data, |
| CGImageRef* out_image_ref) { |
| NSString* png_pathname = |
| [NSString stringWithFormat:@"%@/%d.png", iconset, size]; |
| NSData* png_data = [NSData dataWithContentsOfFile:png_pathname]; |
| if (!png_data) { |
| fprintf(stderr, "Error: Failed to read %s\n", png_pathname.UTF8String); |
| exit(EXIT_FAILURE); |
| } |
| |
| if (out_png_data) |
| *out_png_data = png_data; |
| |
| // Even if the image isn't requested by the caller, still proceed to create |
| // the image in order to check its attributes and the general fact that it |
| // correctly parses as a PNG file. |
| |
| CGDataProviderRef data_provider = |
| CGDataProviderCreateWithCFData((CFDataRef)png_data); |
| if (!data_provider) { |
| fprintf(stderr, "Error: Failed to create a data provider from %s\n", |
| png_pathname.UTF8String); |
| exit(EXIT_FAILURE); |
| } |
| |
| CGImageRef image = CGImageCreateWithPNGDataProvider( |
| data_provider, NULL, false, kCGRenderingIntentDefault); |
| if (!image) { |
| fprintf(stderr, "Error: Failed to create image from %s\n", |
| png_pathname.UTF8String); |
| exit(EXIT_FAILURE); |
| } |
| |
| CGDataProviderRelease(data_provider); |
| |
| size_t image_width = CGImageGetWidth(image); |
| size_t image_height = CGImageGetHeight(image); |
| if (image_width != size || image_height != size) { |
| fprintf(stderr, |
| "Error: Expected %s to be of size %dx%d; found it to be of size " |
| "%zdx%zd\n", |
| png_pathname.UTF8String, size, size, image_width, image_height); |
| exit(EXIT_FAILURE); |
| } |
| |
| if (!CGImageHasAlphaData(image)) { |
| fprintf(stderr, "Error: Expected %s to have alpha data but it does not\n", |
| png_pathname.UTF8String); |
| exit(EXIT_FAILURE); |
| } |
| |
| if (out_image_ref) |
| *out_image_ref = image; |
| else |
| CGImageRelease(image); |
| } |
| |
| NSArray<NSMutableData*>* ARGBDataFromImage(CGImageRef image) { |
| size_t image_width = CGImageGetWidth(image); |
| size_t image_height = CGImageGetHeight(image); |
| CGColorSpaceRef color_space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); |
| CGContextRef context = CGBitmapContextCreate( |
| NULL, image_width, image_height, 8 /* bitsPerComponent */, |
| 4 * image_width /* bytesPerRow */, color_space, |
| kCGImageAlphaPremultipliedFirst | kCGImageByteOrder32Big /* ARGB */); |
| assert(context); |
| CGColorSpaceRelease(color_space); |
| |
| CGContextDrawImage(context, CGRectMake(0, 0, image_width, image_height), |
| image); |
| |
| NSMutableData* alpha_data = |
| [NSMutableData dataWithCapacity:image_width * image_height]; |
| NSMutableData* red_data = |
| [NSMutableData dataWithCapacity:image_width * image_height]; |
| NSMutableData* green_data = |
| [NSMutableData dataWithCapacity:image_width * image_height]; |
| NSMutableData* blue_data = |
| [NSMutableData dataWithCapacity:image_width * image_height]; |
| |
| unsigned char* context_data = CGBitmapContextGetData(context); |
| for (unsigned char* pixel = context_data; |
| pixel < context_data + 4 * image_width * image_height; pixel += 4) { |
| // Unfortunately, as noted in |
| // https://developer.apple.com/library/archive/qa/qa1037/_index.html and |
| // verified as still being true on 10.14, CGBitmapContextCreate() cannot |
| // create unpremultiplied contexts, and therefore messed up the edges of the |
| // PNG file. Therefore, unpremultiply all the image data. |
| // |
| // There is no visible degradation due to precision loss. This only has an |
| // effect at the edges of the icon, anyway, as the opaque parts of the icon |
| // have an alpha of 0xFF, whose inverse is 1, and the unpremultiplying ends |
| // up not changing the value of the pixel. |
| unsigned char* alpha = pixel; |
| unsigned char* red = pixel + 1; |
| unsigned char* green = pixel + 2; |
| unsigned char* blue = pixel + 3; |
| |
| if (*alpha) { |
| float inverse = 255.0 / *alpha; |
| *red = MIN(255, (int)(inverse * *red)); |
| *green = MIN(255, (int)(inverse * *green)); |
| *blue = MIN(255, (int)(inverse * *blue)); |
| } else { |
| *red = 0; |
| *green = 0; |
| *blue = 0; |
| } |
| |
| [alpha_data appendBytes:alpha length:1]; |
| [red_data appendBytes:red length:1]; |
| [green_data appendBytes:green length:1]; |
| [blue_data appendBytes:blue length:1]; |
| } |
| |
| CGContextRelease(context); |
| |
| return @[ alpha_data, red_data, green_data, blue_data ]; |
| } |
| |
| void AppendRLEImageData(NSData* data, NSMutableData* rle_data) { |
| // The packing loop is done with two offsets: |
| // - unpacked_offset: this is the offset of the start of the bytes that have |
| // not yet been written to the block |
| // - current_offset: this is the offset used to search for byte runs |
| // |
| // |unpacked_offset| lags behind. The code scours through the data, looking |
| // for runs of length greater than 3 (since only runs of 3 or longer can be |
| // compressed). As soon as a run is found, all the data from |unpacked_offset| |
| // is dumped as literal data, then the run is dumped, and then the search |
| // continues. |
| |
| const char* data_bytes = data.bytes; |
| const size_t data_length = data.length; |
| size_t unpacked_offset = 0; |
| size_t current_offset = 0; |
| |
| // Search for runs through the block of data, byte by byte. |
| while (current_offset < data_length) { |
| char current_byte = data_bytes[current_offset]; |
| size_t run_length = 1; |
| while (current_offset + run_length < data_length && run_length < 130 && |
| data_bytes[current_offset + run_length] == current_byte) { |
| ++run_length; |
| } |
| if (run_length >= 3) { |
| // A long-enough run was found. First, dump all the data before the run |
| // into the output block. |
| while (unpacked_offset < current_offset) { |
| // Because uncompressed data runs max out at 128 bytes of data, cap the |
| // uncompressed run at 128 bytes. |
| size_t uncompressed_length = MIN(current_offset - unpacked_offset, 128); |
| // Key byte values of 0..127 mean 1..128 bytes of uncompressed data. |
| unsigned char key_byte = uncompressed_length - 1; |
| [rle_data appendBytes:&key_byte length:1]; |
| [rle_data appendBytes:data_bytes + unpacked_offset |
| length:uncompressed_length]; |
| unpacked_offset += uncompressed_length; |
| } |
| // Now that the output block is caught up, put the run that was just found |
| // into it. Key byte values of 128..255 mean 3..130 copies of the |
| // following byte, thus the addition of 125 to the run length. |
| unsigned char key_byte = run_length + 125; |
| [rle_data appendBytes:&key_byte length:1]; |
| [rle_data appendBytes:¤t_byte length:1]; |
| current_offset += run_length; |
| unpacked_offset = current_offset; |
| } else { |
| // The run is too small, so keep looking. |
| current_offset += run_length; |
| } |
| } |
| // At this point, there are no more runs, so pack the rest of the data into |
| // the output block. |
| while (unpacked_offset < current_offset) { |
| // Because uncompressed data runs max out at 128 bytes of data, cap the |
| // uncompressed run at 128 bytes. |
| size_t uncompressed_length = MIN(current_offset - unpacked_offset, 128); |
| // Key byte values of 0..127 mean 1..128 bytes of uncompressed data. |
| unsigned char key_byte = uncompressed_length - 1; |
| [rle_data appendBytes:&key_byte length:1]; |
| [rle_data appendBytes:data_bytes + unpacked_offset |
| length:uncompressed_length]; |
| unpacked_offset += uncompressed_length; |
| } |
| } |
| |
| NSArray<NSData*>* ImageAndMaskIconBlocksForIconOfSize(NSString* iconset, |
| int size) { |
| CGImageRef image; |
| CreateDataOrImageFromPNG(iconset, size, nil, &image); |
| NSArray<NSMutableData*>* argb_data = ARGBDataFromImage(image); |
| CGImageRelease(image); |
| |
| // Notes: |
| // - *Only* the image is RLE-encoded. The mask, no matter how compressible |
| // it is (and boy, is it!), is not RLE-encoded. |
| // - The red, green, and blue data must be *separately* RLE-encoded, and |
| // then combined to make the image block. If they are combined *before* |
| // encoding, then it's likely there will be an RLE run that spans colors |
| // and that will cause the image to be decoded incorrectly. |
| |
| ImageAndMaskType types = ImageAndMaskTypesFromSize(size); |
| |
| NSMutableData* mask_block = argb_data.firstObject; |
| |
| NSMutableData* image_block = [NSMutableData data]; |
| if (types.image_type == 'it32') { |
| // For whatever unknown reason, the 'it32' data starts with four unused |
| // bytes. Not a single reference guide has the slightest clue as to why this |
| // is. |
| [image_block appendBytes:"\0\0\0\0" length:4]; |
| } |
| |
| for (int i = 1; i < argb_data.count; ++i) |
| AppendRLEImageData(argb_data[i], image_block); |
| |
| BlockHeader image_header = MakeBlockHeader( |
| types.image_type, image_block.length + sizeof(BlockHeader)); |
| [image_block replaceBytesInRange:NSMakeRange(0, 0) |
| withBytes:&image_header |
| length:sizeof(image_header)]; |
| |
| BlockHeader mask_header = |
| MakeBlockHeader(types.mask_type, mask_block.length + sizeof(BlockHeader)); |
| [mask_block replaceBytesInRange:NSMakeRange(0, 0) |
| withBytes:&mask_header |
| length:sizeof(mask_header)]; |
| |
| return @[ image_block, mask_block ]; |
| } |
| |
| NSData* PNGIconBlockForIconOfSize(NSString* iconset, int size) { |
| NSData* png_data; |
| CreateDataOrImageFromPNG(iconset, size, &png_data, NULL); |
| |
| NSUInteger icon_block_length = png_data.length + sizeof(BlockHeader); |
| NSMutableData* icon_block = |
| [NSMutableData dataWithCapacity:icon_block_length]; |
| |
| BlockHeader block_header = |
| MakeBlockHeader(PNGIconTypeFromSize(size), icon_block_length); |
| [icon_block appendBytes:&block_header length:sizeof(block_header)]; |
| [icon_block appendData:png_data]; |
| |
| return icon_block; |
| } |
| |
| int main(int argc, char* argv[]) { |
| for (int i = 1; i < argc; ++i) { |
| NSString* iconset = @(argv[i]); |
| fprintf(stdout, "Processing iconset %s\n", iconset.UTF8String); |
| |
| if (![iconset hasSuffix:@".iconset"]) { |
| fprintf(stderr, "Error: %s does not have the '.iconset' suffix\n", |
| iconset.UTF8String); |
| exit(EXIT_FAILURE); |
| } |
| |
| NSMutableArray<NSData*>* blocks = [NSMutableArray array]; |
| |
| // Add the standard Chromium icon sizes. |
| [blocks |
| addObjectsFromArray:ImageAndMaskIconBlocksForIconOfSize(iconset, 16)]; |
| [blocks |
| addObjectsFromArray:ImageAndMaskIconBlocksForIconOfSize(iconset, 32)]; |
| [blocks |
| addObjectsFromArray:ImageAndMaskIconBlocksForIconOfSize(iconset, 128)]; |
| [blocks addObject:PNGIconBlockForIconOfSize(iconset, 256)]; |
| [blocks addObject:PNGIconBlockForIconOfSize(iconset, 512)]; |
| |
| // Add the TOC. It contains every block header, and starts with one of its |
| // own. |
| NSUInteger toc_block_length = (blocks.count + 1) * sizeof(BlockHeader); |
| NSMutableData* toc_block = |
| [NSMutableData dataWithCapacity:toc_block_length]; |
| |
| BlockHeader toc_header = MakeBlockHeader('TOC ', toc_block_length); |
| [toc_block appendBytes:&toc_header length:sizeof(toc_header)]; |
| for (NSData* block in blocks) |
| [toc_block appendBytes:block.bytes length:sizeof(BlockHeader)]; |
| [blocks insertObject:toc_block atIndex:0]; |
| |
| // Build the .icns data. |
| NSMutableData* icns_data = [NSMutableData data]; |
| for (NSData* block in blocks) |
| [icns_data appendData:block]; |
| |
| BlockHeader file_header = |
| MakeBlockHeader('icns', icns_data.length + sizeof(BlockHeader)); |
| [icns_data replaceBytesInRange:NSMakeRange(0, 0) |
| withBytes:&file_header |
| length:sizeof(BlockHeader)]; |
| |
| // Write the .icns file. |
| NSMutableString* icns_name = [NSMutableString stringWithString:iconset]; |
| [icns_name deleteCharactersInRange:NSMakeRange(iconset.length - 8, |
| 8)]; // Strip ".iconset". |
| [icns_name appendString:@".icns"]; |
| |
| BOOL result = [icns_data writeToFile:icns_name atomically:NO]; |
| if (!result) { |
| fprintf(stderr, "Failed to write icns file %s\n", icns_name.UTF8String); |
| exit(EXIT_FAILURE); |
| } |
| } |
| |
| return EXIT_SUCCESS; |
| } |