205 lines
8.3 KiB
Swift
205 lines
8.3 KiB
Swift
import Foundation
|
|
import PDFKit
|
|
import UIKit
|
|
|
|
enum PDFError: Error {
|
|
case fileCorrupted
|
|
case fileNotPDF
|
|
case fileTooLarge(size: Int64, limit: Int64)
|
|
case memoryError
|
|
case incompatibleFormat
|
|
case unknownError
|
|
case internalError(_ error: Error)
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .fileCorrupted:
|
|
return "The PDF file appears to be corrupted or damaged."
|
|
case .fileNotPDF:
|
|
return "The file is not a valid PDF document."
|
|
case .fileTooLarge(let size, let limit):
|
|
let sizeInMB = Double(size) / (1024 * 1024)
|
|
let limitInMB = Double(limit) / (1024 * 1024)
|
|
return "The PDF file is too large to process: \(String(format: "%.1f", sizeInMB)) MB. Maximum supported size is \(String(format: "%.0f", limitInMB)) MB."
|
|
case .memoryError:
|
|
return "Not enough memory to load the PDF file."
|
|
case .incompatibleFormat:
|
|
return "This PDF uses unsupported features or an incompatible format."
|
|
case .unknownError:
|
|
return "An unknown error occurred while opening the PDF file."
|
|
case .internalError(let error):
|
|
return "An internal error occurred while opening the PDF file.\n\(error.localizedDescription)"
|
|
}
|
|
}
|
|
}
|
|
|
|
class PDFDiagnostics {
|
|
// Default values that will be overridden by device detection
|
|
private static var _maxPDFSize: Int64 = 200 * 1024 * 1024 // 200 MB default
|
|
private static var hasInitializedDeviceLimit = false
|
|
|
|
/// Maximum allowed PDF file size in bytes, dynamically adjusted for the device
|
|
static var maxPDFSize: Int64 {
|
|
get {
|
|
// Initialize device-specific limit on first access
|
|
if !hasInitializedDeviceLimit {
|
|
_maxPDFSize = determineOptimalPDFSizeLimit()
|
|
hasInitializedDeviceLimit = true
|
|
}
|
|
return _maxPDFSize
|
|
}
|
|
set { _maxPDFSize = newValue }
|
|
}
|
|
|
|
/// Sets the maximum allowed PDF size in megabytes (overrides automatic detection)
|
|
static func setMaxPDFSizeInMB(_ megabytes: Double) {
|
|
_maxPDFSize = Int64(megabytes * 1024 * 1024)
|
|
hasInitializedDeviceLimit = true // Prevent auto-detection
|
|
}
|
|
|
|
/// Determines the optimal PDF size limit based on device capabilities
|
|
private static func determineOptimalPDFSizeLimit() -> Int64 {
|
|
// Get device memory information
|
|
let physicalMemory = ProcessInfo.processInfo.physicalMemory
|
|
let memoryInGB = Double(physicalMemory) / (1024 * 1024 * 1024)
|
|
|
|
// Base limit primarily on available memory
|
|
let baseLimit: Int64
|
|
if memoryInGB >= 6 {
|
|
// High-memory devices: 6GB+ RAM
|
|
baseLimit = 500 * 1024 * 1024 // 500MB
|
|
} else if memoryInGB >= 4 {
|
|
// Mid-range devices: 4-6GB RAM
|
|
baseLimit = 300 * 1024 * 1024 // 300MB
|
|
} else if memoryInGB >= 2 {
|
|
// Low-memory devices: 2-4GB RAM
|
|
baseLimit = 150 * 1024 * 1024 // 150MB
|
|
} else {
|
|
// Very limited devices: <2GB RAM
|
|
baseLimit = 75 * 1024 * 1024 // 75MB
|
|
}
|
|
|
|
// Apply device type adjustment
|
|
let deviceTypeAdjustedLimit: Int64
|
|
if UIDevice.current.userInterfaceIdiom == .pad {
|
|
// iPads generally have better thermal management and can handle larger files
|
|
deviceTypeAdjustedLimit = Int64(Double(baseLimit) * 1.3)
|
|
} else {
|
|
// iPhones and other devices use the base limit
|
|
deviceTypeAdjustedLimit = baseLimit
|
|
}
|
|
|
|
// Adjust based on current available memory at runtime
|
|
return adjustLimitBasedOnAvailableMemory(deviceTypeAdjustedLimit)
|
|
}
|
|
|
|
/// Further adjusts the limit based on currently available memory
|
|
private static func adjustLimitBasedOnAvailableMemory(_ baseLimit: Int64) -> Int64 {
|
|
// Try to get free memory
|
|
if let freeMemory = getAvailableMemory() {
|
|
let freeMemoryInMB = Double(freeMemory) / (1024 * 1024)
|
|
|
|
// If very low on memory, reduce limits to avoid crashes
|
|
if freeMemoryInMB < 500 {
|
|
return min(baseLimit, 100 * 1024 * 1024) // 100MB max when low on memory
|
|
}
|
|
|
|
// If plenty of free memory, we can be more generous
|
|
if freeMemoryInMB > 2000 { // 2GB+ free
|
|
return Int64(Double(baseLimit) * 1.2) // 20% boost
|
|
}
|
|
}
|
|
|
|
// Check if running in simulator (for development)
|
|
#if targetEnvironment(simulator)
|
|
return 600 * 1024 * 1024 // 600MB for simulator
|
|
#else
|
|
return baseLimit
|
|
#endif
|
|
}
|
|
|
|
/// Attempts to get the amount of free memory available on the device
|
|
private static func getAvailableMemory() -> UInt64? {
|
|
var pageSize: vm_size_t = 0
|
|
var info = mach_task_basic_info()
|
|
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size / MemoryLayout<natural_t>.size)
|
|
|
|
let kerr = withUnsafeMutablePointer(to: &info) {
|
|
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
|
|
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
|
|
}
|
|
}
|
|
|
|
if kerr == KERN_SUCCESS {
|
|
host_page_size(mach_host_self(), &pageSize)
|
|
let totalMemory = ProcessInfo.processInfo.physicalMemory
|
|
let usedMemory = UInt64(info.resident_size)
|
|
return totalMemory - usedMemory
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/// Diagnoses why a PDF document failed to load
|
|
static func diagnoseLoadFailure(at url: URL) -> PDFError {
|
|
// If we have a valid PDF header but PDFDocument failed to load,
|
|
// try to determine if it's corrupted or has format issues
|
|
do {
|
|
// Only read a small portion of the file to check structure
|
|
let data = try Data(contentsOf: url, options: .alwaysMapped)
|
|
|
|
// Try with CGPDFDocument for lower-level diagnosis
|
|
if let provider = CGDataProvider(data: data as CFData),
|
|
let _ = CGPDFDocument(provider) {
|
|
|
|
// If CGPDFDocument loads but PDFKit's PDFDocument doesn't,
|
|
// it might be using unsupported features
|
|
return .incompatibleFormat
|
|
} else {
|
|
// Neither loads - likely corrupted
|
|
return .fileCorrupted
|
|
}
|
|
} catch let dataError as NSError {
|
|
// Check for memory-related errors by examining the error description
|
|
// since the specific error code varies between iOS versions
|
|
let errorDesc = dataError.localizedDescription.lowercased()
|
|
|
|
if dataError.domain == NSCocoaErrorDomain &&
|
|
(errorDesc.contains("memory") ||
|
|
errorDesc.contains("ram") ||
|
|
errorDesc.contains("resource") ||
|
|
errorDesc.contains("allocation")) {
|
|
return .memoryError
|
|
}
|
|
return .fileCorrupted
|
|
}
|
|
}
|
|
|
|
/// Diagnoses if a PDF document will fail to load
|
|
static func diagnosePotentialLoadFailure(at url: URL) -> PDFError? {
|
|
do {
|
|
// Check file size first to avoid memory issues with large files
|
|
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
|
|
if let fileSize = attributes[.size] as? Int64, fileSize > maxPDFSize {
|
|
return .fileTooLarge(size: fileSize, limit: maxPDFSize)
|
|
}
|
|
|
|
// Check if it's a valid PDF by examining the header
|
|
let fileHandle = try FileHandle(forReadingFrom: url)
|
|
defer { fileHandle.closeFile() }
|
|
|
|
// Read the first chunk to check PDF signature
|
|
let headerData = fileHandle.readData(ofLength: min(1024, Int(attributes[.size] as? Int64 ?? 0)))
|
|
if let headerString = String(data: headerData, encoding: .ascii) {
|
|
if !headerString.hasPrefix("%PDF-") {
|
|
return .fileNotPDF
|
|
}
|
|
}
|
|
} catch {
|
|
print("Error diagnosing PDF: \(error.localizedDescription)")
|
|
return .internalError(error)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|