From bc449ac9ea9f0ad7162a3260068abd9598189f8f Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 24 Apr 2023 15:31:14 +0600 Subject: [PATCH 1/9] share via QR code --- DuckDuckGo.xcodeproj/project.pbxproj | 22 ++- .../Contents.json | 20 ++ .../Common/Extensions/NSColorExtension.swift | 4 + DuckDuckGo/Sharing/QRSharingServive.swift | 176 ++++++++++++++++++ .../{Menus => Sharing}/SharingMenu.swift | 3 + 5 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 DuckDuckGo/Assets.xcassets/Colors/LogoBackgroundColor.colorset/Contents.json create mode 100644 DuckDuckGo/Sharing/QRSharingServive.swift rename DuckDuckGo/{Menus => Sharing}/SharingMenu.swift (99%) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 31c3253c44..09eb73de3a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1784,6 +1784,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 /* QRSharingServive.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F7127D29F6779000594A45 /* QRSharingServive.swift */; }; + B6F7127F29F6779000594A45 /* QRSharingServive.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F7127D29F6779000594A45 /* QRSharingServive.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 */; }; @@ -2786,6 +2790,8 @@ B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestsURLExtension.swift; sourceTree = ""; }; B6F41030264D2B23003DA42C /* ProgressExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressExtension.swift; sourceTree = ""; }; B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WKWebViewMockingExtension.swift; sourceTree = ""; }; + B6F7127D29F6779000594A45 /* QRSharingServive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRSharingServive.swift; sourceTree = ""; }; + 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 = ""; }; B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardViewController.swift; sourceTree = ""; }; B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPopover.swift; sourceTree = ""; }; @@ -2820,6 +2826,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B6F7128229F6820A00594A45 /* QuickLookUI.framework in Frameworks */, 984FD3BF299ACF35007334DD /* Bookmarks in Frameworks */, 37A5E2F0298AA1B20047046B /* Persistence in Frameworks */, 378F44E629B4BDEE00899924 /* SwiftUIExtensions in Frameworks */, @@ -2873,6 +2880,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B6F7128129F681EB00594A45 /* QuickLookUI.framework in Frameworks */, 9807F645278CA16F00E1547B /* BrowserServicesKit in Frameworks */, 987799ED299998B1005D8EB6 /* Bookmarks in Frameworks */, 1E950E3F2912A10D0051A99B /* ContentBlocking in Frameworks */, @@ -4070,6 +4078,7 @@ 85AE2FF024A33A2D002D507F /* Frameworks */ = { isa = PBXGroup; children = ( + B6F7128029F681EB00594A45 /* QuickLookUI.framework */, 85AE2FF124A33A2D002D507F /* WebKit.framework */, ); name = Frameworks; @@ -4386,6 +4395,7 @@ B6FA893A269C414900588ECD /* PrivacyDashboard */, AAC6881528626B6F00D54247 /* RecentlyClosed */, 85890634267B6CC500D23B0D /* SecureVault */, + B6F7127C29F6776B00594A45 /* Sharing */, 4B677422255DBEB800025BD8 /* SmarterEncryption */, B68458AE25C7E75100DC17B6 /* StateRestoration */, B6A9E44E26142AF90067D1B9 /* Statistics */, @@ -4876,7 +4886,6 @@ 85480F8925CDC360009424E3 /* MainMenu.storyboard */, AA4BBA3A25C58FA200C4FB0F /* MainMenu.swift */, AA6EF9B425081B4C004754E6 /* MainMenuActions.swift */, - B63ED0E426BB8FB900A9DAD1 /* SharingMenu.swift */, AA7E919628746BCC00AB6B62 /* HistoryMenu.swift */, AAAB9113288EB1D600A057A9 /* CleanThisHistoryMenuItem.swift */, AAAB9115288EB46B00A057A9 /* VisitMenuItem.swift */, @@ -5741,6 +5750,15 @@ path = "tests-server"; sourceTree = ""; }; + B6F7127C29F6776B00594A45 /* Sharing */ = { + isa = PBXGroup; + children = ( + B63ED0E426BB8FB900A9DAD1 /* SharingMenu.swift */, + B6F7127D29F6779000594A45 /* QRSharingServive.swift */, + ); + path = Sharing; + sourceTree = ""; + }; B6FA893A269C414900588ECD /* PrivacyDashboard */ = { isa = PBXGroup; children = ( @@ -6687,6 +6705,7 @@ 3706FB7A293F65D500E42796 /* FileDownloadManager.swift in Sources */, 3706FB7B293F65D500E42796 /* BookmarkImport.swift in Sources */, 3706FB7C293F65D500E42796 /* KeySetDictionary.swift in Sources */, + B6F7127F29F6779000594A45 /* QRSharingServive.swift in Sources */, 3706FB7E293F65D500E42796 /* FireCoordinator.swift in Sources */, 3706FB7F293F65D500E42796 /* GeolocationProvider.swift in Sources */, 3706FB80293F65D500E42796 /* NSAlert+ActiveDownloadsTermination.swift in Sources */, @@ -7576,6 +7595,7 @@ AA9E9A5625A3AE8400D1959D /* NSWindowExtension.swift in Sources */, AAC5E4C725D6A6E8007F5990 /* BookmarkPopover.swift in Sources */, 37CC53F027E8D1440028713D /* PreferencesDownloadsView.swift in Sources */, + B6F7127E29F6779000594A45 /* QRSharingServive.swift in Sources */, B68C2FB227706E6A00BF2C7D /* ProcessExtension.swift in Sources */, B6106BA726A7BECC0013B453 /* PermissionAuthorizationQuery.swift in Sources */, 3171D6BA288984D00068632A /* BadgeAnimationView.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Colors/LogoBackgroundColor.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/LogoBackgroundColor.colorset/Contents.json new file mode 100644 index 0000000000..c6206244f3 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Colors/LogoBackgroundColor.colorset/Contents.json @@ -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 + } +} diff --git a/DuckDuckGo/Common/Extensions/NSColorExtension.swift b/DuckDuckGo/Common/Extensions/NSColorExtension.swift index eb14e28b3e..348f52ee71 100644 --- a/DuckDuckGo/Common/Extensions/NSColorExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSColorExtension.swift @@ -140,4 +140,8 @@ extension NSColor { static let buttonColor: NSColor = NSColor(named: "ButtonColor")! + static var logoBackground: NSColor { + NSColor(named: "LogoBackgroundColor")! + } + } diff --git a/DuckDuckGo/Sharing/QRSharingServive.swift b/DuckDuckGo/Sharing/QRSharingServive.swift new file mode 100644 index 0000000000..b02411c0e6 --- /dev/null +++ b/DuckDuckGo/Sharing/QRSharingServive.swift @@ -0,0 +1,176 @@ +// +// QRSharingServive.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 +import Foundation +import QuickLookUI + +extension String { + + /// Creates a QR code for the current URL in the given color. + func qrImage(using color: NSColor) -> CIImage? { + return qrImage?.tinted(using: color) + } + + /// Returns a black and white QR code for this URL. + var qrImage: CIImage? { + guard let qrFilter = CIFilter(name: "CIQRCodeGenerator"), + let qrData = self.data(using: String.Encoding.ascii) else { return nil } + + qrFilter.setValue(qrData, forKey: "inputMessage") + + let qrTransform = CGAffineTransform(scaleX: 12, y: 12) + return qrFilter.outputImage?.transformed(by: qrTransform) + } + + /// Creates a QR code for the current URL in the given color. + func qrImage(using color: NSColor, logo: NSImage? = nil) -> CIImage? { + let tintedQRImage = qrImage?.tinted(using: color) + + guard let logo = logo?.cgImage(forProposedRect: nil, context: nil, hints: nil) else { + return tintedQRImage + } + + return tintedQRImage?.combined(with: CIImage(cgImage: logo)) + } + +} + +extension CIImage { + /// Inverts the colors and creates a transparent image by converting the mask to alpha. + /// Input image should be black and white. + var transparent: CIImage? { + return inverted?.blackTransparent + } + + /// Inverts the colors. + var inverted: CIImage? { + guard let invertedColorFilter = CIFilter(name: "CIColorInvert") else { return nil } + + invertedColorFilter.setValue(self, forKey: "inputImage") + return invertedColorFilter.outputImage + } + + /// Converts all black to transparent. + var blackTransparent: CIImage? { + guard let blackTransparentFilter = CIFilter(name: "CIMaskToAlpha") else { return nil } + blackTransparentFilter.setValue(self, forKey: "inputImage") + return blackTransparentFilter.outputImage + } + + /// Applies the given color as a tint color. + func tinted(using color: NSColor) -> CIImage? { + guard + let transparentQRImage = transparent, + let filter = CIFilter(name: "CIMultiplyCompositing"), + let colorFilter = CIFilter(name: "CIConstantColorGenerator") else { return nil } + + let ciColor = CIColor(color: color) + colorFilter.setValue(ciColor, forKey: kCIInputColorKey) + let colorImage = colorFilter.outputImage + + filter.setValue(colorImage, forKey: kCIInputImageKey) + filter.setValue(transparentQRImage, forKey: kCIInputBackgroundImageKey) + + return filter.outputImage! + } + + /// Combines the current image with the given image centered. + func combined(with image: CIImage) -> CIImage? { + guard let combinedFilter = CIFilter(name: "CISourceOverCompositing") else { return nil } + let centerTransform = CGAffineTransform(scaleX: 0.5, y: 0.5) + .concatenating(CGAffineTransform(translationX: extent.midX - (image.extent.size.width / 4), y: extent.midY - (image.extent.size.height / 4))) + combinedFilter.setValue(image.transformed(by: centerTransform), forKey: "inputImage") + combinedFilter.setValue(self, forKey: "inputBackgroundImage") + return combinedFilter.outputImage! + } +} + +final class QRSharingService: NSSharingService, QLPreviewPanelDataSource, QLPreviewPanelDelegate { + + static let logo = NSImage(named: "Logo")! + + private var qrImage: NSImage? + private var imageUrl: URL? + + init() { + super.init(title: "QR Code", image: NSImage(named: "Burn")!, alternateImage: nil) { + print("Share") + } + } + + override func canPerform(withItems items: [Any]?) -> Bool { + items?.contains(where: { $0 is String || $0 is URL }) ?? false + } + + override func perform(withItems items: [Any]) { + guard let string = items.lazy.compactMap({ ($0 as? URL)?.absoluteString ?? ($0 as? String) }).first else { return } + + let context = CIContext(options: nil) + + guard let ciImage = string.qrImage(using: .logoBackground, logo: Self.logo), + let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else { + fatalError("Failed to create CGImage from CIImage") + } + + let bitmapRep = NSBitmapImageRep(cgImage: cgImage) + let nsImage = NSImage(size: bitmapRep.size) + nsImage.addRepresentation(bitmapRep) + + let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("png") + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + try? FileManager.default.removeItem(at: url) + } + + try? bitmapRep.representation(using: .png, properties: [:])?.write(to: url) + self.imageUrl = url + + self.qrImage = nsImage + + QLPreviewPanel.shared().dataSource = self + QLPreviewPanel.shared().delegate = self + + if QLPreviewPanel.shared().isVisible { + QLPreviewPanel.shared().reloadData() + } else { + QLPreviewPanel.shared().makeKeyAndOrderFront(nil) + } + } + + func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int { + return 1 // Change this number according to the number of items you want to preview + } + + // Item to preview + func previewPanel(_ panel: QLPreviewPanel!, previewItemAt index: Int) -> QLPreviewItem! { + return imageUrl! as QLPreviewItem + } + + // Frame for the item's icon in your view + func previewPanel(_ panel: QLPreviewPanel!, sourceFrameOnScreenFor item: QLPreviewItem!) -> NSRect { + // Provide the frame of the item's icon in your view (optional) + return .init(origin: .zero, size: qrImage!.size) + } + + // The view responsible for the item's icon + func previewPanel(_ panel: QLPreviewPanel!, transitionImageFor item: QLPreviewItem!, contentRect: UnsafeMutablePointer!) -> Any! { + // Provide the image of the item's icon (optional) + return qrImage + } + +} diff --git a/DuckDuckGo/Menus/SharingMenu.swift b/DuckDuckGo/Sharing/SharingMenu.swift similarity index 99% rename from DuckDuckGo/Menus/SharingMenu.swift rename to DuckDuckGo/Sharing/SharingMenu.swift index 712d8c8e03..96842fc8dd 100644 --- a/DuckDuckGo/Menus/SharingMenu.swift +++ b/DuckDuckGo/Sharing/SharingMenu.swift @@ -30,9 +30,12 @@ final class SharingMenu: NSMenu { // not real sharing URL, used for generating items for NSURL var services = NSSharingService.sharingServices(forItems: [URL.duckDuckGo]) + if let copyLink = NSSharingService(named: .copyLink) { services.insert(copyLink, at: 0) } + services.append(QRSharingService()) + let readingListService = NSSharingService(named: .addToSafariReadingList) for service in services where service != readingListService { let menuItem = NSMenuItem(service: service) From 9b661ae76c61c726fb40d96ea4b28194a4416738 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 3 May 2023 16:56:30 +0600 Subject: [PATCH 2/9] complete QR code sharing --- DuckDuckGo.xcodeproj/project.pbxproj | 6 + .../Images/QR-Icon.imageset/Contents.json | 15 ++ .../Images/QR-Icon.imageset/QR.pdf | Bin 0 -> 1184 bytes .../Common/Extensions/CIImageExtension.swift | 121 ++++++++++ .../Common/Extensions/NSColorExtension.swift | 6 +- .../Common/Extensions/NSImageExtensions.swift | 5 + .../Common/Extensions/NSRectExtension.swift | 4 + DuckDuckGo/Common/Localizables/UserText.swift | 1 + DuckDuckGo/Sharing/QRSharingServive.swift | 216 ++++++++++-------- 9 files changed, 274 insertions(+), 100 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/QR.pdf create mode 100644 DuckDuckGo/Common/Extensions/CIImageExtension.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 09eb73de3a..2b96ccccec 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1492,6 +1492,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 */; }; @@ -2554,6 +2556,7 @@ B603973729BF0EBE00902A34 /* PrivacyDashboardIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardIntegrationTests.swift; sourceTree = ""; }; B603973B29BF1D7D00902A34 /* AutoconsentIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoconsentIntegrationTests.swift; sourceTree = ""; }; B603974D29C1F93600902A34 /* TabPermissionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPermissionsTests.swift; sourceTree = ""; }; + B603FD9D2A02712E00F3FCA9 /* CIImageExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageExtension.swift; sourceTree = ""; }; B6040855274B830F00680351 /* DictionaryExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryExtension.swift; sourceTree = ""; }; B604085B274B8CA400680351 /* Permissions.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Permissions.xcdatamodel; sourceTree = ""; }; B6085D052743905F00A9C456 /* CoreDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStore.swift; sourceTree = ""; }; @@ -5164,6 +5167,7 @@ B6106B9D26A565DA0013B453 /* BundleExtension.swift */, B626A75F2992407C00053070 /* CancellableExtension.swift */, 4BA1A6C1258B0A1300F6F690 /* ContiguousBytesExtension.swift */, + B603FD9D2A02712E00F3FCA9 /* CIImageExtension.swift */, 85AC3AF625D5DBFD00C7D2AA /* DataExtension.swift */, B6A9E46F26146A250067D1B9 /* DateExtension.swift */, B6040855274B830F00680351 /* DictionaryExtension.swift */, @@ -6708,6 +6712,7 @@ B6F7127F29F6779000594A45 /* QRSharingServive.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 */, @@ -7853,6 +7858,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 */, diff --git a/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/Contents.json new file mode 100644 index 0000000000..3c813d5c80 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "QR.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/QR.pdf b/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/QR.pdf new file mode 100644 index 0000000000000000000000000000000000000000..785a40b2fd6ced54a15c1e67584ee29a6342c003 GIT binary patch literal 1184 zcmb7^-*3|}5Xax|ueg^=8&VxRj`KsBCb5p7>1ovoD+ z_fR>X_|A9VeBH%neY+Ga0T9U1eESZFS66s_4Z7LdM+oOU_@J9zzehGFy6DOVgkVu9qBOyfg@UV0 z;Q=BRi$rnmS?$>}Vk(m{Pq3b4pQo;74SRTi20s7yEUxW;&*HX)h5Z!h5gA872Mv zQ>9#Y=CLhxht~M{SZ-ySUN>~2b^Qc4MN16Th-P=gB6nwk_tlnKMLR)`(>W{iNV(#q z|BlG4Qv9rv9k6o;`oUL4U({yzJoTy755O^j<0Z4d7%L?e5*|Zv99GykB&kmu9>tXM z5~<`RO65tdjQd)BTU51f!G67WYm(!lGY5T%7uELiC`ynU)1w`D5IDH2{%Goc_%kWm Pei$c 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 + } + + /// 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: self.width / 2, height: self.height / 2) + + return bitmapRep.representation(using: format, properties: [:]) + } + +} diff --git a/DuckDuckGo/Common/Extensions/NSColorExtension.swift b/DuckDuckGo/Common/Extensions/NSColorExtension.swift index 348f52ee71..75591e835d 100644 --- a/DuckDuckGo/Common/Extensions/NSColorExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSColorExtension.swift @@ -140,8 +140,12 @@ extension NSColor { static let buttonColor: NSColor = NSColor(named: "ButtonColor")! - static var logoBackground: NSColor { + static var logoBackgroundColor: NSColor { NSColor(named: "LogoBackgroundColor")! } + var ciColor: CIColor { + CIColor(color: self)! + } + } diff --git a/DuckDuckGo/Common/Extensions/NSImageExtensions.swift b/DuckDuckGo/Common/Extensions/NSImageExtensions.swift index 75f15542e3..c19c19214a 100644 --- a/DuckDuckGo/Common/Extensions/NSImageExtensions.swift +++ b/DuckDuckGo/Common/Extensions/NSImageExtensions.swift @@ -59,4 +59,9 @@ extension NSImage { return image } + var ciImage: CIImage { + var rect = NSRect(x: 0, y: 0, width: self.size.width * 2, height: self.size.height * 2) + return CIImage(cgImage: self.cgImage(forProposedRect: &rect, context: nil, hints: nil)!) + } + } diff --git a/DuckDuckGo/Common/Extensions/NSRectExtension.swift b/DuckDuckGo/Common/Extensions/NSRectExtension.swift index f9f9a93f79..408e516d4e 100644 --- a/DuckDuckGo/Common/Extensions/NSRectExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSRectExtension.swift @@ -20,6 +20,10 @@ import Foundation extension NSRect { + var center: CGPoint { + CGPoint(x: midX, y: midY) + } + // Apply an offset so that we don't get caught by the "Line of Death" https://textslashplain.com/2017/01/14/the-line-of-death/ func insetFromLineOfDeath() -> NSRect { return insetBy(dx: 0, dy: -5) diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 0274d0a928..a8a02b28eb 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -104,6 +104,7 @@ struct UserText { static let moreMenuItem = NSLocalizedString("sharing.more", value: "More…", comment: "Sharing Menu -> More…") static let findInPageMenuItem = NSLocalizedString("find.in.page.menu.item", value: "Find in Page…", comment: "Menu item title") static let shareMenuItem = NSLocalizedString("share.menu.item", value: "Share", comment: "Menu item title") + static let shareViaQRCodeMenuItem = NSLocalizedString("share.menu.item.qr.code", value: "QR Code", comment: "Menu item title") static let printMenuItem = NSLocalizedString("print.menu.item", value: "Print…", comment: "Menu item title") static let newWindowMenuItem = NSLocalizedString("new.window.menu.item", value: "New Window", comment: "Menu item title") diff --git a/DuckDuckGo/Sharing/QRSharingServive.swift b/DuckDuckGo/Sharing/QRSharingServive.swift index b02411c0e6..361c51bc4a 100644 --- a/DuckDuckGo/Sharing/QRSharingServive.swift +++ b/DuckDuckGo/Sharing/QRSharingServive.swift @@ -16,160 +16,178 @@ // limitations under the License. // -import CoreImage +import Combine import Foundation import QuickLookUI -extension String { +final class QRSharingService: NSSharingService { - /// Creates a QR code for the current URL in the given color. - func qrImage(using color: NSColor) -> CIImage? { - return qrImage?.tinted(using: color) - } + private enum Constants { + static let menuIcon = NSImage(named: "QR-Icon")! - /// Returns a black and white QR code for this URL. - var qrImage: CIImage? { - guard let qrFilter = CIFilter(name: "CIQRCodeGenerator"), - let qrData = self.data(using: String.Encoding.ascii) else { return nil } + static let logo = NSImage(named: "Logo")! + static let logoRadiusFactor: CGFloat = 0.8 + static let logoMargin: CGFloat = 8 + static let logoBackgroundColor = NSColor.white + static let logoSizeFactor: CGFloat = 0.25 - qrFilter.setValue(qrData, forKey: "inputMessage") + static let qrSize: Int = 500 + static let qrCorrectionLevel: CIImage.QRCorrectionLevel? = .high - let qrTransform = CGAffineTransform(scaleX: 12, y: 12) - return qrFilter.outputImage?.transformed(by: qrTransform) + static let backgroundColor = NSColor.white } - /// Creates a QR code for the current URL in the given color. - func qrImage(using color: NSColor, logo: NSImage? = nil) -> CIImage? { - let tintedQRImage = qrImage?.tinted(using: color) + private var qrImage: NSImage? + private var imageUrl: URL? - guard let logo = logo?.cgImage(forProposedRect: nil, context: nil, hints: nil) else { - return tintedQRImage - } + private var cancellables = Set() - return tintedQRImage?.combined(with: CIImage(cgImage: logo)) + init() { + super.init(title: UserText.shareViaQRCodeMenuItem, image: Constants.menuIcon, alternateImage: nil) {} } -} + /// Get ASCII `Data` for an array of items to share that can be represented as strings (e.g., URLs or Strings). + private static func data(for items: [Any]?) -> Data? { + guard let items else { return nil } + + for item in items { + var string: String? { + switch item { + case let url as URL: + return url.absoluteString + case let string as String: + return string + default: + return nil + } + } + if let data = string?.data(using: .nonLossyASCII) { + return data + } + } -extension CIImage { - /// Inverts the colors and creates a transparent image by converting the mask to alpha. - /// Input image should be black and white. - var transparent: CIImage? { - return inverted?.blackTransparent + return nil } - /// Inverts the colors. - var inverted: CIImage? { - guard let invertedColorFilter = CIFilter(name: "CIColorInvert") else { return nil } + private static func qrCode(for items: [Any]) -> CIImage? { + guard let data = Self.data(for: items), + var qr = CIImage.qrCode(for: data, correctionLevel: Constants.qrCorrectionLevel) else { return nil } - invertedColorFilter.setValue(self, forKey: "inputImage") - return invertedColorFilter.outputImage - } - - /// Converts all black to transparent. - var blackTransparent: CIImage? { - guard let blackTransparentFilter = CIFilter(name: "CIMaskToAlpha") else { return nil } - blackTransparentFilter.setValue(self, forKey: "inputImage") - return blackTransparentFilter.outputImage - } + // size of a QR “dot” + let qrSize = qr.extent.size.width - /// Applies the given color as a tint color. - func tinted(using color: NSColor) -> CIImage? { - guard - let transparentQRImage = transparent, - let filter = CIFilter(name: "CIMultiplyCompositing"), - let colorFilter = CIFilter(name: "CIConstantColorGenerator") else { return nil } + // scale + let qrScale = CGFloat(CGFloat(Constants.qrSize) / CGFloat(qrSize)) + qr = qr.scaled(by: qrScale) - let ciColor = CIColor(color: color) - colorFilter.setValue(ciColor, forKey: kCIInputColorKey) - let colorImage = colorFilter.outputImage + // tint + qr = qr.tinted(using: .logoBackgroundColor) - filter.setValue(colorImage, forKey: kCIInputImageKey) - filter.setValue(transparentQRImage, forKey: kCIInputBackgroundImageKey) + // 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: Constants.backgroundColor) + // add background + qr = qr.centered(in: backgroundExtent).composited(over: background) - return filter.outputImage! - } + // add logo + var logo: CIImage { + var image = Constants.logo.ciImage - /// Combines the current image with the given image centered. - func combined(with image: CIImage) -> CIImage? { - guard let combinedFilter = CIFilter(name: "CISourceOverCompositing") else { return nil } - let centerTransform = CGAffineTransform(scaleX: 0.5, y: 0.5) - .concatenating(CGAffineTransform(translationX: extent.midX - (image.extent.size.width / 4), y: extent.midY - (image.extent.size.height / 4))) - combinedFilter.setValue(image.transformed(by: centerTransform), forKey: "inputImage") - combinedFilter.setValue(self, forKey: "inputBackgroundImage") - return combinedFilter.outputImage! - } -} + // cut Dax circle + let maskImage = CIImage.circle(at: image.extent.center, radius: image.extent.width * (Constants.logoRadiusFactor / 2)) + image = image.masked(with: maskImage) -final class QRSharingService: NSSharingService, QLPreviewPanelDataSource, QLPreviewPanelDelegate { + // add background + let backgroundExtent = CGRect(x: 0, y: 0, width: image.extent.width + Constants.logoMargin * 2, height: image.extent.width + Constants.logoMargin * 2) + let background = CIImage.rect(in: backgroundExtent, cornerRadius: backgroundExtent.width / 2, color: Constants.logoBackgroundColor) + image = image.centered(in: backgroundExtent).composited(over: background) - static let logo = NSImage(named: "Logo")! + // scale to logoSizeInQRDots to match exact number of dots + let sizeInDots = CGFloat(Int(qrSize * Constants.logoSizeFactor)) - private var qrImage: NSImage? - private var imageUrl: URL? + image = image.scaled(by: (qrScale * sizeInDots) / image.extent.width) - init() { - super.init(title: "QR Code", image: NSImage(named: "Burn")!, alternateImage: nil) { - print("Share") + return image } + qr = logo.centered(in: qr.extent).composited(over: qr) + + return qr } override func canPerform(withItems items: [Any]?) -> Bool { - items?.contains(where: { $0 is String || $0 is URL }) ?? false + Self.data(for: items) != nil } override func perform(withItems items: [Any]) { - guard let string = items.lazy.compactMap({ ($0 as? URL)?.absoluteString ?? ($0 as? String) }).first else { return } + guard let qr = Self.qrCode(for: items) else { return } - let context = CIContext(options: nil) + let cgImage = qr.cgImage + guard let data = cgImage.bitmapRepresentation(using: .png) else { return } - guard let ciImage = string.qrImage(using: .logoBackground, logo: Self.logo), - let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else { - fatalError("Failed to create CGImage from CIImage") + let fileUrl = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("png") + do { + try data.write(to: fileUrl) + } catch { + return } - let bitmapRep = NSBitmapImageRep(cgImage: cgImage) - let nsImage = NSImage(size: bitmapRep.size) - nsImage.addRepresentation(bitmapRep) + self.imageUrl = fileUrl + self.qrImage = NSImage(cgImage: cgImage, size: NSSize(width: qr.extent.size.width / 2, height: qr.extent.size.height / 2)) - let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("png") - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - try? FileManager.default.removeItem(at: url) - } + let qlPanel: QLPreviewPanel = QLPreviewPanel.shared() + qlPanel.dataSource = self + qlPanel.delegate = self - try? bitmapRep.representation(using: .png, properties: [:])?.write(to: url) - self.imageUrl = url + if qlPanel.isVisible { + qlPanel.reloadData() + } else { + qlPanel.makeKeyAndOrderFront(nil) + } - self.qrImage = nsImage + qlPanel.publisher(for: \.delegate).dropFirst().sink { [weak self] delegate in + if delegate !== self { + self?.cleanup() + } + }.store(in: &cancellables) + qlPanel.publisher(for: \.isVisible).dropFirst().sink { [weak self] isVisible in + if !isVisible { + self?.cleanup() + } + }.store(in: &cancellables) + } - QLPreviewPanel.shared().dataSource = self - QLPreviewPanel.shared().delegate = self + private func cleanup() { + guard let imageUrl else { return } - if QLPreviewPanel.shared().isVisible { - QLPreviewPanel.shared().reloadData() - } else { - QLPreviewPanel.shared().makeKeyAndOrderFront(nil) + if let qlPanel = QLPreviewPanel.shared(), + qlPanel.delegate === self, + qlPanel.isVisible { + qlPanel.orderOut(nil) } + + try? FileManager.default.removeItem(at: imageUrl) + self.imageUrl = nil + self.qrImage = nil + self.cancellables.removeAll() } +} + +extension QRSharingService: QLPreviewPanelDataSource, QLPreviewPanelDelegate { + func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int { - return 1 // Change this number according to the number of items you want to preview + return imageUrl != nil ? 1 : 0 } - // Item to preview func previewPanel(_ panel: QLPreviewPanel!, previewItemAt index: Int) -> QLPreviewItem! { - return imageUrl! as QLPreviewItem + return imageUrl as QLPreviewItem? } - // Frame for the item's icon in your view func previewPanel(_ panel: QLPreviewPanel!, sourceFrameOnScreenFor item: QLPreviewItem!) -> NSRect { - // Provide the frame of the item's icon in your view (optional) - return .init(origin: .zero, size: qrImage!.size) + return qrImage.map { NSRect(origin: .zero, size: $0.size) } ?? .zero } - // The view responsible for the item's icon func previewPanel(_ panel: QLPreviewPanel!, transitionImageFor item: QLPreviewItem!, contentRect: UnsafeMutablePointer!) -> Any! { - // Provide the image of the item's icon (optional) return qrImage } From 197d220093412ab65ee61f20a5cf578dcfd7b7b8 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Thu, 4 May 2023 10:49:54 +0600 Subject: [PATCH 3/9] Change menu icon; Show Dax QR only for DDG URLs; cleanup --- .../Images/QR-Icon.imageset/Contents.json | 2 +- .../Images/QR-Icon.imageset/QR-24.pdf | Bin 0 -> 5210 bytes .../Images/QR-Icon.imageset/QR.pdf | Bin 1184 -> 0 bytes .../Common/Extensions/CIImageExtension.swift | 77 ++++++++++++++++++ DuckDuckGo/Sharing/QRSharingServive.swift | 71 ++++------------ 5 files changed, 93 insertions(+), 57 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/QR-24.pdf delete mode 100644 DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/QR.pdf diff --git a/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/Contents.json index 3c813d5c80..6a2cb7adca 100644 --- a/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "QR.pdf", + "filename" : "QR-24.pdf", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/QR-24.pdf b/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/QR-24.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f097490999c980e354b8e6a6963187d9ee6c911e GIT binary patch literal 5210 zcmcIoO>f&q5WVlO*h_%qKuODQ0fB+Wc7mcU>dL(ZJ+L(6sL)3%GKJe;pILJEy`deT zLILSuQ;)MVvv1xEseN^K^FDEl${1%?_W1iB#@e@U?eg|%_;5coLpOb^#(!;NSBpAd z$;Vz@iRFvW^0q$PvOL=|J=?N4+j4!jWq!70cDAKE$MWjRv)mmXd|6C7tIvkNT;2a6o^0} zcWg^T;hvNgc3_L%Kq;dFlBHQx8o-_LQlCl+&>>Tln~+kg219=04~wBPfkwfWl%lKR zZlO-eubQNx?P}Tu(K|p&v>JqJ38LvFmC$}jldJ$5sd}!YxoWb@ss=?)K-8XHn1E0; zxj-RdutLZWmcSlC5KnJ}5LL}2Nd?r>(ybz+7yPD>zqBepv-k{7gUw51NNn zXWLO6>`!9S70?|~_Pg@PVkjXgOYo%UMFK)OlN9mnQw8cyRj|0lgjFzg>A^6QG^q46 zhsP&QX|-2WowHU6cT6m_i-O1fh6$u#RTkAxtQGhYw;Bzao(rq^AHssu|9;<*rW(WA z1P_e8Je;$%&eKYPsC<&en}pKnq%{QHqLkO0DwBs|^oD{kDj?srVS&8O2P%-0K;b4K z6_p^eqc_}SR6ytyPyLTpIiv>Xr_V$%6rhE9zTe@FN{dOK4JgM7>mO zV6qx(C~{VgDp#0}4NodddPHU_Hn6$U>kAXV0`Yiw$m2?*^)zADi*O;&wNNX0aD;S4wz@5Ud-8tXu3-b+;sJAE~ zoJyylw16=m6sTHNq3CJl6RofG25hEvpiF}^M`1cPwazgnrg1)Tm5NS~^!ww7aJCQ+ zBK(=dbWoJLs>1n14%nX`eRntK?YuyW^An$X`Nu zF-_5Uyo#PSU4Q&?YP;V06RS;dcYQnz-9aMyW0DBoH7CKW{Olhgb(rEI9(Mh(eUMbG z-@(SlIM>H7vev!GzZXP#k{@Wwliy&EfO0$*Hh$d<&F=U(U;1?FKADxR%+zfDuPuur zE3Clb_HPPuS6HQQZxGQN7>-v}MA`{TZQ sV_$C{-b|z{H^<{({f0+^m-pL$JIQ`LeQHj_xSf1m*XGr$+n?Wm1*Y!w3jhEB literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/QR.pdf b/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/QR.pdf deleted file mode 100644 index 785a40b2fd6ced54a15c1e67584ee29a6342c003..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1184 zcmb7^-*3|}5Xax|ueg^=8&VxRj`KsBCb5p7>1ovoD+ z_fR>X_|A9VeBH%neY+Ga0T9U1eESZFS66s_4Z7LdM+oOU_@J9zzehGFy6DOVgkVu9qBOyfg@UV0 z;Q=BRi$rnmS?$>}Vk(m{Pq3b4pQo;74SRTi20s7yEUxW;&*HX)h5Z!h5gA872Mv zQ>9#Y=CLhxht~M{SZ-ySUN>~2b^Qc4MN16Th-P=gB6nwk_tlnKMLR)`(>W{iNV(#q z|BlG4Qv9rv9k6o;`oUL4U({yzJoTy755O^j<0Z4d7%L?e5*|Zv99GykB&kmu9>tXM z5~<`RO65tdjQd)BTU51f!G67WYm(!lGY5T%7uELiC`ynU)1w`D5IDH2{%Goc_%kWm Pei$c 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.qrSizeInPixels) / 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() diff --git a/DuckDuckGo/Sharing/QRSharingServive.swift b/DuckDuckGo/Sharing/QRSharingServive.swift index 361c51bc4a..1134df7c95 100644 --- a/DuckDuckGo/Sharing/QRSharingServive.swift +++ b/DuckDuckGo/Sharing/QRSharingServive.swift @@ -24,17 +24,6 @@ final class QRSharingService: NSSharingService { private enum Constants { static let menuIcon = NSImage(named: "QR-Icon")! - - static let logo = NSImage(named: "Logo")! - static let logoRadiusFactor: CGFloat = 0.8 - static let logoMargin: CGFloat = 8 - static let logoBackgroundColor = NSColor.white - static let logoSizeFactor: CGFloat = 0.25 - - static let qrSize: Int = 500 - static let qrCorrectionLevel: CIImage.QRCorrectionLevel? = .high - - static let backgroundColor = NSColor.white } private var qrImage: NSImage? @@ -69,61 +58,24 @@ final class QRSharingService: NSSharingService { return nil } - private static func qrCode(for items: [Any]) -> CIImage? { - guard let data = Self.data(for: items), - var qr = CIImage.qrCode(for: data, correctionLevel: Constants.qrCorrectionLevel) else { return nil } - - // size of a QR “dot” - let qrSize = qr.extent.size.width - - // scale - let qrScale = CGFloat(CGFloat(Constants.qrSize) / CGFloat(qrSize)) - qr = qr.scaled(by: qrScale) - - // tint - qr = qr.tinted(using: .logoBackgroundColor) - - // 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: Constants.backgroundColor) - // add background - qr = qr.centered(in: backgroundExtent).composited(over: background) - - // add logo - var logo: CIImage { - var image = Constants.logo.ciImage - - // cut Dax circle - let maskImage = CIImage.circle(at: image.extent.center, radius: image.extent.width * (Constants.logoRadiusFactor / 2)) - image = image.masked(with: maskImage) - - // add background - let backgroundExtent = CGRect(x: 0, y: 0, width: image.extent.width + Constants.logoMargin * 2, height: image.extent.width + Constants.logoMargin * 2) - let background = CIImage.rect(in: backgroundExtent, cornerRadius: backgroundExtent.width / 2, color: Constants.logoBackgroundColor) - image = image.centered(in: backgroundExtent).composited(over: background) - - // scale to logoSizeInQRDots to match exact number of dots - let sizeInDots = CGFloat(Int(qrSize * Constants.logoSizeFactor)) - - image = image.scaled(by: (qrScale * sizeInDots) / image.extent.width) - - return image - } - qr = logo.centered(in: qr.extent).composited(over: qr) - - return qr - } - override func canPerform(withItems items: [Any]?) -> Bool { Self.data(for: items) != nil } + private static func qrCode(for items: [Any]) -> CIImage? { + guard let data = Self.data(for: items) else { return nil } + let isDuckDuckGoURL = items.contains(where: { ($0 as? URL)?.isDuckDuckGo ?? false }) + + return CIImage.qrCode(for: data, parameters: isDuckDuckGoURL ? .duckDuckGo : .default) + } + override func perform(withItems items: [Any]) { guard let qr = Self.qrCode(for: items) else { return } let cgImage = qr.cgImage guard let data = cgImage.bitmapRepresentation(using: .png) else { return } + // save to temp directory, will be removed on QLPreviewPanel hide let fileUrl = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("png") do { try data.write(to: fileUrl) @@ -134,6 +86,12 @@ final class QRSharingService: NSSharingService { self.imageUrl = fileUrl self.qrImage = NSImage(cgImage: cgImage, size: NSSize(width: qr.extent.size.width / 2, height: qr.extent.size.height / 2)) + self.showQuickLook() + } + + private func showQuickLook() { + self.cancellables.removeAll() + let qlPanel: QLPreviewPanel = QLPreviewPanel.shared() qlPanel.dataSource = self qlPanel.delegate = self @@ -149,6 +107,7 @@ final class QRSharingService: NSSharingService { self?.cleanup() } }.store(in: &cancellables) + qlPanel.publisher(for: \.isVisible).dropFirst().sink { [weak self] isVisible in if !isVisible { self?.cleanup() From 312a648f04055b7180c7346039c0a15a3c83c1f0 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Thu, 4 May 2023 11:32:34 +0600 Subject: [PATCH 4/9] Update copy (Create QR Code) --- DuckDuckGo/Common/Localizables/UserText.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index a8a02b28eb..abe6acdac8 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -104,7 +104,7 @@ struct UserText { static let moreMenuItem = NSLocalizedString("sharing.more", value: "More…", comment: "Sharing Menu -> More…") static let findInPageMenuItem = NSLocalizedString("find.in.page.menu.item", value: "Find in Page…", comment: "Menu item title") static let shareMenuItem = NSLocalizedString("share.menu.item", value: "Share", comment: "Menu item title") - static let shareViaQRCodeMenuItem = NSLocalizedString("share.menu.item.qr.code", value: "QR Code", comment: "Menu item title") + static let shareViaQRCodeMenuItem = NSLocalizedString("share.menu.item.qr.code", value: "Create QR Code", comment: "Menu item title") static let printMenuItem = NSLocalizedString("print.menu.item", value: "Print…", comment: "Menu item title") static let newWindowMenuItem = NSLocalizedString("new.window.menu.item", value: "New Window", comment: "Menu item title") From ca6a32eb6fe715e5d0009dc304189519f128e3bf Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 8 May 2023 18:44:15 +0600 Subject: [PATCH 5/9] update QR icon --- .../Images/QR-Icon.imageset/Contents.json | 2 +- .../Images/QR-Icon.imageset/QR-16@1.25.pdf | Bin 0 -> 7539 bytes .../Images/QR-Icon.imageset/QR-24.pdf | Bin 5210 -> 0 bytes 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/QR-16@1.25.pdf delete mode 100644 DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/QR-24.pdf diff --git a/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/Contents.json index 6a2cb7adca..e8e484f72e 100644 --- a/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "QR-24.pdf", + "filename" : "QR-16@1.25.pdf", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/QR-16@1.25.pdf b/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/QR-16@1.25.pdf new file mode 100644 index 0000000000000000000000000000000000000000..79368287f5b967eba85c7c6614fc405877d5e1db GIT binary patch literal 7539 zcmbW6TW{M&5QX3QEA}NoQa}_*@d^Y68pjEWwy5j$E$D+vGmZ;gT1hF={`#I>?wlD) zMJ-tm3H@<*W_EVwa3#LDefw^fr%jUNE_2)8eovfx{o2iMz8>CxI4lpt_*WDD>z6J| z+cLw)U0q@1$!C7ko;|WYdt{!UJ+wG`WOeq)?(C7}*&`QckIb|4hn_R?>dNoku(|h} zofMN@|6$lX96q}R;GG|}KkSC(I+?#6{(9_(yZ3M0^#}V`zfb;5<~O-}*e7}F>jIUL z$;FIU=3Y~ULHQyr#;bmvaNOy|zTYH88YjVEoU{fkn?+l-PGKQ;f#OYzqLCg`qZMEH zV-PC?i!>|hyeyo$;0z6gs81GRWk$G4O0lA_q+hyF#87%nmH0BT?CmLxi&GcOi-tnv z*L#w`Rq{Fclx&ip!~PYQ?_Vo6&)L2_gzoAzBs+6{$Ga+Dbd@r0eU(;aTW6KlQJF5n z0$i_^Ql!!7N*Rrsi%^mowlWT<8=vNAmTZ-rI%m){Nha zcPp3ur-!v==!Bx|T%Oit-c-4(WH_ynn3a?H`idsVcyW4*G4#k-tV=rbS&WR~(8TF7 zNX*K~R#;F0X9Ts8bYgVQ0sA)#OW=)!j^hJ4`wem1!X)7{~ ztk7tfLaK|pP{dG-jLjId(J@^dg-laGNl;`OTH!>?)Qu4{zt)n>KCr)eOvX&^Mhrjw~`K+%OFvrP)kHsOJ; z;#OVW7MF+{x;E~Ip1!byJa#G7p4UU!gi(+TqUK;5qf&s0q);EgBqB0A1ZWAf zqmsZTEhz;PrQuz}Xa_cRBz*>(GM~{IKU{^0hzvGqNi#Rqx%%=Ay+G%vGA!S9LdIrq zPY!K}`}nL95)_}f)EMMvA;&txycA8P2Sp47_Bf8s4|{|>m#GVCFLvPgj2Du9d>C9| zwNY_7m+Pkk62{SMmKbmlIOc__u}-@2Bx5wElbWND7!m{TF)vmY-L8~TBBS*jN5D8- zH;2e>4q+%>fB1u<5vm%{gCYh3d;A_}5@4YmOp9>7gbQXwH;2e>4q<2nmA2;cPVkKw zKJMv9z7`h5KkfmfjpYWK6He(!MVzHJLgNbi4yKQ*X#N31?74MC#{fLSuv~!G|U18Qv22YwLQj^Ul6^oi- z`VO<>xl4CrDva&3${`BC)8)|0;SGnWILS+6*Nhua$R@?O5~n!ShedqKDMWiR@o3(V zcsH9;aibJhvPJ%~87pxDdD@(gjkY?*jz>{6W!#f_X=6~NUMQo}na=0N`tYNl31dh~ z`E>FoVV+BBaESWJuNd7kDgj5$G4W4Vf-cvuJgx}Jz}$p{#0|mMh1edrA6JJf z=|qK-R#ZIcIwUpL&oPdbhR&lqK6y+#!hkH(_M>9ckbq)UJ5nQ{ z{B?YNHvi@CKL$6yT|NvE{5ae{F5hgwx{vw`IFAZB>r1c1(%rD%e%bZI-uYK~chTJS zc5@gu@aW(42)teHz#M*K56HX^@<`tTe*bunP@P}FofALT+b@`#pTxfhF_!9yCBMKu z9pb%VT=@0!uv~2)rc=M}hEGZ6nq+)(`hQ(rRg202#}A}DT)B;)D#sH2hp6t__6XtJ zhU`p6HF=8#PEmmuOM(u&<>PADC4Tq5dy90u+;7*zEBEs8{?(Y2`Q3JVaDKrd!SfG~ Z{|tyeyg*y-4q-XXB5#rxFK&K%_b&{Quhakl literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/QR-24.pdf b/DuckDuckGo/Assets.xcassets/Images/QR-Icon.imageset/QR-24.pdf deleted file mode 100644 index f097490999c980e354b8e6a6963187d9ee6c911e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5210 zcmcIoO>f&q5WVlO*h_%qKuODQ0fB+Wc7mcU>dL(ZJ+L(6sL)3%GKJe;pILJEy`deT zLILSuQ;)MVvv1xEseN^K^FDEl${1%?_W1iB#@e@U?eg|%_;5coLpOb^#(!;NSBpAd z$;Vz@iRFvW^0q$PvOL=|J=?N4+j4!jWq!70cDAKE$MWjRv)mmXd|6C7tIvkNT;2a6o^0} zcWg^T;hvNgc3_L%Kq;dFlBHQx8o-_LQlCl+&>>Tln~+kg219=04~wBPfkwfWl%lKR zZlO-eubQNx?P}Tu(K|p&v>JqJ38LvFmC$}jldJ$5sd}!YxoWb@ss=?)K-8XHn1E0; zxj-RdutLZWmcSlC5KnJ}5LL}2Nd?r>(ybz+7yPD>zqBepv-k{7gUw51NNn zXWLO6>`!9S70?|~_Pg@PVkjXgOYo%UMFK)OlN9mnQw8cyRj|0lgjFzg>A^6QG^q46 zhsP&QX|-2WowHU6cT6m_i-O1fh6$u#RTkAxtQGhYw;Bzao(rq^AHssu|9;<*rW(WA z1P_e8Je;$%&eKYPsC<&en}pKnq%{QHqLkO0DwBs|^oD{kDj?srVS&8O2P%-0K;b4K z6_p^eqc_}SR6ytyPyLTpIiv>Xr_V$%6rhE9zTe@FN{dOK4JgM7>mO zV6qx(C~{VgDp#0}4NodddPHU_Hn6$U>kAXV0`Yiw$m2?*^)zADi*O;&wNNX0aD;S4wz@5Ud-8tXu3-b+;sJAE~ zoJyylw16=m6sTHNq3CJl6RofG25hEvpiF}^M`1cPwazgnrg1)Tm5NS~^!ww7aJCQ+ zBK(=dbWoJLs>1n14%nX`eRntK?YuyW^An$X`Nu zF-_5Uyo#PSU4Q&?YP;V06RS;dcYQnz-9aMyW0DBoH7CKW{Olhgb(rEI9(Mh(eUMbG z-@(SlIM>H7vev!GzZXP#k{@Wwliy&EfO0$*Hh$d<&F=U(U;1?FKADxR%+zfDuPuur zE3Clb_HPPuS6HQQZxGQN7>-v}MA`{TZQ sV_$C{-b|z{H^<{({f0+^m-pL$JIQ`LeQHj_xSf1m*XGr$+n?Wm1*Y!w3jhEB From 2dfc76034784215d4da1bf20167d1255451cffac Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Thu, 11 May 2023 11:05:39 +0600 Subject: [PATCH 6/9] fix typo --- DuckDuckGo.xcodeproj/project.pbxproj | 12 ++++++------ ...QRSharingServive.swift => QRSharingService.swift} | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) rename DuckDuckGo/Sharing/{QRSharingServive.swift => QRSharingService.swift} (99%) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 2b96ccccec..d50514a5e0 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1786,8 +1786,8 @@ 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 /* QRSharingServive.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F7127D29F6779000594A45 /* QRSharingServive.swift */; }; - B6F7127F29F6779000594A45 /* QRSharingServive.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F7127D29F6779000594A45 /* QRSharingServive.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 */; }; @@ -2793,7 +2793,7 @@ B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestsURLExtension.swift; sourceTree = ""; }; B6F41030264D2B23003DA42C /* ProgressExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressExtension.swift; sourceTree = ""; }; B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WKWebViewMockingExtension.swift; sourceTree = ""; }; - B6F7127D29F6779000594A45 /* QRSharingServive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRSharingServive.swift; sourceTree = ""; }; + B6F7127D29F6779000594A45 /* QRSharingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRSharingService.swift; sourceTree = ""; }; 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 = ""; }; B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardViewController.swift; sourceTree = ""; }; @@ -5758,7 +5758,7 @@ isa = PBXGroup; children = ( B63ED0E426BB8FB900A9DAD1 /* SharingMenu.swift */, - B6F7127D29F6779000594A45 /* QRSharingServive.swift */, + B6F7127D29F6779000594A45 /* QRSharingService.swift */, ); path = Sharing; sourceTree = ""; @@ -6709,7 +6709,7 @@ 3706FB7A293F65D500E42796 /* FileDownloadManager.swift in Sources */, 3706FB7B293F65D500E42796 /* BookmarkImport.swift in Sources */, 3706FB7C293F65D500E42796 /* KeySetDictionary.swift in Sources */, - B6F7127F29F6779000594A45 /* QRSharingServive.swift in Sources */, + B6F7127F29F6779000594A45 /* QRSharingService.swift in Sources */, 3706FB7E293F65D500E42796 /* FireCoordinator.swift in Sources */, 3706FB7F293F65D500E42796 /* GeolocationProvider.swift in Sources */, B603FD9F2A02712E00F3FCA9 /* CIImageExtension.swift in Sources */, @@ -7600,7 +7600,7 @@ AA9E9A5625A3AE8400D1959D /* NSWindowExtension.swift in Sources */, AAC5E4C725D6A6E8007F5990 /* BookmarkPopover.swift in Sources */, 37CC53F027E8D1440028713D /* PreferencesDownloadsView.swift in Sources */, - B6F7127E29F6779000594A45 /* QRSharingServive.swift in Sources */, + B6F7127E29F6779000594A45 /* QRSharingService.swift in Sources */, B68C2FB227706E6A00BF2C7D /* ProcessExtension.swift in Sources */, B6106BA726A7BECC0013B453 /* PermissionAuthorizationQuery.swift in Sources */, 3171D6BA288984D00068632A /* BadgeAnimationView.swift in Sources */, diff --git a/DuckDuckGo/Sharing/QRSharingServive.swift b/DuckDuckGo/Sharing/QRSharingService.swift similarity index 99% rename from DuckDuckGo/Sharing/QRSharingServive.swift rename to DuckDuckGo/Sharing/QRSharingService.swift index 1134df7c95..36094b90a9 100644 --- a/DuckDuckGo/Sharing/QRSharingServive.swift +++ b/DuckDuckGo/Sharing/QRSharingService.swift @@ -1,5 +1,5 @@ // -// QRSharingServive.swift +// QRSharingService.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // From 4a33b48bfdfc3bd897ac89f513851d873b6a781b Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 12 May 2023 23:52:14 +0600 Subject: [PATCH 7/9] dynamic retina scale factor; update logo --- .../Common/Extensions/CIImageExtension.swift | 25 ++++++++++++------- .../Common/Extensions/NSImageExtensions.swift | 4 +-- .../Common/Extensions/NSScreenExtension.swift | 4 +++ .../Common/Extensions/NSSizeExtension.swift | 4 +++ 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/DuckDuckGo/Common/Extensions/CIImageExtension.swift b/DuckDuckGo/Common/Extensions/CIImageExtension.swift index 6f69314a1a..a353b75cf4 100644 --- a/DuckDuckGo/Common/Extensions/CIImageExtension.swift +++ b/DuckDuckGo/Common/Extensions/CIImageExtension.swift @@ -20,6 +20,10 @@ import CoreImage.CIFilterBuiltins extension CIImage { + static var retinaScaleFactor: CGFloat { + min(NSScreen.maxScaleFactor, 2) // “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() @@ -61,7 +65,7 @@ extension CIImage { fileprivate static let iconSizeFactor: CGFloat = 0.25 - var qrSizeInPixels: Int + var logicalQrSize: Int var correctionLevel: QRCorrectionLevel? var icon: CIImage? @@ -69,20 +73,22 @@ extension CIImage { var color: NSColor var backgroundColor: NSColor - static let `default` = QRCodeParameters(qrSizeInPixels: 500, + 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")! - let logoRadiusFactor: CGFloat = 0.8 - let logoMargin: CGFloat = 8 - let logoBackgroundColor = NSColor.white + let logoRadiusFactor: CGFloat = 0.77 + let logoMargin: CGFloat = 6 + let logoBackgroundColor = NSColor.logoBackgroundColor - var image = logo.ciImage + 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)) @@ -96,7 +102,7 @@ extension CIImage { return image }() - return QRCodeParameters(qrSizeInPixels: QRCodeParameters.default.qrSizeInPixels, + return QRCodeParameters(logicalQrSize: logicalQrSize, correctionLevel: .high, icon: icon, color: .logoBackgroundColor, @@ -111,7 +117,7 @@ extension CIImage { let qrSize = qr.extent.size.width // scale to QR Size in Pixels - let qrScale = CGFloat(CGFloat(parameters.qrSizeInPixels) / CGFloat(qrSize)) + let qrScale = CGFloat((CGFloat(parameters.logicalQrSize) * CIImage.retinaScaleFactor) / CGFloat(qrSize)) qr = qr.scaled(by: qrScale) // tint @@ -190,7 +196,8 @@ 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: self.width / 2, height: self.height / 2) + bitmapRep.size = NSSize(width: Int(CGFloat(self.width) / CIImage.retinaScaleFactor), + height: Int(CGFloat(self.height) / CIImage.retinaScaleFactor)) return bitmapRep.representation(using: format, properties: [:]) } diff --git a/DuckDuckGo/Common/Extensions/NSImageExtensions.swift b/DuckDuckGo/Common/Extensions/NSImageExtensions.swift index c19c19214a..4c73c1ed39 100644 --- a/DuckDuckGo/Common/Extensions/NSImageExtensions.swift +++ b/DuckDuckGo/Common/Extensions/NSImageExtensions.swift @@ -59,8 +59,8 @@ extension NSImage { return image } - var ciImage: CIImage { - var rect = NSRect(x: 0, y: 0, width: self.size.width * 2, height: self.size.height * 2) + 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)!) } diff --git a/DuckDuckGo/Common/Extensions/NSScreenExtension.swift b/DuckDuckGo/Common/Extensions/NSScreenExtension.swift index 5fa38bdd7c..b2a762d53e 100644 --- a/DuckDuckGo/Common/Extensions/NSScreenExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSScreenExtension.swift @@ -24,6 +24,10 @@ extension NSScreen { screens.min(by: { ($0.frame.height - $0.visibleFrame.height) > ($1.frame.height - $1.visibleFrame.height) }) } + static var maxScaleFactor: CGFloat { + screens.map(\.backingScaleFactor).max() ?? 2 + } + func convert(_ point: NSPoint) -> NSPoint { return NSPoint(x: point.x - self.frame.origin.x, y: point.y - self.frame.origin.y) diff --git a/DuckDuckGo/Common/Extensions/NSSizeExtension.swift b/DuckDuckGo/Common/Extensions/NSSizeExtension.swift index a67c2ddc6d..b6b6037c4c 100644 --- a/DuckDuckGo/Common/Extensions/NSSizeExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSSizeExtension.swift @@ -27,4 +27,8 @@ extension NSSize { width < size.width && height < size.height } + func scaled(by scaleFactor: CGFloat) -> NSSize { + NSSize(width: width * scaleFactor, height: height * scaleFactor) + } + } From c05990a8bc332bee9c7d817c534cbf6ec7da13bc Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 12 May 2023 23:53:49 +0600 Subject: [PATCH 8/9] max scale factor --- DuckDuckGo/Common/Extensions/CIImageExtension.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Common/Extensions/CIImageExtension.swift b/DuckDuckGo/Common/Extensions/CIImageExtension.swift index a353b75cf4..7768d48057 100644 --- a/DuckDuckGo/Common/Extensions/CIImageExtension.swift +++ b/DuckDuckGo/Common/Extensions/CIImageExtension.swift @@ -21,7 +21,7 @@ import CoreImage.CIFilterBuiltins extension CIImage { static var retinaScaleFactor: CGFloat { - min(NSScreen.maxScaleFactor, 2) // “retina” or larger + max(NSScreen.maxScaleFactor, 2) // “retina” or larger } /// Generates a `CIImage` of a rounded rectangle with a specified extent and corner radius. From 2ebf0380448f75bd7c13be34896aaab93352b159 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Sat, 13 May 2023 00:03:20 +0600 Subject: [PATCH 9/9] scale preview image by main screen scale factor --- DuckDuckGo/Common/Extensions/CIImageExtension.swift | 2 +- DuckDuckGo/Common/Extensions/NSScreenExtension.swift | 6 ++++-- DuckDuckGo/Sharing/QRSharingService.swift | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/Common/Extensions/CIImageExtension.swift b/DuckDuckGo/Common/Extensions/CIImageExtension.swift index 7768d48057..143ab0e804 100644 --- a/DuckDuckGo/Common/Extensions/CIImageExtension.swift +++ b/DuckDuckGo/Common/Extensions/CIImageExtension.swift @@ -21,7 +21,7 @@ import CoreImage.CIFilterBuiltins extension CIImage { static var retinaScaleFactor: CGFloat { - max(NSScreen.maxScaleFactor, 2) // “retina” or larger + max(NSScreen.maxBackingScaleFactor, NSScreen.defaultBackingScaleFactor) // “retina” or larger } /// Generates a `CIImage` of a rounded rectangle with a specified extent and corner radius. diff --git a/DuckDuckGo/Common/Extensions/NSScreenExtension.swift b/DuckDuckGo/Common/Extensions/NSScreenExtension.swift index b2a762d53e..d8ecfd3735 100644 --- a/DuckDuckGo/Common/Extensions/NSScreenExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSScreenExtension.swift @@ -24,8 +24,10 @@ extension NSScreen { screens.min(by: { ($0.frame.height - $0.visibleFrame.height) > ($1.frame.height - $1.visibleFrame.height) }) } - static var maxScaleFactor: CGFloat { - screens.map(\.backingScaleFactor).max() ?? 2 + static let defaultBackingScaleFactor: CGFloat = 2 + + static var maxBackingScaleFactor: CGFloat { + screens.map(\.backingScaleFactor).max() ?? defaultBackingScaleFactor } func convert(_ point: NSPoint) -> NSPoint { diff --git a/DuckDuckGo/Sharing/QRSharingService.swift b/DuckDuckGo/Sharing/QRSharingService.swift index 36094b90a9..1285643961 100644 --- a/DuckDuckGo/Sharing/QRSharingService.swift +++ b/DuckDuckGo/Sharing/QRSharingService.swift @@ -84,7 +84,7 @@ final class QRSharingService: NSSharingService { } self.imageUrl = fileUrl - self.qrImage = NSImage(cgImage: cgImage, size: NSSize(width: qr.extent.size.width / 2, height: qr.extent.size.height / 2)) + self.qrImage = NSImage(cgImage: cgImage, size: qr.extent.size.scaled(by: 1 / (NSScreen.main?.backingScaleFactor ?? NSScreen.defaultBackingScaleFactor))) self.showQuickLook() }