imagebrowserico

How does the browser decide which bitmap to load from an ICO file?


I'm looking at implementing the Image class from browsers, and are currently implementing ICO support. However, I'm stuck on figuring out the algorithm to decide which image from the ICO-file to load.

This is the code that I'm using inside the browser:

const img = new Image()

img.src = 'test.ico'

img.onload = () => {
  console.log(img.width, img.height)
  document.body.append(img)
}

When testing out some different images I have made the following observations:

  1. Apples favicon contains 4 images of sizes: 32, 16, 32, and 16. The first one is used in both Chrome and Safari.
  2. GitHubs favicon contains 2 images of sizes: 16, and 32. The second one is used in both Chrome and Safari.
  3. Windows98Scaled Icon 02.ico contains 14 images of sizes: 32, 16, 256, 128, 48, 32, 24, 16, 256, 128, 48, 32, 24, and 16. The third one is used in both Chrome and Safari.

One guess is that it takes the first images that has the largest size (e.g. first find the maximum size, then pick the first image that matches that size).

Is there any standards document governing how this should be done? Or any other source of documentation to follow?


Solution

  • According to the source code of Chrome and Firefox, images are first prioritised based on size, and secondarily based on bit depth.

    Firefox:

        if (!biggestEntry ||
            (e.mBitCount >= biggestEntry->mBitCount &&
             e.mSize.width * e.mSize.height >=
                 biggestEntry->mSize.width * biggestEntry->mSize.height)) {
          biggestEntry = &e;
    
          if (!desiredSize) {
            mDirEntry = &e;
          }
        }
    

    https://github.com/mozilla/gecko-dev/blob/10a46f9dacc39a9305ef9cbfb27f8b68e25eccc9/image/decoders/nsICODecoder.cpp#L249-L258

    Chrome:

    bool ICOImageDecoder::CompareEntries(const IconDirectoryEntry& a,
                                         const IconDirectoryEntry& b) {
      // Larger icons are better.  After that, higher bit-depth icons are better.
      const int a_entry_area = a.size_.width() * a.size_.height();
      const int b_entry_area = b.size_.width() * b.size_.height();
      return (a_entry_area == b_entry_area) ? (a.bit_count_ > b.bit_count_)
                                            : (a_entry_area > b_entry_area);
    }
    
    // ...
    
      // Arrange frames in decreasing quality order.
      std::sort(dir_entries_.begin(), dir_entries_.end(), CompareEntries);
    

    https://github.com/chromium/chromium/blob/a23a83b6c0e9295e10b3d9e88c40da7395df5a18/third_party/blink/renderer/platform/image-decoders/ico/ico_image_decoder.cc#L129-L136


    Note that in the case of images with equal size and bit-depth, the behaviour is slightly different. Firefox will always select the last image, whereas Chrome uses std::sort which makes no guarantees on the ordering of equal elements.