blob: 0e4d52198a2267378adf00dca9bcbc8da27a7082 [file]
// 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