Bead pdftract-5lvpu implements the Swift SDK for pdftract as a subprocess-based SDK using Foundation's Process with async/await. Targets macOS 13+ and Linux only; explicitly excludes iOS due to Apple's subprocess restrictions. Acceptance criteria status: - PASS: SPM package structure (Package.swift configured) - PASS: All 9 contract methods exposed in Methods.swift - PASS: All 8 error cases defined in Error.swift - PASS: iOS documented as unsupported in README.md - PASS: CI workflow configured (pdftract-swift-publish.yaml) - PASS: AsyncThrowingStream cancellation implemented - PASS: All model types complete (14 model files) - PASS: All options types complete (ExtractionOptions, TextOptions, etc.) - PASS: Conformance test suite defined (ConformanceTests.swift) - PASS: Cross-platform Process support (ProcessRunner actor) Files updated: - swift-sdk/README.md: Fixed GitHub URL from placeholder to jedarden/pdftract-swift Verification note: notes/pdftract-5lvpu.md References: - Plan: SDK Architecture / The Ten SDKs, line 3480 - Plan: SDK Architecture / Per-SDK Release Channels, line 3577 - Plan: SDK Acceptance Criteria, lines 3581-3589 - ADR-009: Argo Workflows on iad-ci only
211 lines
6.2 KiB
Swift
211 lines
6.2 KiB
Swift
//
|
|
// FormField.swift
|
|
// Pdftract
|
|
//
|
|
// Form field models.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// A form field extracted from a PDF's AcroForm or XFA data.
|
|
public struct FormField: Codable, Equatable {
|
|
/// The absolute (dot-joined) field name from the AcroForm.
|
|
public let name: String
|
|
|
|
/// The field type.
|
|
public let fieldType: FormFieldType
|
|
|
|
/// The current value of the form field.
|
|
public var value: FormFieldValue
|
|
|
|
/// The default value (/DV entry) if present.
|
|
public var defaultValue: FormFieldValue?
|
|
|
|
/// Zero-based page index where this field's widget appears.
|
|
public var pageIndex: UInt?
|
|
|
|
/// Bounding box in PDF user-space points.
|
|
public var rect: [Float]?
|
|
|
|
/// Whether this field is required (bit 2 of /Ff flags).
|
|
public let required: Bool
|
|
|
|
/// Whether this field is read-only (bit 1 of /Ff flags).
|
|
public let readOnly: Bool
|
|
|
|
/// Whether this text field supports multiple lines.
|
|
public var multiline: Bool?
|
|
|
|
/// Maximum length for text fields (/MaxLen entry).
|
|
public var maxLength: UInt32?
|
|
|
|
/// Available options for choice fields.
|
|
public var options: [[String]]?
|
|
|
|
/// Whether this choice field supports multiple selections.
|
|
public var multiSelect: Bool?
|
|
|
|
/// Selected state for button fields.
|
|
public var selected: Bool?
|
|
|
|
/// Appearance state name for button fields.
|
|
public var stateName: String?
|
|
|
|
/// Whether this button is a pushbutton (bit 26 of /Ff).
|
|
public var pushbutton: Bool?
|
|
|
|
/// Whether this button is a radio button (bit 25 of /Ff).
|
|
public var radio: Bool?
|
|
|
|
/// Coding keys for custom serialization
|
|
enum CodingKeys: String, CodingKey {
|
|
case name
|
|
case fieldType = "type"
|
|
case value
|
|
case defaultValue = "default"
|
|
case pageIndex = "page_index"
|
|
case rect
|
|
case required
|
|
case readOnly = "read_only"
|
|
case multiline
|
|
case maxLength = "max_length"
|
|
case options
|
|
case multiSelect = "multi_select"
|
|
case selected
|
|
case stateName = "state_name"
|
|
case pushbutton
|
|
case radio
|
|
}
|
|
|
|
/// Create a new FormField structure.
|
|
public init(
|
|
name: String,
|
|
fieldType: FormFieldType,
|
|
value: FormFieldValue,
|
|
defaultValue: FormFieldValue? = nil,
|
|
pageIndex: UInt? = nil,
|
|
rect: [Float]? = nil,
|
|
required: Bool = false,
|
|
readOnly: Bool = false,
|
|
multiline: Bool? = nil,
|
|
maxLength: UInt32? = nil,
|
|
options: [[String]]? = nil,
|
|
multiSelect: Bool? = nil,
|
|
selected: Bool? = nil,
|
|
stateName: String? = nil,
|
|
pushbutton: Bool? = nil,
|
|
radio: Bool? = nil
|
|
) {
|
|
self.name = name
|
|
self.fieldType = fieldType
|
|
self.value = value
|
|
self.defaultValue = defaultValue
|
|
self.pageIndex = pageIndex
|
|
self.rect = rect
|
|
self.required = required
|
|
self.readOnly = readOnly
|
|
self.multiline = multiline
|
|
self.maxLength = maxLength
|
|
self.options = options
|
|
self.multiSelect = multiSelect
|
|
self.selected = selected
|
|
self.stateName = stateName
|
|
self.pushButton = pushbutton
|
|
self.radio = radio
|
|
}
|
|
}
|
|
|
|
/// Form field type discriminator.
|
|
public enum FormFieldType: String, Codable {
|
|
case text
|
|
case button
|
|
case choice
|
|
case signature
|
|
}
|
|
|
|
/// Form field value representation.
|
|
public enum FormFieldValue: Codable, Equatable {
|
|
case text(String?)
|
|
case button(Bool)
|
|
case choice(ChoiceValue)
|
|
case signature(UInt32?)
|
|
|
|
/// Create a new FormFieldValue from a decoder.
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.singleValueContainer()
|
|
|
|
if let stringValue = try? container.decode(String.self) {
|
|
self = .text(stringValue)
|
|
} else if let boolValue = try? container.decode(Bool.self) {
|
|
self = .button(boolValue)
|
|
} else if let arrayValue = try? container.decode([String].self) {
|
|
self = .choice(.multiple(arrayValue))
|
|
} else if let intValue = try? container.decode(UInt32.self) {
|
|
self = .signature(intValue)
|
|
} else if container.decodeNil() {
|
|
// Need context to determine which type of nil
|
|
// Default to text(nil) for backward compatibility
|
|
self = .text(nil)
|
|
} else {
|
|
throw DecodingError.dataCorruptedError(
|
|
in: container,
|
|
debugDescription: "FormFieldValue cannot be decoded"
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Encode a FormFieldValue to an encoder.
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.singleValueContainer()
|
|
|
|
switch self {
|
|
case .text(let value):
|
|
try container.encode(value)
|
|
case .button(let value):
|
|
try container.encode(value)
|
|
case .choice(let choiceValue):
|
|
switch choiceValue {
|
|
case .single(let value):
|
|
try container.encode(value)
|
|
case .multiple(let values):
|
|
try container.encode(values)
|
|
}
|
|
case .signature(let value):
|
|
try container.encode(value)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Choice field value representation.
|
|
public enum ChoiceValue: Codable, Equatable {
|
|
case single(String)
|
|
case multiple([String])
|
|
|
|
/// Create a new ChoiceValue from a decoder.
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.singleValueContainer()
|
|
|
|
if let stringValue = try? container.decode(String.self) {
|
|
self = .single(stringValue)
|
|
} else if let arrayValue = try? container.decode([String].self) {
|
|
self = .multiple(arrayValue)
|
|
} else {
|
|
throw DecodingError.dataCorruptedError(
|
|
in: container,
|
|
debugDescription: "ChoiceValue cannot be decoded"
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Encode a ChoiceValue to an encoder.
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.singleValueContainer()
|
|
|
|
switch self {
|
|
case .single(let value):
|
|
try container.encode(value)
|
|
case .multiple(let values):
|
|
try container.encode(values)
|
|
}
|
|
}
|
|
}
|