| // 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/SkRRect.h" |
| #include "third_party/skia/include/core/SkRect.h" |
| #include "third_party/skia/include/effects/SkImageFilters.h" |
| #include "third_party/skia/src/core/SkBitmapDevice.h" |
| #include "third_party/skia/src/core/SkDraw.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/gfx/skia_util.h" |
| |
| namespace { |
| const float kCornerRadiusFactor = 0.22f; |
| } |
| |
| 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; |
| } |
| |
| bool HasSolidColorBorder(const gfx::Image& icon) { |
| SkBitmap bitmap = icon.AsBitmap(); |
| int width = bitmap.width(); |
| int height = bitmap.height(); |
| |
| SkColor reference_color = bitmap.getColor(0, 0); |
| |
| for (int x = 0; x < width; ++x) { |
| if (bitmap.getColor(x, 0) != reference_color || |
| bitmap.getColor(x, height - 1) != reference_color) { |
| return false; |
| } |
| } |
| |
| for (int y = 0; y < height; ++y) { |
| if (bitmap.getColor(0, y) != reference_color || |
| bitmap.getColor(width - 1, y) != reference_color) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| // TODO(https://crbug.com/372688523): Implement masking without zooming. |
| gfx::Image MaskDiyAppIcon(const gfx::Image& icon) { |
| SkBitmap bitmap = icon.AsBitmap(); |
| int width = bitmap.width(); |
| int height = bitmap.height(); |
| |
| bool is_solid_color_border = HasSolidColorBorder(icon); |
| |
| SkBitmap output_bitmap; |
| output_bitmap.allocN32Pixels(width, height); |
| SkCanvas canvas(output_bitmap); |
| |
| canvas.clear(SK_ColorTRANSPARENT); |
| |
| float corner_radius = width * kCornerRadiusFactor; |
| int background_margin = width * 0.1; // 10% margin |
| int background_offset_x = background_margin; |
| int background_width = width - 2 * background_margin; |
| SkRect rect = SkRect::MakeXYWH(background_offset_x, background_offset_x, |
| background_width, background_width); |
| SkRRect rrect = SkRRect::MakeRectXY(rect, corner_radius, corner_radius); |
| SkPaint paint; |
| paint.setColor(SK_ColorWHITE); |
| if (is_solid_color_border) { |
| paint.setColor(bitmap.getColor(0, 0)); |
| // Create a mask with rounded corners |
| SkPath path; |
| path.addRRect(rrect); |
| canvas.clipPath(path, SkClipOp::kIntersect, true); |
| } |
| paint.setAntiAlias(true); |
| canvas.drawRRect(rrect, paint); |
| SkRect src_rect = SkRect::MakeWH(width, height); |
| int image_margin = background_width * 0.1; // 10% margin |
| int image_offset_x = background_offset_x + image_margin; |
| int image_width = background_width - 2 * image_margin; |
| SkRect dst_rect = SkRect::MakeXYWH(image_offset_x, image_offset_x, |
| image_width, image_width); |
| canvas.drawImageRect(bitmap.asImage(), src_rect, dst_rect, |
| SkSamplingOptions(), nullptr, |
| SkCanvas::kFast_SrcRectConstraint); |
| |
| return gfx::Image::CreateFrom1xBitmap(output_bitmap); |
| } |
| } // namespace web_app |