ScannerService.swift 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. import Cocoa
  2. import ImageCaptureCore
  3. // MARK: - Scanner Service
  4. @MainActor
  5. final class ScannerService: NSObject {
  6. static let shared = ScannerService()
  7. enum State: Equatable {
  8. case idle
  9. case browsing
  10. case connecting
  11. case ready
  12. case scanning
  13. case error(String)
  14. }
  15. private(set) var state: State = .idle {
  16. didSet { onStateChanged?(state) }
  17. }
  18. private(set) var localDevices: [ICScannerDevice] = [] {
  19. didSet { onDevicesChanged?() }
  20. }
  21. private(set) var sharedDevices: [ICScannerDevice] = [] {
  22. didSet { onDevicesChanged?() }
  23. }
  24. private(set) var selectedDevice: ICScannerDevice? {
  25. didSet { onSelectionChanged?() }
  26. }
  27. var paperSize: PaperSize = AppSettings.defaultPaperSize
  28. var resolution: ScanResolution = AppSettings.scanResolution
  29. var onStateChanged: ((State) -> Void)?
  30. var onDevicesChanged: (() -> Void)?
  31. var onSelectionChanged: (() -> Void)?
  32. var onScanComplete: ((Result<NSImage, Error>) -> Void)?
  33. private let browser = ICDeviceBrowser()
  34. private var scanCompletion: ((Result<NSImage, Error>) -> Void)?
  35. private var shouldScanWhenReady = false
  36. private var scannedImageURL: URL?
  37. private var didFinishCurrentScan = false
  38. private var accumulatedBands: [ICScannerBandData] = []
  39. private var isClosingSession = false
  40. private var isOpeningSession = false
  41. private var isMaintainingSession = false
  42. private var hasEstablishedSession = false
  43. private var lastSelectedDeviceName: String?
  44. private var pendingPreferredDeviceName: String?
  45. private var sessionCloseContinuation: CheckedContinuation<Void, Never>?
  46. private let connectionTimeoutSeconds: UInt64 = 20
  47. private let sessionCloseTimeoutSeconds: UInt64 = 6
  48. private override init() {
  49. super.init()
  50. browser.delegate = self
  51. let maskRaw = ICDeviceTypeMask.scanner.rawValue
  52. | ICDeviceLocationTypeMask.local.rawValue
  53. | ICDeviceLocationTypeMask.shared.rawValue
  54. | ICDeviceLocationTypeMask.bonjour.rawValue
  55. browser.browsedDeviceTypeMask = ICDeviceTypeMask(rawValue: maskRaw) ?? .scanner
  56. }
  57. /// Opens the scanner panel. Reuses an existing session when one is already open.
  58. func prepareForScanning() async {
  59. clearConnectionTimeout()
  60. isOpeningSession = false
  61. isClosingSession = false
  62. didFinishCurrentScan = false
  63. shouldScanWhenReady = false
  64. scanCompletion = nil
  65. scannedImageURL = nil
  66. accumulatedBands = []
  67. if let device = selectedDevice {
  68. device.delegate = self
  69. if device.hasOpenSession {
  70. state = .ready
  71. ensureBrowsing()
  72. return
  73. }
  74. if hasEstablishedSession {
  75. state = .ready
  76. ensureBrowsing()
  77. maintainSession(for: device)
  78. return
  79. }
  80. }
  81. if let device = deviceMatchingLastSelection() {
  82. selectedDevice = device
  83. device.delegate = self
  84. if device.hasOpenSession {
  85. state = .ready
  86. ensureBrowsing()
  87. return
  88. }
  89. if hasEstablishedSession {
  90. state = .ready
  91. ensureBrowsing()
  92. maintainSession(for: device)
  93. return
  94. }
  95. }
  96. pendingPreferredDeviceName = lastSelectedDeviceName
  97. ensureBrowsing()
  98. state = .browsing
  99. }
  100. /// Panel closed — keep the scanner session alive for the next open.
  101. func detachFromUI() {
  102. clearConnectionTimeout()
  103. scanCompletion = nil
  104. shouldScanWhenReady = false
  105. }
  106. /// Fully releases the scanner when leaving the scan feature entirely.
  107. func releaseScanner() async {
  108. clearConnectionTimeout()
  109. detachFromUI()
  110. if let device = selectedDevice {
  111. await closeSession(on: device)
  112. }
  113. if browser.isBrowsing {
  114. browser.stop()
  115. }
  116. resetAfterFullShutdown()
  117. }
  118. /// True when reconnecting after a previous successful scan in this session.
  119. var isWarmReconnecting: Bool {
  120. isMaintainingSession || (isOpeningSession && hasEstablishedSession)
  121. }
  122. func selectDevice(_ device: ICScannerDevice) {
  123. let name = device.name ?? device.locationDescription
  124. if let name { lastSelectedDeviceName = name }
  125. pendingPreferredDeviceName = nil
  126. if selectedDevice === device {
  127. if device.hasOpenSession {
  128. device.delegate = self
  129. state = .ready
  130. } else if case .connecting = state {
  131. return
  132. } else {
  133. Task { await openSession(on: device, warm: hasEstablishedSession) }
  134. }
  135. return
  136. }
  137. selectedDevice = device
  138. if device.hasOpenSession {
  139. device.delegate = self
  140. state = .ready
  141. } else {
  142. Task { await openSession(on: device, warm: hasEstablishedSession) }
  143. }
  144. }
  145. func scan() {
  146. guard let device = selectedDevice else { return }
  147. scanCompletion = onScanComplete
  148. scannedImageURL = nil
  149. didFinishCurrentScan = false
  150. accumulatedBands = []
  151. if case .connecting = state {
  152. shouldScanWhenReady = true
  153. return
  154. }
  155. device.delegate = self
  156. if device.hasOpenSession {
  157. configureFunctionalUnit(on: device)
  158. state = .scanning
  159. device.requestScan()
  160. } else {
  161. shouldScanWhenReady = true
  162. Task { await openSession(on: device, warm: hasEstablishedSession) }
  163. }
  164. }
  165. func cancelScan() {
  166. selectedDevice?.cancelScan()
  167. scanCompletion?(.failure(ScanError.cancelled))
  168. scanCompletion = nil
  169. scannedImageURL = nil
  170. didFinishCurrentScan = true
  171. accumulatedBands = []
  172. state = selectedDevice?.hasOpenSession == true ? .ready : .browsing
  173. }
  174. private func ensureBrowsing() {
  175. if !browser.isBrowsing {
  176. browser.start()
  177. }
  178. }
  179. private func deviceMatchingLastSelection() -> ICScannerDevice? {
  180. guard let name = lastSelectedDeviceName else { return nil }
  181. return (localDevices + sharedDevices).first {
  182. ($0.name ?? $0.locationDescription) == name
  183. }
  184. }
  185. private func resetAfterFullShutdown() {
  186. isClosingSession = false
  187. isOpeningSession = false
  188. isMaintainingSession = false
  189. hasEstablishedSession = false
  190. pendingPreferredDeviceName = nil
  191. selectedDevice = nil
  192. localDevices = []
  193. sharedDevices = []
  194. state = .idle
  195. scannedImageURL = nil
  196. didFinishCurrentScan = false
  197. accumulatedBands = []
  198. shouldScanWhenReady = false
  199. }
  200. private func closeSession(on device: ICScannerDevice) async {
  201. device.delegate = nil
  202. if state == .scanning {
  203. device.cancelScan()
  204. }
  205. guard device.hasOpenSession else {
  206. isClosingSession = false
  207. return
  208. }
  209. isClosingSession = true
  210. do {
  211. try await device.requestCloseSession()
  212. processSessionClosed(on: device, error: nil)
  213. } catch {
  214. processSessionClosed(on: device, error: error)
  215. }
  216. await waitForSessionToCloseAsync()
  217. }
  218. private func waitForSessionToCloseAsync() async {
  219. if !isClosingSession, selectedDevice?.hasOpenSession != true {
  220. return
  221. }
  222. await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
  223. sessionCloseContinuation = continuation
  224. Task { @MainActor in
  225. try? await Task.sleep(nanoseconds: sessionCloseTimeoutSeconds * 1_000_000_000)
  226. resumeSessionCloseWait()
  227. }
  228. }
  229. }
  230. private func resumeSessionCloseWait() {
  231. guard let continuation = sessionCloseContinuation else { return }
  232. sessionCloseContinuation = nil
  233. isClosingSession = false
  234. continuation.resume()
  235. }
  236. private func maintainSession(for device: ICScannerDevice) {
  237. guard selectedDevice === device, !device.hasOpenSession else { return }
  238. guard !isOpeningSession, !isMaintainingSession, !isClosingSession else { return }
  239. isMaintainingSession = true
  240. Task {
  241. await openSession(on: device, warm: true)
  242. isMaintainingSession = false
  243. }
  244. }
  245. private func openSession(on device: ICScannerDevice, warm: Bool = false) async {
  246. guard selectedDevice === device else { return }
  247. clearConnectionTimeout()
  248. device.delegate = self
  249. if device.hasOpenSession {
  250. isOpeningSession = false
  251. state = .ready
  252. if shouldScanWhenReady {
  253. shouldScanWhenReady = false
  254. configureFunctionalUnit(on: device)
  255. state = .scanning
  256. device.requestScan()
  257. }
  258. return
  259. }
  260. isOpeningSession = true
  261. state = (warm && hasEstablishedSession) ? .ready : .connecting
  262. scheduleConnectionTimeout(for: device)
  263. do {
  264. try await device.requestOpenSession()
  265. processSessionOpened(on: device, error: nil)
  266. } catch {
  267. processSessionOpened(on: device, error: error)
  268. }
  269. }
  270. private func processSessionOpened(on scanner: ICScannerDevice, error: Error?) {
  271. guard selectedDevice === scanner else { return }
  272. guard isOpeningSession || state == .connecting else { return }
  273. clearConnectionTimeout()
  274. isOpeningSession = false
  275. if let error {
  276. state = .error(error.localizedDescription)
  277. finishScan(with: .failure(error))
  278. return
  279. }
  280. isClosingSession = false
  281. hasEstablishedSession = true
  282. scanner.delegate = self
  283. if shouldScanWhenReady {
  284. shouldScanWhenReady = false
  285. configureFunctionalUnit(on: scanner)
  286. state = .scanning
  287. scanner.requestScan()
  288. } else {
  289. state = .ready
  290. }
  291. }
  292. private func processSessionClosed(on device: ICDevice, error: Error?) {
  293. let wasClosing = isClosingSession
  294. resumeSessionCloseWait()
  295. guard let scanner = device as? ICScannerDevice,
  296. selectedDevice === scanner else { return }
  297. if wasClosing {
  298. if state != .idle { state = .browsing }
  299. return
  300. }
  301. // Many scanners close the session after each scan — reopen quietly for the next one.
  302. guard state != .idle else { return }
  303. maintainSession(for: scanner)
  304. }
  305. /// Keeps the scanner session open after each scan for the next one.
  306. private func resetAfterScan() {
  307. clearConnectionTimeout()
  308. scanCompletion = nil
  309. scannedImageURL = nil
  310. didFinishCurrentScan = false
  311. accumulatedBands = []
  312. shouldScanWhenReady = false
  313. isOpeningSession = false
  314. if let device = selectedDevice {
  315. device.delegate = self
  316. if device.hasOpenSession {
  317. state = .ready
  318. } else {
  319. state = .ready
  320. maintainSession(for: device)
  321. }
  322. }
  323. }
  324. private var connectionTimeoutTask: Task<Void, Never>?
  325. private func scheduleConnectionTimeout(for device: ICScannerDevice) {
  326. connectionTimeoutTask?.cancel()
  327. connectionTimeoutTask = Task { @MainActor in
  328. try? await Task.sleep(nanoseconds: connectionTimeoutSeconds * 1_000_000_000)
  329. guard !Task.isCancelled else { return }
  330. guard selectedDevice === device, case .connecting = state else { return }
  331. isOpeningSession = false
  332. device.delegate = nil
  333. state = .error("Could not connect to the scanner. Select it again to retry.")
  334. }
  335. }
  336. private func clearConnectionTimeout() {
  337. connectionTimeoutTask?.cancel()
  338. connectionTimeoutTask = nil
  339. }
  340. private func configureFunctionalUnit(on device: ICScannerDevice) {
  341. let unit = device.selectedFunctionalUnit
  342. unit.pixelDataType = .RGB
  343. if let eightBit = ICScannerBitDepth(rawValue: 8) {
  344. unit.bitDepth = eightBit
  345. }
  346. unit.resolution = resolution.dpi
  347. let docType = documentType(for: paperSize)
  348. if let flatbed = unit as? ICScannerFunctionalUnitFlatbed {
  349. flatbed.documentType = docType
  350. } else if let feeder = unit as? ICScannerFunctionalUnitDocumentFeeder {
  351. feeder.documentType = docType
  352. } else if let positive = unit as? ICScannerFunctionalUnitPositiveTransparency {
  353. positive.documentType = docType
  354. } else if let negative = unit as? ICScannerFunctionalUnitNegativeTransparency {
  355. negative.documentType = docType
  356. }
  357. let physicalSize = unit.physicalSize
  358. if physicalSize.width > 0, physicalSize.height > 0 {
  359. unit.scanArea = NSRect(origin: .zero, size: physicalSize)
  360. }
  361. device.transferMode = .fileBased
  362. device.documentName = "Scan"
  363. device.documentUTI = "public.jpeg"
  364. device.downloadsDirectory = scanDownloadsDirectory()
  365. }
  366. private func scanDownloadsDirectory() -> URL {
  367. let directory = FileManager.default.temporaryDirectory
  368. .appendingPathComponent("Scans", isDirectory: true)
  369. try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
  370. return directory
  371. }
  372. private func documentType(for paperSize: PaperSize) -> ICScannerDocumentType {
  373. switch paperSize {
  374. case .a4: return .typeA4
  375. case .letter: return .typeUSLetter
  376. case .legal: return .typeUSLegal
  377. }
  378. }
  379. private func isSharedDevice(_ device: ICDevice) -> Bool {
  380. let locationBits = device.type.rawValue & ICDeviceLocationTypeMask.remote.rawValue
  381. return locationBits != 0
  382. }
  383. private func categorize(_ device: ICScannerDevice, moreComing: Bool) {
  384. if isSharedDevice(device) {
  385. if !sharedDevices.contains(where: { $0 === device }) {
  386. sharedDevices.append(device)
  387. }
  388. } else if !localDevices.contains(where: { $0 === device }) {
  389. localDevices.append(device)
  390. }
  391. autoSelectIfNeeded(device, moreComing: moreComing)
  392. }
  393. private func autoSelectIfNeeded(_ device: ICScannerDevice, moreComing: Bool) {
  394. guard selectedDevice == nil else { return }
  395. let deviceName = device.name ?? device.locationDescription
  396. if let preferred = pendingPreferredDeviceName,
  397. let deviceName,
  398. deviceName == preferred {
  399. pendingPreferredDeviceName = nil
  400. selectDevice(device)
  401. return
  402. }
  403. if !moreComing, localDevices.count + sharedDevices.count == 1 {
  404. selectDevice(device)
  405. }
  406. }
  407. private func remove(_ device: ICDevice) {
  408. localDevices.removeAll { $0 === device }
  409. sharedDevices.removeAll { $0 === device }
  410. if selectedDevice === device {
  411. selectedDevice = nil
  412. state = .browsing
  413. }
  414. }
  415. private func finishScan(with result: Result<NSImage, Error>) {
  416. guard !didFinishCurrentScan else { return }
  417. didFinishCurrentScan = true
  418. scanCompletion?(result)
  419. resetAfterScan()
  420. }
  421. private func imageFromScanURL(_ url: URL) -> NSImage? {
  422. if let data = try? Data(contentsOf: url), let image = NSImage(data: data) {
  423. return image
  424. }
  425. return NSImage(contentsOf: url)
  426. }
  427. private func loadImageFromScanURL(_ url: URL) async -> NSImage? {
  428. for attempt in 0..<6 {
  429. if let image = imageFromScanURL(url) {
  430. return image
  431. }
  432. if attempt < 5 {
  433. try? await Task.sleep(nanoseconds: 250_000_000)
  434. }
  435. }
  436. return nil
  437. }
  438. private func resolveScannedImage() async -> NSImage? {
  439. if let url = scannedImageURL, let image = await loadImageFromScanURL(url) {
  440. return image
  441. }
  442. if !accumulatedBands.isEmpty {
  443. return assembleImage(from: accumulatedBands)
  444. }
  445. return nil
  446. }
  447. }
  448. // MARK: - ICDeviceBrowserDelegate
  449. extension ScannerService: ICDeviceBrowserDelegate {
  450. nonisolated func deviceBrowser(_ browser: ICDeviceBrowser, didAdd device: ICDevice, moreComing: Bool) {
  451. guard let scanner = device as? ICScannerDevice else { return }
  452. Task { @MainActor in
  453. categorize(scanner, moreComing: moreComing)
  454. }
  455. }
  456. nonisolated func deviceBrowser(_ browser: ICDeviceBrowser, didRemove device: ICDevice, moreGoing: Bool) {
  457. Task { @MainActor in
  458. remove(device)
  459. }
  460. }
  461. }
  462. // MARK: - ICDeviceDelegate / ICScannerDeviceDelegate
  463. extension ScannerService: ICDeviceDelegate, ICScannerDeviceDelegate {
  464. nonisolated func didRemove(_ device: ICDevice) {
  465. Task { @MainActor in
  466. remove(device)
  467. }
  468. }
  469. nonisolated func device(_ device: ICDevice, didOpenSessionWithError error: (any Error)?) {
  470. Task { @MainActor in
  471. guard let scanner = device as? ICScannerDevice else { return }
  472. processSessionOpened(on: scanner, error: error)
  473. }
  474. }
  475. nonisolated func device(_ device: ICDevice, didCloseSessionWithError error: (any Error)?) {
  476. Task { @MainActor in
  477. processSessionClosed(on: device, error: error)
  478. }
  479. }
  480. nonisolated func scannerDevice(_ scanner: ICScannerDevice, didScanTo url: URL) {
  481. Task { @MainActor in
  482. scannedImageURL = url
  483. if let image = imageFromScanURL(url) {
  484. finishScan(with: .success(image))
  485. }
  486. }
  487. }
  488. nonisolated func scannerDevice(_ scanner: ICScannerDevice, didScanTo bandData: ICScannerBandData) {
  489. Task { @MainActor in
  490. accumulatedBands.append(bandData)
  491. if bandData.dataNumRows >= bandData.fullImageHeight,
  492. let image = imageFromBandData(bandData) {
  493. finishScan(with: .success(image))
  494. }
  495. }
  496. }
  497. nonisolated func scannerDevice(_ scanner: ICScannerDevice, didCompleteScanWithError error: (any Error)?) {
  498. Task { @MainActor in
  499. if didFinishCurrentScan { return }
  500. if let error {
  501. finishScan(with: .failure(error))
  502. return
  503. }
  504. if let image = await resolveScannedImage() {
  505. finishScan(with: .success(image))
  506. return
  507. }
  508. finishScan(with: .failure(ScanError.scanFailed))
  509. }
  510. }
  511. nonisolated func scannerDeviceDidBecomeAvailable(_ scanner: ICScannerDevice) {
  512. Task { @MainActor in
  513. guard selectedDevice === scanner else { return }
  514. guard state == .connecting || isOpeningSession else { return }
  515. clearConnectionTimeout()
  516. isOpeningSession = false
  517. isClosingSession = false
  518. hasEstablishedSession = true
  519. scanner.delegate = self
  520. if shouldScanWhenReady {
  521. shouldScanWhenReady = false
  522. configureFunctionalUnit(on: scanner)
  523. state = .scanning
  524. scanner.requestScan()
  525. } else {
  526. state = .ready
  527. }
  528. }
  529. }
  530. }
  531. private extension ScanResolution {
  532. var dpi: Int {
  533. switch self {
  534. case .dpi150: return 150
  535. case .dpi300: return 300
  536. case .dpi600: return 600
  537. }
  538. }
  539. }
  540. private extension ScannerService {
  541. func imageFromBandData(_ bandData: ICScannerBandData) -> NSImage? {
  542. guard let data = bandData.dataBuffer,
  543. !data.isEmpty,
  544. bandData.fullImageWidth > 0,
  545. bandData.dataNumRows > 0 else { return nil }
  546. let colorSpace = CGColorSpaceCreateDeviceRGB()
  547. let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.noneSkipLast.rawValue)
  548. guard let provider = CGDataProvider(data: data as CFData),
  549. let cgImage = CGImage(
  550. width: Int(bandData.fullImageWidth),
  551. height: Int(bandData.dataNumRows),
  552. bitsPerComponent: Int(bandData.bitsPerComponent),
  553. bitsPerPixel: Int(bandData.bitsPerPixel),
  554. bytesPerRow: Int(bandData.bytesPerRow),
  555. space: colorSpace,
  556. bitmapInfo: bitmapInfo,
  557. provider: provider,
  558. decode: nil,
  559. shouldInterpolate: true,
  560. intent: .defaultIntent
  561. ) else {
  562. return nil
  563. }
  564. let rep = NSBitmapImageRep(cgImage: cgImage)
  565. let image = NSImage(size: NSSize(width: bandData.fullImageWidth, height: bandData.dataNumRows))
  566. image.addRepresentation(rep)
  567. return verticallyFlipped(image)
  568. }
  569. func assembleImage(from bands: [ICScannerBandData]) -> NSImage? {
  570. guard let first = bands.first,
  571. first.fullImageWidth > 0,
  572. first.fullImageHeight > 0 else { return nil }
  573. let width = Int(first.fullImageWidth)
  574. let height = Int(first.fullImageHeight)
  575. let bytesPerRow = Int(first.bytesPerRow)
  576. guard bytesPerRow > 0 else { return nil }
  577. var fullData = Data(count: bytesPerRow * height)
  578. for band in bands {
  579. guard let bandData = band.dataBuffer, !bandData.isEmpty else { continue }
  580. let startRow = Int(band.dataStartRow)
  581. let startOffset = startRow * bytesPerRow
  582. let copyLength = min(bandData.count, fullData.count - startOffset)
  583. guard startOffset >= 0, copyLength > 0, startOffset + copyLength <= fullData.count else { continue }
  584. fullData.replaceSubrange(startOffset..<(startOffset + copyLength), with: bandData.prefix(copyLength))
  585. }
  586. let colorSpace = CGColorSpaceCreateDeviceRGB()
  587. let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.noneSkipLast.rawValue)
  588. var mutableData = fullData
  589. return mutableData.withUnsafeMutableBytes { buffer in
  590. guard let baseAddress = buffer.baseAddress,
  591. let context = CGContext(
  592. data: baseAddress,
  593. width: width,
  594. height: height,
  595. bitsPerComponent: Int(first.bitsPerComponent),
  596. bytesPerRow: bytesPerRow,
  597. space: colorSpace,
  598. bitmapInfo: bitmapInfo.rawValue
  599. ),
  600. let cgImage = context.makeImage() else {
  601. return nil
  602. }
  603. let rep = NSBitmapImageRep(cgImage: cgImage)
  604. let image = NSImage(size: NSSize(width: width, height: height))
  605. image.addRepresentation(rep)
  606. return verticallyFlipped(image)
  607. }
  608. }
  609. func verticallyFlipped(_ image: NSImage) -> NSImage {
  610. guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return image }
  611. let width = cgImage.width
  612. let height = cgImage.height
  613. let colorSpace = cgImage.colorSpace ?? CGColorSpaceCreateDeviceRGB()
  614. guard let context = CGContext(
  615. data: nil,
  616. width: width,
  617. height: height,
  618. bitsPerComponent: cgImage.bitsPerComponent,
  619. bytesPerRow: 0,
  620. space: colorSpace,
  621. bitmapInfo: cgImage.bitmapInfo.rawValue
  622. ) else {
  623. return image
  624. }
  625. context.translateBy(x: 0, y: CGFloat(height))
  626. context.scaleBy(x: 1, y: -1)
  627. context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
  628. guard let flipped = context.makeImage() else { return image }
  629. return NSImage(cgImage: flipped, size: NSSize(width: width, height: height))
  630. }
  631. }