Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.

Commit 1f5e1f6

Browse files
authored
[Hackdays] Share via QR Code (#1177)
Task/Issue URL: https://app.asana.com/0/42792087274227/1204180319229124/f **Description**: Adds "QR Code" item to Share menu **Steps to test this PR**: 1. Validate QR code sharing works (regular QR code) both for regular and ddg URLs 2. Validate QR code can be scanned for very long URLs (large QR code) <!-- Tagging instructions If this PR isn't ready to be merged for whatever reason it should be marked with the `DO NOT MERGE` label (particularly if it's a draft) If it's pending Product Review/PFR, please add the `Pending Product Review` label. If at any point it isn't actively being worked on/ready for review/otherwise moving forward (besides the above PR/PFR exception) strongly consider closing it (or not opening it in the first place). If you decide not to close it, make sure it's labelled to make it clear the PRs state and comment with more information. --> --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) **When ready for review, remember to post the PR in MM**
1 parent 6a631ff commit 1f5e1f6

File tree

13 files changed

+451
-1
lines changed

13 files changed

+451
-1
lines changed

DuckDuckGo.xcodeproj/project.pbxproj

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1502,6 +1502,8 @@
15021502
B603975129C1FF6000902A34 /* TestsURLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */; };
15031503
B603975229C1FFAD00902A34 /* ExpectedNavigationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B603972B29BEDF2100902A34 /* ExpectedNavigationExtension.swift */; };
15041504
B603975329C1FFAE00902A34 /* ExpectedNavigationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B603972B29BEDF2100902A34 /* ExpectedNavigationExtension.swift */; };
1505+
B603FD9E2A02712E00F3FCA9 /* CIImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B603FD9D2A02712E00F3FCA9 /* CIImageExtension.swift */; };
1506+
B603FD9F2A02712E00F3FCA9 /* CIImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B603FD9D2A02712E00F3FCA9 /* CIImageExtension.swift */; };
15051507
B6040856274B830F00680351 /* DictionaryExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6040855274B830F00680351 /* DictionaryExtension.swift */; };
15061508
B604085C274B8FBA00680351 /* UnprotectedDomains.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B604085A274B8CA300680351 /* UnprotectedDomains.xcdatamodeld */; };
15071509
B6085D062743905F00A9C456 /* CoreDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6085D052743905F00A9C456 /* CoreDataStore.swift */; };
@@ -1796,6 +1798,10 @@
17961798
B6F56568299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */; };
17971799
B6F56569299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */; };
17981800
B6F5656A299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */; };
1801+
B6F7127E29F6779000594A45 /* QRSharingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F7127D29F6779000594A45 /* QRSharingService.swift */; };
1802+
B6F7127F29F6779000594A45 /* QRSharingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F7127D29F6779000594A45 /* QRSharingService.swift */; };
1803+
B6F7128129F681EB00594A45 /* QuickLookUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6F7128029F681EB00594A45 /* QuickLookUI.framework */; };
1804+
B6F7128229F6820A00594A45 /* QuickLookUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6F7128029F681EB00594A45 /* QuickLookUI.framework */; };
17991805
B6FA893D269C423100588ECD /* PrivacyDashboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6FA893C269C423100588ECD /* PrivacyDashboard.storyboard */; };
18001806
B6FA893F269C424500588ECD /* PrivacyDashboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */; };
18011807
B6FA8941269C425400588ECD /* PrivacyDashboardPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */; };
@@ -2566,6 +2572,7 @@
25662572
B603973729BF0EBE00902A34 /* PrivacyDashboardIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardIntegrationTests.swift; sourceTree = "<group>"; };
25672573
B603973B29BF1D7D00902A34 /* AutoconsentIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoconsentIntegrationTests.swift; sourceTree = "<group>"; };
25682574
B603974D29C1F93600902A34 /* TabPermissionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPermissionsTests.swift; sourceTree = "<group>"; };
2575+
B603FD9D2A02712E00F3FCA9 /* CIImageExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageExtension.swift; sourceTree = "<group>"; };
25692576
B6040855274B830F00680351 /* DictionaryExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryExtension.swift; sourceTree = "<group>"; };
25702577
B604085B274B8CA400680351 /* Permissions.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Permissions.xcdatamodel; sourceTree = "<group>"; };
25712578
B6085D052743905F00A9C456 /* CoreDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStore.swift; sourceTree = "<group>"; };
@@ -2803,6 +2810,8 @@
28032810
B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestsURLExtension.swift; sourceTree = "<group>"; };
28042811
B6F41030264D2B23003DA42C /* ProgressExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressExtension.swift; sourceTree = "<group>"; };
28052812
B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WKWebViewMockingExtension.swift; sourceTree = "<group>"; };
2813+
B6F7127D29F6779000594A45 /* QRSharingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRSharingService.swift; sourceTree = "<group>"; };
2814+
B6F7128029F681EB00594A45 /* QuickLookUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookUI.framework; path = System/Library/Frameworks/QuickLookUI.framework; sourceTree = SDKROOT; };
28062815
B6FA893C269C423100588ECD /* PrivacyDashboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PrivacyDashboard.storyboard; sourceTree = "<group>"; };
28072816
B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardViewController.swift; sourceTree = "<group>"; };
28082817
B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPopover.swift; sourceTree = "<group>"; };
@@ -2837,6 +2846,7 @@
28372846
isa = PBXFrameworksBuildPhase;
28382847
buildActionMask = 2147483647;
28392848
files = (
2849+
B6F7128229F6820A00594A45 /* QuickLookUI.framework in Frameworks */,
28402850
984FD3BF299ACF35007334DD /* Bookmarks in Frameworks */,
28412851
37A5E2F0298AA1B20047046B /* Persistence in Frameworks */,
28422852
378F44E629B4BDEE00899924 /* SwiftUIExtensions in Frameworks */,
@@ -2890,6 +2900,7 @@
28902900
isa = PBXFrameworksBuildPhase;
28912901
buildActionMask = 2147483647;
28922902
files = (
2903+
B6F7128129F681EB00594A45 /* QuickLookUI.framework in Frameworks */,
28932904
9807F645278CA16F00E1547B /* BrowserServicesKit in Frameworks */,
28942905
987799ED299998B1005D8EB6 /* Bookmarks in Frameworks */,
28952906
1E950E3F2912A10D0051A99B /* ContentBlocking in Frameworks */,
@@ -4071,6 +4082,7 @@
40714082
85AE2FF024A33A2D002D507F /* Frameworks */ = {
40724083
isa = PBXGroup;
40734084
children = (
4085+
B6F7128029F681EB00594A45 /* QuickLookUI.framework */,
40744086
85AE2FF124A33A2D002D507F /* WebKit.framework */,
40754087
);
40764088
name = Frameworks;
@@ -4388,6 +4400,7 @@
43884400
B6FA893A269C414900588ECD /* PrivacyDashboard */,
43894401
AAC6881528626B6F00D54247 /* RecentlyClosed */,
43904402
85890634267B6CC500D23B0D /* SecureVault */,
4403+
B6F7127C29F6776B00594A45 /* Sharing */,
43914404
4B677422255DBEB800025BD8 /* SmarterEncryption */,
43924405
B68458AE25C7E75100DC17B6 /* StateRestoration */,
43934406
B6A9E44E26142AF90067D1B9 /* Statistics */,
@@ -4878,7 +4891,6 @@
48784891
85480F8925CDC360009424E3 /* MainMenu.storyboard */,
48794892
AA4BBA3A25C58FA200C4FB0F /* MainMenu.swift */,
48804893
AA6EF9B425081B4C004754E6 /* MainMenuActions.swift */,
4881-
B63ED0E426BB8FB900A9DAD1 /* SharingMenu.swift */,
48824894
AA7E919628746BCC00AB6B62 /* HistoryMenu.swift */,
48834895
AAAB9113288EB1D600A057A9 /* CleanThisHistoryMenuItem.swift */,
48844896
AAAB9115288EB46B00A057A9 /* VisitMenuItem.swift */,
@@ -5157,6 +5169,7 @@
51575169
B6106B9D26A565DA0013B453 /* BundleExtension.swift */,
51585170
B626A75F2992407C00053070 /* CancellableExtension.swift */,
51595171
4BA1A6C1258B0A1300F6F690 /* ContiguousBytesExtension.swift */,
5172+
B603FD9D2A02712E00F3FCA9 /* CIImageExtension.swift */,
51605173
85AC3AF625D5DBFD00C7D2AA /* DataExtension.swift */,
51615174
B6A9E46F26146A250067D1B9 /* DateExtension.swift */,
51625175
B6040855274B830F00680351 /* DictionaryExtension.swift */,
@@ -5746,6 +5759,15 @@
57465759
path = "tests-server";
57475760
sourceTree = "<group>";
57485761
};
5762+
B6F7127C29F6776B00594A45 /* Sharing */ = {
5763+
isa = PBXGroup;
5764+
children = (
5765+
B63ED0E426BB8FB900A9DAD1 /* SharingMenu.swift */,
5766+
B6F7127D29F6779000594A45 /* QRSharingService.swift */,
5767+
);
5768+
path = Sharing;
5769+
sourceTree = "<group>";
5770+
};
57495771
B6FA893A269C414900588ECD /* PrivacyDashboard */ = {
57505772
isa = PBXGroup;
57515773
children = (
@@ -6692,8 +6714,10 @@
66926714
3706FB7A293F65D500E42796 /* FileDownloadManager.swift in Sources */,
66936715
3706FB7B293F65D500E42796 /* BookmarkImport.swift in Sources */,
66946716
3706FB7C293F65D500E42796 /* KeySetDictionary.swift in Sources */,
6717+
B6F7127F29F6779000594A45 /* QRSharingService.swift in Sources */,
66956718
3706FB7E293F65D500E42796 /* FireCoordinator.swift in Sources */,
66966719
3706FB7F293F65D500E42796 /* GeolocationProvider.swift in Sources */,
6720+
B603FD9F2A02712E00F3FCA9 /* CIImageExtension.swift in Sources */,
66976721
3706FB80293F65D500E42796 /* NSAlert+ActiveDownloadsTermination.swift in Sources */,
66986722
3707C717294B5D0F00682A9F /* FindInPageTabExtension.swift in Sources */,
66996723
3706FB81293F65D500E42796 /* IndexPathExtension.swift in Sources */,
@@ -7591,6 +7615,7 @@
75917615
AA9E9A5625A3AE8400D1959D /* NSWindowExtension.swift in Sources */,
75927616
AAC5E4C725D6A6E8007F5990 /* BookmarkPopover.swift in Sources */,
75937617
37CC53F027E8D1440028713D /* PreferencesDownloadsView.swift in Sources */,
7618+
B6F7127E29F6779000594A45 /* QRSharingService.swift in Sources */,
75947619
B68C2FB227706E6A00BF2C7D /* ProcessExtension.swift in Sources */,
75957620
B6106BA726A7BECC0013B453 /* PermissionAuthorizationQuery.swift in Sources */,
75967621
3171D6BA288984D00068632A /* BadgeAnimationView.swift in Sources */,
@@ -7849,6 +7874,7 @@
78497874
85625996269C953C00EE44BC /* PasswordManagementViewController.swift in Sources */,
78507875
4BB99D0226FE191E001E4761 /* ImportedBookmarks.swift in Sources */,
78517876
B626A75A29921FAA00053070 /* NavigationActionPolicyExtension.swift in Sources */,
7877+
B603FD9E2A02712E00F3FCA9 /* CIImageExtension.swift in Sources */,
78527878
AA6EF9B3250785D5004754E6 /* NSMenuExtension.swift in Sources */,
78537879
AA7412B524D1536B00D22FE0 /* MainWindowController.swift in Sources */,
78547880
AA9FF95924A1ECF20039E328 /* Tab.swift in Sources */,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"colors" : [
3+
{
4+
"color" : {
5+
"color-space" : "display-p3",
6+
"components" : {
7+
"alpha" : "1.000",
8+
"blue" : "0x3F",
9+
"green" : "0x61",
10+
"red" : "0xCE"
11+
}
12+
},
13+
"idiom" : "universal"
14+
}
15+
],
16+
"info" : {
17+
"author" : "xcode",
18+
"version" : 1
19+
}
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "QR-16@1.25.pdf",
5+
"idiom" : "universal"
6+
}
7+
],
8+
"info" : {
9+
"author" : "xcode",
10+
"version" : 1
11+
},
12+
"properties" : {
13+
"template-rendering-intent" : "template"
14+
}
15+
}
Binary file not shown.
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
//
2+
// CIImageExtension.swift
3+
//
4+
// Copyright © 2023 DuckDuckGo. All rights reserved.
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
19+
import CoreImage.CIFilterBuiltins
20+
21+
extension CIImage {
22+
23+
static var retinaScaleFactor: CGFloat {
24+
max(NSScreen.maxBackingScaleFactor, NSScreen.defaultBackingScaleFactor) // “retina” or larger
25+
}
26+
27+
/// Generates a `CIImage` of a rounded rectangle with a specified extent and corner radius.
28+
static func rect(in extent: CGRect, cornerRadius: CGFloat = 0, color: NSColor? = nil) -> CIImage {
29+
let roundedRectFilter = CIFilter.roundedRectangleGenerator()
30+
roundedRectFilter.extent = extent
31+
roundedRectFilter.radius = Float(cornerRadius)
32+
if let color {
33+
roundedRectFilter.color = color.ciColor
34+
}
35+
36+
return roundedRectFilter.outputImage!
37+
}
38+
39+
/// Generates a `CIImage` of a circle with a specified center point and radius.
40+
static func circle(at center: CGPoint, radius: CGFloat, color: NSColor? = nil) -> CIImage {
41+
return rect(in: CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2), cornerRadius: radius, color: color)
42+
}
43+
44+
enum QRCorrectionLevel: String {
45+
/// 7% of codewords can be restored.
46+
case low = "L"
47+
/// 15% of codewords can be restored.
48+
case medium = "M"
49+
/// 25% of codewords can be restored.
50+
case normal = "Q"
51+
/// 30% of codewords can be restored.
52+
case high = "H"
53+
}
54+
/// Generates a QR code `CIImage` for a given data input.
55+
static func qrCode(for data: Data, correctionLevel: QRCorrectionLevel? = nil) -> CIImage? {
56+
let filter = CIFilter.qrCodeGenerator()
57+
filter.message = data
58+
if let correctionLevel {
59+
filter.correctionLevel = correctionLevel.rawValue
60+
}
61+
return filter.outputImage
62+
}
63+
64+
struct QRCodeParameters {
65+
66+
fileprivate static let iconSizeFactor: CGFloat = 0.25
67+
68+
var logicalQrSize: Int
69+
var correctionLevel: QRCorrectionLevel?
70+
71+
var icon: CIImage?
72+
73+
var color: NSColor
74+
var backgroundColor: NSColor
75+
76+
static let `default` = QRCodeParameters(logicalQrSize: 250,
77+
correctionLevel: nil,
78+
icon: nil,
79+
color: .black,
80+
backgroundColor: .white)
81+
82+
static let duckDuckGo: QRCodeParameters = {
83+
let logicalQrSize = QRCodeParameters.default.logicalQrSize
84+
let icon: CIImage = {
85+
let logo = NSImage(named: "Logo")!
86+
let logoRadiusFactor: CGFloat = 0.77
87+
let logoMargin: CGFloat = 6
88+
let logoBackgroundColor = NSColor.logoBackgroundColor
89+
90+
let logoSize = NSSize(width: logicalQrSize, height: logicalQrSize).scaled(by: CIImage.retinaScaleFactor)
91+
var image = logo.ciImage(with: logoSize)
92+
93+
// cut Dax circle
94+
let maskImage = CIImage.circle(at: image.extent.center, radius: image.extent.width * (logoRadiusFactor / 2))
95+
image = image.masked(with: maskImage)
96+
97+
// add background
98+
let backgroundExtent = CGRect(x: 0, y: 0, width: image.extent.width + logoMargin * 2, height: image.extent.width + logoMargin * 2)
99+
let background = CIImage.rect(in: backgroundExtent, cornerRadius: backgroundExtent.width / 2, color: logoBackgroundColor)
100+
image = image.centered(in: backgroundExtent).composited(over: background)
101+
102+
return image
103+
}()
104+
105+
return QRCodeParameters(logicalQrSize: logicalQrSize,
106+
correctionLevel: .high,
107+
icon: icon,
108+
color: .logoBackgroundColor,
109+
backgroundColor: .white)
110+
}()
111+
}
112+
113+
static func qrCode(for data: Data, parameters: QRCodeParameters = .default) -> CIImage? {
114+
guard var qr = CIImage.qrCode(for: data, correctionLevel: parameters.correctionLevel) else { return nil }
115+
116+
// size of the QR in “dots”
117+
let qrSize = qr.extent.size.width
118+
119+
// scale to QR Size in Pixels
120+
let qrScale = CGFloat((CGFloat(parameters.logicalQrSize) * CIImage.retinaScaleFactor) / CGFloat(qrSize))
121+
qr = qr.scaled(by: qrScale)
122+
123+
// tint
124+
qr = qr.tinted(using: parameters.color)
125+
126+
// extend background by 2 QR dots in each dimension
127+
let backgroundExtent = qr.extent.insetBy(dx: -2 * qrScale, dy: -2 * qrScale)
128+
let background = CIImage.rect(in: backgroundExtent, cornerRadius: qrScale * 2, color: parameters.backgroundColor)
129+
// add background
130+
qr = qr.centered(in: backgroundExtent).composited(over: background)
131+
132+
// add logo
133+
if let icon = parameters.icon {
134+
let sizeInDots = CGFloat(Int(qrSize * QRCodeParameters.iconSizeFactor))
135+
let icon = icon.scaled(by: (qrScale * sizeInDots) / icon.extent.width)
136+
137+
qr = icon.centered(in: qr.extent).composited(over: qr)
138+
}
139+
140+
return qr
141+
}
142+
143+
/// Creates a new `CIImage` by masking the current image with the specified mask image.
144+
func masked(with maskImage: CIImage) -> CIImage {
145+
let filter = CIFilter.blendWithMask()
146+
filter.inputImage = self
147+
filter.maskImage = maskImage
148+
149+
return filter.outputImage!.cropped(to: maskImage.extent)
150+
}
151+
152+
/// Generates a new `CIImage` by scaling the input image by a specified scale factor.
153+
func scaled(by scaleFactor: CGFloat) -> CIImage {
154+
let transform = CGAffineTransform(scaleX: scaleFactor, y: scaleFactor)
155+
return self.transformed(by: transform)
156+
}
157+
158+
/// Returns a new `CIImage` by centering the current image within another image's extent.
159+
func centered(in otherExtent: CGRect) -> CIImage {
160+
self.transformed(by: CGAffineTransform(translationX: otherExtent.midX - extent.midX, y: otherExtent.midY - extent.midY))
161+
}
162+
163+
/// Generates a new `CIImage` by inverting the colors of the input image.
164+
func inverted() -> CIImage! {
165+
let invertedColorFilter = CIFilter.colorInvert()
166+
invertedColorFilter.inputImage = self
167+
168+
return invertedColorFilter.outputImage
169+
}
170+
171+
/// Generates a new `CIImage` by converting black areas of the input image to transparent and other areas to white.
172+
func blackToTransparent() -> CIImage! {
173+
let blackTransparentFilter = CIFilter.maskToAlpha()
174+
blackTransparentFilter.inputImage = self
175+
176+
return blackTransparentFilter.outputImage
177+
}
178+
179+
/// Generates a new `CIImage` by tinting the input image with a specified color using multiply compositing.
180+
func tinted(using color: NSColor) -> CIImage! {
181+
let filter = CIFilter.multiplyCompositing()
182+
filter.inputImage = CIImage(color: color.ciColor)
183+
filter.backgroundImage = self.inverted()?.blackToTransparent()
184+
185+
return filter.outputImage
186+
}
187+
188+
var cgImage: CGImage {
189+
CIContext(options: nil).createCGImage(self, from: self.extent)!
190+
}
191+
192+
}
193+
194+
extension CGImage {
195+
196+
/// Returns image bitmap data with the specified file format.
197+
func bitmapRepresentation(using format: NSBitmapImageRep.FileType) -> Data? {
198+
let bitmapRep = NSBitmapImageRep(cgImage: self)
199+
bitmapRep.size = NSSize(width: Int(CGFloat(self.width) / CIImage.retinaScaleFactor),
200+
height: Int(CGFloat(self.height) / CIImage.retinaScaleFactor))
201+
202+
return bitmapRep.representation(using: format, properties: [:])
203+
}
204+
205+
}

DuckDuckGo/Common/Extensions/NSColorExtension.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,12 @@ extension NSColor {
168168

169169
static let buttonColor: NSColor = NSColor(named: "ButtonColor")!
170170

171+
static var logoBackgroundColor: NSColor {
172+
NSColor(named: "LogoBackgroundColor")!
173+
}
174+
175+
var ciColor: CIColor {
176+
CIColor(color: self)!
177+
}
178+
171179
}

DuckDuckGo/Common/Extensions/NSImageExtensions.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,9 @@ extension NSImage {
5959
return image
6060
}
6161

62+
func ciImage(with size: NSSize?) -> CIImage {
63+
var rect = NSRect(origin: .zero, size: size ?? self.size)
64+
return CIImage(cgImage: self.cgImage(forProposedRect: &rect, context: nil, hints: nil)!)
65+
}
66+
6267
}

0 commit comments

Comments
 (0)