Files
suixinkan_uikit/suixinkan/Features/TravelAlbum/OTG/PTP/PTPUtilities.swift

308 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// PTPUtilities.swift
// otg_swift
//
// Created by hanqiu on 2026/7/3.
//
import Foundation
@preconcurrency import ImageCaptureCore
/// PTP Container ISO 15740
enum PTPContainerType: UInt16 {
///
case command = 0x0001
/// out/in Data
case data = 0x0002
///
case response = 0x0003
/// Sony SDIE
case event = 0x0004
}
/// PTP
struct PTPContainer {
let length: UInt32
let type: UInt16
let code: UInt16
let transactionID: UInt32
let params: [UInt32]
}
/// PTP Container
enum PTPParseError: Error {
/// 12 Container
case tooShort
/// length buffer
case invalidLength
}
/// PTP ISO 15740
enum PTPStandardCommand {
/// GetDeviceInfo (0x1001) PTP 线 Probe
static let getDeviceInfo: UInt16 = 0x1001
/// GetObjectInfo (0x1008)Probe
static let getObjectInfo: UInt16 = 0x1008
/// GetObject (0x1009)Step 6
static let getObject: UInt16 = 0x1009
/// ObjectAdded (0x4002)
static let objectAdded: UInt16 = 0x4002
/// RequestObjectTransfer (0x4009)
static let requestObjectTransfer: UInt16 = 0x4009
/// Response OK (0x2001)
static let responseOK: UInt16 = 0x2001
}
/// PTP ImageCaptureCore
enum PTPHelper {
// MARK: - Build
/// PTP Command Containerlength = 12 + 4×
static func buildCommand(
code: UInt16,
transactionID: UInt32,
parameters: [UInt32] = []
) -> Data {
let length = UInt32(12 + parameters.count * 4)
var data = Data()
appendUInt32LE(length, to: &data)
appendUInt16LE(PTPContainerType.command.rawValue, to: &data)
appendUInt16LE(code, to: &data)
appendUInt32LE(transactionID, to: &data)
for parameter in parameters {
appendUInt32LE(parameter, to: &data)
}
return data
}
// MARK: - Parse
/// PTP Container 4
static func parseContainer(_ data: Data) throws -> PTPContainer {
guard data.count >= 12 else { throw PTPParseError.tooShort }
let length = readUInt32LE(data, offset: 0)
guard length >= 12, data.count >= Int(length) else {
throw PTPParseError.invalidLength
}
let type = readUInt16LE(data, offset: 4)
let code = readUInt16LE(data, offset: 6)
let transactionID = readUInt32LE(data, offset: 8)
var params: [UInt32] = []
var offset = 12
while offset + 4 <= Int(length) {
params.append(readUInt32LE(data, offset: offset))
offset += 4
}
return PTPContainer(
length: length,
type: type,
code: code,
transactionID: transactionID,
params: params
)
}
/// PTP Event Container Sony 0xC203
static func parseEventCode(from eventData: Data) -> UInt16? {
try? parseContainer(eventData).code
}
static func isResponseOK(_ response: Data) -> Bool {
guard !response.isEmpty else { return true }
guard let parsed = try? parseContainer(response) else { return false }
return parsed.code == PTPStandardCommand.responseOK
}
static func hexString(_ data: Data) -> String {
data.map { String(format: "%02X", $0) }.joined(separator: " ")
}
/// +
static func objectSignature(filename: String, size: UInt32) -> String {
"\(filename)_\(size)"
}
/// ObjectInfo offset 8 CompressedSizeUInt32 LE
static func parseObjectCompressedSize(_ data: Data) -> UInt32? {
guard data.count >= 12 else { return nil }
return readUInt32LE(data, offset: 8)
}
/// ObjectInfo PTP UTF-16LE
static func parseObjectInfoFilename(_ data: Data) -> String? {
guard data.count >= 53 else { return nil }
if let filename = parsePTPString(data, offset: 52)?.string, !filename.isEmpty {
return sanitizeFilename(filename)
}
var offset = 52
var codeUnits: [UInt16] = []
while offset + 1 < data.count {
let unit = readUInt16LE(data, offset: offset)
if unit == 0 { break }
codeUnits.append(unit)
offset += 2
}
guard !codeUnits.isEmpty else { return nil }
let raw = String(utf16CodeUnits: codeUnits, count: codeUnits.count)
return sanitizeFilename(raw)
}
/// PTP 1 + UTF-16LE NUL
static func parsePTPString(_ data: Data, offset start: Int) -> (string: String, nextOffset: Int)? {
guard start < data.count else { return nil }
let charCount = Int(data[start])
var offset = start + 1
guard charCount > 0, offset + charCount * 2 <= data.count else { return nil }
var codeUnits: [UInt16] = []
codeUnits.reserveCapacity(charCount)
for index in 0..<charCount {
let unit = readUInt16LE(data, offset: offset + index * 2)
// ZV-E10 charCount NUL
if unit != 0 {
codeUnits.append(unit)
}
}
offset += charCount * 2
guard !codeUnits.isEmpty else { return nil }
let string = String(utf16CodeUnits: codeUnits, count: codeUnits.count)
return (string, offset)
}
/// PTP
static func sanitizeFilename(_ name: String) -> String {
let withoutNulls = name.replacingOccurrences(of: "\0", with: "")
let trimmed = withoutNulls.trimmingCharacters(in: .whitespacesAndNewlines.union(.controlCharacters))
let invalid = CharacterSet(charactersIn: "/\\:?%*|\"<>")
let cleaned = trimmed.components(separatedBy: invalid).joined(separator: "_")
return cleaned.isEmpty ? "photo.jpg" : cleaned
}
/// inData JPEG/PNG
static func isLikelyImageData(_ data: Data) -> Bool {
guard data.count >= 3 else { return false }
if data[0] == 0xFF, data[1] == 0xD8, data[2] == 0xFF { return true }
if data.count >= 4, data[0] == 0x89, data[1] == 0x50, data[2] == 0x4E, data[3] == 0x47 { return true }
return data.count > 1024
}
// MARK: - Send
struct CommandResult {
let success: Bool
let response: Data
let inData: Data
}
/// `requestSendPTPCommand` PTP `transactionID`
/// 线ImageCaptureCore completion resume continuation
@MainActor
static func sendCommand(
on camera: ICCameraDevice,
code: UInt16,
transactionID: inout UInt32,
parameters: [UInt32] = [],
outData: Data? = nil,
label: String
) async -> CommandResult {
let currentID = transactionID
transactionID += 1
let commandData = buildCommand(
code: code,
transactionID: currentID,
parameters: parameters
)
OTGLog.info(.ptp, "PTP command sending: \(label), bytes=\(commandData.count)")
return await withCheckedContinuation { continuation in
let finish: (CommandResult) -> Void = { result in
continuation.resume(returning: result)
}
let sendPTP = {
camera.requestSendPTPCommand(
commandData,
outData: outData,
completion: { inData, response, error in
OTGLog.info(
.ptp,
"PTP callback received: \(label), inData=\(inData.count), response=\(response.count)"
)
if let error {
OTGLog.error(.ptp, "PTP \(label) failed: \(error.localizedDescription)")
finish(CommandResult(success: false, response: response, inData: inData))
return
}
var ok = true
if !response.isEmpty {
if let parsed = try? parseContainer(response) {
// 0x2001 ResponseOK
if parsed.code != PTPStandardCommand.responseOK {
ok = false
OTGLog.warning(
.ptp,
"PTP \(label) response code=0x\(String(format: "%04X", parsed.code))"
)
}
} else {
ok = false
OTGLog.warning(.ptp, "PTP \(label) response parse failed")
}
}
if ok {
OTGLog.info(.ptp, "PTP \(label) OK, inData=\(inData.count) bytes")
}
finish(CommandResult(success: ok, response: response, inData: inData))
}
)
}
// ImageCaptureCore PTP API 线 MainActor
sendPTP()
}
}
// MARK: - Little Endian
static func appendUInt16LE(_ value: UInt16, to data: inout Data) {
var littleEndian = value.littleEndian
withUnsafeBytes(of: &littleEndian) { raw in
data.append(contentsOf: raw)
}
}
static func appendUInt32LE(_ value: UInt32, to data: inout Data) {
var littleEndian = value.littleEndian
withUnsafeBytes(of: &littleEndian) { raw in
data.append(contentsOf: raw)
}
}
static func readUInt16LE(_ data: Data, offset: Int) -> UInt16 {
UInt16(data[offset])
| (UInt16(data[offset + 1]) << 8)
}
static func readUInt32LE(_ data: Data, offset: Int) -> UInt32 {
UInt32(data[offset])
| (UInt32(data[offset + 1]) << 8)
| (UInt32(data[offset + 2]) << 16)
| (UInt32(data[offset + 3]) << 24)
}
}