/* * Copyright (C) 2022 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "ARKitBadgeSystemImage.h" #if USE(SYSTEM_PREVIEW) #import "ColorSpaceCG.h" #import "FloatRect.h" #import "GeometryUtilities.h" #import "GraphicsContext.h" #import "IOSurfacePool.h" #import #import #import #import namespace WebCore { static NSBundle *arKitBundle() { static NSBundle *arKitBundle; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ arKitBundle = []() { #if PLATFORM(IOS_FAMILY_SIMULATOR) dlopen("/System/Library/PrivateFrameworks/AssetViewer.framework/AssetViewer", RTLD_NOW); return [NSBundle bundleForClass:NSClassFromString(@"ASVThumbnailView")]; #else return [NSBundle bundleWithURL:[NSURL fileURLWithPath:@"/System/Library/PrivateFrameworks/AssetViewer.framework"]]; #endif }(); }); return arKitBundle; } static RetainPtr loadARKitPDFPage(NSString *imageName) { NSURL *url = [arKitBundle() URLForResource:imageName withExtension:@"pdf"]; if (!url) return nullptr; auto document = adoptCF(CGPDFDocumentCreateWithURL((CFURLRef)url)); if (!document) return nullptr; if (!CGPDFDocumentGetNumberOfPages(document.get())) return nullptr; return CGPDFDocumentGetPage(document.get(), 1); } static RetainPtr systemPreviewLogo() { static NeverDestroyed> logoPage; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ logoPage.get() = loadARKitPDFPage(@"ARKitBadge"); }); return logoPage; } void ARKitBadgeSystemImage::draw(GraphicsContext& graphicsContext, const FloatRect& rect) const { auto page = systemPreviewLogo(); if (!page) return; static const int largeBadgeDimension = 70; static const int largeBadgeOffset = 20; static const int smallBadgeDimension = 35; static const int smallBadgeOffset = 8; static const int minimumSizeForLargeBadge = 240; bool useSmallBadge = rect.width() < minimumSizeForLargeBadge || rect.height() < minimumSizeForLargeBadge; int badgeOffset = useSmallBadge ? smallBadgeOffset : largeBadgeOffset; int badgeDimension = useSmallBadge ? smallBadgeDimension : largeBadgeDimension; int minimumDimension = badgeDimension + 2 * badgeOffset; if (rect.width() < minimumDimension || rect.height() < minimumDimension) return; CGRect absoluteBadgeRect = CGRectMake(rect.x() + rect.width() - badgeDimension - badgeOffset, rect.y() + badgeOffset, badgeDimension, badgeDimension); CGRect insetBadgeRect = CGRectMake(rect.width() - badgeDimension - badgeOffset, badgeOffset, badgeDimension, badgeDimension); CGRect badgeRect = CGRectMake(0, 0, badgeDimension, badgeDimension); if (!m_image || !m_image->nativeImage()) return; CIImage *inputImage = [CIImage imageWithCGImage:m_image->nativeImage()->platformImage().get()]; // Create a circle to be used for the clipping path in the badge, as well as the drop shadow. RetainPtr circle = adoptCF(CGPathCreateWithRoundedRect(absoluteBadgeRect, badgeDimension / 2, badgeDimension / 2, nullptr)); if (graphicsContext.paintingDisabled()) return; GraphicsContextStateSaver stateSaver(graphicsContext); CGContextRef ctx = graphicsContext.platformContext(); if (!ctx) return; CGContextSaveGState(ctx); // Draw a drop shadow around the circle. // Use the GraphicsContext function, because it calculates the blur radius in context space, // rather than screen space. constexpr auto shadowColor = Color::black.colorWithAlphaByte(26); graphicsContext.setShadow(FloatSize { }, 16, shadowColor); // The circle must have an alpha channel value of 1 for the shadow color to appear. CGFloat circleColorComponents[4] = { 0, 0, 0, 1 }; RetainPtr circleColor = adoptCF(CGColorCreate(sRGBColorSpaceRef(), circleColorComponents)); CGContextSetFillColorWithColor(ctx, circleColor.get()); // Clip out the circle to only show the shadow. CGContextBeginPath(ctx); CGContextAddRect(ctx, rect); CGContextAddPath(ctx, circle.get()); CGContextClosePath(ctx); CGContextEOClip(ctx); // Draw a slightly smaller circle with a shadow, otherwise we'll see a fringe of the solid // black circle around the edges of the clipped path below. CGContextBeginPath(ctx); CGRect slightlySmallerAbsoluteBadgeRect = CGRectMake(absoluteBadgeRect.origin.x + 0.5, absoluteBadgeRect.origin.y + 0.5, badgeDimension - 1, badgeDimension - 1); RetainPtr slightlySmallerCircle = adoptCF(CGPathCreateWithRoundedRect(slightlySmallerAbsoluteBadgeRect, slightlySmallerAbsoluteBadgeRect.size.width / 2, slightlySmallerAbsoluteBadgeRect.size.height / 2, nullptr)); CGContextAddPath(ctx, slightlySmallerCircle.get()); CGContextClosePath(ctx); CGContextFillPath(ctx); CGContextRestoreGState(ctx); // Draw the blurred backdrop. Scale from intrinsic size to render size. CGAffineTransform transform = CGAffineTransformIdentity; transform = CGAffineTransformScale(transform, rect.width() / m_imageSize.width(), rect.height() / m_imageSize.height()); CIImage *scaledImage = [inputImage imageByApplyingTransform:transform]; // CoreImage coordinates are y-up, so we need to flip the badge rectangle within the image frame. CGRect flippedInsetBadgeRect = CGRectMake(insetBadgeRect.origin.x, rect.height() - insetBadgeRect.origin.y - insetBadgeRect.size.height, badgeDimension, badgeDimension); // Create a cropped region with pixel values extending outwards. CIImage *clampedImage = [scaledImage imageByClampingToRect:flippedInsetBadgeRect]; // Blur. CIImage *blurredImage = [clampedImage imageByApplyingGaussianBlurWithSigma:10]; // Saturate. CIFilter *saturationFilter = [CIFilter filterWithName:@"CIColorControls"]; [saturationFilter setValue:blurredImage forKey:kCIInputImageKey]; [saturationFilter setValue:@1.8 forKey:kCIInputSaturationKey]; // Tint. CIFilter *tintFilter1 = [CIFilter filterWithName:@"CIConstantColorGenerator"]; CIColor *tintColor1 = [CIColor colorWithRed:1 green:1 blue:1 alpha:0.18]; [tintFilter1 setValue:tintColor1 forKey:kCIInputColorKey]; // Blend the tint with the saturated output. CIFilter *sourceOverFilter = [CIFilter filterWithName:@"CISourceOverCompositing"]; [sourceOverFilter setValue:tintFilter1.outputImage forKey:kCIInputImageKey]; [sourceOverFilter setValue:saturationFilter.outputImage forKey:kCIInputBackgroundImageKey]; RetainPtr ciContext = [CIContext context]; RetainPtr cgImage; #if HAVE(IOSURFACE_COREIMAGE_SUPPORT) // Crop the result to the badge location. CIImage *croppedImage = [sourceOverFilter.outputImage imageByCroppingToRect:flippedInsetBadgeRect]; CIImage *translatedImage = [croppedImage imageByApplyingTransform:CGAffineTransformMakeTranslation(-flippedInsetBadgeRect.origin.x, -flippedInsetBadgeRect.origin.y)]; auto surfaceDimension = useSmallBadge ? smallBadgeDimension : largeBadgeDimension; std::unique_ptr badgeSurface = IOSurface::create(&IOSurfacePool::sharedPool(), { surfaceDimension, surfaceDimension }, DestinationColorSpace::SRGB()); IOSurfaceRef surface = badgeSurface->surface(); [ciContext render:translatedImage toIOSurface:surface bounds:badgeRect colorSpace:sRGBColorSpaceRef()]; cgImage = useSmallBadge ? badgeSurface->createImage() : badgeSurface->createImage(); #else cgImage = adoptCF([ciContext createCGImage:sourceOverFilter.outputImage fromRect:flippedInsetBadgeRect]); #endif // Before we render the result, we should clip to a circle around the badge rectangle. CGContextSaveGState(ctx); CGContextBeginPath(ctx); CGContextAddPath(ctx, circle.get()); CGContextClosePath(ctx); CGContextClip(ctx); CGContextTranslateCTM(ctx, absoluteBadgeRect.origin.x, absoluteBadgeRect.origin.y); CGContextTranslateCTM(ctx, 0, badgeDimension); CGContextScaleCTM(ctx, 1, -1); CGContextDrawImage(ctx, badgeRect, cgImage.get()); CGSize pdfSize = CGPDFPageGetBoxRect(page.get(), kCGPDFMediaBox).size; CGFloat scaleX = badgeDimension / pdfSize.width; CGFloat scaleY = badgeDimension / pdfSize.height; CGContextScaleCTM(ctx, scaleX, scaleY); CGContextDrawPDFPage(ctx, page.get()); CGContextFlush(ctx); CGContextRestoreGState(ctx); } } // namespace WebCore #endif // USE(SYSTEM_PREVIEW)