chromium/tools/mac/icons/makeicns2.m

// 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, 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:&current_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;
}