Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1502,6 +1502,8 @@
B603975129C1FF6000902A34 /* TestsURLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */; };
B603975229C1FFAD00902A34 /* ExpectedNavigationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B603972B29BEDF2100902A34 /* ExpectedNavigationExtension.swift */; };
B603975329C1FFAE00902A34 /* ExpectedNavigationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B603972B29BEDF2100902A34 /* ExpectedNavigationExtension.swift */; };
B603FD9E2A02712E00F3FCA9 /* CIImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B603FD9D2A02712E00F3FCA9 /* CIImageExtension.swift */; };
B603FD9F2A02712E00F3FCA9 /* CIImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B603FD9D2A02712E00F3FCA9 /* CIImageExtension.swift */; };
B6040856274B830F00680351 /* DictionaryExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6040855274B830F00680351 /* DictionaryExtension.swift */; };
B604085C274B8FBA00680351 /* UnprotectedDomains.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B604085A274B8CA300680351 /* UnprotectedDomains.xcdatamodeld */; };
B6085D062743905F00A9C456 /* CoreDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6085D052743905F00A9C456 /* CoreDataStore.swift */; };
Expand Down Expand Up @@ -1796,6 +1798,10 @@
B6F56568299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */; };
B6F56569299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */; };
B6F5656A299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */; };
B6F7127E29F6779000594A45 /* QRSharingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F7127D29F6779000594A45 /* QRSharingService.swift */; };
B6F7127F29F6779000594A45 /* QRSharingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F7127D29F6779000594A45 /* QRSharingService.swift */; };
B6F7128129F681EB00594A45 /* QuickLookUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6F7128029F681EB00594A45 /* QuickLookUI.framework */; };
B6F7128229F6820A00594A45 /* QuickLookUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6F7128029F681EB00594A45 /* QuickLookUI.framework */; };
B6FA893D269C423100588ECD /* PrivacyDashboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6FA893C269C423100588ECD /* PrivacyDashboard.storyboard */; };
B6FA893F269C424500588ECD /* PrivacyDashboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */; };
B6FA8941269C425400588ECD /* PrivacyDashboardPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */; };
Expand Down Expand Up @@ -2566,6 +2572,7 @@
B603973729BF0EBE00902A34 /* PrivacyDashboardIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardIntegrationTests.swift; sourceTree = "<group>"; };
B603973B29BF1D7D00902A34 /* AutoconsentIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoconsentIntegrationTests.swift; sourceTree = "<group>"; };
B603974D29C1F93600902A34 /* TabPermissionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPermissionsTests.swift; sourceTree = "<group>"; };
B603FD9D2A02712E00F3FCA9 /* CIImageExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageExtension.swift; sourceTree = "<group>"; };
B6040855274B830F00680351 /* DictionaryExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryExtension.swift; sourceTree = "<group>"; };
B604085B274B8CA400680351 /* Permissions.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Permissions.xcdatamodel; sourceTree = "<group>"; };
B6085D052743905F00A9C456 /* CoreDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStore.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2803,6 +2810,8 @@
B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestsURLExtension.swift; sourceTree = "<group>"; };
B6F41030264D2B23003DA42C /* ProgressExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressExtension.swift; sourceTree = "<group>"; };
B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WKWebViewMockingExtension.swift; sourceTree = "<group>"; };
B6F7127D29F6779000594A45 /* QRSharingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRSharingService.swift; sourceTree = "<group>"; };
B6F7128029F681EB00594A45 /* QuickLookUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookUI.framework; path = System/Library/Frameworks/QuickLookUI.framework; sourceTree = SDKROOT; };
B6FA893C269C423100588ECD /* PrivacyDashboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PrivacyDashboard.storyboard; sourceTree = "<group>"; };
B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardViewController.swift; sourceTree = "<group>"; };
B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPopover.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2837,6 +2846,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B6F7128229F6820A00594A45 /* QuickLookUI.framework in Frameworks */,
984FD3BF299ACF35007334DD /* Bookmarks in Frameworks */,
37A5E2F0298AA1B20047046B /* Persistence in Frameworks */,
378F44E629B4BDEE00899924 /* SwiftUIExtensions in Frameworks */,
Expand Down Expand Up @@ -2890,6 +2900,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B6F7128129F681EB00594A45 /* QuickLookUI.framework in Frameworks */,
9807F645278CA16F00E1547B /* BrowserServicesKit in Frameworks */,
987799ED299998B1005D8EB6 /* Bookmarks in Frameworks */,
1E950E3F2912A10D0051A99B /* ContentBlocking in Frameworks */,
Expand Down Expand Up @@ -4071,6 +4082,7 @@
85AE2FF024A33A2D002D507F /* Frameworks */ = {
isa = PBXGroup;
children = (
B6F7128029F681EB00594A45 /* QuickLookUI.framework */,
85AE2FF124A33A2D002D507F /* WebKit.framework */,
);
name = Frameworks;
Expand Down Expand Up @@ -4388,6 +4400,7 @@
B6FA893A269C414900588ECD /* PrivacyDashboard */,
AAC6881528626B6F00D54247 /* RecentlyClosed */,
85890634267B6CC500D23B0D /* SecureVault */,
B6F7127C29F6776B00594A45 /* Sharing */,
4B677422255DBEB800025BD8 /* SmarterEncryption */,
B68458AE25C7E75100DC17B6 /* StateRestoration */,
B6A9E44E26142AF90067D1B9 /* Statistics */,
Expand Down Expand Up @@ -4878,7 +4891,6 @@
85480F8925CDC360009424E3 /* MainMenu.storyboard */,
AA4BBA3A25C58FA200C4FB0F /* MainMenu.swift */,
AA6EF9B425081B4C004754E6 /* MainMenuActions.swift */,
B63ED0E426BB8FB900A9DAD1 /* SharingMenu.swift */,
AA7E919628746BCC00AB6B62 /* HistoryMenu.swift */,
AAAB9113288EB1D600A057A9 /* CleanThisHistoryMenuItem.swift */,
AAAB9115288EB46B00A057A9 /* VisitMenuItem.swift */,
Expand Down Expand Up @@ -5157,6 +5169,7 @@
B6106B9D26A565DA0013B453 /* BundleExtension.swift */,
B626A75F2992407C00053070 /* CancellableExtension.swift */,
4BA1A6C1258B0A1300F6F690 /* ContiguousBytesExtension.swift */,
B603FD9D2A02712E00F3FCA9 /* CIImageExtension.swift */,
85AC3AF625D5DBFD00C7D2AA /* DataExtension.swift */,
B6A9E46F26146A250067D1B9 /* DateExtension.swift */,
B6040855274B830F00680351 /* DictionaryExtension.swift */,
Expand Down Expand Up @@ -5746,6 +5759,15 @@
path = "tests-server";
sourceTree = "<group>";
};
B6F7127C29F6776B00594A45 /* Sharing */ = {
isa = PBXGroup;
children = (
B63ED0E426BB8FB900A9DAD1 /* SharingMenu.swift */,
B6F7127D29F6779000594A45 /* QRSharingService.swift */,
);
path = Sharing;
sourceTree = "<group>";
};
B6FA893A269C414900588ECD /* PrivacyDashboard */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -6692,8 +6714,10 @@
3706FB7A293F65D500E42796 /* FileDownloadManager.swift in Sources */,
3706FB7B293F65D500E42796 /* BookmarkImport.swift in Sources */,
3706FB7C293F65D500E42796 /* KeySetDictionary.swift in Sources */,
B6F7127F29F6779000594A45 /* QRSharingService.swift in Sources */,
3706FB7E293F65D500E42796 /* FireCoordinator.swift in Sources */,
3706FB7F293F65D500E42796 /* GeolocationProvider.swift in Sources */,
B603FD9F2A02712E00F3FCA9 /* CIImageExtension.swift in Sources */,
3706FB80293F65D500E42796 /* NSAlert+ActiveDownloadsTermination.swift in Sources */,
3707C717294B5D0F00682A9F /* FindInPageTabExtension.swift in Sources */,
3706FB81293F65D500E42796 /* IndexPathExtension.swift in Sources */,
Expand Down Expand Up @@ -7591,6 +7615,7 @@
AA9E9A5625A3AE8400D1959D /* NSWindowExtension.swift in Sources */,
AAC5E4C725D6A6E8007F5990 /* BookmarkPopover.swift in Sources */,
37CC53F027E8D1440028713D /* PreferencesDownloadsView.swift in Sources */,
B6F7127E29F6779000594A45 /* QRSharingService.swift in Sources */,
B68C2FB227706E6A00BF2C7D /* ProcessExtension.swift in Sources */,
B6106BA726A7BECC0013B453 /* PermissionAuthorizationQuery.swift in Sources */,
3171D6BA288984D00068632A /* BadgeAnimationView.swift in Sources */,
Expand Down Expand Up @@ -7849,6 +7874,7 @@
85625996269C953C00EE44BC /* PasswordManagementViewController.swift in Sources */,
4BB99D0226FE191E001E4761 /* ImportedBookmarks.swift in Sources */,
B626A75A29921FAA00053070 /* NavigationActionPolicyExtension.swift in Sources */,
B603FD9E2A02712E00F3FCA9 /* CIImageExtension.swift in Sources */,
AA6EF9B3250785D5004754E6 /* NSMenuExtension.swift in Sources */,
AA7412B524D1536B00D22FE0 /* MainWindowController.swift in Sources */,
AA9FF95924A1ECF20039E328 /* Tab.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0x3F",
"green" : "0x61",
"red" : "0xCE"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
15 changes: 15 additions & 0 deletions DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "QR-16@1.25.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
Binary file not shown.
205 changes: 205 additions & 0 deletions DuckDuckGo/Common/Extensions/CIImageExtension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
//
// CIImageExtension.swift
//
// Copyright © 2023 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import CoreImage.CIFilterBuiltins

extension CIImage {

static var retinaScaleFactor: CGFloat {
max(NSScreen.maxBackingScaleFactor, NSScreen.defaultBackingScaleFactor) // “retina” or larger
}

/// Generates a `CIImage` of a rounded rectangle with a specified extent and corner radius.
static func rect(in extent: CGRect, cornerRadius: CGFloat = 0, color: NSColor? = nil) -> CIImage {
let roundedRectFilter = CIFilter.roundedRectangleGenerator()
roundedRectFilter.extent = extent
roundedRectFilter.radius = Float(cornerRadius)
if let color {
roundedRectFilter.color = color.ciColor
}

return roundedRectFilter.outputImage!
}

/// Generates a `CIImage` of a circle with a specified center point and radius.
static func circle(at center: CGPoint, radius: CGFloat, color: NSColor? = nil) -> CIImage {
return rect(in: CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2), cornerRadius: radius, color: color)
}

enum QRCorrectionLevel: String {
/// 7% of codewords can be restored.
case low = "L"
/// 15% of codewords can be restored.
case medium = "M"
/// 25% of codewords can be restored.
case normal = "Q"
/// 30% of codewords can be restored.
case high = "H"
}
/// Generates a QR code `CIImage` for a given data input.
static func qrCode(for data: Data, correctionLevel: QRCorrectionLevel? = nil) -> CIImage? {
let filter = CIFilter.qrCodeGenerator()
filter.message = data
if let correctionLevel {
filter.correctionLevel = correctionLevel.rawValue
}
return filter.outputImage
}

struct QRCodeParameters {

fileprivate static let iconSizeFactor: CGFloat = 0.25

var logicalQrSize: Int
var correctionLevel: QRCorrectionLevel?

var icon: CIImage?

var color: NSColor
var backgroundColor: NSColor

static let `default` = QRCodeParameters(logicalQrSize: 250,
correctionLevel: nil,
icon: nil,
color: .black,
backgroundColor: .white)

static let duckDuckGo: QRCodeParameters = {
let logicalQrSize = QRCodeParameters.default.logicalQrSize
let icon: CIImage = {
let logo = NSImage(named: "Logo")!
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is using the old Dax icon. Please replace with the new one.

let logoRadiusFactor: CGFloat = 0.77
let logoMargin: CGFloat = 6
let logoBackgroundColor = NSColor.logoBackgroundColor

let logoSize = NSSize(width: logicalQrSize, height: logicalQrSize).scaled(by: CIImage.retinaScaleFactor)
var image = logo.ciImage(with: logoSize)

// cut Dax circle
let maskImage = CIImage.circle(at: image.extent.center, radius: image.extent.width * (logoRadiusFactor / 2))
image = image.masked(with: maskImage)

// add background
let backgroundExtent = CGRect(x: 0, y: 0, width: image.extent.width + logoMargin * 2, height: image.extent.width + logoMargin * 2)
let background = CIImage.rect(in: backgroundExtent, cornerRadius: backgroundExtent.width / 2, color: logoBackgroundColor)
image = image.centered(in: backgroundExtent).composited(over: background)

return image
}()

return QRCodeParameters(logicalQrSize: logicalQrSize,
correctionLevel: .high,
icon: icon,
color: .logoBackgroundColor,
backgroundColor: .white)
}()
}

static func qrCode(for data: Data, parameters: QRCodeParameters = .default) -> CIImage? {
guard var qr = CIImage.qrCode(for: data, correctionLevel: parameters.correctionLevel) else { return nil }

// size of the QR in “dots”
let qrSize = qr.extent.size.width

// scale to QR Size in Pixels
let qrScale = CGFloat((CGFloat(parameters.logicalQrSize) * CIImage.retinaScaleFactor) / CGFloat(qrSize))
qr = qr.scaled(by: qrScale)

// tint
qr = qr.tinted(using: parameters.color)

// extend background by 2 QR dots in each dimension
let backgroundExtent = qr.extent.insetBy(dx: -2 * qrScale, dy: -2 * qrScale)
let background = CIImage.rect(in: backgroundExtent, cornerRadius: qrScale * 2, color: parameters.backgroundColor)
// add background
qr = qr.centered(in: backgroundExtent).composited(over: background)

// add logo
if let icon = parameters.icon {
let sizeInDots = CGFloat(Int(qrSize * QRCodeParameters.iconSizeFactor))
let icon = icon.scaled(by: (qrScale * sizeInDots) / icon.extent.width)

qr = icon.centered(in: qr.extent).composited(over: qr)
}

return qr
}

/// Creates a new `CIImage` by masking the current image with the specified mask image.
func masked(with maskImage: CIImage) -> CIImage {
let filter = CIFilter.blendWithMask()
filter.inputImage = self
filter.maskImage = maskImage

return filter.outputImage!.cropped(to: maskImage.extent)
}

/// Generates a new `CIImage` by scaling the input image by a specified scale factor.
func scaled(by scaleFactor: CGFloat) -> CIImage {
let transform = CGAffineTransform(scaleX: scaleFactor, y: scaleFactor)
return self.transformed(by: transform)
}

/// Returns a new `CIImage` by centering the current image within another image's extent.
func centered(in otherExtent: CGRect) -> CIImage {
self.transformed(by: CGAffineTransform(translationX: otherExtent.midX - extent.midX, y: otherExtent.midY - extent.midY))
}

/// Generates a new `CIImage` by inverting the colors of the input image.
func inverted() -> CIImage! {
let invertedColorFilter = CIFilter.colorInvert()
invertedColorFilter.inputImage = self

return invertedColorFilter.outputImage
}

/// Generates a new `CIImage` by converting black areas of the input image to transparent and other areas to white.
func blackToTransparent() -> CIImage! {
let blackTransparentFilter = CIFilter.maskToAlpha()
blackTransparentFilter.inputImage = self

return blackTransparentFilter.outputImage
}

/// Generates a new `CIImage` by tinting the input image with a specified color using multiply compositing.
func tinted(using color: NSColor) -> CIImage! {
let filter = CIFilter.multiplyCompositing()
filter.inputImage = CIImage(color: color.ciColor)
filter.backgroundImage = self.inverted()?.blackToTransparent()

return filter.outputImage
}

var cgImage: CGImage {
CIContext(options: nil).createCGImage(self, from: self.extent)!
}

}

extension CGImage {

/// Returns image bitmap data with the specified file format.
func bitmapRepresentation(using format: NSBitmapImageRep.FileType) -> Data? {
let bitmapRep = NSBitmapImageRep(cgImage: self)
bitmapRep.size = NSSize(width: Int(CGFloat(self.width) / CIImage.retinaScaleFactor),
height: Int(CGFloat(self.height) / CIImage.retinaScaleFactor))

return bitmapRep.representation(using: format, properties: [:])
}

}
8 changes: 8 additions & 0 deletions DuckDuckGo/Common/Extensions/NSColorExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,12 @@ extension NSColor {

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

static var logoBackgroundColor: NSColor {
NSColor(named: "LogoBackgroundColor")!
}

var ciColor: CIColor {
CIColor(color: self)!
}

}
5 changes: 5 additions & 0 deletions DuckDuckGo/Common/Extensions/NSImageExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,9 @@ extension NSImage {
return image
}

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

}
Loading