blob: 62c8393668c17de8efd5e74b051f87a9a50516bf [file] [log] [blame] [edit]
// Copyright (C) 2025 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 HAVE_PDFKIT
import Foundation
import WebKit
import PDFKit
import CoreGraphics
#if canImport(UIKit)
import UIKit
typealias CocoaColor = UIColor
#else
import AppKit
typealias CocoaColor = NSColor
#endif
@_objcImplementation extension TestPDFAnnotation {
@nonobjc private var annotation: PDFAnnotation
var isLink: Bool {
let link = annotation.type == "Link"
assert(!link || annotation.action is PDFActionURL)
return link
}
var bounds: CGRect {
annotation.bounds
}
var linkURL: URL? {
guard isLink else {
return nil
}
return (annotation.action as? PDFActionURL)?.url
}
@objc(initWithPDFAnnotation:)
init(pdfAnnotation: PDFAnnotation) {
self.annotation = pdfAnnotation
}
}
@_objcImplementation extension TestPDFPage {
@nonobjc private var page: PDFPage
lazy var text: String = {
page.string?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
}()
var bounds: CGRect {
page.bounds(for: .mediaBox)
}
var characterCount: Int {
text.count
}
lazy var annotations: [TestPDFAnnotation] = { @MainActor in
page.annotations.map(TestPDFAnnotation.init(pdfAnnotation:))
}()
@objc(characterIndexAtPoint:)
func characterIndex(at point: CGPoint) -> Int {
page.characterIndex(at: point)
}
@objc(colorAtPoint:)
func color(at point: CGPoint) -> CocoaColor {
let boundsRect = bounds
let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)
#if swift(>=6.2)
// FIXME: document safety invariants here.
#if HAVE_CGCONTEXT_INIT_WITH_BITMAP_INFO_AND_NULLABLE_COLORSPACE
let context = unsafe CGContext(
data: nil,
width: Int(boundsRect.size.width),
height: Int(boundsRect.size.height),
bitsPerComponent: 8,
bytesPerRow: 0,
space: colorSpace,
bitmapInfo: CGBitmapInfo(alpha: .premultipliedLast, byteOrder: .order32Big)
)
#else
let context = unsafe CGContext(
data: nil,
width: Int(boundsRect.size.width),
height: Int(boundsRect.size.height),
bitsPerComponent: 8,
bytesPerRow: 0,
space: colorSpace!,
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).union(.byteOrder32Big).rawValue
)
#endif
#else
#if HAVE_CGCONTEXT_INIT_WITH_BITMAP_INFO_AND_NULLABLE_COLORSPACE
let context = CGContext(
data: nil,
width: Int(boundsRect.size.width),
height: Int(boundsRect.size.height),
bitsPerComponent: 8,
bytesPerRow: 0,
space: colorSpace,
bitmapInfo: CGBitmapInfo(alpha: .premultipliedLast, byteOrder: .order32Big)
)
#else
let context = CGContext(
data: nil,
width: Int(boundsRect.size.width),
height: Int(boundsRect.size.height),
bitsPerComponent: 8,
bytesPerRow: 0,
space: colorSpace!,
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).union(.byteOrder32Big).rawValue
)
#endif
#endif
guard let context, let cgPage = page.pageRef else {
fatalError()
}
CGContextDrawPDFPageWithAnnotations(context, cgPage, nil)
let x = Int(point.x)
let y = Int(point.y)
let index = (y * x * 4) + (x * 4)
#if swift(>=6.2)
// FIXME: document safety invariants here. Is CGContext always guaranteed to
// contain a context.data of the right size at this point?
let pixels = unsafe UnsafeMutableRawBufferPointer(start: context.data, count: context.width * context.height * 4)
let r = unsafe pixels[index]
let g = unsafe pixels[index + 1]
let b = unsafe pixels[index + 2]
let a = unsafe pixels[index + 3]
#else
let pixels = UnsafeMutableRawBufferPointer(start: context.data, count: context.width * context.height * 4)
let r = pixels[index]
let g = pixels[index + 1]
let b = pixels[index + 2]
let a = pixels[index + 3]
#endif
if a == 0 {
return .clear
}
let red = CGFloat(r) * 255 / CGFloat(a) / 255.0
let green = CGFloat(g) * 255 / CGFloat(a) / 255.0
let blue = CGFloat(b) * 255 / CGFloat(a) / 255.0
let alpha = CGFloat(a) / 255.0
return CocoaColor(red: red, green: green, blue: blue, alpha: alpha)
}
@objc(rectForCharacterAtIndex:)
func rectForCharacter(at index: Int) -> CGRect {
page.characterBounds(at: index)
}
@objc(initWithPDFPage:)
init(pdfPage: PDFPage) {
self.page = pdfPage
}
}
@_objcImplementation extension TestPDFDocument {
@nonobjc private let document: PDFDocument
@nonobjc private var pages: [TestPDFPage?]
var pageCount: Int {
document.pageCount
}
@objc(initFromData:)
init(from data: Data) {
let document = PDFDocument(data: data)!
self.document = document
self.pages = .init(repeating: nil, count: document.pageCount)
}
@objc(pageAtIndex:)
func page(at index: Int) -> TestPDFPage? {
precondition((0..<pageCount).contains(index))
if pages[index] == nil {
guard let page = document.page(at: index) else {
return nil
}
pages[index] = TestPDFPage(pdfPage: page)
}
return pages[index]
}
}
#endif // HAVE_PDFKIT