chromium/chrome/browser/web_applications/os_integration/mac/icon_utils.mm

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/web_applications/os_integration/mac/icon_utils.h"

#import <Cocoa/Cocoa.h>

#include "base/check_op.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkPaint.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/include/effects/SkImageFilters.h"
#include "ui/gfx/image/image.h"

namespace web_app {

gfx::Image CreateAppleMaskedAppIcon(const gfx::Image& base_icon) {
  // According to Apple design templates, a macOS icon should be a rounded
  // rect surrounded by some transparent padding.  The rounded rect's size
  // is approximately 80% of the overall icon, but this percentage varies.
  // Exact mask size and shape gleaned from Apple icon design templates,
  // specifically the March 2023 macOS Production Templates Sketch file at
  // https://developer.apple.com/design/resources/.  A few corner radius
  // values were unavailable in the file because the relevant shapes were
  // represenated as plain paths rather than rounded rects.
  //
  // The Web App Manifest spec defines a safe zone for maskable icons
  // (https://www.w3.org/TR/appmanifest/#icon-masks) in a centered circle
  // with diameter 80% of the overall icon.  Since the macOS icon mask
  // is a rounded rect that is never smaller than 80% of the overall icon,
  // it is within spec to simply draw our base icon full size and clip
  // whatever is outside of the rounded rect.  This is what is currently
  // implemented, even though is is different from what Apple does in macOS
  // Sonoma web apps (where instead they first scale the icon to cover just
  // the rounded rect, only clipping the corners).  Somewhere in the middle
  // of these options might be ideal, although with the current icon loading
  // code icons have already been resized to neatly fill entire standard sized
  // icons by the time this code runs, so doing any more resizing here would
  // not be great.
  int base_size = base_icon.Width();
  SkScalar icon_grid_bounding_box_inset;
  SkScalar icon_grid_bounding_box_corner_radius;
  SkScalar shadow_y_offset;
  SkScalar shadow_blur_radius;
  switch (base_size) {
    case 16:
      // An exact value for the 16 corner radius was not available.
      // this corner radius is derived by dividing the 32 radius by 2
      icon_grid_bounding_box_inset = 1.0;
      icon_grid_bounding_box_corner_radius = 2.785;
      shadow_y_offset = 0.5;
      shadow_blur_radius = 0.5;
      break;
    case 32:
      icon_grid_bounding_box_inset = 2.0;
      icon_grid_bounding_box_corner_radius = 5.75;
      shadow_y_offset = 1.0;
      shadow_blur_radius = 1.0;
      break;
    case 64:
      icon_grid_bounding_box_inset = 6.0;
      icon_grid_bounding_box_corner_radius = 11.5;
      shadow_y_offset = 2;
      shadow_blur_radius = 2;
      break;
    case 128:
      // An exact value for the 128 corner radius was not available.
      // this corner radius is derived by dividing the 256 radius by 2
      // or by multiplying the 64 radius by 2, both calculations
      // have the same result.
      icon_grid_bounding_box_inset = 12.0;
      icon_grid_bounding_box_corner_radius = 23.0;
      shadow_y_offset = 1.25;
      shadow_blur_radius = 1.25;
      break;
    case 256:
      icon_grid_bounding_box_inset = 25.0;
      icon_grid_bounding_box_corner_radius = 46.0;
      shadow_y_offset = 2.5;
      shadow_blur_radius = 2.5;
      break;
    case 512:
      icon_grid_bounding_box_inset = 50.0;
      icon_grid_bounding_box_corner_radius = 92.0;
      shadow_y_offset = 5.0;
      shadow_blur_radius = 5.0;
      break;
    case 1024:
      // An exact value for the 1024 corner radius was not available.
      // this corner radius is derived by multiplying the 512 radius by 2
      icon_grid_bounding_box_inset = 100.0;
      icon_grid_bounding_box_corner_radius = 184.0;
      shadow_y_offset = 10.0;
      shadow_blur_radius = 10.0;
      break;
    default:
      // Use 1024 sizes as a reference for approximating any size mask if needed
      icon_grid_bounding_box_inset = base_size * 100.0 / 1024.0;
      icon_grid_bounding_box_corner_radius = base_size * 184.0 / 1024.0;
      shadow_y_offset = base_size * 10.0 / 1024.0;
      shadow_blur_radius = base_size * 10.0 / 1024.0;
      break;
  }

  // Creat a bitmap and canvas for drawing the masked icon
  SkImageInfo info =
      SkImageInfo::Make(base_size, base_size, SkColorType::kN32_SkColorType,
                        SkAlphaType::kUnpremul_SkAlphaType);
  SkBitmap bitmap;
  bitmap.allocPixels(info);
  SkCanvas canvas(bitmap);
  SkRect base_rect = SkRect::MakeIWH(base_size, base_size);

  // Create a path for the icon mask. The mask will match Apple's icon grid
  // bounding box.
  SkPath icon_grid_bounding_box_path;
  SkRect icon_grid_bounding_box_rect = base_rect.makeInset(
      icon_grid_bounding_box_inset, icon_grid_bounding_box_inset);
  icon_grid_bounding_box_path.addRoundRect(
      icon_grid_bounding_box_rect, icon_grid_bounding_box_corner_radius,
      icon_grid_bounding_box_corner_radius);

  // Draw the shadow
  SkPaint shadowPaint;
  shadowPaint.setStyle(SkPaint::kFill_Style);
  shadowPaint.setARGB(77, 0, 0, 0);
  shadowPaint.setImageFilter(
      SkImageFilters::Blur(shadow_blur_radius, shadow_blur_radius, nullptr));
  canvas.save();
  canvas.translate(0, shadow_y_offset);
  canvas.drawPath(icon_grid_bounding_box_path, shadowPaint);
  canvas.restore();

  // Clip to the mask
  canvas.clipPath(icon_grid_bounding_box_path, /*doAntiAlias=*/true);

  // Draw the base icon on a white background
  // If the base icon is opaque, we shouldn't see any white. Unfortunately,
  // first filling the clip with white and then overlaying the base icon
  // results in white artifacts around the corners.  So, we'll use an unclipped
  // intermediate canvas to overlay the base icon on a full white background,
  // and then draw the intermediate canvas in the clip in one shot to avoid
  // white around the edges.
  SkBitmap opaque_bitmap;
  opaque_bitmap.allocPixels(info);
  SkCanvas opaque_canvas(opaque_bitmap);
  SkPaint backgroundPaint;
  backgroundPaint.setStyle(SkPaint::kFill_Style);
  backgroundPaint.setARGB(255, 255, 255, 255);
  opaque_canvas.drawRect(base_rect, backgroundPaint);
  opaque_canvas.drawImage(SkImages::RasterFromBitmap(base_icon.AsBitmap()), 0,
                          0);
  canvas.drawImage(SkImages::RasterFromBitmap(opaque_bitmap), 0, 0);

  // Create the final image.
  return gfx::Image::CreateFrom1xBitmap(bitmap);
}

NSImageRep* OverlayImageRep(NSImage* background, NSImageRep* overlay) {
  DCHECK(background);
  NSInteger dimension = overlay.pixelsWide;
  DCHECK_EQ(dimension, overlay.pixelsHigh);
  NSBitmapImageRep* canvas = [[NSBitmapImageRep alloc]
      initWithBitmapDataPlanes:nullptr
                    pixelsWide:dimension
                    pixelsHigh:dimension
                 bitsPerSample:8
               samplesPerPixel:4
                      hasAlpha:YES
                      isPlanar:NO
                colorSpaceName:NSCalibratedRGBColorSpace
                   bytesPerRow:0
                  bitsPerPixel:0];

  // There isn't a colorspace name constant for sRGB, so retag.
  canvas = [canvas
      bitmapImageRepByRetaggingWithColorSpace:NSColorSpace.sRGBColorSpace];

  // Communicate the DIP scale (1.0). TODO(tapted): Investigate HiDPI.
  canvas.size = NSMakeSize(dimension, dimension);

  NSGraphicsContext* drawing_context =
      [NSGraphicsContext graphicsContextWithBitmapImageRep:canvas];
  [NSGraphicsContext saveGraphicsState];
  NSGraphicsContext.currentContext = drawing_context;
  [background drawInRect:NSMakeRect(0, 0, dimension, dimension)
                fromRect:NSZeroRect
               operation:NSCompositingOperationCopy
                fraction:1.0];
  [overlay drawInRect:NSMakeRect(0, 0, dimension, dimension)
             fromRect:NSZeroRect
            operation:NSCompositingOperationSourceOver
             fraction:1.0
       respectFlipped:NO
                hints:nil];
  [NSGraphicsContext restoreGraphicsState];
  return canvas;
}

}  // namespace web_app