| // Copyright (C) 2026 Apple Inc. All rights reserved. |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions |
| // are met: |
| // 1. Redistributions of source code must retain the above copyright |
| // notice, this list of conditions and the following disclaimer. |
| // 2. Redistributions in binary form must reproduce the above copyright |
| // notice, this list of conditions and the following disclaimer in the |
| // documentation and/or other materials provided with the distribution. |
| // |
| // THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| // THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| // BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| // THE POSSIBILITY OF SUCH DAMAGE. |
| |
| #if ENABLE_SWIFTUI && ENABLE_CXX_INTEROP && ENABLE_IMAGE_ANALYSIS |
| |
| import Foundation |
| |
| /// An object that contains information about both the location and content of text and glyphs that the framework recognizes in an image. |
| public struct ImageAnalysisResult: Codable, Sendable { |
| /// Information about regions of text that an image-analysis request detects. |
| public struct Line: Codable, Sendable { |
| /// Text recognized in an image through a text recognition request. |
| public struct TextInfo: Codable, Sendable { |
| /// The top candidate for recognized text. |
| public let text: String |
| |
| /// The bounding region of the text. |
| public let quad: Quad |
| } |
| |
| /// The top candidate for recognized text. |
| public let text: String |
| |
| /// The bounding region of the line. |
| public let quad: Quad |
| |
| /// The pieces of text that compose this line. |
| public let children: [TextInfo] |
| } |
| |
| /// A type representing a bounding quadrilateral. |
| public struct Quad: Codable, Sendable { |
| /// The coordinates of the upper-left corner of the quadrilateral. |
| public let topLeft: CGPoint |
| |
| /// The coordinates of the upper-right corner of the quadrilateral. |
| public let topRight: CGPoint |
| |
| /// The coordinates of the lower-left corner of the quadrilateral. |
| public let bottomLeft: CGPoint |
| |
| /// The coordinates of the lower-right corner of quadrilateral. |
| public let bottomRight: CGPoint |
| } |
| |
| /// The lines of text that compose this analysis. |
| public let lines: [Line] |
| |
| /// Creates a result with the given recognized-text lines. |
| public init(lines: [Line]) { |
| self.lines = lines |
| } |
| } |
| |
| extension ImageAnalysisResult { |
| /// Parses valid JSON data from a URL into a new `ImageAnalysisResult` value. |
| /// |
| /// - Parameter url: The URL to parse, which must contain valid JSON data in the correct schema. |
| /// - Throws: An error if decoding fails. |
| public init(parsing url: URL) throws { |
| let decoder = JSONDecoder() |
| self = try decoder.decode(Self.self, from: Data(contentsOf: url)) |
| } |
| } |
| |
| /// Runs `body` with `-[VKCImageAnalyzer processRequest:progressHandler:completionHandler:]` swizzled |
| /// to return `analysis` instead of dispatching to Vision. |
| /// |
| /// The original implementation is restored when `body` returns (or throws). |
| /// |
| /// - Parameters: |
| /// - response: The response to provide in place of the Vision framework's normal response. |
| /// - failureType: The type of the error of the result, if any. |
| /// - delay: An option duration which, if specified, delays the response from being provided by the specified duration. |
| /// - body: The code to execute with the replaced implementation. |
| /// - Throws: The error thrown by `body`, if any. |
| public nonisolated(nonsending) func withMockedImageAnalyzer<ResultFailure, BodyFailure>( |
| response: Result<ImageAnalysisResult, ResultFailure>, |
| failureType: ResultFailure.Type = ResultFailure.self, |
| after delay: Duration? = nil, |
| perform body: () async throws(BodyFailure) -> sending Void |
| ) async throws(BodyFailure) where ResultFailure: Error, BodyFailure: Error { |
| typealias ObjCVKImageAnalysisRequestID = Int32 |
| typealias ObjCVKCImageAnalysis = AnyObject |
| typealias ObjCCompletionHandler = @Sendable @convention(block) (ObjCVKCImageAnalysis?, NSError?) -> Void |
| typealias ObjCProgressHandler = @Sendable @convention(block) (Double) -> Void |
| typealias ObjCImplementation = |
| @Sendable @convention(block) (AnyObject, AnyObject?, ObjCProgressHandler?, @escaping ObjCCompletionHandler) -> |
| ObjCVKImageAnalysisRequestID |
| |
| guard let analyzerClass: AnyClass = TestWebKitAPI.getImageAnalyzerClass() else { |
| fatalError() |
| } |
| |
| let processRequest: ObjCImplementation = { _, _, _, completion in |
| Task.immediate { |
| if let delay { |
| try? await Task.sleep(for: delay) |
| } |
| |
| switch response { |
| case .success(let result): |
| completion(TestVKImageAnalysis(result), nil) |
| |
| case .failure(let error): |
| completion(nil, error as NSError) |
| } |
| } |
| |
| return 0 |
| } |
| |
| try await withSwizzledObjectiveCInstanceMethod( |
| replacing: analyzerClass, |
| name: NSSelectorFromString("processRequest:progressHandler:completionHandler:"), |
| with: processRequest, |
| perform: body |
| ) |
| } |
| |
| /// Runs `body` with `-[VKCImageAnalyzer processRequest:progressHandler:completionHandler:]` swizzled |
| /// to return `analysis` instead of dispatching to Vision. |
| /// |
| /// The original implementation is restored when `body` returns (or throws). |
| /// |
| /// - Parameters: |
| /// - response: The response to provide in place of the Vision framework's normal response. |
| /// - delay: An option duration which, if specified, delays the response from being provided by the specified duration. |
| /// - body: The code to execute with the replaced implementation. |
| /// - Throws: The error thrown by `body`, if any. |
| public nonisolated(nonsending) func withMockedImageAnalyzer<BodyFailure>( |
| response: Result<ImageAnalysisResult, Never>, |
| after delay: Duration? = nil, |
| perform body: () async throws(BodyFailure) -> sending Void |
| ) async throws(BodyFailure) { |
| try await withMockedImageAnalyzer(response: response, failureType: Never.self, after: delay, perform: body) |
| } |
| |
| // The following classes stand in for VKC's result types. WebKit reads them purely via |
| // Objective-C message sends (VKQuad / VKWKTextInfo / VKWKLineInfo / VKImageAnalysis selectors), so |
| // matching the selectors is sufficient; no inheritance from the real (opaque) VK types is required. |
| |
| @objc(TestVKQuad) |
| private final class TestVKQuad: NSObject { |
| @objc |
| let topLeft: CGPoint |
| @objc |
| let topRight: CGPoint |
| @objc |
| let bottomLeft: CGPoint |
| @objc |
| let bottomRight: CGPoint |
| |
| init(_ quad: ImageAnalysisResult.Quad) { |
| self.topLeft = quad.topLeft |
| self.topRight = quad.topRight |
| self.bottomLeft = quad.bottomLeft |
| self.bottomRight = quad.bottomRight |
| } |
| } |
| |
| @objc(TestVKWKTextInfo) |
| private class TestVKWKTextInfo: NSObject { |
| @objc |
| let string: String |
| @objc |
| let quad: TestVKQuad |
| |
| convenience init(_ textInfo: ImageAnalysisResult.Line.TextInfo) { |
| self.init(string: textInfo.text, quad: .init(textInfo.quad)) |
| } |
| |
| init(string: String, quad: TestVKQuad) { |
| self.string = string |
| self.quad = quad |
| } |
| } |
| |
| @objc(TestVKWKLineInfo) |
| private final class TestVKWKLineInfo: TestVKWKTextInfo { |
| @objc |
| let children: [TestVKWKTextInfo] |
| |
| init(_ lineInfo: ImageAnalysisResult.Line) { |
| self.children = lineInfo.children.map(TestVKWKTextInfo.init) |
| super.init(string: lineInfo.text, quad: .init(lineInfo.quad)) |
| } |
| } |
| |
| @objc(TestVKImageAnalysis) |
| private final class TestVKImageAnalysis: NSObject { |
| @objc |
| let allLines: [TestVKWKLineInfo] |
| |
| init(_ analysis: ImageAnalysisResult) { |
| self.allLines = analysis.lines.map(TestVKWKLineInfo.init) |
| super.init() |
| } |
| |
| init(lines: [TestVKWKLineInfo]) { |
| allLines = lines |
| } |
| |
| @objc(hasResultsForAnalysisTypes:) |
| func hasResults(forAnalysisTypes types: UInt) -> Bool { |
| !allLines.isEmpty |
| } |
| } |
| |
| #endif // ENABLE_SWIFTUI && ENABLE_CXX_INTEROP && ENABLE_IMAGE_ANALYSIS |