From a12541335108eb58aeadda80e150f01a1e0ca89d Mon Sep 17 00:00:00 2001 From: Vigneswaran Rajkumar <118706051+IAmVigneswaran@users.noreply.github.com> Date: Sun, 19 Jul 2026 15:03:28 +0800 Subject: [PATCH] Add detached FCPXML Authoring and strengthen Projection for 3.2.0 Introduce a value-graph Authoring API with omit-on-write, share VersionFeatureGate with version conversion, fill Corners/Panner and MatchProperty gaps, and document layer coverage in Coverage.md. --- .cursorrules | 18 +- AGENT.md | 24 +- ARCHITECTURE.md | 81 +- CHANGELOG.md | 22 +- Documentation/Coverage.md | 556 ++++++++++++++ Documentation/Manual.md | 32 +- Documentation/Manual/00-Index.md | 33 +- Documentation/Manual/01-Overview.md | 3 +- Documentation/Manual/04-Service-Logging.md | 2 +- .../Manual/05-Validation-CutDetection.md | 2 +- .../Manual/06-Version-Conversion-Export.md | 7 +- Documentation/Manual/07-Timeline-Export.md | 7 +- Documentation/Manual/08-Detached-Authoring.md | 155 ++++ ...ulation.md => 09-Timeline-Manipulation.md} | 4 +- ...ne-Metadata.md => 10-Timeline-Metadata.md} | 4 +- ...action-Media.md => 11-Extraction-Media.md} | 10 +- ...rojection.md => 12-Timeline-Projection.md} | 31 +- ...a-Processing.md => 13-Media-Processing.md} | 4 +- ...{13-Typed-Models.md => 14-Typed-Models.md} | 8 +- ...XML-Extensions.md => 15-XML-Extensions.md} | 8 +- ...-Level-Model.md => 16-High-Level-Model.md} | 6 +- ...atform-iOS.md => 17-Cross-Platform-iOS.md} | 8 +- ...rs-Utilities.md => 18-Errors-Utilities.md} | 4 +- Documentation/Manual/{18-CLI.md => 19-CLI.md} | 14 +- .../{19-Reporting.md => 20-Reporting.md} | 16 +- .../Manual/{20-Examples.md => 21-Examples.md} | 37 +- Documentation/README.md | 54 +- GUARDRAILS.md | 35 +- README.md | 26 +- .../FCPXMLAuthoredCompoundClips.swift | 698 ++++++++++++++++++ .../Authoring/FCPXMLAuthoredDocument.swift | 125 ++++ .../Authoring/FCPXMLAuthoredResources.swift | 252 +++++++ .../Authoring/FCPXMLAuthoredSpineItems.swift | 337 +++++++++ .../Authoring/FCPXMLAuthoredStory.swift | 333 +++++++++ .../Authoring/FCPXMLAuthoringContext.swift | 50 ++ .../Authoring/FCPXMLAuthoringElement.swift | 41 + .../Authoring/FCPXMLAuthoringError.swift | 47 ++ .../Authoring/FCPXMLVersionAvailability.swift | 70 ++ .../Classes/FCPXMLVersionFeatureGate.swift | 107 +++ .../FCPXMLVersionConverter.swift | 48 +- .../Adjustments/FCPXMLAdjustmentCorners.swift | 65 ++ .../Adjustments/FCPXMLAdjustmentPanner.swift | 107 +++ .../Model/Clips/FCPXMLClip+Adjustments.swift | 154 ++++ .../Structure/FCPXMLSmartCollection.swift | 19 +- .../FCPXMLSmartCollectionMatchTypes.swift | 22 +- .../Structure/FCPXMLSmartCollectionRule.swift | 6 + .../Retiming/AudioSplitRetiming.swift | 40 +- .../Projection/Retiming/RetimingSegment.swift | 68 ++ .../Projection/TimelineOccupancyIndex.swift | 87 ++- .../TimelineProjectionOptions.swift | 16 + Sources/OpenFCPXMLKitCLI/README.md | 8 +- Tests/ExcelReportTest/Output/README.md | 2 +- Tests/ExcelReportTest/README.md | 4 +- .../FCPXMLAdjustmentTests.swift | 95 +++ .../FCPXMLAuthoringTests.swift | 571 ++++++++++++++ .../FCPXMLProjectionCoverageTests.swift | 112 +++ .../FCPXMLProjectionEdgeCaseCorpusTests.swift | 215 ++++++ .../FCPXMLSmartCollectionTests.swift | 26 + .../FCPXMLTimelineProjectionTests.swift | 36 + .../FCPXMLVersionFeatureGateTests.swift | 57 ++ Tests/README.md | 17 +- Tests/Submitted FCPXML/README.md | 2 +- 62 files changed, 4797 insertions(+), 251 deletions(-) create mode 100644 Documentation/Coverage.md create mode 100644 Documentation/Manual/08-Detached-Authoring.md rename Documentation/Manual/{08-Timeline-Manipulation.md => 09-Timeline-Manipulation.md} (96%) rename Documentation/Manual/{09-Timeline-Metadata.md => 10-Timeline-Metadata.md} (95%) rename Documentation/Manual/{10-Extraction-Media.md => 11-Extraction-Media.md} (90%) rename Documentation/Manual/{11-Timeline-Projection.md => 12-Timeline-Projection.md} (84%) rename Documentation/Manual/{12-Media-Processing.md => 13-Media-Processing.md} (97%) rename Documentation/Manual/{13-Typed-Models.md => 14-Typed-Models.md} (96%) rename Documentation/Manual/{14-XML-Extensions.md => 15-XML-Extensions.md} (92%) rename Documentation/Manual/{15-High-Level-Model.md => 16-High-Level-Model.md} (89%) rename Documentation/Manual/{16-Cross-Platform-iOS.md => 17-Cross-Platform-iOS.md} (94%) rename Documentation/Manual/{17-Errors-Utilities.md => 18-Errors-Utilities.md} (97%) rename Documentation/Manual/{18-CLI.md => 19-CLI.md} (97%) rename Documentation/Manual/{19-Reporting.md => 20-Reporting.md} (98%) rename Documentation/Manual/{20-Examples.md => 21-Examples.md} (89%) create mode 100644 Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredCompoundClips.swift create mode 100644 Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredDocument.swift create mode 100644 Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredResources.swift create mode 100644 Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredSpineItems.swift create mode 100644 Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredStory.swift create mode 100644 Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoringContext.swift create mode 100644 Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoringElement.swift create mode 100644 Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoringError.swift create mode 100644 Sources/OpenFCPXMLKit/Authoring/FCPXMLVersionAvailability.swift create mode 100644 Sources/OpenFCPXMLKit/Classes/FCPXMLVersionFeatureGate.swift create mode 100644 Sources/OpenFCPXMLKit/Model/Adjustments/FCPXMLAdjustmentCorners.swift create mode 100644 Sources/OpenFCPXMLKit/Model/Adjustments/FCPXMLAdjustmentPanner.swift create mode 100644 Tests/OpenFCPXMLKitTests/FCPXMLAuthoringTests.swift create mode 100644 Tests/OpenFCPXMLKitTests/FCPXMLProjectionEdgeCaseCorpusTests.swift create mode 100644 Tests/OpenFCPXMLKitTests/FCPXMLVersionFeatureGateTests.swift diff --git a/.cursorrules b/.cursorrules index 792adf3..278a4bd 100644 --- a/.cursorrules +++ b/.cursorrules @@ -4,7 +4,7 @@ OpenFCPXMLKit is a modern, fully modular Swift 6 framework for Final Cut Pro FCP Keep this file in sync with AGENT.md. Both should describe the same overview, architecture, test structure, and conventions. When you update one, update the other. -**Hard constraints:** [GUARDRAILS.md](GUARDRAILS.md) — must / must-not for layers, naming, FCPXML compatibility, reporting honesty, and fixtures. Prefer GUARDRAILS for “what not to do”; [ARCHITECTURE.md](ARCHITECTURE.md) for structure and diagrams. +**Hard constraints:** [GUARDRAILS.md](GUARDRAILS.md) — must / must-not for layers, naming, FCPXML compatibility, reporting honesty, and fixtures. Prefer GUARDRAILS for “what not to do”; [ARCHITECTURE.md](ARCHITECTURE.md) for structure and diagrams; [Documentation/Coverage.md](Documentation/Coverage.md) for element / layer inventory matrices. **Naming:** Use OpenFCPXMLKit naming exclusively in all code, documentation, comments, and agent files (`ServiceLogger`, `createService()`, `OFKXML*` types). Do not use legacy project names or identifiers from prior forks. Never use the terms "PBF" or "Production's Best Friend" in source code, code comments, symbol names, or CLI/log output; describe the reporting feature neutrally (e.g. "Excel report", "PDF report", "role inventory report", "workbook export"). Those terms may appear only in prose documentation (README, CHANGELOG, Manual, and these agent guides) — never in the codebase itself. @@ -42,7 +42,7 @@ OpenFCPXMLKit targets macOS 26+, iOS 26+, Xcode 26+, and Swift 6.3 with full con **Backward compatibility:** The entire codebase must remain backward compatible with FCPXML 1.5. Optional attributes and elements introduced in later versions (e.g. 1.11, 1.13) must be omitted or ignored when reading/writing or converting to 1.5; mark such features in code comments with the minimum FCPXML version (e.g. `FCPXML 1.13+`). -Current status: **1084** tests listed in `swift test list` (**1078** in `OpenFCPXMLKitTests` + **6** optional `ExcelReportTest`; all Swift Testing `@Test`); FCPXML versions 1.5–1.14 supported (DTDs included, full parsing, typed element-type coverage for all DTD elements via FCPXMLElementType); Final Cut Pro frame rates (23.976, 24, 25, 29.97, 30, 50, 59.94, 60); thread-safe and concurrency-compliant with comprehensive async/await support; no known security vulnerabilities. Version conversion automatically drops elements not in the target version’s DTD (e.g. adjust-colorConform, adjust-stereo-3D); DTD validation runs per version (validateDocumentAgainstDTD, validateDocumentAgainstDeclaredVersion) and after CLI convert. FCPXMLVersion.supportsBundleFormat is true for 1.10+ (.fcpxmld bundle); 1.5–1.9 support only single-file .fcpxml. FCPXML creation: create FCPXML documents from scratch with events, projects, resources, and clips via XMLDocumentManager, XMLDocument initializers, or FCPXMLService. Timeline manipulation: ripple insert (shifts subsequent clips), auto lane assignment, clip queries (by lane, time range, asset ID), lane range computation, secondary storylines. Timeline metadata: markers, chapter markers, keywords, ratings, custom metadata, timestamps (createdAt, modifiedAt). FCPXMLTimecode: custom timecode type (arithmetic, frame alignment, CMTime conversion, FCPXML string parsing). MIME type detection, asset validation, silence detection, asset duration measurement, parallel file I/O, still image asset support. TimelineFormat enhancements: presets (hd720p, dci4K, hd1080i, hd720i), computed properties (aspectRatio, isHD, isUHD, interlaced). Typed adjustment models: Crop, Transform, Blend, Stabilization, Volume, Loudness, NoiseReduction, HumReduction, Equalization, MatchEqualization, Transform360, ColorConform, Stereo3D, VoiceIsolation with full clip integration. Typed effect/filter models: VideoFilter, AudioFilter, VideoFilterMask with FilterParameter support and keyframe animation (auxValue support FCPXML 1.11+). Typed caption/title models: Caption and Title with TextStyle and TextStyleDefinition for full text formatting. SmartCollection models: SmartCollection with match-clip, match-media, match-ratings, match-text, match-usage (1.9+), match-representation (1.10+), match-markers (1.10+), match-analysis-type (1.14). Keyframe animation: KeyframeAnimation, Keyframe with interpolation types, FadeIn/FadeOut with fade types, integrated with FilterParameter. CMTime Codable extension: Direct CMTime encoding/decoding as FCPXML time strings. Collection organization: CollectionFolder and KeywordCollection models for organizing clips and media. Live Drawing (FCPXML 1.11+): LiveDrawing model for live-drawing story elements. HiddenClipMarker (FCPXML 1.13+): HiddenClipMarker model for hidden clip markers. Format/Asset 1.13+: Format heroEye, Asset heroEyeOverride, Asset mediaReps (multiple media-rep). Cross-platform XML abstraction: protocol layer (OFKXMLNode, OFKXMLElement, OFKXMLDocument, OFKXMLFactory); Foundation backend on macOS; AEXML backend on iOS; OFKXMLDefaultFactory() for platform dispatch; FCPXMLStructuralValidator for cross-platform structural validation; FCPXMLDTDValidator platform-conditional (full DTD on macOS, structural fallback on iOS). Comprehensive test coverage: **1078** tests across 60 FCPXML sample files including 360 video, auditions, conform-rate, still images, multicam, secondary storylines, audio keyframes (FCPXMLAudioKeyframeTests: adjust-volume param keyframeAnimation parsing, decibel/time validation, fadeIn/fadeOut integration, secondary storyline detection), keyword collections/folders, Photoshop integration, smart collections, and reporting column layout/exclusion/disabled-clip/workbook/PDF formatting tests. Excel and PDF reporting: multi-sheet `.xlsx` workbooks via `FinalCutPro.FCPXML.buildReport(options:)` (ReportBuilder, ReportOptions presets, ReportExcelExport on XLKit) and optional `.pdf` via `ReportPDFExport` (CoreGraphics; cover page with black “About This PDF Export” + `info.circle`, TOC with accent colour chips + content-tint washes, per-sheet tints, column-width expansion after exclusions, section pagination; same Report configuration as Excel); sheets for Role Inventory (**Selected Roles Inventory** + per-role sheets with expanded column layout and dynamic metadata keys), Markers, Keywords, Titles & Generators, Transitions, Video & Audio Effects, Speed Change Effects, **Summary** (project title in **B1**, narrow Row column A, black role-duration data), and **Media Summary** (Row + red missing-media paths); 1-based **Row** on all tabular Excel/PDF sheets by default (`ensuringRowColumn` / `allowsInjectedRowColumn`); inventory and section-sheet cell formatting, role exclusions, global column exclusion (`ReportColumn` / `excludedColumns`, including `ReportColumn.row`), disabled-clip filtering (`excludeDisabledClips`), project-name / compound-clip-name filtering (`allReportTimelineSources()`; standalone compound-clip exports without ``), `ReportTimecodeFormat` / `--timecode-format`, inventory-first `ReportBuildPhase` progress callbacks; optional `copyrightLabel` / CLI `--label-copyright` (Excel cover **A2**; PDF cover + footer centre). Extraction presets: Captions, Effects, FrameData, Markers, Roles, Titles. Experimental CLI (OpenFCPXMLKit-CLI): single binary with embedded DTDs; --check-version, --convert-version (stripping + DTD validation), --extension-type (fcpxmld | fcpxml; default fcpxmld; 1.5–1.9 always .fcpxml), --validate, --media-copy, --create-project (new empty FCPXML project: --width, --height, --rate, --project-version, output-dir; DTD validation before write; FCP-style output with DOCTYPE, colorSpace, default smart collections), --report (Excel report: role inventory by default; --report-full, per-section flags including --report-markers, --report-keywords, --report-titles-generators, --report-transitions, --report-effects, --report-speed-change-effects, --report-summary, --report-media-summary, --media-resolution, --media-summary-distinguish-proxy, --exclude-role, --exclude-column, --exclude-disabled-clips, --include-markers-outside-clip-boundaries, --protect-sheets, --timecode-format, --report-project, --label-copyright, --create-pdf); --log writes user-visible output for all commands to the log file; see Sources/OpenFCPXMLKitCLI/README.md. +Current status: **1114** tests listed in `swift test list` (**1108** in `OpenFCPXMLKitTests` + **6** optional `ExcelReportTest`; all Swift Testing `@Test`); FCPXML versions 1.5–1.14 supported (DTDs included, full parsing, typed element-type coverage for all DTD elements via FCPXMLElementType); Final Cut Pro frame rates (23.976, 24, 25, 29.97, 30, 50, 59.94, 60); thread-safe and concurrency-compliant with comprehensive async/await support; no known security vulnerabilities. Version conversion automatically drops elements not in the target version’s DTD (e.g. adjust-colorConform, adjust-stereo-3D); DTD validation runs per version (validateDocumentAgainstDTD, validateDocumentAgainstDeclaredVersion) and after CLI convert. FCPXMLVersion.supportsBundleFormat is true for 1.10+ (.fcpxmld bundle); 1.5–1.9 support only single-file .fcpxml. FCPXML creation: create FCPXML documents from scratch with events, projects, resources, and clips via XMLDocumentManager, XMLDocument initializers, or FCPXMLService. Timeline manipulation: ripple insert (shifts subsequent clips), auto lane assignment, clip queries (by lane, time range, asset ID), lane range computation, secondary storylines. Timeline metadata: markers, chapter markers, keywords, ratings, custom metadata, timestamps (createdAt, modifiedAt). FCPXMLTimecode: custom timecode type (arithmetic, frame alignment, CMTime conversion, FCPXML string parsing). MIME type detection, asset validation, silence detection, asset duration measurement, parallel file I/O, still image asset support. TimelineFormat enhancements: presets (hd720p, dci4K, hd1080i, hd720i), computed properties (aspectRatio, isHD, isUHD, interlaced). Typed adjustment models: Crop, Corners, Transform, Blend, Stabilization, Volume, Panner, Loudness, NoiseReduction, HumReduction, Equalization, MatchEqualization, Transform360, ColorConform, Stereo3D, VoiceIsolation with full clip integration. Typed effect/filter models: VideoFilter, AudioFilter, VideoFilterMask with FilterParameter support and keyframe animation (auxValue support FCPXML 1.11+). Typed caption/title models: Caption and Title with TextStyle and TextStyleDefinition for full text formatting. SmartCollection models: SmartCollection with match-clip, match-media, match-ratings, match-text, match-usage (1.9+), match-representation (1.10+), match-markers (1.10+), match-analysis-type (1.14). Keyframe animation: KeyframeAnimation, Keyframe with interpolation types, FadeIn/FadeOut with fade types, integrated with FilterParameter. CMTime Codable extension: Direct CMTime encoding/decoding as FCPXML time strings. Collection organization: CollectionFolder and KeywordCollection models for organizing clips and media. Live Drawing (FCPXML 1.11+): LiveDrawing model for live-drawing story elements. HiddenClipMarker (FCPXML 1.13+): HiddenClipMarker model for hidden clip markers. Format/Asset 1.13+: Format heroEye, Asset heroEyeOverride, Asset mediaReps (multiple media-rep). Cross-platform XML abstraction: protocol layer (OFKXMLNode, OFKXMLElement, OFKXMLDocument, OFKXMLFactory); Foundation backend on macOS; AEXML backend on iOS; OFKXMLDefaultFactory() for platform dispatch; FCPXMLStructuralValidator for cross-platform structural validation; FCPXMLDTDValidator platform-conditional (full DTD on macOS, structural fallback on iOS). Comprehensive test coverage: **1108** tests across 60 FCPXML sample files including 360 video, auditions, conform-rate, still images, multicam, secondary storylines, audio keyframes (FCPXMLAudioKeyframeTests: adjust-volume param keyframeAnimation parsing, decibel/time validation, fadeIn/fadeOut integration, secondary storyline detection), keyword collections/folders, Photoshop integration, smart collections, and reporting column layout/exclusion/disabled-clip/workbook/PDF formatting tests. Excel and PDF reporting: multi-sheet `.xlsx` workbooks via `FinalCutPro.FCPXML.buildReport(options:)` (ReportBuilder, ReportOptions presets, ReportExcelExport on XLKit) and optional `.pdf` via `ReportPDFExport` (CoreGraphics; cover page with black “About This PDF Export” + `info.circle`, TOC with accent colour chips + content-tint washes, per-sheet tints, column-width expansion after exclusions, section pagination; same Report configuration as Excel); sheets for Role Inventory (**Selected Roles Inventory** + per-role sheets with expanded column layout and dynamic metadata keys), Markers, Keywords, Titles & Generators, Transitions, Video & Audio Effects, Speed Change Effects, **Summary** (project title in **B1**, narrow Row column A, black role-duration data), and **Media Summary** (Row + red missing-media paths); 1-based **Row** on all tabular Excel/PDF sheets by default (`ensuringRowColumn` / `allowsInjectedRowColumn`); inventory and section-sheet cell formatting, role exclusions, global column exclusion (`ReportColumn` / `excludedColumns`, including `ReportColumn.row`), disabled-clip filtering (`excludeDisabledClips`), project-name / compound-clip-name filtering (`allReportTimelineSources()`; standalone compound-clip exports without ``), `ReportTimecodeFormat` / `--timecode-format`, inventory-first `ReportBuildPhase` progress callbacks; optional `copyrightLabel` / CLI `--label-copyright` (Excel cover **A2**; PDF cover + footer centre). Extraction presets: Captions, Effects, FrameData, Markers, Roles, Titles. Experimental CLI (OpenFCPXMLKit-CLI): single binary with embedded DTDs; --check-version, --convert-version (stripping + DTD validation), --extension-type (fcpxmld | fcpxml; default fcpxmld; 1.5–1.9 always .fcpxml), --validate, --media-copy, --create-project (new empty FCPXML project: --width, --height, --rate, --project-version, output-dir; DTD validation before write; FCP-style output with DOCTYPE, colorSpace, default smart collections), --report (Excel report: role inventory by default; --report-full, per-section flags including --report-markers, --report-keywords, --report-titles-generators, --report-transitions, --report-effects, --report-speed-change-effects, --report-summary, --report-media-summary, --media-resolution, --media-summary-distinguish-proxy, --exclude-role, --exclude-column, --exclude-disabled-clips, --include-markers-outside-clip-boundaries, --protect-sheets, --timecode-format, --report-project, --label-copyright, --create-pdf); --log writes user-visible output for all commands to the log file; see Sources/OpenFCPXMLKitCLI/README.md. Xcode 26 dynamic linking compatibility: `swift-log` (`Logging`) is an explicit direct dependency in `Package.swift` to satisfy stricter transitive dylib linking rules when building OpenFCPXMLKit as a dynamic framework. @@ -54,7 +54,7 @@ The project was fully rewritten and refactored to achieve: - A protocol-oriented design: parsing, timecode conversion, XML manipulation, error handling, MIME type detection, asset validation, silence detection, asset duration measurement, and parallel file I/O are defined as protocols (e.g. FCPXMLParsing, TimecodeConversion, XMLDocumentOperations, ErrorHandling, MIMETypeDetection, AssetValidation, SilenceDetection, AssetDurationMeasurement, ParallelFileIO) with sync and async/await methods. - A single injection point for extension APIs that cannot take parameters: FCPXMLUtility.defaultForExtensions (concurrency-safe). No hidden concrete types in extensions; for custom services use the modular API with the using: parameter. -- Consistent source layout: Analysis, Classes, Delegates, Errors, Extensions (including +Modular and +Codable), Implementations, Protocols, Services, Utilities, Annotations, Export, Timeline, Timing, Validation, FileIO, Logging, Format, Model (with subfolders), Parsing, Extraction, Projection (TimelineProjector / MulticamProjection / RefClipProjection), Reporting (including Excel/ for XLKit workbook export and PDF/ for CoreGraphics PDF export; `ReportBuilder` resolves via `allReportTimelineSources()`), XML (Protocols: OFKXMLNode, OFKXMLElement, OFKXMLDocument, OFKXMLDTDProtocol, OFKXMLFactory; Foundation/ and AEXML/ backends; OFKXMLDefaultFactory), and FCPXML DTDs. +- Consistent source layout: Analysis, Classes, Delegates, Errors, Extensions (including +Modular and +Codable), Implementations, Protocols, Services, Utilities, Annotations, Export, Timeline, Timing, Validation, FileIO, Logging, Format, Model (with subfolders), Parsing, Extraction, Projection (TimelineProjector / MulticamProjection / RefClipProjection), Authoring (detached `FinalCutPro.FCPXML.Authoring` value graph + version omit-on-write), Reporting (including Excel/ for XLKit workbook export and PDF/ for CoreGraphics PDF export; `ReportBuilder` resolves via `allReportTimelineSources()`), XML (Protocols: OFKXMLNode, OFKXMLElement, OFKXMLDocument, OFKXMLDTDProtocol, OFKXMLFactory; Foundation/ and AEXML/ backends; OFKXMLDefaultFactory), and FCPXML DTDs. - A structured test suite: shared resources, file tests per sample, logic/parsing tests, timeline/export/validation tests, API and edge-case tests, and performance tests, all documented in Tests/README.md. Foundation XML types (XMLDocument, XMLElement), protocol types (OFKXMLDocument, OFKXMLElement), and SwiftTimecode types are not Sendable. The codebase avoids Task-based concurrency for these types but provides async/await APIs that are concurrency-safe for Swift 6. If these dependencies become Sendable in the future, further parallelisation can be introduced. @@ -70,7 +70,7 @@ Foundation XML types (XMLDocument, XMLElement), protocol types (OFKXMLDocument, - Logging: ServiceLogger (ServiceLogLevel: trace, debug, info, notice, warning, error, critical); NoOpServiceLogger, PrintServiceLogger, FileServiceLogger. FCPXMLUtility and FCPXMLService use injected logger; CLI supports --log, --log-level, --quiet. When --log is set, all CLI commands (check-version, convert-version, validate, media-copy, create-project) write their user-visible messages to the log file. Extension types use #if canImport(Logging) as fallback without DI. - FCPXMLVersion (DTD validation, 1.5-1.14) and FinalCutPro.FCPXML.Version (parsing, 1.0-1.14) are bridged via .fcpxmlVersion, .dtdVersion, and init(from:) converters. - Reporting: Excel and PDF report builders in `Reporting/` consume Extraction/Model and **Projection**; extend Model/Parsing → Extraction → **Projection** before adding report-only XML walks. Sheet formatting, column layout (`RoleInventoryColumnLayout`), global column exclusion (`ReportColumnExclusion` with `ensuringRowColumn` / `allowsInjectedRowColumn` — **Row** on all tabular Excel/PDF sheets by default; including format-suffixed headers), disabled-clip filtering (`excludeDisabledClips`), `ReportTimecodeFormat` / format-aware headers, inventory-first `ReportBuildPhase.enabledPhases(for:)` progress, shared row colours (`FCPXMLReportRowColorPolicy` for Excel and PDF), workbook cell colours (`FCPXMLReportWorkbookExporter`, `RoleRowColorContext`), PDF layout (`Reporting/PDF/` via `ReportPDFExport`: cover notes + black header/SF Symbol; TOC accent colour chips + content-tint washes via `FCPXMLReportPDFSheetPlan` colour index; remaining columns expand to fill `contentWidth` via `FCPXMLReportPDFTableLayout` after packing/`excludedColumns`; **Row** on all sheets via `ensuringRowColumn` / `allowsInjectedRowColumn`), and inclusion rules stay in Reporting. Build `Report` once; export to Excel, PDF, or both with the same options. Summary and Media Summary are separate sheets (Excel Summary project title in **B1**; **Row** on all tabular sheets). Optional `copyrightLabel` / `--label-copyright` (Excel cover **A2**; PDF cover + footer centre). Timeline resolution via `allReportTimelineSources()` / `ReportTimelineSource` (projects + standalone compound-clip exports). See ARCHITECTURE.md §2.7. -- Timeline Projection: Mid-layer under `Sources/OpenFCPXMLKit/Projection/` projecting sequences into playable `MediaUsageWindow`s (channels, `LanePath`, `RetimingSegment` from identity or `timeMap` segments including reverse; conform-rate scale via shared table; nested spines / anchored children; J/L cuts; multicam / ref-clip / audition unfold; video/audio leaves with channel filtering). Role Inventory, Markers, Keywords, Titles & Generators, Transitions, Effects, Speed Change, Media Summary, and Summary consume shared `ReportProjectionContext` windows (projected once per timeline); `TimelineOccupancyIndex` supports overlap queries. Excel/PDF stay presentation-thin. Markers/Keywords/Titles/Transitions/Effects are Projection-first with Extraction fallback. See ARCHITECTURE.md §2.7 and Manual 11. +- Timeline Projection: Mid-layer under `Sources/OpenFCPXMLKit/Projection/` projecting sequences into playable `MediaUsageWindow`s (channels, `LanePath`, `RetimingSegment` from identity or `timeMap` segments including reverse; conform-rate scale via shared table; nested spines / anchored children; J/L cuts; multicam / ref-clip / audition unfold; video/audio leaves with channel filtering). Role Inventory, Markers, Keywords, Titles & Generators, Transitions, Effects, Speed Change, Media Summary, and Summary consume shared `ReportProjectionContext` windows (projected once per timeline); `TimelineOccupancyIndex` supports overlap queries. Excel/PDF stay presentation-thin. Markers/Keywords/Titles/Transitions/Effects are Projection-first with Extraction fallback. See ARCHITECTURE.md §2.7 and Manual 13. - Module-scoped errors: FCPXMLError for parsing, FCPXMLLoadError for file I/O (notAFile, readFailed), FCPXMLExportError/FCPXMLBundleExportError for export, FinalCutPro.FCPXML.ParseError (with LocalizedError). Parse failures from all layers surface as FCPXMLError.parsingFailed. FCPXMLElementError uses String element names for Sendable compliance. FCPXMLDocumentError uses camelCase cases (dtdResourceNotFound, dtdResourceUnreadable). - All code is Sendable where appropriate; `@unchecked Sendable` removed from delegates (internal-only, used synchronously). The project builds and tests with Swift 6 strict concurrency (-strict-concurrency=complete). CI runs a job that enforces this. - No known vulnerabilities in dependencies (including SwiftTimecode 3.1.2) as of July 2025. No unsafe pointers, dynamic code execution, or C APIs; concurrency is structured and type-safe. @@ -131,7 +131,7 @@ SwiftTimecode usage: use Timecode(.realTime(seconds: seconds), at: frameRate) in ## File Organisation -Source structure: layout is Analysis (EditPoint, CutDetectionResult), Classes (FinalCutPro, FCPXML core types including `allReportTimelineSources` / `ReportTimelineSource`, FCPXMLElementType, FCPXMLUtility, FCPXMLVersion, FCPXMLRoot, FCPXMLRootVersion), Delegates, Errors (FCPXMLError, FCPXMLParseError, TimelineError), Extensions (including +Modular and +Codable; FCPXML extensions operate on OFKXMLDocument/OFKXMLElement), Implementations (FCPXMLParser, TimecodeConverter, XMLDocumentManager, ErrorHandler, CutDetector, FCPXMLVersionConverter, MediaExtractor, MIMETypeDetector, AssetValidator, SilenceDetector, AssetDurationMeasurer, ParallelFileIOExecutor), Protocols (FCPXMLParsing, TimecodeConversion, XMLDocumentOperations, ErrorHandling, CutDetection, FCPXMLVersionConverting, MediaExtraction, MIMETypeDetection, AssetValidation, SilenceDetection, AssetDurationMeasurement, ParallelFileIO), Services, Utilities (ModularUtilities, FCPXMLTimeUtilities, SequencePlusAnySequence, XMLElementAncestorWalking, XMLElementSequenceAttributes), Annotations (creation-oriented value types; for parsing models see Model/), Export (FCPXMLExporter, FCPXMLBundleExporter, FCPXMLExportAsset), Timeline (Timeline with manipulation methods, TimelineClip with asset validation methods, TimelineFormat with presets and computed properties), Timing (FCPXMLTimecode), Validation (FCPXMLValidator, FCPXMLDTDValidator, FCPXMLStructuralValidator, ValidationResult, ValidationError/Warning), FileIO (FCPXMLFileLoader), Media (MediaReference, MediaExtractionResult, MediaCopyResult), Logging (ServiceLogger, ServiceLogLevel, NoOpServiceLogger, PrintServiceLogger, FileServiceLogger), Format (ColorSpace), Model (element models with subfolders: Adjustments (CropAdjustment, TransformAdjustment, BlendAdjustment, StabilizationAdjustment, VolumeAdjustment, LoudnessAdjustment, NoiseReductionAdjustment, HumReductionAdjustment, EqualizationAdjustment, MatchEqualizationAdjustment, Transform360Adjustment), Animations (KeyframeAnimation, Keyframe, FadeIn, FadeOut, FadeType), Attributes, Clips including Clip+Adjustments, Title+Typed, CommonElements including Text, TextStyle, TextStyleDefinition, ElementTypes, Filters (VideoFilter, AudioFilter, VideoFilterMask, FilterParameter), Occlusion, Protocols, Resources, Roles, Structure including CollectionFolder, KeywordCollection), Parsing (XML parsing extensions), Extraction (extraction logic with Context/, Effects/, and Presets/ subfolders; presets: Captions, Effects, FrameData, Markers, Roles, Titles), Projection (TimelineProjecting, TimelineProjector, …; Retiming/ + Walk/ including MulticamProjection, RefClipProjection, ChannelKindFilter), Reporting (Excel and PDF report export: Report/ReportOptions/ReportBuilder/ReportTimecodeFormat/ReportBuildProgress, ReportMediaResolutionPolicy, Builders/, Sections/, Rows/, Support/ including RoleInventoryColumnLayout, ReportColumnExclusion, ReportFormatting, FCPXMLReportRowColorPolicy; Excel/ for XLKit workbook export (cover **A1** branding / **A2** `copyrightLabel`); PDF/ for CoreGraphics PDF export via ReportPDFExport (cover/footer `copyrightLabel`); consumes Extraction and Projection; owns presentation only — see ARCHITECTURE.md §2.7), XML (Protocols: OFKXMLNode, OFKXMLElement, OFKXMLDocument, OFKXMLDTDProtocol, OFKXMLFactory; Foundation/ and AEXML/ backends; OFKXMLDefaultFactory), FCPXML DTDs. Group related functionality in extensions; keep files focused on single responsibilities; use clear file naming conventions; organise imports logically; maintain the existing directory structure. +Source structure: layout is Analysis (EditPoint, CutDetectionResult), Classes (FinalCutPro, FCPXML core types including `allReportTimelineSources` / `ReportTimelineSource`, FCPXMLElementType, FCPXMLUtility, FCPXMLVersion, FCPXMLVersionFeatureGate, FCPXMLRoot, FCPXMLRootVersion), Authoring (detached `FinalCutPro.FCPXML.Authoring` value graph — Document/Resources/SpineItem including sync/ref/mc-clip/audition/caption; VersionAvailability omit-on-write; do not use in Reporting), Delegates, Errors (FCPXMLError, FCPXMLParseError, TimelineError), Extensions (including +Modular and +Codable; FCPXML extensions operate on OFKXMLDocument/OFKXMLElement), Implementations (FCPXMLParser, TimecodeConverter, XMLDocumentManager, ErrorHandler, CutDetector, FCPXMLVersionConverter, MediaExtractor, MIMETypeDetector, AssetValidator, SilenceDetector, AssetDurationMeasurer, ParallelFileIOExecutor), Protocols (FCPXMLParsing, TimecodeConversion, XMLDocumentOperations, ErrorHandling, CutDetection, FCPXMLVersionConverting, MediaExtraction, MIMETypeDetection, AssetValidation, SilenceDetection, AssetDurationMeasurement, ParallelFileIO), Services, Utilities (ModularUtilities, FCPXMLTimeUtilities, SequencePlusAnySequence, XMLElementAncestorWalking, XMLElementSequenceAttributes), Annotations (creation-oriented value types; for parsing models see Model/), Export (FCPXMLExporter, FCPXMLBundleExporter, FCPXMLExportAsset), Timeline (Timeline with manipulation methods, TimelineClip with asset validation methods, TimelineFormat with presets and computed properties), Timing (FCPXMLTimecode), Validation (FCPXMLValidator, FCPXMLDTDValidator, FCPXMLStructuralValidator, ValidationResult, ValidationError/Warning), FileIO (FCPXMLFileLoader), Media (MediaReference, MediaExtractionResult, MediaCopyResult), Logging (ServiceLogger, ServiceLogLevel, NoOpServiceLogger, PrintServiceLogger, FileServiceLogger), Format (ColorSpace), Model (element models with subfolders: Adjustments (CropAdjustment, CornersAdjustment, TransformAdjustment, BlendAdjustment, StabilizationAdjustment, VolumeAdjustment, PannerAdjustment, LoudnessAdjustment, NoiseReductionAdjustment, HumReductionAdjustment, EqualizationAdjustment, MatchEqualizationAdjustment, Transform360Adjustment), Animations (KeyframeAnimation, Keyframe, FadeIn, FadeOut, FadeType), Attributes, Clips including Clip+Adjustments, Title+Typed, CommonElements including Text, TextStyle, TextStyleDefinition, ElementTypes, Filters (VideoFilter, AudioFilter, VideoFilterMask, FilterParameter), Occlusion, Protocols, Resources, Roles, Structure including CollectionFolder, KeywordCollection), Parsing (XML parsing extensions), Extraction (extraction logic with Context/, Effects/, and Presets/ subfolders; presets: Captions, Effects, FrameData, Markers, Roles, Titles), Projection (TimelineProjecting, TimelineProjector, TimelineProjectionOptions.trackAnalysis, RetimingSegment clipped/composing, TimelineOccupancyIndex; Retiming/ + Walk/ including MulticamProjection, RefClipProjection, ChannelKindFilter), Reporting (Excel and PDF report export: Report/ReportOptions/ReportBuilder/ReportTimecodeFormat/ReportBuildProgress, ReportMediaResolutionPolicy, Builders/, Sections/, Rows/, Support/ including RoleInventoryColumnLayout, ReportColumnExclusion, ReportFormatting, FCPXMLReportRowColorPolicy; Excel/ for XLKit workbook export (cover **A1** branding / **A2** `copyrightLabel`); PDF/ for CoreGraphics PDF export via ReportPDFExport (cover/footer `copyrightLabel`); consumes Extraction and Projection; owns presentation only — see ARCHITECTURE.md §2.7), XML (Protocols: OFKXMLNode, OFKXMLElement, OFKXMLDocument, OFKXMLDTDProtocol, OFKXMLFactory; Foundation/ and AEXML/ backends; OFKXMLDefaultFactory), FCPXML DTDs. Group related functionality in extensions; keep files focused on single responsibilities; use clear file naming conventions; organise imports logically; maintain the existing directory structure. --- @@ -147,9 +147,9 @@ Tests live under Tests/. Full description is in Tests/README.md. Summary: - Tests/FCPXML Samples/FCPXML/: sample .fcpxml files (60 samples). File tests and logic tests load these via `FCPXMLTestSampleLoading` / `FCPXMLTestingSampleSupport`; missing **bundled** samples **fail**; optional fixtures use `Test.cancel`. -- Tests/OpenFCPXMLKitTests/: FCPXMLTestResources.swift (packageRoot, fcpxmlSamplesDirectory, urlForFCPXMLSample, FCPXMLSampleName); FCPXMLTestSampleLoading.swift / FCPXMLTestingSampleSupport.swift (tryLoad* / require*; fcpxmlFrameRateSampleNames, allFCPXMLSampleNames; Test.cancel for optional fixtures); FCPXMLReportingReportFixture.swift and FCPXMLReportingReportTestSupport.swift (optional reporting integration fixtures/assertions). OpenFCPXMLKitTests.swift: main @Suite with dependencies created in init(); MARK comments group tests. FileTests/: one test class per sample or category (e.g. FCPXMLFileTest_24, FCPXMLFileTest_AllSamples, FCPXMLFileTest_FrameRates, FCPXMLFileTest_GeneralDemo, FCPXMLFileTest_HiddenMarkers, FCPXMLFileTest_360Video, FCPXMLFileTest_AuditionSample, FCPXMLFileTest_ImageSample, FCPXMLFileTest_Multicam, FCPXMLFileTest_Photoshop, FCPXMLFileTest_SmartCollection). LogicAndParsing/: FCPXMLRootVersionTests, FCPXMLStructureTests, FCPXMLFormatAssetTests. Every test-case class is FCPXML-prefixed except the module-named umbrella OpenFCPXMLKitTests. FCPXMLSubmittedFCPXMLSmokeTests: optional Inbox parse smoke. FCPXMLTimelineManipulationTests: ripple insert, auto lane assignment, clip queries, lane range, timestamps, metadata, secondary storylines, audio keyframes; injectable "now" for timestamp tests via lock-based NowBox (no DispatchSemaphore); do-catch for insertClipAutoLane/insertingClipAutoLane to verify success. FCPXMLTimecodeTests: initialization, arithmetic, comparison, CMTime conversion, frame alignment, hashing, codable. FCPXMLMIMETypeDetectionTests: sync and async detection for various file types. FCPXMLAssetValidationTests: asset existence, lane compatibility, TimelineClip integration. FCPXMLSilenceDetectionTests: silence detection at start/end of audio files. FCPXMLAssetDurationMeasurementTests: duration measurement for audio/video/images. FCPXMLParallelFileIOTests: concurrent read/write operations. FCPXMLAudioEnhancementTests: NoiseReduction, HumReduction, Equalization, MatchEqualization, Clip integration. FCPXMLTransform360Tests: coordinate types, spherical/cartesian, clip integration. FCPXMLCaptionTitleTests: TextStyle, TextStyleDefinition, Caption/Title integration, CaptionSample file test. FCPXMLKeyframeAnimationTests: FadeIn, FadeOut, Keyframe, KeyframeAnimation, FilterParameter integration. FCPXMLAudioKeyframeTests: audio keyframes in adjust-volume (param name="amount" with keyframeAnimation); parsing from FCPXML samples; decibel values (-3dB, -37dB); time values (FCPXML fractional format); fadeIn/fadeOut integration; multiple keyframes in sequence; secondary storyline and nested clip detection; TimelineWithSecondaryStorylineWithAudioKeyframes, TimelineSample file tests. FCPXMLCMTimeCodableTests: CMTime encoding/decoding as FCPXML time strings. FCPXMLCollectionTests: CollectionFolder, KeywordCollection, nested structures. FCPXMLSmartCollectionTests: SmartCollection models, match rules (MatchUsage, MatchRepresentation, MatchMarkers, MatchAnalysisType), round-trip, version stripping. FCPXMLAdjustmentTests: typed adjustment models and clip integration. FCPXMLFilterTests: VideoFilter, AudioFilter, VideoFilterMask, FilterParameter. FCPXMLImportOptionsTests: import options and library location parsing. FCPXMLCodableTests: Codable round-trip for model types. FCPXMLMediaExtractionTests: media reference extraction and copy (CLI --media-copy flow). FCPXMLDTDValidatorTests: per-version DTD validation. FCPXMLStructuralValidatorTests: cross-platform structural validation. FCPXMLAEXMLSerializationParityTests: AEXML vs Foundation serialization parity. FCPXMLTimelineExportValidationTests: timeline, exporters (empty timeline creation and project-creation export at different sizes and frame rates; includeDefaultSmartCollections and DTD validation), validators, file loader. FCPXMLAPIAndEdgeCaseTests: async load API, ServiceLogger injection, edge cases, Live Drawing (1.11+), HiddenClipMarker (1.13+). FCPXMLCutDetectionTests: edit points, transitions, gaps, CutSample file test. FCPXMLPerformanceTests: parameterised and basic performance tests. Reporting/extraction tests are all FCPXML-prefixed (FCPXMLCompoundClipReportTests, FCPXMLRoleInventoryReportTests, FCPXMLRoleInventoryColumnLayoutTests, FCPXMLMarkersReportTests, FCPXMLKeywordsReportTests, FCPXMLTitlesReportTests, FCPXMLTransitionsReportTests, FCPXMLEffectsReportTests, FCPXMLSpeedChangeEffectsReportTests, FCPXMLSummaryReportTests, FCPXMLReportExcelExportTests, FCPXMLReportPDFExportTests, FCPXMLReportPDFSheetPlanTests, FCPXMLReportPDFTableLayoutTests, FCPXMLReportFormattingTests, FCPXMLReportRoleExclusionTests, FCPXMLReportTimecodeFormatTests, FCPXMLReportBuildPhaseTests, FCPXMLReportColumnExclusionTests, FCPXMLReportExcludeDisabledClipsTests, FCPXMLRoleDisplayPreferenceTests, FCPXMLRoleInventoryClipCollectorTests, FCPXMLRoleInventoryRoleSheetOrderingTests, FCPXMLSummaryRoleDurationAggregatorTests, FCPXMLEffectsReportPolicyTests, FCPXMLSpeedChangeFormattingTests, FCPXMLDisplayClipNameTests, FCPXMLTitleDisplayTests, FCPXMLExtractionScopeTests, FCPXMLExtractedElementTests, FCPXMLEffectsCollectorTests, FCPXMLRolesExtractionPresetTests, FCPXMLEffectAppleSuppliedTests, FCPXMLClipParsingCarriesAudioTests, FCPXMLTransformAdjustmentParsingTests, FCPXMLTransitionSpinePlacementTests). +- Tests/OpenFCPXMLKitTests/: FCPXMLTestResources.swift (packageRoot, fcpxmlSamplesDirectory, urlForFCPXMLSample, FCPXMLSampleName); FCPXMLTestSampleLoading.swift / FCPXMLTestingSampleSupport.swift (tryLoad* / require*; fcpxmlFrameRateSampleNames, allFCPXMLSampleNames; Test.cancel for optional fixtures); FCPXMLReportingReportFixture.swift and FCPXMLReportingReportTestSupport.swift (optional reporting integration fixtures/assertions). OpenFCPXMLKitTests.swift: main @Suite with dependencies created in init(); MARK comments group tests. FileTests/: one test class per sample or category (e.g. FCPXMLFileTest_24, FCPXMLFileTest_AllSamples, FCPXMLFileTest_FrameRates, FCPXMLFileTest_GeneralDemo, FCPXMLFileTest_HiddenMarkers, FCPXMLFileTest_360Video, FCPXMLFileTest_AuditionSample, FCPXMLFileTest_ImageSample, FCPXMLFileTest_Multicam, FCPXMLFileTest_Photoshop, FCPXMLFileTest_SmartCollection). LogicAndParsing/: FCPXMLRootVersionTests, FCPXMLStructureTests, FCPXMLFormatAssetTests. Every test-case class is FCPXML-prefixed except the module-named umbrella OpenFCPXMLKitTests. FCPXMLAuthoringTests / FCPXMLVersionFeatureGateTests / FCPXMLProjectionEdgeCaseCorpusTests: detached Authoring, feature gate, Projection edge corpus. FCPXMLSubmittedFCPXMLSmokeTests: optional Inbox parse smoke. FCPXMLTimelineManipulationTests: ripple insert, auto lane assignment, clip queries, lane range, timestamps, metadata, secondary storylines, audio keyframes; injectable "now" for timestamp tests via lock-based NowBox (no DispatchSemaphore); do-catch for insertClipAutoLane/insertingClipAutoLane to verify success. FCPXMLTimecodeTests: initialization, arithmetic, comparison, CMTime conversion, frame alignment, hashing, codable. FCPXMLMIMETypeDetectionTests: sync and async detection for various file types. FCPXMLAssetValidationTests: asset existence, lane compatibility, TimelineClip integration. FCPXMLSilenceDetectionTests: silence detection at start/end of audio files. FCPXMLAssetDurationMeasurementTests: duration measurement for audio/video/images. FCPXMLParallelFileIOTests: concurrent read/write operations. FCPXMLAudioEnhancementTests: NoiseReduction, HumReduction, Equalization, MatchEqualization, Clip integration. FCPXMLTransform360Tests: coordinate types, spherical/cartesian, clip integration. FCPXMLCaptionTitleTests: TextStyle, TextStyleDefinition, Caption/Title integration, CaptionSample file test. FCPXMLKeyframeAnimationTests: FadeIn, FadeOut, Keyframe, KeyframeAnimation, FilterParameter integration. FCPXMLAudioKeyframeTests: audio keyframes in adjust-volume (param name="amount" with keyframeAnimation); parsing from FCPXML samples; decibel values (-3dB, -37dB); time values (FCPXML fractional format); fadeIn/fadeOut integration; multiple keyframes in sequence; secondary storyline and nested clip detection; TimelineWithSecondaryStorylineWithAudioKeyframes, TimelineSample file tests. FCPXMLCMTimeCodableTests: CMTime encoding/decoding as FCPXML time strings. FCPXMLCollectionTests: CollectionFolder, KeywordCollection, nested structures. FCPXMLSmartCollectionTests: SmartCollection models, match rules (MatchUsage, MatchRepresentation, MatchMarkers, MatchAnalysisType), round-trip, version stripping. FCPXMLAdjustmentTests: typed adjustment models and clip integration. FCPXMLFilterTests: VideoFilter, AudioFilter, VideoFilterMask, FilterParameter. FCPXMLImportOptionsTests: import options and library location parsing. FCPXMLCodableTests: Codable round-trip for model types. FCPXMLMediaExtractionTests: media reference extraction and copy (CLI --media-copy flow). FCPXMLDTDValidatorTests: per-version DTD validation. FCPXMLStructuralValidatorTests: cross-platform structural validation. FCPXMLAEXMLSerializationParityTests: AEXML vs Foundation serialization parity. FCPXMLTimelineExportValidationTests: timeline, exporters (empty timeline creation and project-creation export at different sizes and frame rates; includeDefaultSmartCollections and DTD validation), validators, file loader. FCPXMLAPIAndEdgeCaseTests: async load API, ServiceLogger injection, edge cases, Live Drawing (1.11+), HiddenClipMarker (1.13+). FCPXMLCutDetectionTests: edit points, transitions, gaps, CutSample file test. FCPXMLPerformanceTests: parameterised and basic performance tests. Reporting/extraction tests are all FCPXML-prefixed (FCPXMLCompoundClipReportTests, FCPXMLRoleInventoryReportTests, FCPXMLRoleInventoryColumnLayoutTests, FCPXMLMarkersReportTests, FCPXMLKeywordsReportTests, FCPXMLTitlesReportTests, FCPXMLTransitionsReportTests, FCPXMLEffectsReportTests, FCPXMLSpeedChangeEffectsReportTests, FCPXMLSummaryReportTests, FCPXMLReportExcelExportTests, FCPXMLReportPDFExportTests, FCPXMLReportPDFSheetPlanTests, FCPXMLReportPDFTableLayoutTests, FCPXMLReportFormattingTests, FCPXMLReportRoleExclusionTests, FCPXMLReportTimecodeFormatTests, FCPXMLReportBuildPhaseTests, FCPXMLReportColumnExclusionTests, FCPXMLReportExcludeDisabledClipsTests, FCPXMLRoleDisplayPreferenceTests, FCPXMLRoleInventoryClipCollectorTests, FCPXMLRoleInventoryRoleSheetOrderingTests, FCPXMLSummaryRoleDurationAggregatorTests, FCPXMLEffectsReportPolicyTests, FCPXMLSpeedChangeFormattingTests, FCPXMLDisplayClipNameTests, FCPXMLTitleDisplayTests, FCPXMLExtractionScopeTests, FCPXMLExtractedElementTests, FCPXMLEffectsCollectorTests, FCPXMLRolesExtractionPresetTests, FCPXMLEffectAppleSuppliedTests, FCPXMLClipParsingCarriesAudioTests, FCPXMLTransformAdjustmentParsingTests, FCPXMLTransitionSpinePlacementTests). -Use descriptive `@Test` / `@Suite` names; group related tests logically; use meaningful `#expect` / `#require` assertions. The suite is **Swift Testing only** (no XCTest). Current total: **1084** tests listed in `swift test list` (**1078** in `OpenFCPXMLKitTests` + **6** optional `ExcelReportTest`) covering all functionality including async/await, timeline manipulation, metadata, timestamps, FCPXMLTimecode, MIME type detection, asset validation, silence detection, asset duration measurement, parallel file I/O, version conversion stripping, per-version DTD validation, extract-then-copy (CLI --media-copy flow), synchronized clip matching, secondary storyline traversal, clip identification, URL resolution, version conversion edge cases, typed adjustment models (including Transform360, ColorConform, Stereo3D, VoiceIsolation), typed effect/filter models, typed caption/title models, smart collections (match-clip, match-media, match-ratings, match-text, match-usage, match-representation, match-markers, match-analysis-type), keyframe animation, audio keyframes (FCPXMLAudioKeyframeTests: adjust-volume param keyframeAnimation parsing, decibel/time validation, fadeIn/fadeOut integration), CMTime Codable extension, collection organization, Live Drawing (1.11+), HiddenClipMarker (1.13+), Format/Asset 1.13+ (heroEye, heroEyeOverride, mediaReps), FCPXMLExporter clip-level metadata export and XML declaration standalone="no", FCPXMLTimelineManipulationTests refactor (lock-based NowBox, do-catch for throwing APIs), Excel reporting (Selected Roles Inventory column layout, Summary/Media Summary split, ReportTimecodeFormat / format-aware headers, inventory-first ReportBuildPhase progress, global column exclusion, disabled-clip filtering, workbook cell formatting via FCPXMLReportWorkbookExporter), 360 video features, auditions, conform-rate, still images, multicam, secondary storylines, audio keyframes, keyword collections/folders, and Photoshop integration. +Use descriptive `@Test` / `@Suite` names; group related tests logically; use meaningful `#expect` / `#require` assertions. The suite is **Swift Testing only** (no XCTest). Current total: **1114** tests listed in `swift test list` (**1108** in `OpenFCPXMLKitTests` + **6** optional `ExcelReportTest`) covering all functionality including async/await, timeline manipulation, metadata, timestamps, FCPXMLTimecode, MIME type detection, asset validation, silence detection, asset duration measurement, parallel file I/O, version conversion stripping, per-version DTD validation, extract-then-copy (CLI --media-copy flow), synchronized clip matching, secondary storyline traversal, clip identification, URL resolution, version conversion edge cases, typed adjustment models (including Transform360, ColorConform, Stereo3D, VoiceIsolation), typed effect/filter models, typed caption/title models, smart collections (match-clip, match-media, match-ratings, match-text, match-usage, match-representation, match-markers, match-analysis-type), keyframe animation, audio keyframes (FCPXMLAudioKeyframeTests: adjust-volume param keyframeAnimation parsing, decibel/time validation, fadeIn/fadeOut integration), CMTime Codable extension, collection organization, Live Drawing (1.11+), HiddenClipMarker (1.13+), Format/Asset 1.13+ (heroEye, heroEyeOverride, mediaReps), FCPXMLExporter clip-level metadata export and XML declaration standalone="no", FCPXMLTimelineManipulationTests refactor (lock-based NowBox, do-catch for throwing APIs), Excel reporting (Selected Roles Inventory column layout, Summary/Media Summary split, ReportTimecodeFormat / format-aware headers, inventory-first ReportBuildPhase progress, global column exclusion, disabled-clip filtering, workbook cell formatting via FCPXMLReportWorkbookExporter), 360 video features, auditions, conform-rate, still images, multicam, secondary storylines, audio keyframes, keyword collections/folders, and Photoshop integration. --- @@ -165,7 +165,7 @@ SwiftTimecode integration: use SwiftTimecode for all timecode operations; suppor ## Testing Requirements -Test coverage: unit tests for all public APIs; integration tests for complex workflows; performance tests for time-critical operations; concurrency tests for async operations; test all supported frame rates (Final Cut Pro compatible). Current: **1084** tests listed in `swift test list` (including AEXML parity, FCPXMLDTDValidatorTests, FCPXMLStructuralValidatorTests, FCPXMLReportTimecodeFormatTests, FCPXMLReportBuildPhaseTests, FCPXMLReportColumnExclusionTests, FCPXMLReportExcludeDisabledClipsTests, FCPXMLRoleInventoryColumnLayoutTests, FCPXMLReportExcelExportTests workbook cell formatting, FCPXMLReportPDFExportTests PDF export, FCPXMLReportPDFSheetPlanTests TOC colour-index parity, FCPXMLReportPDFTableLayoutTests column-width expansion, optional ExcelReportTest integration) covering all functionality including async/await, timeline manipulation, metadata, timestamps, FCPXMLTimecode, MIME type detection, asset validation, silence detection, asset duration measurement, parallel file I/O, version conversion, DTD validation, extract-then-copy flow, synchronized clip matching, secondary storyline traversal, clip identification, URL resolution, version conversion edge cases, typed adjustment models (including Transform360, ColorConform, Stereo3D, VoiceIsolation), typed effect/filter models, typed caption/title models, smart collections (match-clip, match-media, match-ratings, match-text, match-usage, match-representation, match-markers, match-analysis-type), keyframe animation, audio keyframes (FCPXMLAudioKeyframeTests: adjust-volume param keyframeAnimation parsing, decibel/time validation, fadeIn/fadeOut integration), CMTime Codable extension, collection organization, Live Drawing (1.11+), HiddenClipMarker (1.13+), Format/Asset 1.13+ (heroEye, heroEyeOverride, mediaReps), FCPXMLExporter clip-level metadata export and XML declaration standalone="no", FCPXMLTimelineManipulationTests (lock-based NowBox, do-catch), Excel reporting enhancements, PDF report export (TOC colour chips, column expansion after exclusions), 360 video features, auditions, conform-rate, still images, multicam, secondary storylines, audio keyframes, keyword collections/folders, and Photoshop integration. +Test coverage: unit tests for all public APIs; integration tests for complex workflows; performance tests for time-critical operations; concurrency tests for async operations; test all supported frame rates (Final Cut Pro compatible). Current: **1114** tests listed in `swift test list` (including AEXML parity, FCPXMLDTDValidatorTests, FCPXMLStructuralValidatorTests, FCPXMLReportTimecodeFormatTests, FCPXMLReportBuildPhaseTests, FCPXMLReportColumnExclusionTests, FCPXMLReportExcludeDisabledClipsTests, FCPXMLRoleInventoryColumnLayoutTests, FCPXMLReportExcelExportTests workbook cell formatting, FCPXMLReportPDFExportTests PDF export, FCPXMLReportPDFSheetPlanTests TOC colour-index parity, FCPXMLReportPDFTableLayoutTests column-width expansion, optional ExcelReportTest integration) covering all functionality including async/await, timeline manipulation, metadata, timestamps, FCPXMLTimecode, MIME type detection, asset validation, silence detection, asset duration measurement, parallel file I/O, version conversion, DTD validation, extract-then-copy flow, synchronized clip matching, secondary storyline traversal, clip identification, URL resolution, version conversion edge cases, typed adjustment models (including Transform360, ColorConform, Stereo3D, VoiceIsolation), typed effect/filter models, typed caption/title models, smart collections (match-clip, match-media, match-ratings, match-text, match-usage, match-representation, match-markers, match-analysis-type), keyframe animation, audio keyframes (FCPXMLAudioKeyframeTests: adjust-volume param keyframeAnimation parsing, decibel/time validation, fadeIn/fadeOut integration), CMTime Codable extension, collection organization, Live Drawing (1.11+), HiddenClipMarker (1.13+), Format/Asset 1.13+ (heroEye, heroEyeOverride, mediaReps), FCPXMLExporter clip-level metadata export and XML declaration standalone="no", FCPXMLTimelineManipulationTests (lock-based NowBox, do-catch), Excel reporting enhancements, PDF report export (TOC colour chips, column expansion after exclusions), 360 video features, auditions, conform-rate, still images, multicam, secondary storylines, audio keyframes, keyword collections/folders, and Photoshop integration. Test data: use realistic FCPXML samples; include edge cases and error conditions; test all supported frame rates; validate against actual Final Cut Pro output where applicable. @@ -258,7 +258,7 @@ Code review process: review for Swift 6.3 compliance; check concurrency implemen ## Documentation Sync -Keep this file in sync with AGENT.md. Hard must / must-not constraints live in **GUARDRAILS.md** (update Signs when a design lock or regression is learned — Sign: `swift-testing-only`; keep ARCHITECTURE.md for structure and mermaid). Both AGENT.md and this file must reflect: changelog styling (CHANGELOG.md: Keep a Changelog format, version links to release tags, ✨ New Features / 🔧 Improvements / 🐛 Bug Fixes); project overview and codebase rewrite/refactor; architecture and single injection point (FCPXMLUtility.defaultForExtensions); source layout (Analysis, Classes, Delegates, Errors, Extensions including +Modular and +Codable, Implementations, Protocols, Services, Utilities, Annotations, Export, Timeline, Timing, Validation, FileIO, Logging, Format, Model including Adjustments, Animations, Filters, Clips with +Adjustments and +Typed, CommonElements with TextStyle/TextStyleDefinition, Structure with CollectionFolder/KeywordCollection/SmartCollection, Parsing, Extraction, Projection (TimelineProjector / MulticamProjection / RefClipProjection / ChannelKindFilter), Reporting including Excel/ and PDF/, XML with Protocols/Foundation/AEXML/OFKXMLDefaultFactory, FCPXML DTDs; reporting vs core layers in ARCHITECTURE.md §2.7); test structure (Tests/ layout — **Swift Testing only**, no XCTest; harness `FCPXMLTestResources` / `FCPXMLTestSampleLoading` / `FCPXMLTestingSampleSupport`; FileTests/ including FCPXMLFileTest_GeneralDemo, FCPXMLFileTest_HiddenMarkers, FCPXMLFileTest_360Video, FCPXMLFileTest_AuditionSample, FCPXMLFileTest_ImageSample, FCPXMLFileTest_Multicam, FCPXMLFileTest_Photoshop, FCPXMLFileTest_SmartCollection, LogicAndParsing/ including FCPXMLFormatAssetTests, FCPXMLCutDetectionTests, FCPXMLTimelineProjectionTests, FCPXMLVersionConversionTests, FCPXMLMediaExtractionTests, FCPXMLTimelineManipulationTests, FCPXMLTimecodeTests, FCPXMLMIMETypeDetectionTests, FCPXMLAssetValidationTests, FCPXMLSilenceDetectionTests, FCPXMLAssetDurationMeasurementTests, FCPXMLParallelFileIOTests, FCPXMLAudioEnhancementTests, FCPXMLTransform360Tests, FCPXMLCaptionTitleTests, FCPXMLKeyframeAnimationTests, FCPXMLAudioKeyframeTests, FCPXMLCMTimeCodableTests, FCPXMLCollectionTests, FCPXMLSmartCollectionTests, FCPXMLAdjustmentTests, FCPXMLFilterTests, FCPXMLImportOptionsTests, FCPXMLCodableTests, FCPXMLAEXMLSerializationParityTests, FCPXMLDTDValidatorTests, FCPXMLStructuralValidatorTests, OpenFCPXMLKitTests.swift, FCPXMLTimelineExportValidationTests, FCPXMLAPIAndEdgeCaseTests, FCPXMLPerformanceTests (`ContinuousClock` budgets), FCPXMLTimelineProjectionTests, FCPXMLProjectionCoverageTests, FCPXMLParsingCoverageTests, FCPXMLEngineHygieneTests, FCPXMLReportObligationCorpusTests, FCPXMLExtractionNestFidelityTests, FCPXMLRoleInheritanceMatrixTests, FCPXMLExtractionProjectionPolicyTests, FCPXMLMarkersKeywordsProjectionTests, FCPXMLTitlesProjectionTests, FCPXMLTransitionsProjectionTests, FCPXMLEffectsProjectionTests, FCPXMLSubmittedFCPXMLSmokeTests, and FCPXML-prefixed reporting/extraction tests (FCPXMLRoleInventoryReportTests, FCPXMLMarkersReportTests, FCPXMLKeywordsReportTests, FCPXMLTitlesReportTests, FCPXMLTransitionsReportTests, FCPXMLEffectsReportTests, FCPXMLSpeedChangeEffectsReportTests, FCPXMLSummaryReportTests, FCPXMLReportExcelExportTests, FCPXMLReportPDFExportTests, FCPXMLReportPDFSheetPlanTests, FCPXMLReportPDFTableLayoutTests, FCPXMLReportFormattingTests, FCPXMLReportRoleExclusionTests, FCPXMLReportTimecodeFormatTests, FCPXMLReportBuildPhaseTests, FCPXMLRoleDisplayPreferenceTests, FCPXMLRoleInventoryClipCollectorTests, FCPXMLRoleInventoryRoleSheetOrderingTests, FCPXMLSummaryRoleDurationAggregatorTests, FCPXMLEffectsReportPolicyTests, FCPXMLSpeedChangeFormattingTests, FCPXMLDisplayClipNameTests, FCPXMLTitleDisplayTests, FCPXMLExtractionScopeTests, FCPXMLExtractedElementTests, FCPXMLEffectsCollectorTests, FCPXMLRolesExtractionPresetTests, FCPXMLEffectAppleSuppliedTests, FCPXMLClipParsingCarriesAudioTests, FCPXMLTransformAdjustmentParsingTests, FCPXMLTransitionSpinePlacementTests); every test-case class is FCPXML-prefixed except the module-named umbrella OpenFCPXMLKitTests; empty timeline creation and project-creation export at different sizes and frame rates in FCPXMLTimelineExportValidationTests (clip-level metadata export, XML declaration standalone="no")). Cross-platform XML (OFKXML*, FCPXMLStructuralValidator, iOS). Private `Tests/Submitted FCPXML/` inbox (gitignored contents; never commit private FCPXML to GitHub). FCPXML 1.5–1.14 and FCPXMLElementType; FCPXMLVersion.supportsBundleFormat (1.10+); version conversion with element stripping and per-version DTD validation; FCPXML creation from scratch; timeline manipulation (ripple insert, auto lane assignment, clip queries, lane range, secondary storylines); timeline metadata (markers, chapter markers, keywords, ratings, timestamps); FCPXMLTimecode custom type; MIME type detection; asset validation (including still images); silence detection; asset duration measurement; parallel file I/O; TimelineFormat enhancements; typed adjustment models (Crop, Transform, Blend, Stabilization, Volume, Loudness, NoiseReduction, HumReduction, Equalization, MatchEqualization, Transform360, ColorConform, Stereo3D, VoiceIsolation); typed effect/filter models (VideoFilter, AudioFilter, VideoFilterMask, FilterParameter with keyframe animation and auxValue 1.11+); typed caption/title models (Caption, Title with TextStyle, TextStyleDefinition); smart collections (SmartCollection with match-clip, match-media, match-ratings, match-text, match-usage, match-representation, match-markers, match-analysis-type); keyframe animation (KeyframeAnimation, Keyframe, FadeIn, FadeOut); audio keyframes (FCPXMLAudioKeyframeTests: adjust-volume param keyframeAnimation parsing, decibel/time validation, fadeIn/fadeOut integration); CMTime Codable extension; collection organization (CollectionFolder, KeywordCollection); Live Drawing (1.11+); HiddenClipMarker (1.13+); Format/Asset 1.13+ (heroEye, heroEyeOverride, mediaReps); experimental CLI (OpenFCPXMLKit-CLI, single binary, embedded DTDs, --check-version, --convert-version, --extension-type fcpxml|fcpxmld, --validate, --media-copy, --create-project with DTD validation and FCP-style output, --report Excel/PDF report with --report-full/per-section flags/--report-summary/--report-media-summary/--media-resolution/--media-summary-distinguish-proxy/--exclude-role/--exclude-column/--exclude-disabled-clips/--include-markers-outside-clip-boundaries/--protect-sheets/--timecode-format/--report-project/--label-copyright/--create-pdf, --log/--log-level/--quiet with log file capturing all command output); Excel and PDF reporting subsystem (`allReportTimelineSources` / compound-clip timelines) (Reporting/ builders, Sections/Rows, Support including RoleInventoryColumnLayout, ReportColumnExclusion, ReportFormatting, ReportTimecodeFormat, ReportBuildProgress; Excel/ via XLKit; buildReport/ReportExcelExport/ReportPDFExport; Summary and Media Summary sheets; excludeDisabledClips/excludedColumns/timecodeFormat/copyrightLabel/includeMarkersOutsideClipBoundaries/protectSheets; `--label-copyright`; inventory-first enabledPhases; PDF cover notes + TOC colour chips / SheetPlan colorIndex; TableLayout column expansion to contentWidth and Row inject); extraction presets (Captions, Effects, FrameData, Markers, Roles, Titles); Manual chapters including 11 Timeline Projection / 16 Cross-Platform / 18 CLI / 19 Reporting / 20 Examples; Tests/ExcelReportTest optional integration (**6** Swift Testing; `Test.cancel` without fixture); Tests/Submitted FCPXML private inbox; Final Cut Pro frame rates; Swift 6 concurrency (Sendable, async/await, CI strict-concurrency job); Xcode 26 dynamic framework linking compatibility via explicit `swift-log` (`Logging`) dependency in `Package.swift`; SwiftExtensions 3.0.0+ and SwiftSemanticVersion 1.0.0+ (`SemanticVersion` for `FinalCutPro.FCPXML.Version`); current test count **1084** listed in `swift test list` (**1078** + **6**; all Swift Testing). When updating either file, update both and keep terminology and examples consistent. +Keep this file in sync with AGENT.md. Hard must / must-not constraints live in **GUARDRAILS.md** (update Signs when a design lock or regression is learned — Sign: `swift-testing-only`; keep ARCHITECTURE.md for structure and mermaid). Both AGENT.md and this file must reflect: changelog styling (CHANGELOG.md: Keep a Changelog format, version links to release tags, ✨ New Features / 🔧 Improvements / 🐛 Bug Fixes); project overview and codebase rewrite/refactor; architecture and single injection point (FCPXMLUtility.defaultForExtensions); source layout (Analysis, Classes, Delegates, Errors, Extensions including +Modular and +Codable, Implementations, Protocols, Services, Utilities, Annotations, Export, Timeline, Timing, Validation, FileIO, Logging, Format, Model including Adjustments, Animations, Filters, Clips with +Adjustments and +Typed, CommonElements with TextStyle/TextStyleDefinition, Structure with CollectionFolder/KeywordCollection/SmartCollection, Parsing, Extraction, Projection (TimelineProjector / MulticamProjection / RefClipProjection / ChannelKindFilter), Reporting including Excel/ and PDF/, XML with Protocols/Foundation/AEXML/OFKXMLDefaultFactory, FCPXML DTDs; reporting vs core layers in ARCHITECTURE.md §2.7); test structure (Tests/ layout — **Swift Testing only**, no XCTest; harness `FCPXMLTestResources` / `FCPXMLTestSampleLoading` / `FCPXMLTestingSampleSupport`; FileTests/ including FCPXMLFileTest_GeneralDemo, FCPXMLFileTest_HiddenMarkers, FCPXMLFileTest_360Video, FCPXMLFileTest_AuditionSample, FCPXMLFileTest_ImageSample, FCPXMLFileTest_Multicam, FCPXMLFileTest_Photoshop, FCPXMLFileTest_SmartCollection, LogicAndParsing/ including FCPXMLFormatAssetTests, FCPXMLCutDetectionTests, FCPXMLTimelineProjectionTests, FCPXMLVersionConversionTests, FCPXMLMediaExtractionTests, FCPXMLTimelineManipulationTests, FCPXMLTimecodeTests, FCPXMLMIMETypeDetectionTests, FCPXMLAssetValidationTests, FCPXMLSilenceDetectionTests, FCPXMLAssetDurationMeasurementTests, FCPXMLParallelFileIOTests, FCPXMLAudioEnhancementTests, FCPXMLTransform360Tests, FCPXMLCaptionTitleTests, FCPXMLKeyframeAnimationTests, FCPXMLAudioKeyframeTests, FCPXMLCMTimeCodableTests, FCPXMLCollectionTests, FCPXMLSmartCollectionTests, FCPXMLAdjustmentTests, FCPXMLFilterTests, FCPXMLImportOptionsTests, FCPXMLCodableTests, FCPXMLAEXMLSerializationParityTests, FCPXMLDTDValidatorTests, FCPXMLStructuralValidatorTests, OpenFCPXMLKitTests.swift, FCPXMLTimelineExportValidationTests, FCPXMLAPIAndEdgeCaseTests, FCPXMLPerformanceTests (`ContinuousClock` budgets), FCPXMLTimelineProjectionTests, FCPXMLProjectionCoverageTests, FCPXMLParsingCoverageTests, FCPXMLEngineHygieneTests, FCPXMLReportObligationCorpusTests, FCPXMLExtractionNestFidelityTests, FCPXMLRoleInheritanceMatrixTests, FCPXMLExtractionProjectionPolicyTests, FCPXMLMarkersKeywordsProjectionTests, FCPXMLTitlesProjectionTests, FCPXMLTransitionsProjectionTests, FCPXMLEffectsProjectionTests, FCPXMLSubmittedFCPXMLSmokeTests, and FCPXML-prefixed reporting/extraction tests (FCPXMLRoleInventoryReportTests, FCPXMLMarkersReportTests, FCPXMLKeywordsReportTests, FCPXMLTitlesReportTests, FCPXMLTransitionsReportTests, FCPXMLEffectsReportTests, FCPXMLSpeedChangeEffectsReportTests, FCPXMLSummaryReportTests, FCPXMLReportExcelExportTests, FCPXMLReportPDFExportTests, FCPXMLReportPDFSheetPlanTests, FCPXMLReportPDFTableLayoutTests, FCPXMLReportFormattingTests, FCPXMLReportRoleExclusionTests, FCPXMLReportTimecodeFormatTests, FCPXMLReportBuildPhaseTests, FCPXMLRoleDisplayPreferenceTests, FCPXMLRoleInventoryClipCollectorTests, FCPXMLRoleInventoryRoleSheetOrderingTests, FCPXMLSummaryRoleDurationAggregatorTests, FCPXMLEffectsReportPolicyTests, FCPXMLSpeedChangeFormattingTests, FCPXMLDisplayClipNameTests, FCPXMLTitleDisplayTests, FCPXMLExtractionScopeTests, FCPXMLExtractedElementTests, FCPXMLEffectsCollectorTests, FCPXMLRolesExtractionPresetTests, FCPXMLEffectAppleSuppliedTests, FCPXMLClipParsingCarriesAudioTests, FCPXMLTransformAdjustmentParsingTests, FCPXMLTransitionSpinePlacementTests); every test-case class is FCPXML-prefixed except the module-named umbrella OpenFCPXMLKitTests; empty timeline creation and project-creation export at different sizes and frame rates in FCPXMLTimelineExportValidationTests (clip-level metadata export, XML declaration standalone="no")). Cross-platform XML (OFKXML*, FCPXMLStructuralValidator, iOS). Private `Tests/Submitted FCPXML/` inbox (gitignored contents; never commit private FCPXML to GitHub). FCPXML 1.5–1.14 and FCPXMLElementType; FCPXMLVersion.supportsBundleFormat (1.10+); version conversion with element stripping and per-version DTD validation; FCPXML creation from scratch; timeline manipulation (ripple insert, auto lane assignment, clip queries, lane range, secondary storylines); timeline metadata (markers, chapter markers, keywords, ratings, timestamps); FCPXMLTimecode custom type; MIME type detection; asset validation (including still images); silence detection; asset duration measurement; parallel file I/O; TimelineFormat enhancements; typed adjustment models (Crop, Corners, Transform, Blend, Stabilization, Volume, Panner, Loudness, NoiseReduction, HumReduction, Equalization, MatchEqualization, Transform360, ColorConform, Stereo3D, VoiceIsolation); typed effect/filter models (VideoFilter, AudioFilter, VideoFilterMask, FilterParameter with keyframe animation and auxValue 1.11+); typed caption/title models (Caption, Title with TextStyle, TextStyleDefinition); smart collections (SmartCollection with match-clip, match-media, match-ratings, match-text, match-usage, match-representation, match-markers, match-analysis-type); keyframe animation (KeyframeAnimation, Keyframe, FadeIn, FadeOut); audio keyframes (FCPXMLAudioKeyframeTests: adjust-volume param keyframeAnimation parsing, decibel/time validation, fadeIn/fadeOut integration); CMTime Codable extension; collection organization (CollectionFolder, KeywordCollection); Live Drawing (1.11+); HiddenClipMarker (1.13+); Format/Asset 1.13+ (heroEye, heroEyeOverride, mediaReps); experimental CLI (OpenFCPXMLKit-CLI, single binary, embedded DTDs, --check-version, --convert-version, --extension-type fcpxml|fcpxmld, --validate, --media-copy, --create-project with DTD validation and FCP-style output, --report Excel/PDF report with --report-full/per-section flags/--report-summary/--report-media-summary/--media-resolution/--media-summary-distinguish-proxy/--exclude-role/--exclude-column/--exclude-disabled-clips/--include-markers-outside-clip-boundaries/--protect-sheets/--timecode-format/--report-project/--label-copyright/--create-pdf, --log/--log-level/--quiet with log file capturing all command output); Excel and PDF reporting subsystem (`allReportTimelineSources` / compound-clip timelines) (Reporting/ builders, Sections/Rows, Support including RoleInventoryColumnLayout, ReportColumnExclusion, ReportFormatting, ReportTimecodeFormat, ReportBuildProgress; Excel/ via XLKit; buildReport/ReportExcelExport/ReportPDFExport; Summary and Media Summary sheets; excludeDisabledClips/excludedColumns/timecodeFormat/copyrightLabel/includeMarkersOutsideClipBoundaries/protectSheets; `--label-copyright`; inventory-first enabledPhases; PDF cover notes + TOC colour chips / SheetPlan colorIndex; TableLayout column expansion to contentWidth and Row inject); extraction presets (Captions, Effects, FrameData, Markers, Roles, Titles); Manual chapters including 08 Detached Authoring / 12 Timeline Projection / 17 Cross-Platform / 19 CLI / 20 Reporting / 21 Examples; Tests/ExcelReportTest optional integration (**6** Swift Testing; `Test.cancel` without fixture); Tests/Submitted FCPXML private inbox; Final Cut Pro frame rates; Swift 6 concurrency (Sendable, async/await, CI strict-concurrency job); Xcode 26 dynamic framework linking compatibility via explicit `swift-log` (`Logging`) dependency in `Package.swift`; SwiftExtensions 3.0.0+ and SwiftSemanticVersion 1.0.0+ (`SemanticVersion` for `FinalCutPro.FCPXML.Version`); current test count **1114** listed in `swift test list` (**1108** + **6**; all Swift Testing). When updating either file, update both and keep terminology and examples consistent. --- diff --git a/AGENT.md b/AGENT.md index 8ae37bc..aff4310 100644 --- a/AGENT.md +++ b/AGENT.md @@ -4,7 +4,7 @@ OpenFCPXMLKit is a modern, fully modular Swift 6 framework for Final Cut Pro FCP Keep this file in sync with `.cursorrules`. Both should describe the same overview, architecture, test structure, and conventions. When you update one, update the other. -**Hard constraints:** [GUARDRAILS.md](GUARDRAILS.md) — must / must-not for layers, naming, FCPXML compatibility, reporting honesty, and fixtures. Prefer GUARDRAILS for “what not to do”; [ARCHITECTURE.md](ARCHITECTURE.md) for structure and diagrams. +**Hard constraints:** [GUARDRAILS.md](GUARDRAILS.md) — must / must-not for layers, naming, FCPXML compatibility, reporting honesty, and fixtures. Prefer GUARDRAILS for “what not to do”; [ARCHITECTURE.md](ARCHITECTURE.md) for structure and diagrams; [Documentation/Coverage.md](Documentation/Coverage.md) for element / layer inventory matrices. **Naming:** Use OpenFCPXMLKit naming exclusively in all code, documentation, comments, and agent files (`ServiceLogger`, `createService()`, `OFKXML*` types). Do not use legacy project names or identifiers from prior forks. Never use the terms "PBF" or "Production's Best Friend" in source code, code comments, symbol names, or CLI/log output; describe the reporting feature neutrally (e.g. "Excel report", "PDF report", "role inventory report", "workbook export"). Those terms may appear only in prose documentation (README, CHANGELOG, Manual, and these agent guides) — never in the codebase itself. @@ -16,11 +16,11 @@ Keep this file in sync with `.cursorrules`. Both should describe the same overvi - [Codebase Rewrite and Refactor](#codebase-rewrite-and-refactor) - [Architecture Guidelines](#architecture-guidelines) - [Modularity and Safety](#modularity-and-safety) +- [Development Patterns](#development-patterns) - [Code Style and Formatting](#code-style-and-formatting) - [File Organisation](#file-organisation) - [Test Structure](#test-structure) - [Dependencies](#dependencies) -- [Development Patterns](#development-patterns) - [Testing Requirements](#testing-requirements) - [Error Handling](#error-handling) - [Performance Considerations](#performance-considerations) @@ -43,7 +43,7 @@ OpenFCPXMLKit targets macOS 26+, iOS 26+, Xcode 26+, and Swift 6.3 with full con **Backward compatibility:** The entire codebase must remain backward compatible with FCPXML 1.5. Optional attributes and elements introduced in later versions (e.g. 1.11, 1.13) must be omitted or ignored when reading/writing or converting to 1.5; mark such features in code comments with the minimum FCPXML version (e.g. `FCPXML 1.13+`). -Current status: **1084** tests listed in `swift test list` (**1078** in `OpenFCPXMLKitTests` + **6** optional `ExcelReportTest`; all Swift Testing `@Test`); FCPXML versions 1.5–1.14 supported (DTDs included, full parsing, typed element-type coverage for all DTD elements via FCPXMLElementType); Final Cut Pro frame rates (23.976, 24, 25, 29.97, 30, 50, 59.94, 60); thread-safe and concurrency-compliant with comprehensive async/await support; no known security vulnerabilities. Version conversion automatically drops elements not in the target version’s DTD (e.g. adjust-colorConform, adjust-stereo-3D); DTD validation runs per version (validateDocumentAgainstDTD, validateDocumentAgainstDeclaredVersion) and after CLI convert. FCPXMLVersion.supportsBundleFormat is true for 1.10+ (.fcpxmld bundle); 1.5–1.9 support only single-file .fcpxml. FCPXML creation: create FCPXML documents from scratch with events, projects, resources, and clips via XMLDocumentManager, XMLDocument initializers, or FCPXMLService. Timeline manipulation: ripple insert (shifts subsequent clips), auto lane assignment, clip queries (by lane, time range, asset ID), lane range computation, secondary storylines. Timeline metadata: markers, chapter markers, keywords, ratings, custom metadata, timestamps (createdAt, modifiedAt). FCPXMLTimecode: custom timecode type (arithmetic, frame alignment, CMTime conversion, FCPXML string parsing). MIME type detection, asset validation, silence detection, asset duration measurement, parallel file I/O, still image asset support. TimelineFormat enhancements: presets (hd720p, dci4K, hd1080i, hd720i), computed properties (aspectRatio, isHD, isUHD, interlaced). Typed adjustment models: Crop, Transform, Blend, Stabilization, Volume, Loudness, NoiseReduction, HumReduction, Equalization, MatchEqualization, Transform360, ColorConform, Stereo3D, VoiceIsolation with full clip integration. Typed effect/filter models: VideoFilter, AudioFilter, VideoFilterMask with FilterParameter support and keyframe animation (auxValue support FCPXML 1.11+). Typed caption/title models: Caption and Title with TextStyle and TextStyleDefinition for full text formatting. SmartCollection models: SmartCollection with match-clip, match-media, match-ratings, match-text, match-usage (1.9+), match-representation (1.10+), match-markers (1.10+), match-analysis-type (1.14). Keyframe animation: KeyframeAnimation, Keyframe with interpolation types, FadeIn/FadeOut with fade types, integrated with FilterParameter. CMTime Codable extension: Direct CMTime encoding/decoding as FCPXML time strings. Collection organization: CollectionFolder and KeywordCollection models for organizing clips and media. Live Drawing (FCPXML 1.11+): LiveDrawing model for live-drawing story elements. HiddenClipMarker (FCPXML 1.13+): HiddenClipMarker model for hidden clip markers. Format/Asset 1.13+: Format heroEye, Asset heroEyeOverride, Asset mediaReps (multiple media-rep). Cross-platform XML abstraction: protocol layer (OFKXMLNode, OFKXMLElement, OFKXMLDocument, OFKXMLFactory); Foundation backend on macOS (unchanged behaviour); AEXML backend on iOS; OFKXMLDefaultFactory() for platform dispatch; FCPXMLStructuralValidator for cross-platform structural validation; FCPXMLDTDValidator is platform-conditional (full DTD on macOS, structural fallback on iOS). Comprehensive test coverage: **1078** tests across 60 FCPXML sample files including 360 video, auditions, conform-rate, still images, multicam, secondary storylines, audio keyframes (FCPXMLAudioKeyframeTests: adjust-volume param keyframeAnimation parsing, decibel/time validation, fadeIn/fadeOut integration, secondary storyline detection), keyword collections/folders, Photoshop integration, smart collections, and reporting column layout/exclusion/disabled-clip/workbook/PDF formatting tests. Excel and PDF reporting: multi-sheet `.xlsx` workbooks via `FinalCutPro.FCPXML.buildReport(options:)` (ReportBuilder, ReportOptions presets, ReportExcelExport on XLKit) and optional `.pdf` via `ReportPDFExport` (CoreGraphics; cover page with black “About This PDF Export” + `info.circle`, TOC with accent colour chips + content-tint washes, per-sheet tints, column-width expansion after exclusions, section pagination; same Report configuration as Excel); sheets for Role Inventory (**Selected Roles Inventory** + per-role sheets with expanded column layout and dynamic metadata keys), Markers, Keywords, Titles & Generators, Transitions, Video & Audio Effects, Speed Change Effects, **Summary** (project title in **B1**, narrow Row column A, black role-duration data), and **Media Summary** (Row + red missing-media paths); 1-based **Row** on all tabular Excel/PDF sheets by default (`ensuringRowColumn` / `allowsInjectedRowColumn`); inventory and section-sheet cell formatting, role exclusions, global column exclusion (`ReportColumn` / `excludedColumns`, including `ReportColumn.row`), disabled-clip filtering (`excludeDisabledClips`), project-name / compound-clip-name filtering (`allReportTimelineSources()`; standalone compound-clip exports without ``), `ReportTimecodeFormat` / `--timecode-format`, inventory-first `ReportBuildPhase` progress callbacks; optional `copyrightLabel` / CLI `--label-copyright` (Excel cover **A2**; PDF cover + footer centre). Extraction presets: Captions, Effects, FrameData, Markers, Roles, Titles. Experimental CLI (OpenFCPXMLKit-CLI): single binary with embedded DTDs; --check-version, --convert-version (stripping + DTD validation), --extension-type (fcpxmld | fcpxml; default fcpxmld; 1.5–1.9 always .fcpxml), --validate, --media-copy, --create-project (new empty FCPXML project: --width, --height, --rate, --project-version, output-dir; DTD validation before write; FCP-style output with DOCTYPE, colorSpace, default smart collections), --report (Excel report: role inventory by default; --report-full, per-section flags including --report-markers, --report-keywords, --report-titles-generators, --report-transitions, --report-effects, --report-speed-change-effects, --report-summary, --report-media-summary, --media-resolution, --media-summary-distinguish-proxy, --exclude-role, --exclude-column, --exclude-disabled-clips, --include-markers-outside-clip-boundaries, --protect-sheets, --timecode-format, --report-project, --label-copyright, --create-pdf); --log writes user-visible output for all commands to the log file; see Sources/OpenFCPXMLKitCLI/README.md. +Current status: **1114** tests listed in `swift test list` (**1108** in `OpenFCPXMLKitTests` + **6** optional `ExcelReportTest`; all Swift Testing `@Test`); FCPXML versions 1.5–1.14 supported (DTDs included, full parsing, typed element-type coverage for all DTD elements via FCPXMLElementType); Final Cut Pro frame rates (23.976, 24, 25, 29.97, 30, 50, 59.94, 60); thread-safe and concurrency-compliant with comprehensive async/await support; no known security vulnerabilities. Version conversion automatically drops elements not in the target version’s DTD (e.g. adjust-colorConform, adjust-stereo-3D); DTD validation runs per version (validateDocumentAgainstDTD, validateDocumentAgainstDeclaredVersion) and after CLI convert. FCPXMLVersion.supportsBundleFormat is true for 1.10+ (.fcpxmld bundle); 1.5–1.9 support only single-file .fcpxml. FCPXML creation: create FCPXML documents from scratch with events, projects, resources, and clips via XMLDocumentManager, XMLDocument initializers, or FCPXMLService. Timeline manipulation: ripple insert (shifts subsequent clips), auto lane assignment, clip queries (by lane, time range, asset ID), lane range computation, secondary storylines. Timeline metadata: markers, chapter markers, keywords, ratings, custom metadata, timestamps (createdAt, modifiedAt). FCPXMLTimecode: custom timecode type (arithmetic, frame alignment, CMTime conversion, FCPXML string parsing). MIME type detection, asset validation, silence detection, asset duration measurement, parallel file I/O, still image asset support. TimelineFormat enhancements: presets (hd720p, dci4K, hd1080i, hd720i), computed properties (aspectRatio, isHD, isUHD, interlaced). Typed adjustment models: Crop, Corners, Transform, Blend, Stabilization, Volume, Panner, Loudness, NoiseReduction, HumReduction, Equalization, MatchEqualization, Transform360, ColorConform, Stereo3D, VoiceIsolation with full clip integration. Typed effect/filter models: VideoFilter, AudioFilter, VideoFilterMask with FilterParameter support and keyframe animation (auxValue support FCPXML 1.11+). Typed caption/title models: Caption and Title with TextStyle and TextStyleDefinition for full text formatting. SmartCollection models: SmartCollection with match-clip, match-media, match-ratings, match-text, match-usage (1.9+), match-representation (1.10+), match-markers (1.10+), match-analysis-type (1.14). Keyframe animation: KeyframeAnimation, Keyframe with interpolation types, FadeIn/FadeOut with fade types, integrated with FilterParameter. CMTime Codable extension: Direct CMTime encoding/decoding as FCPXML time strings. Collection organization: CollectionFolder and KeywordCollection models for organizing clips and media. Live Drawing (FCPXML 1.11+): LiveDrawing model for live-drawing story elements. HiddenClipMarker (FCPXML 1.13+): HiddenClipMarker model for hidden clip markers. Format/Asset 1.13+: Format heroEye, Asset heroEyeOverride, Asset mediaReps (multiple media-rep). Cross-platform XML abstraction: protocol layer (OFKXMLNode, OFKXMLElement, OFKXMLDocument, OFKXMLFactory); Foundation backend on macOS (unchanged behaviour); AEXML backend on iOS; OFKXMLDefaultFactory() for platform dispatch; FCPXMLStructuralValidator for cross-platform structural validation; FCPXMLDTDValidator is platform-conditional (full DTD on macOS, structural fallback on iOS). Comprehensive test coverage: **1108** tests across 60 FCPXML sample files including 360 video, auditions, conform-rate, still images, multicam, secondary storylines, audio keyframes (FCPXMLAudioKeyframeTests: adjust-volume param keyframeAnimation parsing, decibel/time validation, fadeIn/fadeOut integration, secondary storyline detection), keyword collections/folders, Photoshop integration, smart collections, and reporting column layout/exclusion/disabled-clip/workbook/PDF formatting tests. Excel and PDF reporting: multi-sheet `.xlsx` workbooks via `FinalCutPro.FCPXML.buildReport(options:)` (ReportBuilder, ReportOptions presets, ReportExcelExport on XLKit) and optional `.pdf` via `ReportPDFExport` (CoreGraphics; cover page with black “About This PDF Export” + `info.circle`, TOC with accent colour chips + content-tint washes, per-sheet tints, column-width expansion after exclusions, section pagination; same Report configuration as Excel); sheets for Role Inventory (**Selected Roles Inventory** + per-role sheets with expanded column layout and dynamic metadata keys), Markers, Keywords, Titles & Generators, Transitions, Video & Audio Effects, Speed Change Effects, **Summary** (project title in **B1**, narrow Row column A, black role-duration data), and **Media Summary** (Row + red missing-media paths); 1-based **Row** on all tabular Excel/PDF sheets by default (`ensuringRowColumn` / `allowsInjectedRowColumn`); inventory and section-sheet cell formatting, role exclusions, global column exclusion (`ReportColumn` / `excludedColumns`, including `ReportColumn.row`), disabled-clip filtering (`excludeDisabledClips`), project-name / compound-clip-name filtering (`allReportTimelineSources()`; standalone compound-clip exports without ``), `ReportTimecodeFormat` / `--timecode-format`, inventory-first `ReportBuildPhase` progress callbacks; optional `copyrightLabel` / CLI `--label-copyright` (Excel cover **A2**; PDF cover + footer centre). Extraction presets: Captions, Effects, FrameData, Markers, Roles, Titles. Experimental CLI (OpenFCPXMLKit-CLI): single binary with embedded DTDs; --check-version, --convert-version (stripping + DTD validation), --extension-type (fcpxmld | fcpxml; default fcpxmld; 1.5–1.9 always .fcpxml), --validate, --media-copy, --create-project (new empty FCPXML project: --width, --height, --rate, --project-version, output-dir; DTD validation before write; FCP-style output with DOCTYPE, colorSpace, default smart collections), --report (Excel report: role inventory by default; --report-full, per-section flags including --report-markers, --report-keywords, --report-titles-generators, --report-transitions, --report-effects, --report-speed-change-effects, --report-summary, --report-media-summary, --media-resolution, --media-summary-distinguish-proxy, --exclude-role, --exclude-column, --exclude-disabled-clips, --include-markers-outside-clip-boundaries, --protect-sheets, --timecode-format, --report-project, --label-copyright, --create-pdf); --log writes user-visible output for all commands to the log file; see Sources/OpenFCPXMLKitCLI/README.md. Xcode 26 dynamic linking compatibility: `swift-log` (`Logging`) is an explicit direct dependency in `Package.swift` to satisfy stricter transitive dylib linking rules when building OpenFCPXMLKit as a dynamic framework. @@ -55,7 +55,7 @@ The project was fully rewritten and refactored to achieve: - A protocol-oriented design: parsing, timecode conversion, XML manipulation, error handling, MIME type detection, asset validation, silence detection, asset duration measurement, and parallel file I/O are defined as protocols (e.g. FCPXMLParsing, TimecodeConversion, XMLDocumentOperations, ErrorHandling, MIMETypeDetection, AssetValidation, SilenceDetection, AssetDurationMeasurement, ParallelFileIO) with sync and async/await methods. - A single injection point for extension APIs that cannot take parameters: `FCPXMLUtility.defaultForExtensions` (concurrency-safe). No hidden concrete types in extensions; for custom services use the modular API with the `using:` parameter. -- Consistent source layout: Analysis, Classes, Delegates, Errors, Extensions (including +Modular and +Codable), Implementations, Protocols, Services, Utilities, Annotations, Export, Timeline, Timing, Validation, FileIO, Logging, Format, Model (with subfolders), Parsing, Extraction, Projection (TimelineProjector / MulticamProjection / RefClipProjection), **Reporting** (including **Excel/** for XLKit workbook export and **PDF/** for CoreGraphics PDF export), **XML** (Protocols: OFKXMLNode, OFKXMLElement, OFKXMLDocument, OFKXMLDTDProtocol, OFKXMLFactory; Foundation/: Foundation backends; AEXML/: AEXML backends; OFKXMLDefaultFactory), and FCPXML DTDs. +- Consistent source layout: Analysis, Classes, Delegates, Errors, Extensions (including +Modular and +Codable), Implementations, Protocols, Services, Utilities, Annotations, Export, Timeline, Timing, Validation, FileIO, Logging, Format, Model (with subfolders), Parsing, Extraction, Projection (TimelineProjector / MulticamProjection / RefClipProjection), Authoring (detached `FinalCutPro.FCPXML.Authoring` value graph), **Reporting** (including **Excel/** for XLKit workbook export and **PDF/** for CoreGraphics PDF export), **XML** (Protocols: OFKXMLNode, OFKXMLElement, OFKXMLDocument, OFKXMLDTDProtocol, OFKXMLFactory; Foundation/: Foundation backends; AEXML/: AEXML backends; OFKXMLDefaultFactory), and FCPXML DTDs. - A structured test suite: shared resources, file tests per sample, logic/parsing tests, timeline/export/validation tests, API and edge-case tests, and performance tests, all documented in Tests/README.md. Foundation XML types (XMLDocument, XMLElement) and the protocol types that wrap them (OFKXMLDocument, OFKXMLElement) and SwiftTimecode types are not Sendable. The codebase avoids Task-based concurrency for these types but provides async/await APIs that are concurrency-safe for Swift 6. If these dependencies become Sendable in the future, further parallelisation can be introduced. @@ -76,7 +76,7 @@ Foundation XML types (XMLDocument, XMLElement) and the protocol types that wrap - Versioning: FCPXMLVersion (DTD validation, 1.5-1.14) and FinalCutPro.FCPXML.Version (parsing, 1.0-1.14) are bridged via .fcpxmlVersion, .dtdVersion, and init(from:) converters. init(from:) uses safe fallback to `.latest` instead of force unwrap. - Errors: Module-scoped error types (FCPXMLError for parsing, FCPXMLLoadError for file I/O, FCPXMLExportError/FCPXMLBundleExportError for export, FinalCutPro.FCPXML.ParseError with LocalizedError). Parse failures from FCPXMLFileLoader surface as FCPXMLError.parsingFailed so consumers handle a single parse-error type. FCPXMLElementError uses String element names for Sendable compliance. - Reporting: Excel and PDF report builders in `Reporting/` consume Extraction/Model and **Projection**; they map facts to row/section models and serialise to XLKit workbooks (`Reporting/Excel/` via `ReportExcelExport`) and CoreGraphics PDFs (`Reporting/PDF/` via `ReportPDFExport`). Build `Report` once with `FinalCutPro.FCPXML.buildReport(options:)`; export to Excel, PDF, or both with the same section flags, `excludedColumns`, `timecodeFormat`, `copyrightLabel`, and role/disabled-clip filtering. Timeline resolution: `allReportTimelineSources()` / `ReportTimelineSource`; `ReportOptions.projectName` / CLI `--report-project`. Build / progress order: `ReportBuildPhase.enabledPhases(for:)` (inventory-first). Shared row colours: `FCPXMLReportRowColorPolicy` (Excel and PDF). PDF presentation: black cover header band with white `info.circle` + “About This PDF Export” (`FCPXMLReportPDFCoverNotes`); TOC accent colour chips + content-tint washes keyed to `FCPXMLReportPDFSheetPlan` sequential `colorIndex`; remaining table columns expand to fill A4 landscape `contentWidth` via `FCPXMLReportPDFTableLayout` after packing/`excludedColumns` (pinned/injected **Row** honors `allowsInjectedRowColumn`). Support: `RoleInventoryColumnLayout`, `ReportColumnExclusion`, `ReportFormatting`, `FCPXMLReportWorkbookExporter` (Excel cell formatting). Extend Model/Parsing → Extraction → **Projection** before Reporting presentation. See ARCHITECTURE.md §2.7. -- **Timeline Projection:** Mid-layer under `Sources/OpenFCPXMLKit/Projection/` projecting sequences into playable `MediaUsageWindow`s (media channels, `LanePath`, `RetimingSegment` from identity or `timeMap` segments including reverse; conform-rate scale via shared table; nested spines / anchored children; J/L cuts; multicam / ref-clip / audition unfold; video/audio leaves with channel filtering). Role Inventory, Markers, Keywords, Titles & Generators, Transitions, Effects, Speed Change, Media Summary, and Summary consume shared `ReportProjectionContext` windows (projected once per timeline); `TimelineOccupancyIndex` supports overlap queries. Excel/PDF stay presentation-thin. Markers/Keywords/Titles/Transitions/Effects are Projection-first with Extraction fallback. See ARCHITECTURE.md §2.7 and Manual 11. +- **Timeline Projection:** Mid-layer under `Sources/OpenFCPXMLKit/Projection/` projecting sequences into playable `MediaUsageWindow`s (media channels, `LanePath`, `RetimingSegment` from identity or `timeMap` segments including reverse; conform-rate scale via shared table; nested spines / anchored children; J/L cuts; multicam / ref-clip / audition unfold; video/audio leaves with channel filtering). Role Inventory, Markers, Keywords, Titles & Generators, Transitions, Effects, Speed Change, Media Summary, and Summary consume shared `ReportProjectionContext` windows (projected once per timeline); `TimelineOccupancyIndex` supports overlap queries. Excel/PDF stay presentation-thin. Markers/Keywords/Titles/Transitions/Effects are Projection-first with Extraction fallback. See ARCHITECTURE.md §2.7 and Manual 12. --- @@ -130,7 +130,8 @@ Rules: Source layout under Sources/OpenFCPXMLKit/: - Analysis: EditPoint (edit type, source relationship), CutDetectionResult (edit points and counts). -- Classes: FinalCutPro (namespace enum), FCPXML (core struct, init, properties including `allProjects`, `allTimelines`, `allReportTimelineSources` / `ReportTimelineSource` for project + standalone compound-clip report timelines), FCPXMLRoot, FCPXMLRootVersion, FCPXMLElementType, FCPXMLUtility, FCPXMLVersion. +- Classes: FinalCutPro (namespace enum), FCPXML (core struct, init, properties including `allProjects`, `allTimelines`, `allReportTimelineSources` / `ReportTimelineSource` for project + standalone compound-clip report timelines), FCPXMLRoot, FCPXMLRootVersion, FCPXMLElementType, FCPXMLUtility, FCPXMLVersion, FCPXMLVersionFeatureGate (shared DTD feature introductions; Authoring omit-on-write + converter fallback). +- Authoring: Detached (non-live) document value graph under `FinalCutPro.FCPXML.Authoring` — `Document`, `Resources` (Format/Asset/Effect/Media), story (`Library`→`Spine`), `SpineItem` (asset-clip, gap, title, transition, video, audio, caption, sync-clip, ref-clip, mc-clip, audition), `VersionAvailability` omit-on-write. Parallel to live Model/ and Timeline Export; do not use inside Reporting. See Manual 08. - XML: Platform-agnostic XML layer — Protocols (OFKXMLNode, OFKXMLElement, OFKXMLDocument, OFKXMLDTDProtocol, OFKXMLFactory), Foundation/ (FoundationXMLElement, FoundationXMLDocument, FoundationXMLDTD, FoundationXMLFactory), AEXML/ (AEXMLBackendElement, AEXMLBackendDocument, AEXMLBackendFactory), OFKXMLDefaultFactory (platform dispatch: Foundation on macOS, AEXML on iOS). - Delegates: AttributeParserDelegate (property: `values`), FCPXMLParserDelegate (properties: `roles`, `resourceIDs`, `textStyleIDs`; O(1) deduplication via Set). - Errors: FCPXMLError, FCPXMLParseError, TimelineError. @@ -148,10 +149,11 @@ Source layout under Sources/OpenFCPXMLKit/: - Media: MediaReference, MediaExtractionResult, MediaCopyResult. - Logging: ServiceLogger, ServiceLogLevel (trace–critical), NoOpServiceLogger, PrintServiceLogger, FileServiceLogger. - Format: ColorSpace. -- Model: FCPXML element models for the parsing layer (previously nested under FinalCutPro/FCPXML/). Subfolders: Adjustments (CropAdjustment, TransformAdjustment, BlendAdjustment, StabilizationAdjustment, VolumeAdjustment, LoudnessAdjustment, NoiseReductionAdjustment, HumReductionAdjustment, EqualizationAdjustment, MatchEqualizationAdjustment, Transform360Adjustment, ColorConformAdjustment, Stereo3DAdjustment, VoiceIsolationAdjustment), Animations (KeyframeAnimation, Keyframe, FadeIn, FadeOut, FadeType), Attributes (AudioLayout, AudioRate, ClipSourceEnable, FrameSampling, TimecodeFormat), Clips (AssetClip, Audio, Audition, Clip including Clip+Adjustments, Gap, MCClip, MulticamSource, RefClip, SyncClip, SyncSource, Title including Title+Typed, Transition, Video), CommonElements (AudioChannelSource, AudioRoleSource, ConformRate, MediaRep, Metadata, Text, TextStyle, TextStyleDefinition, TimeMap), ElementTypes (AnyElementModelType, ElementModelType, ElementType, protocols), Filters (VideoFilter, AudioFilter, VideoFilterMask, FilterParameter), Occlusion (ElementOcclusion, Element Occlusion), Protocols (FCPXMLElement, FCPXMLAttribute, element attribute/children/story protocols), Resources (Asset, Effect, Format, Locator, Media, MediaMulticam, ObjectTracker), Roles (AudioRole, CaptionRole, VideoRole, AncestorRoles, AnyRole, RoleType, FCPXMLRole), Structure (Event, Library, Project, CollectionFolder, KeywordCollection, SmartCollection). Root files: AnyTimeline, Caption including Caption+Typed, Keyword, Marker, Sequence, Spine. +- Model: FCPXML element models for the parsing layer (previously nested under FinalCutPro/FCPXML/). Subfolders: Adjustments (CropAdjustment, CornersAdjustment, TransformAdjustment, BlendAdjustment, StabilizationAdjustment, VolumeAdjustment, PannerAdjustment, LoudnessAdjustment, NoiseReductionAdjustment, HumReductionAdjustment, EqualizationAdjustment, MatchEqualizationAdjustment, Transform360Adjustment, ColorConformAdjustment, Stereo3DAdjustment, VoiceIsolationAdjustment), Animations (KeyframeAnimation, Keyframe, FadeIn, FadeOut, FadeType), Attributes (AudioLayout, AudioRate, ClipSourceEnable, FrameSampling, TimecodeFormat), Clips (AssetClip, Audio, Audition, Clip including Clip+Adjustments, Gap, MCClip, MulticamSource, RefClip, SyncClip, SyncSource, Title including Title+Typed, Transition, Video), CommonElements (AudioChannelSource, AudioRoleSource, ConformRate, MediaRep, Metadata, Text, TextStyle, TextStyleDefinition, TimeMap), ElementTypes (AnyElementModelType, ElementModelType, ElementType, protocols), Filters (VideoFilter, AudioFilter, VideoFilterMask, FilterParameter), Occlusion (ElementOcclusion, Element Occlusion), Protocols (FCPXMLElement, FCPXMLAttribute, element attribute/children/story protocols), Resources (Asset, Effect, Format, Locator, Media, MediaMulticam, ObjectTracker), Roles (AudioRole, CaptionRole, VideoRole, AncestorRoles, AnyRole, RoleType, FCPXMLRole), Structure (Event, Library, Project, CollectionFolder, KeywordCollection, SmartCollection). Root files: AnyTimeline, Caption including Caption+Typed, Keyword, Marker, Sequence, Spine. - Parsing: XML parsing extensions (Attributes, Clip Parsing, Elements Parsing, Metadata Parsing, Resources Parsing, Roles Parsing, Root Parsing, Time and Frame Rate Parsing). - Extraction: Element extraction logic. Subfolders: Context (DisplayClipName, ElementContext, ElementContextItems, ElementContextTools, FrameRateSource), Effects (EffectsCollector, ExtractedEffect), Presets (CaptionsExtractionPreset, EffectsExtractionPreset, FrameDataPreset, MarkersExtractionPreset, RolesExtractionPreset, TitlesExtractionPreset, FCPXMLExtractionPreset). Root files: Extract, ExtractableChildren, ExtractedElement, ExtractedElementStruct, ExtractedModelElement, Extraction, ExtractionScope. -- Projection: Timeline analysis mid-layer. `TimelineProjecting`, `TimelineProjector`, `TimelineProjectionOptions`, `MediaChannel`, `MediaUsageWindow`, `LanePath`, `RetimingSegment`; Retiming/`TimeMap+RetimingSegments`, `ConformRate+Retiming`, `ClipRetiming`, `AudioSplitRetiming`; Walk/`AssetChannelExpansion`, `SpineProjection`, `ProjectionTiming`, `MulticamProjection`, `RefClipProjection`, `ChannelKindFilter`. Nested/multicam/ref/audition unfold + channel filtering. Reporting: `ReportProjectionContext` / `ReportBuilder` project-once for inventory, markers, keywords, titles, transitions, effects, speed-change, media summary, and summary. See ARCHITECTURE.md §2.7. +- Projection: Timeline analysis mid-layer. `TimelineProjecting`, `TimelineProjector`, `TimelineProjectionOptions` (incl. `.trackAnalysis`), `MediaChannel`, `MediaUsageWindow`, `LanePath`, `RetimingSegment` (`clipped`, `composing`), `TimelineOccupancyIndex` (start-sorted overlap); Retiming/`TimeMap+RetimingSegments`, `ConformRate+Retiming`, `ClipRetiming`, `AudioSplitRetiming`; Walk/`AssetChannelExpansion`, `SpineProjection`, `ProjectionTiming`, `MulticamProjection`, `RefClipProjection`, `ChannelKindFilter`. Nested/multicam/ref/audition unfold + channel filtering. Reporting: `ReportProjectionContext` / `ReportBuilder` project-once for inventory, markers, keywords, titles, transitions, effects, speed-change, media summary, and summary. See ARCHITECTURE.md §2.7 and Manual 12. +- Authoring: Detached document value graph (`FinalCutPro.FCPXML.Authoring`) — `Document`, resources/story structs, `Element` encode protocol, `VersionAvailability` omit-on-write; parallel to live Model wrappers; not for Reporting. - Reporting: Excel and PDF report export. Top-level: Report, ReportOptions (including `copyrightLabel`), ReportBuilder (resolves timelines via `allReportTimelineSources()` / `ReportTimelineSource`), ReportTimecodeFormat, ReportBuildProgress (`ReportBuildPhase.enabledPhases(for:)` — inventory-first). Subfolders: Builders (RoleInventory, Markers, Keywords, Titles, Transitions, Effects, SpeedChangeEffects, Summary, MediaSummary), Sections and Rows (typed section/row models with `columnHeaders(timecodeFormat:)`), Support (RoleInventoryClipCollector, RoleInventoryRowBuilder, RoleInventoryColumnLayout, RoleInventoryRoleSheetOrdering, RoleInventoryTimelineBounds, ReportFormatting, ReportRoleExclusion, ReportColumnExclusion (`ensuringRowColumn` / `allowsInjectedRowColumn`), ReportClipCategory, FCPXMLReportRowColorPolicy, EffectsReportPolicy, SpeedChangeFormatting, SummaryRoleDurationAggregator), Excel (ReportExcelExport, FCPXMLReportWorkbookExporter with Summary title in **B1**, cover **A1** branding / **A2** `copyrightLabel`, ReportWorkbookColumnAutoFit via XLKit — narrow Row column), PDF (ReportPDFExport, FCPXMLReportPDFExporter, FCPXMLReportPDFCanvas (cover black header + info.circle; branding + optional `copyrightLabel`; TOC accent chips + content-tint washes; footer centre copyright), FCPXMLReportPDFSheetPlan sequential `colorIndex` per sheet title, FCPXMLReportPDFTableLayout pack + expand columns to fill `contentWidth` after exclusions, FCPXMLReportPDFStyle, FCPXMLReportPDFCoverNotes). `Report.exportBrandingText` and `Report.copyrightLabel` for Excel cover and PDF cover/footer. Consumes Extraction and Projection; owns presentation only; see ARCHITECTURE.md §2.7. - FCPXML DTDs: version 1.5-1.14 and README. @@ -212,7 +214,7 @@ Tests live under Tests/. The suite is organised as follows. - FCPXMLAPIAndEdgeCaseTests: FCPXMLFileLoader async load(from:), ServiceLogger injection (NoOp, Print), edge cases (empty/invalid/malformed XML, invalid paths), validation types, Live Drawing (1.11+), HiddenClipMarker (1.13+). - FCPXMLPerformanceTests: Performance smoke (`ContinuousClock` sanity budgets for parse / load / project — hang guards, not XCTest baselines). -Test organisation: use descriptive `@Test` names; group related tests in `@Suite`s; use meaningful `#expect` / `#require` assertions. The suite is **Swift Testing only** (no XCTest). Test all supported frame rates (Final Cut Pro compatible). Use realistic FCPXML samples and edge cases; validate against actual FCP behaviour where applicable. Current total: **1084** tests listed in `swift test list` (**1078** in `OpenFCPXMLKitTests` + **6** optional `ExcelReportTest`) covering all functionality including async/await, timeline manipulation, metadata, timestamps, FCPXMLTimecode, MIME type detection, asset validation, silence detection, asset duration measurement, parallel file I/O, cut detection, version conversion stripping, per-version DTD validation, extract-then-copy (CLI --media-copy flow), synchronized clip matching, secondary storyline traversal, clip identification, URL resolution, version conversion edge cases, typed adjustment models (including Transform360, ColorConform, Stereo3D, VoiceIsolation), typed effect/filter models, typed caption/title models, smart collections (match-clip, match-media, match-ratings, match-text, match-usage, match-representation, match-markers, match-analysis-type), keyframe animation, audio keyframes (FCPXMLAudioKeyframeTests: adjust-volume param keyframeAnimation parsing, decibel/time validation, fadeIn/fadeOut integration), CMTime Codable extension, collection organization, Live Drawing (1.11+), HiddenClipMarker (1.13+), Format/Asset 1.13+ (heroEye, heroEyeOverride, mediaReps), FCPXMLExporter clip-level metadata export and XML declaration standalone="no", FCPXMLTimelineManipulationTests refactor (lock-based NowBox, do-catch for throwing APIs), Excel reporting (Selected Roles Inventory column layout, Summary/Media Summary split, ReportTimecodeFormat / format-aware headers, inventory-first ReportBuildPhase progress, global column exclusion, disabled-clip filtering, workbook cell formatting via FCPXMLReportWorkbookExporter), 360 video features, auditions, conform-rate, still images, multicam, secondary storylines, audio keyframes, keyword collections/folders, and Photoshop integration. +Test organisation: use descriptive `@Test` names; group related tests in `@Suite`s; use meaningful `#expect` / `#require` assertions. The suite is **Swift Testing only** (no XCTest). Test all supported frame rates (Final Cut Pro compatible). Use realistic FCPXML samples and edge cases; validate against actual FCP behaviour where applicable. Current total: **1114** tests listed in `swift test list` (**1108** in `OpenFCPXMLKitTests` + **6** optional `ExcelReportTest`) covering all functionality including async/await, timeline manipulation, metadata, timestamps, FCPXMLTimecode, MIME type detection, asset validation, silence detection, asset duration measurement, parallel file I/O, cut detection, version conversion stripping, per-version DTD validation, extract-then-copy (CLI --media-copy flow), synchronized clip matching, secondary storyline traversal, clip identification, URL resolution, version conversion edge cases, typed adjustment models (including Transform360, ColorConform, Stereo3D, VoiceIsolation), typed effect/filter models, typed caption/title models, smart collections (match-clip, match-media, match-ratings, match-text, match-usage, match-representation, match-markers, match-analysis-type), keyframe animation, audio keyframes (FCPXMLAudioKeyframeTests: adjust-volume param keyframeAnimation parsing, decibel/time validation, fadeIn/fadeOut integration), CMTime Codable extension, collection organization, Live Drawing (1.11+), HiddenClipMarker (1.13+), Format/Asset 1.13+ (heroEye, heroEyeOverride, mediaReps), FCPXMLExporter clip-level metadata export and XML declaration standalone="no", FCPXMLTimelineManipulationTests refactor (lock-based NowBox, do-catch for throwing APIs), Excel reporting (Selected Roles Inventory column layout, Summary/Media Summary split, ReportTimecodeFormat / format-aware headers, inventory-first ReportBuildPhase progress, global column exclusion, disabled-clip filtering, workbook cell formatting via FCPXMLReportWorkbookExporter), 360 video features, auditions, conform-rate, still images, multicam, secondary storylines, audio keyframes, keyword collections/folders, and Photoshop integration. --- @@ -233,7 +235,7 @@ SwiftTimecode usage: use `Timecode(.realTime(seconds: seconds), at: frameRate)` ## Testing Requirements -Test coverage: unit tests for all public APIs; integration tests for complex workflows; performance tests for time-critical operations; concurrency tests for async operations; test all supported frame rates (Final Cut Pro compatible). Current: **1084** tests listed in `swift test list` (including AEXML serialization parity, FCPXMLDTDValidatorTests, FCPXMLStructuralValidatorTests, FCPXMLReportTimecodeFormatTests, FCPXMLReportBuildPhaseTests, FCPXMLReportColumnExclusionTests, FCPXMLReportExcludeDisabledClipsTests, FCPXMLRoleInventoryColumnLayoutTests, FCPXMLReportExcelExportTests workbook cell formatting, FCPXMLReportPDFExportTests PDF export, FCPXMLReportPDFSheetPlanTests TOC colour-index parity, FCPXMLReportPDFTableLayoutTests column-width expansion, optional ExcelReportTest integration) covering all functionality including async/await, timeline manipulation, metadata, timestamps, FCPXMLTimecode, MIME type detection, asset validation, silence detection, asset duration measurement, parallel file I/O, version conversion, DTD validation, extract-then-copy flow, synchronized clip matching, secondary storyline traversal, clip identification, URL resolution, version conversion edge cases, typed adjustment models (including Transform360, ColorConform, Stereo3D, VoiceIsolation), typed effect/filter models, typed caption/title models, smart collections (match-clip, match-media, match-ratings, match-text, match-usage, match-representation, match-markers, match-analysis-type), keyframe animation, audio keyframes (FCPXMLAudioKeyframeTests: adjust-volume param keyframeAnimation parsing, decibel/time validation, fadeIn/fadeOut integration), CMTime Codable extension, collection organization, Live Drawing (1.11+), HiddenClipMarker (1.13+), Format/Asset 1.13+ (heroEye, heroEyeOverride, mediaReps), FCPXMLExporter clip-level metadata export and XML declaration standalone="no", FCPXMLTimelineManipulationTests (lock-based NowBox, do-catch), Excel reporting enhancements, PDF report export (TOC colour chips, column expansion after exclusions), 360 video features, auditions, conform-rate, still images, multicam, secondary storylines, audio keyframes, keyword collections/folders, and Photoshop integration. +Test coverage: unit tests for all public APIs; integration tests for complex workflows; performance tests for time-critical operations; concurrency tests for async operations; test all supported frame rates (Final Cut Pro compatible). Current: **1114** tests listed in `swift test list` (including AEXML serialization parity, FCPXMLDTDValidatorTests, FCPXMLStructuralValidatorTests, FCPXMLReportTimecodeFormatTests, FCPXMLReportBuildPhaseTests, FCPXMLReportColumnExclusionTests, FCPXMLReportExcludeDisabledClipsTests, FCPXMLRoleInventoryColumnLayoutTests, FCPXMLReportExcelExportTests workbook cell formatting, FCPXMLReportPDFExportTests PDF export, FCPXMLReportPDFSheetPlanTests TOC colour-index parity, FCPXMLReportPDFTableLayoutTests column-width expansion, optional ExcelReportTest integration) covering all functionality including async/await, timeline manipulation, metadata, timestamps, FCPXMLTimecode, MIME type detection, asset validation, silence detection, asset duration measurement, parallel file I/O, version conversion, DTD validation, extract-then-copy flow, synchronized clip matching, secondary storyline traversal, clip identification, URL resolution, version conversion edge cases, typed adjustment models (including Transform360, ColorConform, Stereo3D, VoiceIsolation), typed effect/filter models, typed caption/title models, smart collections (match-clip, match-media, match-ratings, match-text, match-usage, match-representation, match-markers, match-analysis-type), keyframe animation, audio keyframes (FCPXMLAudioKeyframeTests: adjust-volume param keyframeAnimation parsing, decibel/time validation, fadeIn/fadeOut integration), CMTime Codable extension, collection organization, Live Drawing (1.11+), HiddenClipMarker (1.13+), Format/Asset 1.13+ (heroEye, heroEyeOverride, mediaReps), FCPXMLExporter clip-level metadata export and XML declaration standalone="no", FCPXMLTimelineManipulationTests (lock-based NowBox, do-catch), Excel reporting enhancements, PDF report export (TOC colour chips, column expansion after exclusions), 360 video features, auditions, conform-rate, still images, multicam, secondary storylines, audio keyframes, keyword collections/folders, and Photoshop integration. Test data: use realistic FCPXML samples; include edge cases and error conditions; test all supported frame rates; validate against actual Final Cut Pro output where applicable. @@ -322,7 +324,7 @@ Keep AGENT.md and .cursorrules in sync. Both must reflect: changelog styling (CH - Architecture (protocols, implementations, extensions, service, utilities) and single injection point (FCPXMLUtility.defaultForExtensions). - Source layout (Analysis, Classes, Delegates, Errors, Extensions including +Modular and +Codable, Implementations, Protocols, Services, Utilities, Annotations, Export, Timeline, Timing, Validation, FileIO, Logging, Format, Model including Adjustments, Animations, Filters, Clips with +Adjustments and +Typed, CommonElements with TextStyle/TextStyleDefinition, Structure with CollectionFolder/KeywordCollection/SmartCollection, Parsing, Extraction, Projection (TimelineProjector / MulticamProjection / RefClipProjection / ChannelKindFilter), Reporting including Excel/ and PDF/, XML with Protocols/Foundation/AEXML/OFKXMLDefaultFactory, FCPXML DTDs; reporting vs core layers in ARCHITECTURE.md §2.7). - Test structure (Tests/ layout — **Swift Testing only**, no XCTest; harness `FCPXMLTestResources` / `FCPXMLTestSampleLoading` / `FCPXMLTestingSampleSupport`; FileTests/ including FCPXMLFileTest_GeneralDemo, FCPXMLFileTest_HiddenMarkers, FCPXMLFileTest_360Video, FCPXMLFileTest_AuditionSample, FCPXMLFileTest_ImageSample, FCPXMLFileTest_Multicam, FCPXMLFileTest_Photoshop, FCPXMLFileTest_SmartCollection, LogicAndParsing/ including FCPXMLFormatAssetTests, FCPXMLCutDetectionTests, FCPXMLVersionConversionTests, FCPXMLMediaExtractionTests, FCPXMLTimelineManipulationTests, FCPXMLTimecodeTests, FCPXMLMIMETypeDetectionTests, FCPXMLAssetValidationTests, FCPXMLSilenceDetectionTests, FCPXMLAssetDurationMeasurementTests, FCPXMLParallelFileIOTests, FCPXMLAudioEnhancementTests, FCPXMLTransform360Tests, FCPXMLCaptionTitleTests, FCPXMLKeyframeAnimationTests, FCPXMLAudioKeyframeTests, FCPXMLCMTimeCodableTests, FCPXMLCollectionTests, FCPXMLSmartCollectionTests, FCPXMLAdjustmentTests, FCPXMLFilterTests, FCPXMLImportOptionsTests, FCPXMLCodableTests, FCPXMLAEXMLSerializationParityTests, FCPXMLDTDValidatorTests, FCPXMLStructuralValidatorTests, OpenFCPXMLKitTests.swift, FCPXMLTimelineExportValidationTests, FCPXMLAPIAndEdgeCaseTests, FCPXMLPerformanceTests (`ContinuousClock` budgets), FCPXMLTimelineProjectionTests, FCPXMLProjectionCoverageTests, FCPXMLParsingCoverageTests, FCPXMLEngineHygieneTests, FCPXMLReportObligationCorpusTests, FCPXMLExtractionNestFidelityTests, FCPXMLRoleInheritanceMatrixTests, FCPXMLExtractionProjectionPolicyTests, FCPXMLMarkersKeywordsProjectionTests, FCPXMLTitlesProjectionTests, FCPXMLTransitionsProjectionTests, FCPXMLEffectsProjectionTests, FCPXMLSubmittedFCPXMLSmokeTests, and report/extraction tests (FCPXMLRoleInventoryReportTests, FCPXMLMarkersReportTests, FCPXMLKeywordsReportTests, FCPXMLTitlesReportTests, FCPXMLTransitionsReportTests, FCPXMLEffectsReportTests, FCPXMLSpeedChangeEffectsReportTests, FCPXMLSummaryReportTests, FCPXMLReportExcelExportTests, FCPXMLReportPDFExportTests, FCPXMLReportPDFSheetPlanTests, FCPXMLReportPDFTableLayoutTests, FCPXMLReportFormattingTests, FCPXMLReportRoleExclusionTests, FCPXMLReportTimecodeFormatTests, FCPXMLReportBuildPhaseTests, FCPXMLRoleDisplayPreferenceTests, FCPXMLRoleInventoryClipCollectorTests, FCPXMLRoleInventoryRoleSheetOrderingTests, FCPXMLSummaryRoleDurationAggregatorTests, FCPXMLEffectsReportPolicyTests, FCPXMLSpeedChangeFormattingTests, FCPXMLDisplayClipNameTests, FCPXMLTitleDisplayTests, FCPXMLExtractionScopeTests, FCPXMLExtractedElementTests, FCPXMLEffectsCollectorTests, FCPXMLRolesExtractionPresetTests, FCPXMLEffectAppleSuppliedTests, FCPXMLClipParsingCarriesAudioTests, FCPXMLTransformAdjustmentParsingTests, FCPXMLTransitionSpinePlacementTests); every test-case class is FCPXML-prefixed except the module-named umbrella OpenFCPXMLKitTests; empty timeline creation and project-creation export at different sizes and frame rates in FCPXMLTimelineExportValidationTests (clip-level metadata export, XML declaration standalone="no")). Cross-platform XML (OFKXML*, Foundation vs AEXML, FCPXMLStructuralValidator, iOS). Private `Tests/Submitted FCPXML/` inbox (gitignored contents; never commit private FCPXML to GitHub). -- FCPXML 1.5–1.14 and FCPXMLElementType; FCPXMLVersion.supportsBundleFormat (1.10+); version conversion with element stripping and per-version DTD validation; FCPXML creation from scratch; timeline manipulation (ripple insert, auto lane assignment, clip queries, lane range, secondary storylines); timeline metadata (markers, chapter markers, keywords, ratings, timestamps); FCPXMLTimecode custom type; MIME type detection; asset validation (including still images); silence detection; asset duration measurement; parallel file I/O; TimelineFormat enhancements; typed adjustment models (Crop, Transform, Blend, Stabilization, Volume, Loudness, NoiseReduction, HumReduction, Equalization, MatchEqualization, Transform360, ColorConform, Stereo3D, VoiceIsolation); typed effect/filter models (VideoFilter, AudioFilter, VideoFilterMask, FilterParameter with keyframe animation and auxValue 1.11+); typed caption/title models (Caption, Title with TextStyle, TextStyleDefinition); smart collections (SmartCollection with match-clip, match-media, match-ratings, match-text, match-usage, match-representation, match-markers, match-analysis-type); keyframe animation (KeyframeAnimation, Keyframe, FadeIn, FadeOut); audio keyframes (FCPXMLAudioKeyframeTests: adjust-volume param keyframeAnimation parsing, decibel/time validation, fadeIn/fadeOut integration); CMTime Codable extension; collection organization (CollectionFolder, KeywordCollection); Live Drawing (1.11+); HiddenClipMarker (1.13+); Format/Asset 1.13+ (heroEye, heroEyeOverride, mediaReps); **Timeline Projection** (`TimelineProjecting` / `TimelineProjector` / `MediaUsageWindow` / `ReportProjectionContext`; project-once; Projection-first Markers/Keywords/Titles/Transitions/Effects); experimental CLI (OpenFCPXMLKit-CLI, single binary, embedded DTDs, --check-version, --convert-version, --extension-type fcpxml|fcpxmld, --validate, --media-copy, --create-project with DTD validation and FCP-style output, --report Excel/PDF report with --report-full/per-section flags/--report-summary/--report-media-summary/--media-resolution/--media-summary-distinguish-proxy/--exclude-role/--exclude-column/--exclude-disabled-clips/--include-markers-outside-clip-boundaries/--protect-sheets/--timecode-format/--report-project/--label-copyright/--create-pdf, --log/--log-level/--quiet with log file capturing all command output); Excel and PDF reporting subsystem (`allReportTimelineSources` / compound-clip timelines) (Reporting/ builders, Sections/Rows, Support including ReportProjectionContext, RoleInventoryColumnLayout, ReportColumnExclusion, ReportFormatting, ReportTimecodeFormat, ReportBuildProgress, ReportMediaResolutionPolicy; Excel/ via XLKit; buildReport/ReportExcelExport/ReportPDFExport; Summary and Media Summary sheets; excludeDisabledClips/excludedColumns/timecodeFormat/copyrightLabel/includeMarkersOutsideClipBoundaries/protectSheets/mediaResolutionPolicy; `--label-copyright`; inventory-first enabledPhases with `.projecting`; PDF cover notes + TOC colour chips / SheetPlan colorIndex; TableLayout column expansion to contentWidth and Row inject); extraction presets (Captions, Effects, FrameData, Markers, Roles, Titles); Manual chapters including [11 — Timeline Projection](Documentation/Manual/11-Timeline-Projection.md), [16 — Cross-Platform & iOS](Documentation/Manual/16-Cross-Platform-iOS.md), [18 — CLI](Documentation/Manual/18-CLI.md), [19 — Reporting](Documentation/Manual/19-Reporting.md), [20 — Examples](Documentation/Manual/20-Examples.md); Tests/ExcelReportTest optional integration; Tests/Submitted FCPXML private inbox (gitignored; never commit private FCPXML); Final Cut Pro frame rates; Swift 6 concurrency (Sendable, async/await, CI strict-concurrency job); Xcode 26 dynamic framework linking compatibility via explicit `swift-log` (`Logging`) dependency in `Package.swift`; SwiftExtensions 3.0.0+ and SwiftSemanticVersion 1.0.0+ (`SemanticVersion` for `FinalCutPro.FCPXML.Version`). Current test count: **1084** listed in `swift test list` (`1078` OpenFCPXMLKitTests + `6` ExcelReportTest; **all Swift Testing**). +- FCPXML 1.5–1.14 and FCPXMLElementType; FCPXMLVersion.supportsBundleFormat (1.10+); version conversion with element stripping and per-version DTD validation; FCPXML creation from scratch; timeline manipulation (ripple insert, auto lane assignment, clip queries, lane range, secondary storylines); timeline metadata (markers, chapter markers, keywords, ratings, timestamps); FCPXMLTimecode custom type; MIME type detection; asset validation (including still images); silence detection; asset duration measurement; parallel file I/O; TimelineFormat enhancements; typed adjustment models (Crop, Corners, Transform, Blend, Stabilization, Volume, Panner, Loudness, NoiseReduction, HumReduction, Equalization, MatchEqualization, Transform360, ColorConform, Stereo3D, VoiceIsolation); typed effect/filter models (VideoFilter, AudioFilter, VideoFilterMask, FilterParameter with keyframe animation and auxValue 1.11+); typed caption/title models (Caption, Title with TextStyle, TextStyleDefinition); smart collections (SmartCollection with match-clip, match-media, match-ratings, match-text, match-usage, match-representation, match-markers, match-analysis-type); keyframe animation (KeyframeAnimation, Keyframe, FadeIn, FadeOut); audio keyframes (FCPXMLAudioKeyframeTests: adjust-volume param keyframeAnimation parsing, decibel/time validation, fadeIn/fadeOut integration); CMTime Codable extension; collection organization (CollectionFolder, KeywordCollection); Live Drawing (1.11+); HiddenClipMarker (1.13+); Format/Asset 1.13+ (heroEye, heroEyeOverride, mediaReps); **Timeline Projection** (`TimelineProjecting` / `TimelineProjector` / `MediaUsageWindow` / `ReportProjectionContext`; project-once; Projection-first Markers/Keywords/Titles/Transitions/Effects); experimental CLI (OpenFCPXMLKit-CLI, single binary, embedded DTDs, --check-version, --convert-version, --extension-type fcpxml|fcpxmld, --validate, --media-copy, --create-project with DTD validation and FCP-style output, --report Excel/PDF report with --report-full/per-section flags/--report-summary/--report-media-summary/--media-resolution/--media-summary-distinguish-proxy/--exclude-role/--exclude-column/--exclude-disabled-clips/--include-markers-outside-clip-boundaries/--protect-sheets/--timecode-format/--report-project/--label-copyright/--create-pdf, --log/--log-level/--quiet with log file capturing all command output); Excel and PDF reporting subsystem (`allReportTimelineSources` / compound-clip timelines) (Reporting/ builders, Sections/Rows, Support including ReportProjectionContext, RoleInventoryColumnLayout, ReportColumnExclusion, ReportFormatting, ReportTimecodeFormat, ReportBuildProgress, ReportMediaResolutionPolicy; Excel/ via XLKit; buildReport/ReportExcelExport/ReportPDFExport; Summary and Media Summary sheets; excludeDisabledClips/excludedColumns/timecodeFormat/copyrightLabel/includeMarkersOutsideClipBoundaries/protectSheets/mediaResolutionPolicy; `--label-copyright`; inventory-first enabledPhases with `.projecting`; PDF cover notes + TOC colour chips / SheetPlan colorIndex; TableLayout column expansion to contentWidth and Row inject); extraction presets (Captions, Effects, FrameData, Markers, Roles, Titles); Manual chapters including [08 — Detached Authoring](Documentation/Manual/08-Detached-Authoring.md), [12 — Timeline Projection](Documentation/Manual/12-Timeline-Projection.md), [17 — Cross-Platform & iOS](Documentation/Manual/17-Cross-Platform-iOS.md), [19 — CLI](Documentation/Manual/19-CLI.md), [20 — Reporting](Documentation/Manual/20-Reporting.md), [21 — Examples](Documentation/Manual/21-Examples.md); Tests/ExcelReportTest optional integration; Tests/Submitted FCPXML private inbox (gitignored; never commit private FCPXML); Final Cut Pro frame rates; Swift 6 concurrency (Sendable, async/await, CI strict-concurrency job); Xcode 26 dynamic framework linking compatibility via explicit `swift-log` (`Logging`) dependency in `Package.swift`; SwiftExtensions 3.0.0+ and SwiftSemanticVersion 1.0.0+ (`SemanticVersion` for `FinalCutPro.FCPXML.Version`). Current test count: **1114** listed in `swift test list` (`1108` OpenFCPXMLKitTests + `6` ExcelReportTest; **all Swift Testing**). When updating either file, apply the same information to both and keep terminology and examples consistent. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3f6f083..5ba54db 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2,7 +2,37 @@ A guide for contributors: project structure, architecture, naming, styling, and design decisions. -**See also:** [GUARDRAILS.md](GUARDRAILS.md) (must / must-not), [.cursorrules](.cursorrules), [AGENT.md](AGENT.md), [Tests/README.md](Tests/README.md). +**See also:** [GUARDRAILS.md](GUARDRAILS.md) (must / must-not), [.cursorrules](.cursorrules), [AGENT.md](AGENT.md), [Tests/README.md](Tests/README.md), [Documentation/Coverage.md](Documentation/Coverage.md) (element / layer inventory). + +--- + +## Table of Contents + +- [1. Project overview](#1-project-overview) +- [2. Architecture](#2-architecture) + - [2.1 Protocol-oriented design](#21-protocol-oriented-design) + - [2.2 Single injection point for extensions](#22-single-injection-point-for-extensions) + - [2.3 Facades](#23-facades) + - [2.4 Concurrency](#24-concurrency) + - [2.5 Cross-platform XML (iOS support)](#25-cross-platform-xml-ios-support) + - [2.6 Error handling](#26-error-handling) + - [2.7 Reporting and core layers](#27-reporting-and-core-layers) +- [3. Project structure](#3-project-structure) + - [3.1 Codebase map](#31-codebase-map) + - [3.2 Library folders](#32-library-folders) +- [4. Naming conventions](#4-naming-conventions) + - [4.1 Swift identifiers](#41-swift-identifiers) + - [4.2 File names](#42-file-names) + - [4.3 Special file names (collision avoidance)](#43-special-file-names-collision-avoidance) +- [5. Code style & file header](#5-code-style--file-header) + - [5.1 Swift style](#51-swift-style) + - [5.2 File header](#52-file-header-required-for-new-swift-files) + - [5.3 Documentation](#53-documentation) +- [6. Design decisions](#6-design-decisions) +- [7. CLI](#7-cli) +- [8. Tests](#8-tests) +- [9. Git & quality](#9-git--quality) +- [10. References](#10-references) --- @@ -16,7 +46,7 @@ OpenFCPXMLKit is a **Swift 6** framework for Final Cut Pro FCPXML: parsing, crea - **Repository:** https://github.com/TheAcharya/OpenFCPXMLKit - **Dependencies:** SwiftTimecode 3.1.2+, SwiftExtensions 3.0.0+, SwiftSemanticVersion 1.0.0+, swift-log 1.14.0+, AEXML 4.7.0+, swift-argument-parser 1.8.2+ (CLI only), Foundation, CoreMedia. - **FCPXML:** Versions 1.5–1.14 (DTDs included); Final Cut Pro frame rates (23.976, 24, 25, 29.97, 30, 50, 59.94, 60). -- **Tests:** **1084** tests listed in `swift test list` — **1078** in `OpenFCPXMLKitTests` + **6** optional `ExcelReportTest` (all Swift Testing `@Test`; no XCTest); **60** sample `.fcpxml` files under `Tests/FCPXML Samples/FCPXML/`; private local inbox under `Tests/Submitted FCPXML/` (gitignored — never commit private FCPXML). +- **Tests:** **1114** tests listed in `swift test list` — **1108** in `OpenFCPXMLKitTests` + **6** optional `ExcelReportTest` (all Swift Testing `@Test`; no XCTest); **60** sample `.fcpxml` files under `Tests/FCPXML Samples/FCPXML/`; private local inbox under `Tests/Submitted FCPXML/` (gitignored — never commit private FCPXML). --- @@ -93,7 +123,7 @@ Projection/ TimelineProjection → MediaUsageWindow Reporting/ Row models, builders, sheet-specific presentation rules ``` -**Timeline Projection:** Mid-layer under `Sources/OpenFCPXMLKit/Projection/` that projects sequences into playable **media usage windows** (channel, lane path, retiming): identity and `timeMap`/`conform-rate` retiming; nested spines / anchored children and J/L cuts; multicam active/all angles, ref-clip sequence unfold, audition mask, `video`/`audio` leaves, `ChannelKindFilter` / `srcEnable`. When any of Role Inventory, Markers, Keywords, Titles & Generators, Transitions, Effects, Speed Change, Media Summary, or Summary is enabled, `ReportBuilder` projects **once** per timeline and shares `ReportProjectionContext` (windows + `ProjectedClipAnnotations` + `TimelineOccupancyIndex`). Markers / Keywords / Titles / Transitions / Effects are **Projection-first** with Extraction fallback. Excel/PDF remain presentation-only. See Manual [11 — Timeline Projection](Documentation/Manual/11-Timeline-Projection.md). +**Timeline Projection:** Mid-layer under `Sources/OpenFCPXMLKit/Projection/` that projects sequences into playable **media usage windows** (channel, lane path, retiming): identity and `timeMap`/`conform-rate` retiming; nested spines / anchored children and J/L cuts; multicam active/all angles, ref-clip sequence unfold, audition mask, `video`/`audio` leaves, `ChannelKindFilter` / `srcEnable`. When any of Role Inventory, Markers, Keywords, Titles & Generators, Transitions, Effects, Speed Change, Media Summary, or Summary is enabled, `ReportBuilder` projects **once** per timeline and shares `ReportProjectionContext` (windows + `ProjectedClipAnnotations` + `TimelineOccupancyIndex`). Markers / Keywords / Titles / Transitions / Effects are **Projection-first** with Extraction fallback. Excel/PDF remain presentation-only. See Manual [12 — Timeline Projection](Documentation/Manual/12-Timeline-Projection.md). **1. Model and Parsing** — Add or extend typed coverage first: @@ -182,7 +212,7 @@ flowchart TB SRC --> CLI["OpenFCPXMLKitCLI → OpenFCPXMLKit-CLI"] SRC --> GEN["GenerateEmbeddedDTDs"] - TST --> OKT["OpenFCPXMLKitTests — 1078 Swift Testing"] + TST --> OKT["OpenFCPXMLKitTests — 1108 Swift Testing"] TST --> ERT["ExcelReportTest — 6 optional Swift Testing"] TST --> SMP["FCPXML Samples/ — 60 .fcpxml files"] TST --> SUB["Submitted FCPXML/ — private inbox gitignored"] @@ -205,10 +235,12 @@ flowchart TB UMB["OpenFCPXMLKitTests.swift — @Suite umbrella"] FILE["FileTests/ — per-sample suites"] LOGIC["LogicAndParsing/"] + AUTH["FCPXMLAuthoringTests · VersionFeatureGateTests"] PROJ["Projection + Extraction + Reporting suites"] PERF["FCPXMLPerformanceTests — ContinuousClock budgets"] UMB --- FILE UMB --- LOGIC + UMB --- AUTH UMB --- PROJ UMB --- PERF end @@ -238,6 +270,26 @@ flowchart TB REP["Reporting/ — ReportBuilder · Sections · Excel · PDF"] DTD --> XML --> PRS --> MDL --> EXT --> PRJ --> REP + + AUTH["Authoring/ — detached Document value graph · VersionAvailability"] + GATE["Classes/VersionFeatureGate — shared DTD feature registry"] + MDL -.->|"parallel create path"| AUTH + GATE --> AUTH + GATE -.->|"converter fallback"| PRS +``` + +#### Authoring (parallel create path — not in Reporting stack) + +```mermaid +flowchart LR + DOC["Authoring.Document"] + RES["Resources · Format · Asset · Effect · Media"] + LIB["Library · Event · Project · Sequence · Spine"] + ITEM["SpineItem — asset-clip · gap · title · transition · video · audio · caption · sync/ref/mc-clip · audition"] + XMLOut["makeXMLDocument / xmlString"] + + DOC --> RES + DOC --> LIB --> ITEM --> XMLOut ``` #### Reporting, Projection consume, and CLI @@ -246,14 +298,16 @@ flowchart TB flowchart TB subgraph PRJ_DETAIL["Projection/"] direction TB - P_API["TimelineProjecting · TimelineProjector · TimelineProjectionOptions"] - P_WIN["MediaUsageWindow · MediaChannel · LanePath · RetimingSegment"] + P_API["TimelineProjecting · TimelineProjector · TimelineProjectionOptions · trackAnalysis"] + P_WIN["MediaUsageWindow · MediaChannel · LanePath · RetimingSegment · clipped/composing"] P_WALK["Walk/ — Spine · Multicam · RefClip · ChannelKindFilter · ProjectionTiming"] P_RET["Retiming/ — TimeMap · ConformRate · ClipRetiming · AudioSplit"] + P_OCC["TimelineOccupancyIndex — start-sorted overlap"] P_ANN["WindowAnnotations · WindowAnnotationBuilder"] P_API --> P_WIN P_API --> P_WALK P_API --> P_RET + P_API --> P_OCC P_API --> P_ANN end @@ -316,7 +370,7 @@ flowchart TB end ``` -**Cross-cutting library folders** (alongside the layer stack): Analysis, Annotations, Classes, Delegates, Errors, Extensions (+Modular, +Codable), Implementations, Protocols, Services, Utilities, Export, Timeline, Timing, Validation, FileIO, Media, Logging, Format. Root: `Version.swift`. +**Cross-cutting library folders** (alongside the layer stack): Analysis, Annotations, Authoring, Classes (incl. `VersionFeatureGate`), Delegates, Errors, Extensions (+Modular, +Codable), Implementations, Protocols, Services, Utilities, Export, Timeline, Timing, Validation, FileIO, Media, Logging, Format. Root: `Version.swift`. **Tests** (see §8 and the Tests layout mermaid above): Swift Testing only; harness under `OpenFCPXMLKitTests/`; optional `ExcelReportTest/`; public `FCPXML Samples/`; private `Submitted FCPXML/`. @@ -327,7 +381,8 @@ Source layout under **`Sources/OpenFCPXMLKit/`**: | Folder | Purpose | |--------|---------| | **Analysis** | EditPoint, CutDetectionResult (cut detection). | -| **Classes** | FinalCutPro, FCPXML, FCPXMLElementType, FCPXMLUtility, FCPXMLVersion, FCPXMLRoot, FCPXMLRootVersion, FCPXMLInit, FCPXMLProperties (`allProjects`, `allTimelines`, `allReportTimelineSources` / `ReportTimelineSource` for project + standalone compound-clip report timelines). | +| **Authoring** | Detached (non-live) document value graph under `FinalCutPro.FCPXML.Authoring` — independent structs that encode/decode via explicit `Element` protocol + `VersionAvailability` omit-on-write (no Mirror/property-wrapper codecs; parallel to live `Model/` wrappers and `Export/` Timeline path). Spine coverage includes asset-clip, gap, title, transition, video, audio, caption, sync-clip, ref-clip, mc-clip, audition; resources include format/asset/effect/media (compound sequence + multicam). Do not use inside Reporting. Shared DTD feature introductions: ``FinalCutPro/FCPXML/VersionFeatureGate`` (also backs ``FCPXMLVersionConverter`` fallback strip lists). See Manual [08 — Detached Authoring](Documentation/Manual/08-Detached-Authoring.md). | +| **Classes** | FinalCutPro, FCPXML, FCPXMLElementType, FCPXMLUtility, FCPXMLVersion, FCPXMLVersionFeatureGate, FCPXMLRoot, FCPXMLRootVersion, FCPXMLInit, FCPXMLProperties (`allProjects`, `allTimelines`, `allReportTimelineSources` / `ReportTimelineSource` for project + standalone compound-clip report timelines). | | **Delegates** | AttributeParserDelegate, FCPXMLParserDelegate (internal). | | **Errors** | FCPXMLError, FCPXMLParseError, TimelineError. | | **Extensions** | CMTime, XMLElement, XMLDocument (+Modular, +Codable, and non-modular). FCPXML extensions operate on OFKXMLElement/OFKXMLDocument protocol types. | @@ -344,10 +399,10 @@ Source layout under **`Sources/OpenFCPXMLKit/`**: | **Logging** | ServiceLogger, ServiceLogLevel, NoOp/Print/FileServiceLogger. | | **Media** | MediaReference, MediaExtractionResult, MediaCopyResult. | | **Format** | ColorSpace. | -| **Model** | FCPXML element models: Adjustments, Animations, Attributes, Clips, CommonElements, ElementTypes, Filters, Occlusion, Protocols, Resources, Roles, Structure (CollectionFolder, KeywordCollection, etc.); `FCPXMLMarkerClipBoundary` (marker start vs host media range). | +| **Model** | FCPXML element models: Adjustments (incl. Corners, Panner), Animations, Attributes, Clips, CommonElements, ElementTypes, Filters, Occlusion, Protocols, Resources, Roles, Structure (CollectionFolder, KeywordCollection, etc.); `FCPXMLMarkerClipBoundary` (marker start vs host media range). | | **Parsing** | XML parsing extensions (Attributes, Clip, Elements, Metadata, Resources, Roles, Root, Time and Frame Rate). | | **Extraction** | `fcpExtract`, ExtractedElement, ExtractionScope, ExtractableChildren. **Context/** (DisplayClipName, ElementContext, ElementContextItems/Tools, FrameRateSource), **Effects/** (EffectsCollector, ExtractedEffect), **Presets/** (Captions, Effects, FrameData, Markers, Roles, Titles, plus the base ExtractionPreset). | -| **Projection** | Timeline analysis mid-layer. `TimelineProjecting`, `TimelineProjector`, `TimelineProjectionOptions`, `MediaChannel`, `MediaUsageWindow`, `LanePath`, `RetimingSegment`, `TimelineOccupancyIndex`; **Retiming/** + **Walk/** including `MulticamProjection`, `RefClipProjection`, `ChannelKindFilter`; **WindowAnnotations** / `WindowAnnotationBuilder` (markers include `isOutsideClipBoundaries`). Multicam/ref/audition unfold + nested lanes + J/L cuts. Reporting consume via `ReportProjectionContext` + `TimelineOccupancyIndex` (Role Inventory, Markers, Keywords, Titles, Transitions, Effects, Speed Change, Media Summary, Summary project-once; annotation sections Projection-first with Extraction fallback). | +| **Projection** | Timeline analysis mid-layer. `TimelineProjecting`, `TimelineProjector`, `TimelineProjectionOptions` (incl. `.trackAnalysis`), `MediaChannel`, `MediaUsageWindow`, `LanePath`, `RetimingSegment` (`clipped`, `composing`), `TimelineOccupancyIndex` (start-sorted overlap); **Retiming/** + **Walk/** including `MulticamProjection`, `RefClipProjection`, `ChannelKindFilter`; **WindowAnnotations** / `WindowAnnotationBuilder` (markers include `isOutsideClipBoundaries`). Multicam/ref/audition unfold + nested lanes + J/L cuts. Reporting consume via `ReportProjectionContext` + `TimelineOccupancyIndex` (Role Inventory, Markers, Keywords, Titles, Transitions, Effects, Speed Change, Media Summary, Summary project-once; annotation sections Projection-first with Extraction fallback). | | **Reporting** | Excel and PDF report export. Top-level: `Report`, `ReportOptions` (including `copyrightLabel`, `includeMarkersOutsideClipBoundaries`, `protectSheets`), `ReportBuilder`, `ReportTimecodeFormat` (`.smpteFrames` / `.frames` / `.feetAndFrames` / `.smpteNoFrames`), `ReportBuildProgress` (`ReportBuildPhase.enabledPhases(for:)` — inventory-first product order shared by builder, CLI, and GUI progress). **Builders/** — per-sheet builders including Markers (optional **Hidden** column), `MediaSummaryReportBuilder`, and `SummaryReportBuilder`. **Sections/** and **Rows/** — typed section/row models with `columnHeaders(timecodeFormat:)`. **Support/** — `ReportProjectionContext` / `TimelineOccupancyIndex`, collectors/layout/exclusion/formatting/row-colour helpers (`ensuringRowColumn` / `allowsInjectedRowColumn` — **Row** on all tabular Excel/PDF sheets). **Excel/** — `ReportExcelExport`, `FCPXMLReportWorkbookExporter` (Summary title in **B1**; optional worksheet protection when `protectSheets`), `ReportWorkbookColumnAutoFit`. **PDF/** — `ReportPDFExport` and layout helpers (ignores `protectSheets`). Timeline pick via `allReportTimelineSources()` (see §2.7). Consumes Extraction and Projection; owns presentation only. | | **XML** | Platform-agnostic XML layer: Protocols (OFKXMLNode, OFKXMLElement, OFKXMLDocument, OFKXMLDTDProtocol, OFKXMLFactory), Foundation/ (Foundation backends), AEXML/ (AEXML backends), OFKXMLDefaultFactory. | | **FCPXML DTDs** | Version 1.5–1.14 DTDs. | @@ -434,20 +489,20 @@ Source layout under **`Sources/OpenFCPXMLKit/`**: Binary name: **`OpenFCPXMLKit-CLI`**. Mutually exclusive modes: `--check-version`, `--convert-version`, `--extension-type` (fcpxmld | fcpxml), `--validate`, `--media-copy`, `--report`, `--create-project` (requires `--width`, `--height`, `--rate`, `--project-version`, output-dir). -**`--report`** builds an Excel workbook from a normal project **or** a standalone compound-clip export (role inventory by default — **Selected Roles Inventory** + per-role sheets). `--report-full` adds every optional sheet. Per-section flags: `--report-markers`, `--report-keywords`, `--report-titles-generators`, `--report-transitions`, `--report-effects`, `--report-speed-change-effects`, `--report-summary`, `--report-media-summary`. **`--create-pdf`** also writes a `.pdf` from the same built `Report` (sections, column exclusions, timecode format). Filtering: `--exclude-role` (repeatable), `--exclude-column` (repeatable; global column omission), `--exclude-disabled-clips` (omit `enabled="0"` clips), `--include-markers-outside-clip-boundaries` (out-of-bounds markers + Markers **Hidden** column), `--protect-sheets` (Excel worksheet edit lock on every sheet — not encryption; PDF unaffected), `--report-project` (project or compound-clip name), `--label-copyright`. Timecode cells: `--timecode-format` (`HH:MM:SS:FF` default, `Frames`, `Feet+Frames`, `HH:MM:SS`). Progress labels follow `ReportBuildPhase.enabledPhases(for:)` (inventory first), then Saving Workbook, then Saving PDF when `--create-pdf` is set. Log options: `--log`, `--log-level`, `--quiet`. See `Sources/OpenFCPXMLKitCLI/README.md` and `Documentation/Manual/18-CLI.md`. +**`--report`** builds an Excel workbook from a normal project **or** a standalone compound-clip export (role inventory by default — **Selected Roles Inventory** + per-role sheets). `--report-full` adds every optional sheet. Per-section flags: `--report-markers`, `--report-keywords`, `--report-titles-generators`, `--report-transitions`, `--report-effects`, `--report-speed-change-effects`, `--report-summary`, `--report-media-summary`. **`--create-pdf`** also writes a `.pdf` from the same built `Report` (sections, column exclusions, timecode format). Filtering: `--exclude-role` (repeatable), `--exclude-column` (repeatable; global column omission), `--exclude-disabled-clips` (omit `enabled="0"` clips), `--include-markers-outside-clip-boundaries` (out-of-bounds markers + Markers **Hidden** column), `--protect-sheets` (Excel worksheet edit lock on every sheet — not encryption; PDF unaffected), `--report-project` (project or compound-clip name), `--label-copyright`. Timecode cells: `--timecode-format` (`HH:MM:SS:FF` default, `Frames`, `Feet+Frames`, `HH:MM:SS`). Progress labels follow `ReportBuildPhase.enabledPhases(for:)` (inventory first), then Saving Workbook, then Saving PDF when `--create-pdf` is set. Log options: `--log`, `--log-level`, `--quiet`. See `Sources/OpenFCPXMLKitCLI/README.md` and `Documentation/Manual/19-CLI.md`. --- ## 8. Tests -- **Count:** **1084** listed in `swift test list` — **1078** in `OpenFCPXMLKitTests` + **6** in optional `ExcelReportTest` (all Swift Testing `@Test`; **no XCTest** in `Tests/`). ExcelReportTest **cancels** via `Test.cancel` without a local `.fcpxml`/`.fcpxmld` fixture. +- **Count:** **1114** listed in `swift test list` — **1108** in `OpenFCPXMLKitTests` + **6** in optional `ExcelReportTest` (all Swift Testing `@Test`; **no XCTest** in `Tests/`). ExcelReportTest **cancels** via `Test.cancel` without a local `.fcpxml`/`.fcpxmld` fixture. - **Framework:** Swift Testing exclusively (`@Suite` / `@Test` / `#expect` / `#require`). See GUARDRAILS Sign: `swift-testing-only`. - **Location:** `Tests/OpenFCPXMLKitTests/`; public samples in `Tests/FCPXML Samples/FCPXML/` (60 files, including `HiddenMarkers.fcpxml`); optional integration under `Tests/ExcelReportTest/`; private investigation inbox under `Tests/Submitted FCPXML/` (gitignored `Inbox/` / `Notes/` — never commit private FCPXML to GitHub; see `Tests/Submitted FCPXML/README.md`). - **Harness:** `FCPXMLTestResources.swift` (paths); `FCPXMLTestSampleLoading.swift` + `FCPXMLTestSampleError.swift` (`tryLoad*`); `FCPXMLTestingSampleSupport.swift` (`require*` — bundled samples **fail** if missing; optional fixtures use `Test.cancel`); `FCPXMLReportingReportFixture.swift` / `FCPXMLReportingReportTestSupport.swift` for optional reporting fixtures; `FCPXMLSubmittedFCPXMLSmokeTests` for optional Inbox parse smoke; `ExcelReportFixture` for the ExcelReportTest target. - **Performance:** `FCPXMLPerformanceTests` uses `ContinuousClock().measure` with generous sanity budgets (hang guards), not XCTest `measure` baselines. - **Reporting tests:** `FCPXMLCompoundClipReportTests` (standalone compound-clip FCPXML / `allReportTimelineSources()`), `FCPXMLMarkersReportTests` / `FCPXMLFileTest_HiddenMarkers` (out-of-bounds markers + **Hidden** column), `FCPXMLReportTimecodeFormatTests` (DF/NDF, all four formats, format-aware headers, full-report shape), `FCPXMLReportBuildPhaseTests` (inventory-first `enabledPhases` / `onPhaseStarted` order), `FCPXMLRoleInventoryColumnLayoutTests`, `FCPXMLReportColumnExclusionTests` (including `ensuringRowColumn` / `allowsInjectedRowColumn`, suffixed Timeline In headers, Row on all sheets), `FCPXMLReportExcludeDisabledClipsTests`, `FCPXMLReportExcelExportTests` (workbook cell formatting; Summary **B1**; section-sheet Row columns; **`protectSheets`** sheet protection), `FCPXMLReportPDFExportTests` (cover notes / black header + `info.circle`, TOC, section parity, pagination, branding), `FCPXMLReportPDFSheetPlanTests` (TOC accent chips share sequential `colorIndex` with content-page tints), `FCPXMLReportPDFTableLayoutTests` (remaining columns expand to fill page width after exclusions; pinned Row; `allowInjectedRowColumn`; horizontal chunks still fill `contentWidth`), `FCPXMLReportFormattingTests` (SMPTE / Frames / Feet+Frames / HH:MM:SS formatting and numeric sort guardrails), plus role inventory, section, Projection-first section tests, and related support tests. Optional `ExcelReportTest` writes `OFK-Default` / `OFK-Full` / `OFK-ExcludedColumns` / `OFK-Copyright` / `OFK-OutsideClipBoundaries` / `OFK-ProtectedSheets` among other outputs. See **Tests/README.md** for the full file tree. - **Coverage:** Unit, integration, and performance smoke tests; sync and async; all supported frame rates and FCPXML versions. See **Tests/README.md** for categories and how to run tests. -- **Manual cross-links:** [11 — Timeline Projection](Documentation/Manual/11-Timeline-Projection.md), [16 — Cross-Platform & iOS](Documentation/Manual/16-Cross-Platform-iOS.md), [18 — CLI](Documentation/Manual/18-CLI.md), [19 — Reporting](Documentation/Manual/19-Reporting.md), [20 — Examples](Documentation/Manual/20-Examples.md). +- **Manual cross-links:** [11 — Timeline Projection](Documentation/Manual/12-Timeline-Projection.md), [16 — Cross-Platform & iOS](Documentation/Manual/17-Cross-Platform-iOS.md), [18 — CLI](Documentation/Manual/19-CLI.md), [19 — Reporting](Documentation/Manual/20-Reporting.md), [20 — Examples](Documentation/Manual/21-Examples.md). --- diff --git a/CHANGELOG.md b/CHANGELOG.md index e82293e..6d73590 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,26 @@ OpenFCPXMLKit uses **New Features**, **Improvements**, and **Bug Fixes** for eac --- +## [3.2.0](https://github.com/TheAcharya/OpenFCPXMLKit/releases/tag/3.2.0) - 2026-07-19 + +### ✨ New Features + +- **Detached Authoring:** New `FinalCutPro.FCPXML.Authoring` value-graph layer for composing FCPXML without live XML ownership. `Authoring.Document` encode (`makeXMLDocument` / `xmlString`) and limited decode; omit-on-write via `VersionAvailability` / `VersionFeatureGate`. Spine items: asset-clip, gap, title, transition, video, audio, caption, sync-clip, ref-clip, mc-clip, audition; resources: format, asset, effect, media (compound sequence + multicam). Parallel to live `Model/` and `Timeline`/`Export/` — do not use inside Reporting. Tests: `FCPXMLAuthoringTests`. Manual: [08 — Detached Authoring](Documentation/Manual/08-Detached-Authoring.md). +- **Version feature gate:** Public `FinalCutPro.FCPXML.VersionFeatureGate` registry of DTD feature introductions (elements/attributes). Shared by Authoring omit-on-write and `FCPXMLVersionConverter` fallback strip lists. Tests: `FCPXMLVersionFeatureGateTests`. +- **Model adjustments:** Typed `CornersAdjustment` (`adjust-corners`) and `PannerAdjustment` (`adjust-panner`) with clip accessors; MatchProperty / smart-collection DTD rule gaps filled (`projection` / `stereoscopic` / `cinematic`, `isSet` / `isNotSet`). +- **Projection time algebra:** `RetimingSegment.clipped` / `composing(parents:children:)`; `TimelineOccupancyIndex` start-sorted binary-search overlap; `TimelineProjectionOptions.trackAnalysis` preset; audioStart-only J/L split handling in projection. Tests: `FCPXMLProjectionEdgeCaseCorpusTests` and related Projection suites. + +### 🔧 Improvements + +- **Manual reorder:** Inserted **08 — Detached Authoring**; subsequent chapters renumbered (**09–21**). Timeline Projection is **12**, CLI **19**, Reporting **20**, Examples **21**. Documentation hub, README, ARCHITECTURE (Mermaid + folder map), AGENT, `.cursorrules`, GUARDRAILS, and Tests READMEs updated. +- **Documentation sync:** Test counts refreshed to **1114** listed in `swift test list` (**1108** OpenFCPXMLKitTests + **6** ExcelReportTest); ARCHITECTURE Mermaid includes Authoring + VersionFeatureGate; Manual 06/12/14 examples updated for feature gate, occupancy/retiming, Corners/Panner; added [Documentation/Coverage.md](Documentation/Coverage.md) (detailed FCPXML layer matrices). + +### 🐛 Bug Fixes + +- None in this release. + +--- + ## [3.1.2](https://github.com/TheAcharya/OpenFCPXMLKit/releases/tag/3.1.2) - 2026-07-19 ### ✨ New Features @@ -53,7 +73,7 @@ OpenFCPXMLKit uses **New Features**, **Improvements**, and **Bug Fixes** for eac - **Titles & Generators → Projection:** Report builder prefers ``WindowTitleAnnotation`` collected during timeline Projection (title/generator story hosts, including titles with no nested story children). Extraction remains fallback when Projection has no title annotations. Tests: `FCPXMLTitlesProjectionTests`. - **Markers + Keywords → Projection:** Report builders prefer ``ProjectedClipAnnotations`` collected during timeline Projection (covers title-hosted markers such as BasicMarkers). Extraction remains fallback when Projection has no marker/keyword annotations. Tests: `FCPXMLMarkersKeywordsProjectionTests`. - **Timeline Projection:** New `Projection/` mid-layer between Extraction and Reporting. `TimelineProjecting` / `TimelineProjector` emit Sendable `MediaUsageWindow` values (per `MediaChannel`, `LanePath`, `RetimingSegment`) for visible usages with identity or `timeMap` retiming (normalized segments, reverse detection, multi-segment windows); `ConformRate` scale via shared `fcpConformRateScalingFactor`. Recursive story walk for nested spines and anchored children (`SpineProjection`, `ProjectionTiming`); J/L cuts via `AudioSplitRetiming`. Unfolds `mc-clip` angles (`MulticamProjection`), `ref-clip` media sequences (`RefClipProjection`), auditions, and `video`/`audio` leaves with `ChannelKindFilter` / `srcEnable`. Role Inventory, Speed Change, Media Summary, Effects, and Summary consume shared `ReportProjectionContext` windows (projected once per timeline); Speed Change prefers non-identity `RetimingSegment` facts; Role Inventory overlays timeline bounds from matching windows; Media Summary prefers window media URLs; Summary uses projection-backed spans; `TimelineOccupancyIndex` for overlap queries. Extraction remains the source for roles/metadata discovery where annotations are absent. -- **Reporting contracts:** Per-sheet obligation contracts (Manual 19); `ReportMediaResolutionPolicy` (`.failSoft` / `.failLoud`) with CLI `--media-resolution`; Media Summary optional Missing Original / Missing Proxy columns (`mediaSummaryDistinguishProxyAndOriginal`, `--media-summary-distinguish-proxy`). Tests: `FCPXMLReportObligationCorpusTests`. +- **Reporting contracts:** Per-sheet obligation contracts (Manual 20); `ReportMediaResolutionPolicy` (`.failSoft` / `.failLoud`) with CLI `--media-resolution`; Media Summary optional Missing Original / Missing Proxy columns (`mediaSummaryDistinguishProxyAndOriginal`, `--media-summary-distinguish-proxy`). Tests: `FCPXMLReportObligationCorpusTests`. - **Engine hygiene:** Project-once Projection contract for report sections that share windows; version-strip honesty (writers must not re-emit newer-schema facts into older targets); Double-safe Projection timing composition. Tests: `FCPXMLEngineHygieneTests` (project-once, strip honesty, Complex smoke budget) + Complex projection `measure` in `FCPXMLPerformanceTests`. ### 🔧 Improvements diff --git a/Documentation/Coverage.md b/Documentation/Coverage.md new file mode 100644 index 0000000..f06ac2a --- /dev/null +++ b/Documentation/Coverage.md @@ -0,0 +1,556 @@ +# OpenFCPXMLKit — FCPXML Coverage + +Living inventory of how OpenFCPXMLKit covers Final Cut Pro FCPXML across layers. Prefer this file when asking “is element *X* typed / authored / projected / reported?” Prefer [GUARDRAILS.md](../GUARDRAILS.md) for must / must-not, and [ARCHITECTURE.md](../ARCHITECTURE.md) §2.7 for where new work belongs. + +**Keep in sync** when adding Model types, Authoring encode/decode, Extraction presets, Projection walks, or Reporting sheets. Suite context: **1114** tests listed (`swift test list`); FCPXML **1.5–1.14**. + +**Related Manual:** [08 — Detached Authoring](Manual/08-Detached-Authoring.md) · [11 — Extraction](Manual/11-Extraction-Media.md) · [12 — Projection](Manual/12-Timeline-Projection.md) · [14 — Typed Models](Manual/14-Typed-Models.md) · [20 — Reporting](Manual/20-Reporting.md) + +--- + +## Table of Contents + +- [1. Legend & how to read this matrix](#1-legend--how-to-read-this-matrix) +- [2. Layer overview](#2-layer-overview) +- [3. Resources](#3-resources) +- [4. Structure (library → sequence)](#4-structure-library--sequence) +- [5. Story / spine items](#5-story--spine-items) +- [6. Adjustments](#6-adjustments) +- [7. Filters, masks & parameters](#7-filters-masks--parameters) +- [8. Animations & keyframes](#8-animations--keyframes) +- [9. Annotations (markers, keywords, text)](#9-annotations-markers-keywords-text) +- [10. Smart collections](#10-smart-collections) +- [11. Retiming & channels](#11-retiming--channels) +- [12. Version-gated features](#12-version-gated-features) +- [13. Extraction presets](#13-extraction-presets) +- [14. Projection walk](#14-projection-walk) +- [15. Reporting sheets](#15-reporting-sheets) +- [16. Authoring intentional gaps](#16-authoring-intentional-gaps) +- [17. Capability summary matrix](#17-capability-summary-matrix) +- [18. Appendix — full `FCPXMLElementType` catalogue](#18-appendix--full-fcpxmlelementtype-catalogue) + +--- + +## 1. Legend & how to read this matrix + +| Token | Meaning | +|-------|---------| +| **yes** | First-class typed API or full walk/export in that layer | +| **partial** | Some attributes / children / hosts only | +| **name** | Present in `FCPXMLElementType` (and usually in the live XML tree) but no dedicated typed Model wrapper beyond the enum / generic access | +| **—** | Not applicable, or not implemented in that layer | + +**Layer columns** + +| Column | Meaning | +|--------|---------| +| **Type** | `FCPXMLElementType` case (DTD inventory) | +| **Model** | Live typed wrapper under `Model/` (or Annotations creation twin) | +| **Auth** | Detached `FinalCutPro.FCPXML.Authoring` encode/decode | +| **Ext** | Extraction (`fcpExtract` / presets) | +| **Proj** | Timeline Projection (`TimelineProjector` walk / annotations) | +| **Rep** | Excel/PDF Reporting consumes the fact (sheet or inventory) | +| **Export** | `Timeline` / `FCPXMLExporter` path (creation-oriented) | + +**Two element enums** + +| Enum | Role | +|------|------| +| `FCPXMLElementType` | Full DTD inventory (~**115** named cases + `none`; **113** unique DTD tags + 2 inferred `media@*` kinds) | +| `FinalCutPro.FCPXML.ElementType` | Smaller live-model filter set — does **not** list every DTD name | + +**Important:** Every DTD element name is **identifiable** via `FCPXMLElementType`. That is not the same as a typed Model struct, Authoring support, or a report sheet. + +--- + +## 2. Layer overview + +```text +XML / DTD → Parsing → Model → Extraction → Projection → Reporting + ↘ + Authoring (parallel create; omit-on-write) + ↘ + Timeline / Export (parallel create) +``` + +| Layer | Owns | Must not | +|-------|------|----------| +| **Model / Parsing** | Typed facts, attributes, children | Report presentation | +| **Authoring** | Detached value graph → XML | Live wrappers; Reporting imports | +| **Extraction** | Discovery + context (roles, occlusion, presets) | Sheet layout | +| **Projection** | Playable occupancy, retiming, unfold | Excel/PDF styling | +| **Reporting** | Rows, columns, colours, workbooks/PDFs | New FCPXML semantics | +| **Timeline Export** | In-memory `Timeline` → FCPXML | Authoring types | + +--- + +## 3. Resources + +| Element | Type case | Model | Auth | Ext | Proj | Rep | Notes | +|---------|-----------|-------|------|-----|------|-----|-------| +| `fcpxml` | `fcpxml` | `Root` | **yes** `Document` | — | — | — | Root + version | +| `resources` | `resourceList` | via root | **yes** `Resources` | — | resolve | — | | +| `asset` | `assetResource` | `Asset` | **yes** `Asset` | — | resolve | Media Summary | Model: `heroEyeOverride` 1.13+, `mediaReps`; Auth: **partial** (no `heroEyeOverride`) | +| `format` | `formatResource` | `Format` | **yes** `Format` | — | — | — | Model: `heroEye` 1.13+; Auth: **partial** (no `heroEye`) | +| `media` | `mediaResource` | `Media` | **yes** `Media` | — | unfold | — | | +| `media`+`` | `multicamResource` (`media@multicam`) | `Media.Multicam` | **yes** `Multicam` | — | **yes** | — | Inferred kind | +| `media`+`` | `compoundResource` (`media@sequence`) | `Media` + sequence | **yes** `MediaSequence` | — | **yes** | — | Inferred kind | +| `effect` | `effectResource` | `Effect` | **yes** `Effect` | partial | partial | Effects names | Titles / transitions / filters `ref` | +| `locator` | `locator` | `Locator` | — | — | — | — | FCPXML 1.14+ | +| `media-rep` | `mediaRep` | `MediaRep` | **yes** `MediaRep` | — | URLs | Media Summary | | +| `metadata` / `md` | `metadata` / `md` | `Metadata` / `Metadatum` | — | — | — | Inventory dynamic keys | | +| `bookmark` | `bookmark` | protocol child | — | — | — | — | | +| `object-tracker` | `objectTracker` | `ObjectTracker` | — | — | — | — | Gate **1.10+** | +| `tracking-shape` | `trackingShape` | `TrackingShape` | — | — | — | — | | +| `import-options` / `option` | `importOptions` / `option` | `ImportOption` (value) | — | — | — | — | 1.14+ | +| `multicam` | `multicam` | `Media.Multicam` | **yes** | — | **yes** | — | Inside `media` | +| `mc-angle` | `mcAngle` | `Media.Multicam.Angle` | **yes** `MCAngle` | — | **yes** | — | | + +--- + +## 4. Structure (library → sequence) + +| Element | Type case | Model | Auth | Export | Notes | +|---------|-----------|-------|------|--------|-------| +| `library` | `library` | `Library` | **yes** | **yes** | | +| `event` | `event` | `Event` | **yes** | **yes** | | +| `project` | `project` | `Project` | **yes** | **yes** | Report timeline source | +| `sequence` | `sequence` | `Sequence` | **yes** | **yes** | Also inside compound `media` | +| `spine` | `spine` | `Spine` | **yes** | **yes** | Primary + nested | +| `collection-folder` | `folder` | `CollectionFolder` | — | — | | +| `keyword-collection` | `keywordCollection` | `KeywordCollection` | — | — | | +| `smart-collection` | `smartCollection` | `SmartCollection` | — | **yes** (optional defaults) | See §10 | + +--- + +## 5. Story / spine items + +| Element | Type case | Model | Auth | Ext | Proj | Rep | Notes | +|---------|-----------|-------|------|-----|------|-----|-------| +| `asset-clip` | `assetClip` | `AssetClip` | **yes** (+ volume / cinematic) | **yes** | **yes** leaf | Inventory / sections | | +| `clip` | `clip` | `Clip` (+ adj/filters) | — | **yes** | **yes** shell | via windows | Auth intentional gap | +| `ref-clip` | `compoundClip` | `RefClip` | **yes** | **yes** | **yes** unfold | Inventory | | +| `sync-clip` | `synchronizedClip` | `SyncClip` | **yes** | **yes** | **yes** shell | Inventory | | +| `sync-source` | `syncSource` | `SyncClip.SyncSource` | **yes** | — | — | — | | +| `mc-clip` | `multicamClip` | `MCClip` | **yes** | **yes** | **yes** unfold | Inventory | | +| `mc-source` | `mcSource` | `MulticamSource` | **yes** | — | **yes** | — | | +| `video` | `video` | `Video` | **yes** | **yes** | **yes** leaf | Inventory | | +| `audio` | `audio` | `Audio` | **yes** | **yes** | **yes** leaf | Inventory | | +| `audition` | `audition` | `Audition` | **yes** | **yes** | **yes** | Inventory | `.active` / `.all` options | +| `gap` | `gap` | `Gap` | **yes** | — | **yes** shell | — | No media windows | +| `title` | `title` | `Title` (+ typed) | **partial** attrs | **yes** Titles | **yes** annot | Titles sheet | Auth: no text styles | +| `transition` | `transition` | `Transition` | **partial** attrs | **yes** | **yes** annot | Transitions sheet | | +| `caption` | `caption` | `Caption` | **yes** (+ note) | **yes** Captions | skip leaf* | — | *Proj: not a media channel | +| `live-drawing` | `liveDrawing` | `LiveDrawing` | — | ? | — | — | Gate **1.11+** | + +### Authoring `SpineItem` cases + +| Case | Element | +|------|---------| +| `.assetClip` | `asset-clip` | +| `.gap` | `gap` | +| `.title` | `title` | +| `.transition` | `transition` | +| `.video` / `.audio` | `video` / `audio` | +| `.caption` | `caption` | +| `.syncClip` | `sync-clip` | +| `.refClip` | `ref-clip` | +| `.mcClip` | `mc-clip` | +| `.audition` | `audition` | + +**Not in Authoring `SpineItem`:** generic `clip`, `live-drawing`, markers/keywords, filter instances, most adjustments, `timeMap` / `conform-rate`. + +--- + +## 6. Adjustments + +All live models live under `Model/Adjustments/` and integrate via `Clip+Adjustments` (and related clip types) unless noted. + +| Element | Model | Auth | Ext / Effects collector | Proj annot | Gate | +|---------|-------|------|---------------------------|------------|------| +| `adjust-crop` | `CropAdjustment` (+ `crop-rect` / `trim-rect` / `pan-rect`) | — | **yes** | **yes** | — | +| `adjust-corners` | `CornersAdjustment` | — | **yes** | **yes** | — | +| `adjust-conform` | `ConformAdjustment` | — | **yes** | **yes** | — | +| `adjust-transform` | `TransformAdjustment` | — | **yes** | **yes** | — | +| `adjust-blend` | `BlendAdjustment` | — | **yes** | **yes** | — | +| `adjust-stabilization` | `StabilizationAdjustment` | — | **yes** | **yes** | — | +| `adjust-rollingShutter` | `RollingShutterAdjustment` | — | **yes** | **yes** | — | +| `adjust-360-transform` | `Transform360Adjustment` | — | **yes** | **yes** | — | +| `adjust-reorient` | `ReorientAdjustment` | — | **yes** | **yes** | — | +| `adjust-orientation` | `OrientationAdjustment` | — | **yes** | **yes** | — | +| `adjust-cinematic` | `CinematicAdjustment` | **yes** (on `AssetClip`) | **yes** | **yes** | **1.10+** | +| `adjust-colorConform` | `ColorConformAdjustment` | — | **yes** | **yes** | **1.11+** | +| `adjust-stereo-3D` | `Stereo3DAdjustment` | — | **yes** | **yes** | **1.13+** | +| `adjust-loudness` | `LoudnessAdjustment` | — | **yes** | **yes** | — | +| `adjust-noiseReduction` | `NoiseReductionAdjustment` | — | **yes** | **yes** | — | +| `adjust-humReduction` | `HumReductionAdjustment` | — | **yes** | **yes** | — | +| `adjust-EQ` | `EqualizationAdjustment` | — | **yes** | **yes** | — | +| `adjust-matchEQ` | `MatchEqualizationAdjustment` | — | **yes** | **yes** | — | +| `adjust-voiceIsolation` | `VoiceIsolationAdjustment` | — | **yes** | **yes** | **1.11+** (gate; also 1.14 docs elsewhere) | +| `adjust-volume` | `VolumeAdjustment` | **yes** | **yes** | **yes** | — | +| `adjust-panner` | `PannerAdjustment` | — | **yes** | **yes** | — | + +**Count:** 20 typed adjustments (+ nested crop rects / `Point` helper). +**Authoring:** only `adjust-volume` + `adjust-cinematic`. + +--- + +## 7. Filters, masks & parameters + +| Element | Model | Auth | Ext | Proj | Rep | +|---------|-------|------|-----|------|-----| +| `filter-video` | `VideoFilter` | — | **yes** Effects | **yes** annot | Effects sheet | +| `filter-audio` | `AudioFilter` | — | **yes** | **yes** | Effects sheet | +| `filter-video-mask` | `VideoFilterMask` | — | partial | partial | via Effects | +| `mask-shape` | `MaskShape` | — | — | — | — | +| `mask-isolation` | `MaskIsolation` | — | — | — | — | +| `param` | `FilterParameter` | — | **yes** | **yes** | — | +| `data` | keyed data on filters | — | — | — | — | +| `mute` | `Mute` | — | — | — | — | + +**EffectsCollector hosts (collection):** `title`, `asset-clip`, `sync-clip`, `ref-clip`, `mc-clip`, `clip`, `audio`, `video` +**EffectsExtractionPreset top-level hosts:** `title`, `asset-clip`, `sync-clip` (narrower than collector) + +--- + +## 8. Animations & keyframes + +| Element | Model | Auth | Notes | +|---------|-------|------|-------| +| `keyframeAnimation` | `KeyframeAnimation` | — | Under params / volume | +| `keyframe` | `Keyframe` | — | `auxValue` gated **1.11+** | +| `fadeIn` / `fadeOut` | `FadeIn` / `FadeOut` | — | | +| `param` + nested animation | `FilterParameter` | — | `auxValue` gated **1.11+** | + +Authoring does **not** encode keyframes or fades. Projection **retiming** (`timeMap` / conform) is separate from parameter keyframes (see §11). + +--- + +## 9. Annotations (markers, keywords, text) + +| Element | Model / Annotations | Auth | Ext | Proj | Rep | +|---------|---------------------|------|-----|------|-----| +| `marker` | `Marker` | — | **yes** Markers preset | **yes** | Markers (Proj-first) | +| `chapter-marker` | `Marker` / `ChapterMarker` | — | **yes** | **yes** | Markers | +| `analysis-marker` | `AnalysisMarker` | — | **yes** (Markers preset) | partial? | Markers? | +| `hidden-clip-marker` | `HiddenClipMarker` | — | — | — | — | Gate **1.13+**; ≠ Markers **Hidden** column | +| `keyword` | `Keyword` | — | context / Keywords | **yes** | Keywords (Proj-first) | +| `rating` | creation `Rating`; Model **name** | — | — | — | — | No dedicated sheet | +| `note` | child / attrs | **partial** (Caption) | — | — | — | +| `text` | `Text` | — | — | partial (title) | Titles | +| `text-style` / `text-style-def` | `TextStyle` / `TextStyleDefinition` | — | — | — | — | + +**Reporting vs DTD:** Markers sheet **Hidden** (✓/✗) means *start outside host media range* (`includeMarkersOutsideClipBoundaries`). It is **not** `hidden-clip-marker`. + +--- + +## 10. Smart collections + +| Element | Model | Auth | Min version | +|---------|-------|------|-------------| +| `smart-collection` | `SmartCollection` | — | — | +| `match-text` | `MatchText` | — | always | +| `match-ratings` | `MatchRatings` | — | always | +| `match-media` | `MatchMedia` | — | always | +| `match-clip` | `MatchClip` | — | always | +| `match-stabilization` | `MatchStabilization` | — | always | +| `match-keywords` / `keyword-name` | `MatchKeywords` / `KeywordName` | — | always | +| `match-shot` / `shot-type` | `MatchShot` / `ShotType` | — | always | +| `stabilization-type` | `StabilizationType` | — | always | +| `match-property` | `MatchProperty` | — | keys + `isSet`/`isNotSet` **1.11+** | +| `match-time` / `match-timeRange` | `MatchTime` / `MatchTimeRange` | — | always | +| `match-roles` / `role` | `MatchRoles` / `Role` | — | always | +| `match-usage` | `MatchUsage` | — | **1.9+** | +| `match-representation` | `MatchRepresentation` | — | **1.10+** | +| `match-markers` | `MatchMarkers` | — | **1.10+** | +| `match-analysis-type` | `MatchAnalysisType` | — | **1.14+** | + +`SmartCollectionRule` operators include includes / includesAny / includesAll / doesNotInclude* / is / isNot / isAfter / isBefore / isInLast / isNotInLast / startsWith / endsWith / **isSet** / **isNotSet**. + +--- + +## 11. Retiming & channels + +| Element | Model | Auth | Proj | Rep | +|---------|-------|------|------|-----| +| `conform-rate` | `ConformRate` | — | **yes** scale | Speed / Summary | +| `timeMap` / `timept` | `TimeMap` / `TimePoint` | — | **yes** segments | Speed Change sheet | +| `audio-channel-source` | `AudioChannelSource` | — | partial (expand) | Inventory channels | +| `audio-role-source` | `AudioRoleSource` | — | — | roles | + +**Projection APIs (not elements):** `RetimingSegment` (`clipped`, `composing`), `TimelineOccupancyIndex`, `AudioSplitRetiming` (J/L via `audioStart` / `audioDuration`), `TimelineProjectionOptions.trackAnalysis`. + +--- + +## 12. Version-gated features + +From `FinalCutPro.FCPXML.VersionFeatureGate` (Authoring omit-on-write + converter fallback when DTD allowlists unavailable). Prefer DTD allowlist stripping when DTDs are present. + +### Elements + +| Element | Min version | +|---------|-------------| +| `match-usage` | 1.9 | +| `object-tracker` | 1.10 | +| `adjust-cinematic` | 1.10 | +| `match-representation` | 1.10 | +| `match-markers` | 1.10 | +| `adjust-colorConform` | 1.11 | +| `adjust-voiceIsolation` | 1.11 | +| `live-drawing` | 1.11 | +| `adjust-stereo-3D` | 1.13 | +| `hidden-clip-marker` | 1.13 | +| `match-analysis-type` | 1.14 | + +### Attributes + +| Element | Attribute | Min version | +|---------|-----------|-------------| +| `param` | `auxValue` | 1.11 | +| `keyframe` | `auxValue` | 1.11 | +| `format` | `heroEye` | 1.13 | +| `asset` | `heroEyeOverride` | 1.13 | + +**Floor:** Entire codebase remains compatible with FCPXML **1.5** (omit or ignore newer optional features when targeting 1.5). + +--- + +## 13. Extraction presets + +| Preset | Extracts | +|--------|----------| +| `MarkersExtractionPreset` | `marker`, `chapter-marker`, `analysis-marker` → `ExtractedMarker` | +| `EffectsExtractionPreset` | effects on `title` / `asset-clip` / `sync-clip` → `ExtractedEffect` | +| `TitlesExtractionPreset` | `title` (main-timeline visibility rules) | +| `CaptionsExtractionPreset` | `caption` → `ExtractedCaption` | +| `RolesExtractionPreset` | inherited roles by `RoleType` | +| `FrameDataPreset` | clip occupancy / frame data | + +Scope flags (`ExtractionScope`, occlusion, audition/MC masks, `includeDisabled`) apply across presets. See Manual 11. + +--- + +## 14. Projection walk + +`SpineProjection` (and helpers) walk: + +| Story kind | Behaviour | +|------------|-----------| +| `asset-clip` / `video` / `audio` | Media leaves → `MediaUsageWindow` | +| Nested `spine` | Recurse | +| `audition` | Active or all (`TimelineProjectionOptions`) | +| `mc-clip` | Angle unfold (`MulticamProjection`) | +| `ref-clip` | Compound sequence unfold (`RefClipProjection`) | +| `title` / `transition` | Annotations (no media channel) | +| `clip` / `sync-clip` / `gap` | Shells: children + annotations | +| Bare markers / keywords / captions as top-level leaves | Skipped as media; annotations attached via hosts | + +Options presets: `.mainTimeline`, `.trackAnalysis`, `.forReport(...)`. + +--- + +## 15. Reporting sheets + +| Sheet | `ReportOptions` | Primary source | Fallback | +|-------|-----------------|----------------|----------| +| Selected Roles Inventory (+ per-role) | `includeRoleInventory` | **Projection** windows | Extraction clip walk | +| Markers | `includeMarkers` | **Projection** annotations | MarkersExtractionPreset | +| Keywords | `includeKeywords` | **Projection** | Extraction keyword walk | +| Titles & Generators | `includeTitlesAndGenerators` | **Projection** | TitlesExtractionPreset | +| Transitions | `includeTransitions` | **Projection** | Extraction | +| Video & Audio Effects | `includeEffects` | **Projection** annot | EffectsExtractionPreset | +| Speed Change Effects | `includeSpeedChangeEffects` | **Projection** retiming | Extraction `timeMap` | +| Summary | `includeSummary` | **Projection** + inventory agg | — | +| Media Summary | `includeMediaSummary` | **Projection** / media-reps | Document fallback | + +Cover / TOC are presentation-only (Excel XLKit / PDF CoreGraphics). Build once via `buildReport(options:)`; project-once when any consuming section is enabled (`ReportBuildPhase` includes `.projecting`). + +--- + +## 16. Authoring intentional gaps + +Detached Authoring is **incremental**. Documented non-goals / not-yet: + +| Area | Status | +|------|--------| +| Markers / chapter markers / keywords / ratings | **not modeled** | +| Most filters / masks | **not modeled** | +| Metadata / `md` / bookmark | **not modeled** | +| Generic `` | **not modeled** (prefer `asset-clip` / compounds) | +| Adjustments beyond volume + cinematic | **not modeled** | +| Keyframes / fades / `timeMap` / `conform-rate` | **not modeled** | +| Smart collections / `match-*` | **not modeled** | +| `live-drawing`, `locator`, `object-tracker` | **not modeled** | +| `format.heroEye`, `asset.heroEyeOverride` | Model **yes**; Auth **no** | +| Decode | **limited subset** round-trip | +| Use inside Reporting | **forbidden** (Sign: `authoring-not-in-reporting`) | + +--- + +## 17. Capability summary matrix + +| Capability | Auth | Model | Ext | Proj | Rep | Timeline Export | +|------------|------|-------|-----|------|-----|-----------------| +| Resources (format/asset/effect/media) | **yes** (no locator) | **yes** | — | resolve | Media Summary | **yes** | +| Library → spine | **yes** | **yes** | walk | walk | timeline sources | **yes** | +| Generic `` | — | **yes** | **yes** | **yes** shell | via windows | **yes** | +| Compounds / multicam / audition | **yes** | **yes** | **yes** | **yes** unfold | Inventory | partial | +| Captions | **yes** | **yes** | **yes** | skip leaf | — | ? | +| Titles / transitions | **partial** | **yes** | **yes** | **yes** annot | Titles / Transitions | **yes** | +| Adjustments | volume + cinematic | **all 20** | Effects | annot | Effects | — | +| Filters / masks | — | **yes** | Effects | annot | Effects | — | +| Parameter keyframes | — | **yes** | via params | ≠ retiming | — | — | +| `timeMap` / conform retiming | — | **yes** | partial | **yes** | Speed | — | +| Markers / keywords | — | **yes** | **yes** | **yes** | Markers / Keywords | **yes** on Timeline | +| Smart collections | — | **yes** | — | — | — | default set | +| Version omit-on-write / strip | **yes** gate | convert | — | — | — | — | + +--- + +## 18. Appendix — full `FCPXMLElementType` catalogue + +Cases from `Sources/OpenFCPXMLKit/Classes/FCPXMLElementType.swift` (plus `none` with no raw value). + +| Case | rawValue | +|------|----------| +| `none` | *(none)* | +| `fcpxml` | `fcpxml` | +| `importOptions` | `import-options` | +| `option` | `option` | +| `resourceList` | `resources` | +| `library` | `library` | +| `event` | `event` | +| `project` | `project` | +| `assetResource` | `asset` | +| `formatResource` | `format` | +| `mediaResource` | `media` | +| `effectResource` | `effect` | +| `locator` | `locator` | +| `multicamResource` | `media@multicam` *(inferred; tag `media`)* | +| `compoundResource` | `media@sequence` *(inferred)* | +| `mediaRep` | `media-rep` | +| `metadata` | `metadata` | +| `md` | `md` | +| `bookmark` | `bookmark` | +| `fadeIn` | `fadeIn` | +| `fadeOut` | `fadeOut` | +| `keyframeAnimation` | `keyframeAnimation` | +| `keyframe` | `keyframe` | +| `mute` | `mute` | +| `param` | `param` | +| `data` | `data` | +| `cropRect` | `crop-rect` | +| `trimRect` | `trim-rect` | +| `panRect` | `pan-rect` | +| `adjustCrop` | `adjust-crop` | +| `adjustCorners` | `adjust-corners` | +| `adjustConform` | `adjust-conform` | +| `adjustTransform` | `adjust-transform` | +| `adjustBlend` | `adjust-blend` | +| `adjustStabilization` | `adjust-stabilization` | +| `adjustRollingShutter` | `adjust-rollingShutter` | +| `adjust360Transform` | `adjust-360-transform` | +| `adjustReorient` | `adjust-reorient` | +| `adjustOrientation` | `adjust-orientation` | +| `adjustCinematic` | `adjust-cinematic` | +| `adjustColorConform` | `adjust-colorConform` | +| `adjustStereo3D` | `adjust-stereo-3D` | +| `adjustLoudness` | `adjust-loudness` | +| `adjustNoiseReduction` | `adjust-noiseReduction` | +| `adjustHumReduction` | `adjust-humReduction` | +| `adjustEQ` | `adjust-EQ` | +| `adjustMatchEQ` | `adjust-matchEQ` | +| `adjustVoiceIsolation` | `adjust-voiceIsolation` | +| `adjustVolume` | `adjust-volume` | +| `adjustPanner` | `adjust-panner` | +| `trackingShape` | `tracking-shape` | +| `objectTracker` | `object-tracker` | +| `audioChannelSource` | `audio-channel-source` | +| `audioRoleSource` | `audio-role-source` | +| `sequence` | `sequence` | +| `spine` | `spine` | +| `multicam` | `multicam` | +| `mcAngle` | `mc-angle` | +| `multicamClip` | `mc-clip` | +| `mcSource` | `mc-source` | +| `clip` | `clip` | +| `compoundClip` | `ref-clip` | +| `synchronizedClip` | `sync-clip` | +| `syncSource` | `sync-source` | +| `assetClip` | `asset-clip` | +| `audio` | `audio` | +| `video` | `video` | +| `liveDrawing` | `live-drawing` | +| `audition` | `audition` | +| `caption` | `caption` | +| `gap` | `gap` | +| `title` | `title` | +| `transition` | `transition` | +| `text` | `text` | +| `textStyleDef` | `text-style-def` | +| `textStyle` | `text-style` | +| `filterVideo` | `filter-video` | +| `filterVideoMask` | `filter-video-mask` | +| `maskShape` | `mask-shape` | +| `maskIsolation` | `mask-isolation` | +| `filterAudio` | `filter-audio` | +| `conformRate` | `conform-rate` | +| `timeMap` | `timeMap` | +| `timept` | `timept` | +| `marker` | `marker` | +| `rating` | `rating` | +| `keyword` | `keyword` | +| `analysisMarker` | `analysis-marker` | +| `hiddenClipMarker` | `hidden-clip-marker` | +| `chapterMarker` | `chapter-marker` | +| `note` | `note` | +| `keywordCollection` | `keyword-collection` | +| `folder` | `collection-folder` | +| `smartCollection` | `smart-collection` | +| `matchText` | `match-text` | +| `matchRatings` | `match-ratings` | +| `matchMedia` | `match-media` | +| `matchClip` | `match-clip` | +| `matchStabilization` | `match-stabilization` | +| `matchKeywords` | `match-keywords` | +| `keywordName` | `keyword-name` | +| `matchShot` | `match-shot` | +| `shotType` | `shot-type` | +| `stabilizationType` | `stabilization-type` | +| `matchProperty` | `match-property` | +| `matchTime` | `match-time` | +| `matchTimeRange` | `match-timeRange` | +| `matchRoles` | `match-roles` | +| `role` | `role` | +| `matchUsage` | `match-usage` | +| `matchRepresentation` | `match-representation` | +| `matchMarkers` | `match-markers` | +| `matchAnalysisType` | `match-analysis-type` | +| `reserved` | `reserved` | +| `array` | `array` | +| `string` | `string` | + +### Authoring types → XML (quick index) + +| Type | Encodes | +|------|---------| +| `Document` | `fcpxml` | +| `Resources` / `Format` / `Asset` / `MediaRep` / `Effect` / `Media` | matching resource tags | +| `MediaSequence` / `Multicam` / `MCAngle` | `sequence` / `multicam` / `mc-angle` | +| `Library` / `Event` / `Project` / `Sequence` / `Spine` | matching | +| Spine story structs | see §5 | +| `VolumeAdjustment` / `CinematicAdjustment` | `adjust-volume` / `adjust-cinematic` | +| Enums | `SpineItem`, `SyncClipContent`, `AuditionCandidate`, `MediaContent` | + +Infrastructure (no element): `Authoring` namespace, `Context`, `Element` protocol, `VersionAvailability`, `Error`. + +--- + +## Maintenance + +When coverage changes: + +1. Update the relevant section table(s) in this file. +2. If Authoring gains types, update §5 / §16 and Manual 08. +3. If Reporting gains sheets, update §15 and Manual 20. +4. Mention material coverage shifts in `CHANGELOG.md` under Improvements. diff --git a/Documentation/Manual.md b/Documentation/Manual.md index 3afd3a3..c5e2e3d 100644 --- a/Documentation/Manual.md +++ b/Documentation/Manual.md @@ -17,21 +17,22 @@ From the index you can reach all chapters: - **03** — Timecode & Timing (SwiftTimecode, FCPXMLTimecode, CMTime) - **04** — Service & Logging (FCPXMLService, ModularUtilities) - **05** — Validation & Cut Detection -- **06** — Version Conversion & Export +- **06** — Version Conversion & Export (`VersionFeatureGate`) - **07** — Timeline & Export -- **08** — Timeline Manipulation (ripple insert, auto lane, clip queries) -- **09** — Timeline Metadata (markers, keywords, ratings, timestamps) -- **10** — Extraction & Media (scope, presets, media copy) -- **11** — Timeline Projection (`TimelineProjector`, `MediaUsageWindow`, report project-once) -- **12** — Media Processing (MIME, asset validation, silence, duration, parallel I/O) -- **13** — Typed Models (adjustments, filters, captions, keyframes, Live Drawing, collections) -- **14** — XML Extensions (OFKXMLDocument, OFKXMLElement) -- **15** — High-Level Model (FinalCutPro.FCPXML) -- **16** — Cross-Platform & iOS (OFKXML abstraction, Foundation vs AEXML) -- **17** — Errors & Utilities -- **18** — CLI (OpenFCPXMLKit-CLI) -- **19** — Reporting, Excel & PDF Export (Projection-first sections, ReportTimecodeFormat, ReportBuildPhase, XLKit workbook, CoreGraphics PDF) -- **20** — Examples (workflows and code) +- **08** — Detached Authoring (`FinalCutPro.FCPXML.Authoring`) +- **09** — Timeline Manipulation (ripple insert, auto lane, clip queries) +- **10** — Timeline Metadata (markers, keywords, ratings, timestamps) +- **11** — Extraction & Media (scope, presets, media copy) +- **12** — Timeline Projection (`TimelineProjector`, `MediaUsageWindow`, report project-once) +- **13** — Media Processing (MIME, asset validation, silence, duration, parallel I/O) +- **14** — Typed Models (adjustments incl. Corners/Panner, filters, captions, keyframes, collections) +- **15** — XML Extensions (OFKXMLDocument, OFKXMLElement) +- **16** — High-Level Model (FinalCutPro.FCPXML) +- **17** — Cross-Platform & iOS (OFKXML abstraction, Foundation vs AEXML) +- **18** — Errors & Utilities +- **19** — CLI (OpenFCPXMLKit-CLI) +- **20** — Reporting, Excel & PDF Export (Projection-first sections, ReportTimecodeFormat, ReportBuildPhase, XLKit workbook, CoreGraphics PDF) +- **21** — Examples (workflows and code) --- @@ -40,5 +41,4 @@ From the index you can reach all chapters: - **Documentation index:** [README.md](README.md) - **CLI reference:** [../Sources/OpenFCPXMLKitCLI/README.md](../Sources/OpenFCPXMLKitCLI/README.md) - **Project README:** [../README.md](../README.md) -- **Tests:** [../Tests/README.md](../Tests/README.md) — **1084** listed tests (all Swift Testing) - +- **Tests:** [../Tests/README.md](../Tests/README.md) — **1114** listed tests (all Swift Testing) diff --git a/Documentation/Manual/00-Index.md b/Documentation/Manual/00-Index.md index b8f89ba..7d9b271 100644 --- a/Documentation/Manual/00-Index.md +++ b/Documentation/Manual/00-Index.md @@ -13,21 +13,22 @@ Complete manual and usage guide for **OpenFCPXMLKit**, a Swift 6 framework for F | [03 — Timecode & Timing](03-Timecode-Timing.md) | SwiftTimecode, FCPXMLTimecode, CMTime, conversions, frame alignment | | [04 — Service & Logging](04-Service-Logging.md) | FCPXMLService, ModularUtilities, createService, logging | | [05 — Validation & Cut Detection](05-Validation-CutDetection.md) | Semantic and DTD validation, cut detection API | -| [06 — Version Conversion & Export](06-Version-Conversion-Export.md) | Version conversion, save as .fcpxml / .fcpxmld, exporters | +| [06 — Version Conversion & Export](06-Version-Conversion-Export.md) | Version conversion, `VersionFeatureGate`, save as .fcpxml / .fcpxmld | | [07 — Timeline & Export](07-Timeline-Export.md) | Timeline, TimelineClip, TimelineFormat, FCPXMLExporter, bundle export | -| [08 — Timeline Manipulation](08-Timeline-Manipulation.md) | Ripple insert, auto lane assignment, clip queries, lane range | -| [09 — Timeline Metadata](09-Timeline-Metadata.md) | Markers, chapter markers, keywords, ratings, timestamps | -| [10 — Extraction & Media](10-Extraction-Media.md) | Extraction scope and presets, media extraction and copy | -| [11 — Timeline Projection](11-Timeline-Projection.md) | `TimelineProjector`, `MediaUsageWindow`, options, occupancy, report project-once | -| [12 — Media Processing](12-Media-Processing.md) | MIME type, asset validation, silence detection, duration, parallel I/O | -| [13 — Typed Models](13-Typed-Models.md) | Adjustments, filters, captions/titles, keyframe animation, Live Drawing, collections | -| [14 — XML Extensions](14-XML-Extensions.md) | OFKXMLDocument and OFKXMLElement FCPXML extensions (cross-platform) | -| [15 — High-Level Model](15-High-Level-Model.md) | FinalCutPro.FCPXML, Root, events, projects | -| [16 — Cross-Platform & iOS](16-Cross-Platform-iOS.md) | XML abstraction layer, OFKXML protocols, Foundation vs AEXML backends, iOS support | -| [17 — Errors & Utilities](17-Errors-Utilities.md) | Error types, ErrorHandling, ProgressBar, FCPXMLUID | -| [18 — CLI](18-CLI.md) | Experimental command-line interface (OpenFCPXMLKit-CLI) | -| [19 — Reporting, Excel & PDF Export](19-Reporting.md) | Report builder, ReportOptions, ReportTimecodeFormat, ReportBuildPhase, Projection-first sections, Excel + PDF | -| [20 — Examples](20-Examples.md) | End-to-end workflows and code examples | +| [08 — Detached Authoring](08-Detached-Authoring.md) | `FinalCutPro.FCPXML.Authoring` value graph, omit-on-write, spine compounds | +| [09 — Timeline Manipulation](09-Timeline-Manipulation.md) | Ripple insert, auto lane assignment, clip queries, lane range | +| [10 — Timeline Metadata](10-Timeline-Metadata.md) | Markers, chapter markers, keywords, ratings, timestamps | +| [11 — Extraction & Media](11-Extraction-Media.md) | Extraction scope and presets, media extraction and copy | +| [12 — Timeline Projection](12-Timeline-Projection.md) | `TimelineProjector`, `MediaUsageWindow`, options, occupancy, report project-once | +| [13 — Media Processing](13-Media-Processing.md) | MIME type, asset validation, silence detection, duration, parallel I/O | +| [14 — Typed Models](14-Typed-Models.md) | Adjustments (incl. Corners/Panner), filters, captions/titles, keyframes, collections | +| [15 — XML Extensions](15-XML-Extensions.md) | OFKXMLDocument and OFKXMLElement FCPXML extensions (cross-platform) | +| [16 — High-Level Model](16-High-Level-Model.md) | FinalCutPro.FCPXML, Root, events, projects | +| [17 — Cross-Platform & iOS](17-Cross-Platform-iOS.md) | XML abstraction layer, OFKXML protocols, Foundation vs AEXML backends, iOS support | +| [18 — Errors & Utilities](18-Errors-Utilities.md) | Error types, ErrorHandling, ProgressBar, FCPXMLUID | +| [19 — CLI](19-CLI.md) | Experimental command-line interface (OpenFCPXMLKit-CLI) | +| [20 — Reporting, Excel & PDF Export](20-Reporting.md) | Report builder, ReportOptions, ReportTimecodeFormat, ReportBuildPhase, Projection-first sections, Excel + PDF | +| [21 — Examples](21-Examples.md) | End-to-end workflows and code examples | --- @@ -36,8 +37,8 @@ Complete manual and usage guide for **OpenFCPXMLKit**, a Swift 6 framework for F - **Documentation hub:** [../README.md](../README.md) - **Project README:** [../../README.md](../../README.md) - **Architecture:** [../../ARCHITECTURE.md](../../ARCHITECTURE.md) — layers and codebase map +- **Coverage:** [../Coverage.md](../Coverage.md) — FCPXML element / layer matrices (Model · Authoring · Extraction · Projection · Reporting) - **Guardrails:** [../../GUARDRAILS.md](../../GUARDRAILS.md) — must / must-not for contributors and agents - **CLI reference:** [../../Sources/OpenFCPXMLKitCLI/README.md](../../Sources/OpenFCPXMLKitCLI/README.md) -- **Tests:** [../../Tests/README.md](../../Tests/README.md) — suite layout (**1084** listed tests, all Swift Testing); [Submitted FCPXML](../../Tests/Submitted%20FCPXML/README.md) for private local investigation (never commit private FCPXML) +- **Tests:** [../../Tests/README.md](../../Tests/README.md) — suite layout (**1114** listed tests, all Swift Testing); [Submitted FCPXML](../../Tests/Submitted%20FCPXML/README.md) for private local investigation (never commit private FCPXML) - **FCPXML reference:** [fcp.cafe/developers/fcpxml](https://fcp.cafe/developers/fcpxml) - diff --git a/Documentation/Manual/01-Overview.md b/Documentation/Manual/01-Overview.md index 32d5794..6844f94 100644 --- a/Documentation/Manual/01-Overview.md +++ b/Documentation/Manual/01-Overview.md @@ -22,6 +22,7 @@ OpenFCPXMLKit provides a comprehensive API for parsing, creating, and manipulati | **FCPXMLValidator** | Semantic validation (root, resources, ref resolution) | | **FCPXMLDTDValidator** | DTD schema validation for a given FCPXML version | | **FCPXMLExporter** / **FCPXMLBundleExporter** | Export `Timeline` to FCPXML string or `.fcpxmld` bundle | +| **`FinalCutPro.FCPXML.Authoring.Document`** | Detached value-graph authoring (encode/decode limited subset; omit-on-write) | | **TimelineProjector** | Project a report timeline into `MediaUsageWindow` values (playable occupancy) | | **ReportExcelExport** / **ReportPDFExport** | Export a built `Report` to `.xlsx` (XLKit) or `.pdf` (CoreGraphics) | @@ -50,7 +51,7 @@ All core behaviour is defined by **protocols** with both sync and async APIs. De Semantic validators (`FCPXMLValidator`, `FCPXMLDTDValidator`, `FCPXMLStructuralValidator`) are injectable concrete types, not protocol-backed. -**Layer stack (bottom → top):** XML → Parsing → Model → Extraction → **Projection** → Reporting. Extend lower layers before adding report-only XML walks. See [11 — Timeline Projection](11-Timeline-Projection.md), [ARCHITECTURE.md](../../ARCHITECTURE.md) §2.7, and [GUARDRAILS.md](../../GUARDRAILS.md) for must / must-not constraints. +**Layer stack (bottom → top):** XML → Parsing → Model → Extraction → **Projection** → Reporting. Extend lower layers before adding report-only XML walks. See [12 — Timeline Projection](12-Timeline-Projection.md), [ARCHITECTURE.md](../../ARCHITECTURE.md) §2.7, and [GUARDRAILS.md](../../GUARDRAILS.md) for must / must-not constraints. ## Logging diff --git a/Documentation/Manual/04-Service-Logging.md b/Documentation/Manual/04-Service-Logging.md index fe31c61..fbba668 100644 --- a/Documentation/Manual/04-Service-Logging.md +++ b/Documentation/Manual/04-Service-Logging.md @@ -81,7 +81,7 @@ let noOp = NoOpServiceLogger() let quietService = FCPXMLService(logger: noOp) ``` -The service logs parsing, version conversion, DTD validation, save, media extraction, and media copy. CLI supports `--log`, `--log-level`, `--quiet` (see [18 — CLI](18-CLI.md)). +The service logs parsing, version conversion, DTD validation, save, media extraction, and media copy. CLI supports `--log`, `--log-level`, `--quiet` (see [19 — CLI](19-CLI.md)). --- diff --git a/Documentation/Manual/05-Validation-CutDetection.md b/Documentation/Manual/05-Validation-CutDetection.md index 6cbbe9b..2cc4118 100644 --- a/Documentation/Manual/05-Validation-CutDetection.md +++ b/Documentation/Manual/05-Validation-CutDetection.md @@ -45,7 +45,7 @@ else { **FCPXMLStructuralValidator** works on all platforms (including iOS). It checks: root element name `fcpxml`, required `version` attribute, required `resources` child, at least one content element (library/event/project), and an element-name allowlist for FCPXML 1.5–1.14. Use it when full DTD validation is not available (e.g. on iOS). -**Parity:** Structural success is **not** equivalent to macOS DTD success. On iOS, `FCPXMLDTDValidator` always runs structural checks and may report `.structuralValidationOnly`. See [16 — Cross-Platform & iOS](16-Cross-Platform-iOS.md). +**Parity:** Structural success is **not** equivalent to macOS DTD success. On iOS, `FCPXMLDTDValidator` always runs structural checks and may report `.structuralValidationOnly`. See [17 — Cross-Platform & iOS](17-Cross-Platform-iOS.md). --- diff --git a/Documentation/Manual/06-Version-Conversion-Export.md b/Documentation/Manual/06-Version-Conversion-Export.md index 22cff4e..d08d035 100644 --- a/Documentation/Manual/06-Version-Conversion-Export.md +++ b/Documentation/Manual/06-Version-Conversion-Export.md @@ -20,7 +20,12 @@ let validation = service.validateDocumentAgainstDTD(converted, version: .v1_10) guard validation.isValid else { /* handle errors */ } ``` -Allowlists are derived at runtime from the target DTD (**EmbeddedDTDProvider** in CLI, bundle in library). Fallback to hand-maintained lists when DTD data is unavailable. +Allowlists are derived at runtime from the target DTD (**EmbeddedDTDProvider** in CLI, bundle in library). When DTD data is unavailable, the converter falls back to **`FinalCutPro.FCPXML.VersionFeatureGate`** (`elementNamesToOmit(at:)` / `attributeNamesToOmit(onElement:at:)`), the same registry used by detached Authoring omit-on-write (see [08 — Detached Authoring](08-Detached-Authoring.md)). + +```swift +let omit = FinalCutPro.FCPXML.VersionFeatureGate.elementNamesToOmit(at: .v1_09) +// includes e.g. adjust-cinematic (introduced in 1.10) +``` ### Write honesty vs report reads diff --git a/Documentation/Manual/07-Timeline-Export.md b/Documentation/Manual/07-Timeline-Export.md index 321a759..e148503 100644 --- a/Documentation/Manual/07-Timeline-Export.md +++ b/Documentation/Manual/07-Timeline-Export.md @@ -57,7 +57,7 @@ let asset = FCPXMLExportAsset( ## Export to FCPXML string -**FCPXMLExporter** produces an FCPXML string. Supports timelines with **zero clips** (empty spine) or with clips; when clips are present, every `assetRef` must match an asset `id`. The output includes a **DOCTYPE** declaration, **format** `colorSpace` (e.g. `1-1-1 (Rec. 709)`), and optionally FCP-style default smart collections. **Timeline-level and clip-level metadata** (markers, chapter markers, keywords, ratings, custom metadata) are included when present — see [09 — Timeline Metadata](09-Timeline-Metadata.md) for setting metadata on timelines and clips. The XML declaration uses `standalone="no"` for compatibility with external DTD validation (e.g. xmllint). +**FCPXMLExporter** produces an FCPXML string. Supports timelines with **zero clips** (empty spine) or with clips; when clips are present, every `assetRef` must match an asset `id`. The output includes a **DOCTYPE** declaration, **format** `colorSpace` (e.g. `1-1-1 (Rec. 709)`), and optionally FCP-style default smart collections. **Timeline-level and clip-level metadata** (markers, chapter markers, keywords, ratings, custom metadata) are included when present — see [10 — Timeline Metadata](10-Timeline-Metadata.md) for setting metadata on timelines and clips. The XML declaration uses `standalone="no"` for compatibility with external DTD validation (e.g. xmllint). ```swift let exporter = FCPXMLExporter(version: .default) @@ -66,7 +66,7 @@ let xmlString = try exporter.export(timeline: timeline, assets: [asset]) Optional parameters for FCP-style document identity and library location: -- **eventUid** — Event `uid` attribute; if `nil`, a new UID is generated (see **FCPXMLUID** in [17 — Errors & Utilities](17-Errors-Utilities.md)). +- **eventUid** — Event `uid` attribute; if `nil`, a new UID is generated (see **FCPXMLUID** in [18 — Errors & Utilities](18-Errors-Utilities.md)). - **projectUid** — Project `uid` attribute; if `nil`, a new UID is generated. - **libraryLocation** — Library `location` attribute (e.g. file URL of the library bundle). - **includeDefaultSmartCollections** — If `true`, adds FCP-style default smart collections under the library (Projects, All Video, Audio Only, Stills, Favorites). Default: `false`. Set to `true` when creating new projects for FCP import. @@ -109,5 +109,6 @@ let bundleURL = try bundleExporter.exportBundle( ## Next -- [08 — Timeline Manipulation](08-Timeline-Manipulation.md) — Ripple insert, auto lane, clip queries. +- [08 — Detached Authoring](08-Detached-Authoring.md) — Detached value-graph document authoring (parallel to Timeline export). +- [09 — Timeline Manipulation](09-Timeline-Manipulation.md) — Ripple insert, auto lane, clip queries. diff --git a/Documentation/Manual/08-Detached-Authoring.md b/Documentation/Manual/08-Detached-Authoring.md new file mode 100644 index 0000000..cda3456 --- /dev/null +++ b/Documentation/Manual/08-Detached-Authoring.md @@ -0,0 +1,155 @@ +# 08 — Detached Authoring + +[← Manual Index](00-Index.md) + +--- + +## Overview + +**`FinalCutPro.FCPXML.Authoring`** is a **detached** (non-live) document value graph for building and round-tripping FCPXML without wrapping live XML nodes. + +| Layer | Ownership | Use when | +|-------|-----------|----------| +| **`Model/`** | Live `OFKXMLElement` wrappers | Parse / inspect / mutate an existing document | +| **`Timeline/` + `Export/`** | In-memory timeline → exporter | Build timelines programmatically for empty projects / clip export | +| **`Authoring/`** | Independent `Sendable` value types | Compose a document graph, encode to XML, decode a limited subset back | + +Authoring does **not** replace Model or Timeline export. Do **not** use Authoring inside **Reporting** — reports consume Extraction → Projection only (see [20 — Reporting](20-Reporting.md) and [ARCHITECTURE.md](../../ARCHITECTURE.md) §2.7). + +Coverage is intentional and **incremental**: story spine compounds and common resources are supported; markers, most filters, metadata, and generic `` are not yet modeled in Authoring. + +--- + +## Version availability & feature gate + +Every Authoring `Element` exposes `availability: VersionAvailability`. Encoding uses `encodeIfAvailable(into:context:)` so unavailable features are **omitted** for the document’s target version (omit-on-write). + +```swift +let availability = FinalCutPro.FCPXML.VersionAvailability.from(.v1_10) +availability.contains(.v1_09) // false +availability.contains(.v1_14) // true +``` + +**`FinalCutPro.FCPXML.VersionFeatureGate`** is the shared registry of DTD feature introductions (elements and attributes). Authoring (e.g. `CinematicAdjustment` → `adjust-cinematic`, 1.10+) and **`FCPXMLVersionConverter`** fallback strip lists both consult it. Prefer DTD allowlist stripping when DTDs are present; the gate is the explicit API + converter fallback. See [06 — Version Conversion & Export](06-Version-Conversion-Export.md). + +--- + +## Document entry points + +```swift +typealias Authoring = FinalCutPro.FCPXML.Authoring + +let format = Authoring.Format( + id: "r1", + frameDuration: "100/2400s", + width: 1920, + height: 1080, + name: "FFVideoFormat1080p24" +) +let asset = Authoring.Asset( + id: "r2", + name: "Clip", + hasVideo: true, + hasAudio: true, + duration: "10s", + formatID: "r1", + mediaReps: [Authoring.MediaRep(src: "file:///tmp/clip.mov")] +) +var clip = Authoring.AssetClip( + ref: "r2", + offset: "0s", + duration: "5s", + name: "Clip", + start: "0s" +) +clip.cinematic = Authoring.CinematicAdjustment(aperture: "wide") // omitted below 1.10 +clip.volume = Authoring.VolumeAdjustment(amount: "-3dB") + +let document = Authoring.Document.simpleProject( + version: .v1_14, + format: format, + asset: asset, + clip: clip, + sequenceDuration: "5s" +) + +let xml = try document.xmlString() +let ofkDoc = try document.makeXMLDocument() + +// Decode a limited Authoring subset back from live XML +let live = try OFKXMLDefaultFactory().makeDocument(xmlString: xml, options: .fcpxmlDefaults) +let roundTrip = try Authoring.Document(xmlDocument: live) +``` + +For richer graphs, build `Document(version:resources:library:)` with nested `Library` → `Event` → `Project` → `Sequence` → `Spine(items:)`. + +--- + +## Resources + +`Authoring.Resources` holds: + +| Property | Element | +|----------|---------| +| `formats` | `` | +| `assets` | `` (+ `MediaRep`) | +| `effects` | `` (titles, transitions, filters) | +| `media` | `` — compound `MediaSequence` or `Multicam` (`MCAngle`) | + +--- + +## Spine items + +`Authoring.SpineItem` (indirect enum): + +| Case | Element | +|------|---------| +| `.assetClip` | `` (optional volume / cinematic) | +| `.gap` | `` | +| `.title` | `` | +| `.transition` | `<transition>` | +| `.video` / `.audio` | `<video>` / `<audio>` leaves | +| `.caption` | `<caption>` | +| `.syncClip` | `<sync-clip>` (+ nested contents, `SyncSource`) | +| `.refClip` | `<ref-clip>` → compound `<media>` | +| `.mcClip` | `<mc-clip>` (+ `MCSource`) → multicam `<media>` | +| `.audition` | `<audition>` (first candidate active) | + +Example (compound + caption): + +```swift +let compound = Authoring.Media( + id: "r3", + name: "Compound", + content: .sequence( + Authoring.MediaSequence( + formatID: "r1", + duration: "4s", + spine: Authoring.Spine(items: [ + .assetClip(Authoring.AssetClip(ref: "r2", offset: "0s", duration: "4s", start: "0s")) + ]) + ) + ) +) + +let spine = Authoring.Spine(items: [ + .refClip(Authoring.RefClip(ref: "r3", offset: "0s", duration: "4s", start: "0s")), + .caption(Authoring.Caption(offset: "0s", duration: "2s", lane: 1, role: "iTT?caption")), +]) +``` + +--- + +## Related chapters + +- [07 — Timeline & Export](07-Timeline-Export.md) — live `Timeline` path (parallel creation style) +- [12 — Timeline Projection](12-Timeline-Projection.md) — analyse authored or parsed timelines +- [14 — Typed Models](14-Typed-Models.md) — live Model adjustments (Corners, Panner, …) +- [16 — High-Level Model](16-High-Level-Model.md) — `FinalCutPro.FCPXML` live document API +- [21 — Examples](21-Examples.md) — end-to-end workflows + +--- + +## Next + +- [09 — Timeline Manipulation](09-Timeline-Manipulation.md) — ripple insert, auto lane, clip queries. diff --git a/Documentation/Manual/08-Timeline-Manipulation.md b/Documentation/Manual/09-Timeline-Manipulation.md similarity index 96% rename from Documentation/Manual/08-Timeline-Manipulation.md rename to Documentation/Manual/09-Timeline-Manipulation.md index 4c51323..c373993 100644 --- a/Documentation/Manual/08-Timeline-Manipulation.md +++ b/Documentation/Manual/09-Timeline-Manipulation.md @@ -1,4 +1,4 @@ -# 08 — Timeline Manipulation +# 09 — Timeline Manipulation [← Manual Index](00-Index.md) @@ -121,4 +121,4 @@ if let laneRange = timeline.laneRange { ## Next -- [09 — Timeline Metadata](09-Timeline-Metadata.md) — Markers, chapters, keywords, ratings, timestamps. +- [10 — Timeline Metadata](10-Timeline-Metadata.md) — Markers, chapters, keywords, ratings, timestamps. diff --git a/Documentation/Manual/09-Timeline-Metadata.md b/Documentation/Manual/10-Timeline-Metadata.md similarity index 95% rename from Documentation/Manual/09-Timeline-Metadata.md rename to Documentation/Manual/10-Timeline-Metadata.md index fd08cb5..bc2927a 100644 --- a/Documentation/Manual/09-Timeline-Metadata.md +++ b/Documentation/Manual/10-Timeline-Metadata.md @@ -1,4 +1,4 @@ -# 09 — Timeline Metadata +# 10 — Timeline Metadata [← Manual Index](00-Index.md) @@ -95,4 +95,4 @@ clip.addRating(rating) ## Next -- [10 — Extraction & Media](10-Extraction-Media.md) — Extraction scope/presets, media extraction and copy. +- [11 — Extraction & Media](11-Extraction-Media.md) — Extraction scope/presets, media extraction and copy. diff --git a/Documentation/Manual/10-Extraction-Media.md b/Documentation/Manual/11-Extraction-Media.md similarity index 90% rename from Documentation/Manual/10-Extraction-Media.md rename to Documentation/Manual/11-Extraction-Media.md index 17b2d4c..c75ee58 100644 --- a/Documentation/Manual/10-Extraction-Media.md +++ b/Documentation/Manual/11-Extraction-Media.md @@ -1,4 +1,4 @@ -# 10 — Extraction & Media +# 11 — Extraction & Media [← Manual Index](00-Index.md) @@ -19,7 +19,7 @@ Extract elements from an FCPXML tree by type or using **presets**. **FinalCutPro Call **extract(types:scope:)** on an `FCPXMLElement` (or **fcpExtract(types:scope:)** on `OFKXMLElement`) for `[FinalCutPro.FCPXML.ExtractedElement]`. Call **extract(preset:scope:)** for a preset's result type. APIs are async. -Titles and Effects **extraction presets** remain useful for discovery and tests. Report sections for Titles, Effects, Markers, Keywords, and Transitions prefer **Timeline Projection** annotations when available (Extraction fallback) — see [11 — Timeline Projection](11-Timeline-Projection.md) and [19 — Reporting, Excel & PDF Export](19-Reporting.md). +Titles and Effects **extraction presets** remain useful for discovery and tests. Report sections for Titles, Effects, Markers, Keywords, and Transitions prefer **Timeline Projection** annotations when available (Extraction fallback) — see [12 — Timeline Projection](12-Timeline-Projection.md) and [20 — Reporting, Excel & PDF Export](20-Reporting.md). ```swift let element: FCPXMLElement = // ... e.g. from document @@ -77,9 +77,9 @@ for entry in copyResult.failed { /* error */ } ## Next -- [11 — Timeline Projection](11-Timeline-Projection.md) — playable media windows between Extraction and Reporting. -- [12 — Media Processing](12-Media-Processing.md) — MIME type, asset validation, silence, duration, parallel I/O. -- [19 — Reporting, Excel & PDF Export](19-Reporting.md) — build reports from Projection + Extraction and export to `.xlsx` or `.pdf`. +- [12 — Timeline Projection](12-Timeline-Projection.md) — playable media windows between Extraction and Reporting. +- [13 — Media Processing](13-Media-Processing.md) — MIME type, asset validation, silence, duration, parallel I/O. +- [20 — Reporting, Excel & PDF Export](20-Reporting.md) — build reports from Projection + Extraction and export to `.xlsx` or `.pdf`. [← Manual Index](00-Index.md) diff --git a/Documentation/Manual/11-Timeline-Projection.md b/Documentation/Manual/12-Timeline-Projection.md similarity index 84% rename from Documentation/Manual/11-Timeline-Projection.md rename to Documentation/Manual/12-Timeline-Projection.md index 072b3d2..0e2cda5 100644 --- a/Documentation/Manual/11-Timeline-Projection.md +++ b/Documentation/Manual/12-Timeline-Projection.md @@ -1,4 +1,4 @@ -# 11 — Timeline Projection +# 12 — Timeline Projection [← Manual Index](00-Index.md) @@ -20,7 +20,7 @@ Parsing → Model → Extraction → Projection → Reporting (Excel / PDF) Use Projection directly when you need timeline geometry. Prefer `buildReport` when you want Excel/PDF — `ReportBuilder` projects **once** per timeline and shares `ReportProjectionContext` across consuming sections. -Related: [03 — Timecode & Timing](03-Timecode-Timing.md) (Double-safe composition), [10 — Extraction & Media](10-Extraction-Media.md), [19 — Reporting](19-Reporting.md), [ARCHITECTURE.md](../../ARCHITECTURE.md) §2.7. +Related: [03 — Timecode & Timing](03-Timecode-Timing.md) (Double-safe composition), [11 — Extraction & Media](11-Extraction-Media.md), [20 — Reporting](20-Reporting.md), [ARCHITECTURE.md](../../ARCHITECTURE.md) §2.7. --- @@ -53,6 +53,9 @@ options.expandAllSourceChannels = true // one window per video/audio src (defa // Preset aligned with main-timeline Extraction visibility let main = FinalCutPro.FCPXML.TimelineProjectionOptions.mainTimeline + +// Preset for playable “active mix” track analysis (active audition/angles; all source channels) +let track = FinalCutPro.FCPXML.TimelineProjectionOptions.trackAnalysis ``` Report builds use `TimelineProjectionOptions.forReport(...)` so `excludeDisabledClips` and annotation needs stay consistent across sections. @@ -105,7 +108,7 @@ try await projector.project(from: source, fcpxml: fcpxml, options: options) { wi - Nested spines / anchored children and J/L cuts (`audioStart` / `audioDuration`) - `mc-clip` angles (active or all; split video/audio), `ref-clip` media sequences, auditions - `video` / `audio` leaves with `ChannelKindFilter` / `srcEnable` -- Optional annotations when `includeAnnotations` is on (roles, volume/effects breadcrumbs, markers/keywords/titles/transitions/effects for reporting). Marker annotations include **`isOutsideClipBoundaries`** (start outside host media range) for Markers report filtering / the opt-in **Hidden** column — see [19 — Reporting](19-Reporting.md#markers). +- Optional annotations when `includeAnnotations` is on (roles, volume/effects breadcrumbs, markers/keywords/titles/transitions/effects for reporting). Marker annotations include **`isOutsideClipBoundaries`** (start outside host media range) for Markers report filtering / the opt-in **Hidden** column — see [19 — Reporting](20-Reporting.md#markers). Timing composition uses **`ProjectionTiming`** (Double intermediates → `Fraction` at 12 decimal places). Do not use SwiftTimecode `Fraction.+` / `.-` for absolute timeline placement when mixing conform-scaled values with literal FCPXML rationals — see [03 — Timecode & Timing](03-Timecode-Timing.md). @@ -116,10 +119,20 @@ Timing composition uses **`ProjectionTiming`** (Double intermediates → `Fracti ```swift let index = FinalCutPro.FCPXML.TimelineOccupancyIndex(windows: windows) let occupiedSeconds = index.occupiedDuration() // union of window intervals in seconds -// Overlap-aware Summary uses this path when -// ReportOptions.summaryOverlapAwareDurations == true (API-only; default off). +let overlapping = index.windows(overlapping: start, end: end) // start-sorted binary-search overlap + +// Retiming algebra (compose nested warps; clip to a timeline range) +let composed = FinalCutPro.FCPXML.RetimingSegment.composing( + parents: parentSegments, + children: childSegments +) +if let first = composed.first, + let clipped = first.clipped(toTimelineStart: inPoint, timelineEnd: outPoint) { + _ = clipped +} ``` +Overlap-aware Summary uses this path when `ReportOptions.summaryOverlapAwareDurations == true` (API-only; default off). --- ## Reporting integration @@ -146,7 +159,7 @@ let report = try await fcpxml.buildReport(options: options) { phase in CLI: `--media-resolution fail-soft|fail-loud`, `--media-summary-distinguish-proxy`. Overlap-aware Summary and per-source inventory rows are library options only. -See [19 — Reporting, Excel & PDF Export](19-Reporting.md). +See [20 — Reporting, Excel & PDF Export](20-Reporting.md). For private complex exports used only while debugging Projection/reporting, use [Submitted FCPXML](../../Tests/Submitted%20FCPXML/README.md) (gitignored; never commit to GitHub). @@ -154,9 +167,9 @@ For private complex exports used only while debugging Projection/reporting, use ## Next -- [12 — Media Processing](12-Media-Processing.md) — MIME type, asset validation, silence, duration, parallel I/O. -- [19 — Reporting, Excel & PDF Export](19-Reporting.md) — build Excel/PDF from Projection + Extraction. -- [20 — Examples](20-Examples.md) — end-to-end workflows. +- [13 — Media Processing](13-Media-Processing.md) — MIME type, asset validation, silence, duration, parallel I/O. +- [20 — Reporting, Excel & PDF Export](20-Reporting.md) — build Excel/PDF from Projection + Extraction. +- [21 — Examples](21-Examples.md) — end-to-end workflows. [← Manual Index](00-Index.md) diff --git a/Documentation/Manual/12-Media-Processing.md b/Documentation/Manual/13-Media-Processing.md similarity index 97% rename from Documentation/Manual/12-Media-Processing.md rename to Documentation/Manual/13-Media-Processing.md index 4f0f2b2..b1c3e86 100644 --- a/Documentation/Manual/12-Media-Processing.md +++ b/Documentation/Manual/13-Media-Processing.md @@ -1,4 +1,4 @@ -# 12 — Media Processing +# 13 — Media Processing [← Manual Index](00-Index.md) @@ -107,5 +107,5 @@ let readResult = await executor.readFiles(urlsToRead) ## Next -- [13 — Typed Models](13-Typed-Models.md) — Adjustments, filters, captions, keyframes, Live Drawing, collections. +- [14 — Typed Models](14-Typed-Models.md) — Adjustments, filters, captions, keyframes, Live Drawing, collections. diff --git a/Documentation/Manual/13-Typed-Models.md b/Documentation/Manual/14-Typed-Models.md similarity index 96% rename from Documentation/Manual/13-Typed-Models.md rename to Documentation/Manual/14-Typed-Models.md index af012a5..c00ac6c 100644 --- a/Documentation/Manual/13-Typed-Models.md +++ b/Documentation/Manual/14-Typed-Models.md @@ -1,4 +1,4 @@ -# 13 — Typed Models +# 14 — Typed Models [← Manual Index](00-Index.md) @@ -15,10 +15,12 @@ Typed models with **Clip** accessors (see **FCPXMLClip+Adjustments**): | Model | Notes | |-------|--------| | **CropAdjustment** | Crop, trim, pan modes | +| **CornersAdjustment** | Four-corner pin (`adjust-corners`) | | **TransformAdjustment** | Position, scale, rotation, anchor | | **BlendAdjustment** | Blend amount and mode | | **StabilizationAdjustment** | automatic, inertiaCam, smoothCam | | **VolumeAdjustment** | Volume level | +| **PannerAdjustment** | Stereo/surround panner (`adjust-panner`) | | **LoudnessAdjustment** | Loudness parameters | | **NoiseReductionAdjustment** | Amount | | **HumReductionAdjustment** | 50Hz / 60Hz | @@ -152,7 +154,7 @@ let marker = FinalCutPro.FCPXML.HiddenClipMarker() // Add to clip via fcpxAnnotations / addToClip(annotationElements:) ``` -Do **not** confuse this DTD element with the Markers **report** concept of “Hidden”: report **Hidden** means a normal `marker` / `chapter-marker` whose `start` is outside the host clip’s media range (omitted by default; opt in with `includeMarkersOutsideClipBoundaries` / `--include-markers-outside-clip-boundaries`). See [19 — Reporting](19-Reporting.md#markers). +Do **not** confuse this DTD element with the Markers **report** concept of “Hidden”: report **Hidden** means a normal `marker` / `chapter-marker` whose `start` is outside the host clip’s media range (omitted by default; opt in with `includeMarkersOutsideClipBoundaries` / `--include-markers-outside-clip-boundaries`). See [19 — Reporting](20-Reporting.md#markers). --- @@ -193,7 +195,7 @@ let parentFolder = FinalCutPro.FCPXML.CollectionFolder( ## Next -- [14 — XML Extensions](14-XML-Extensions.md) — OFKXMLDocument and OFKXMLElement FCPXML APIs. +- [15 — XML Extensions](15-XML-Extensions.md) — OFKXMLDocument and OFKXMLElement FCPXML APIs. [← Manual Index](00-Index.md) diff --git a/Documentation/Manual/14-XML-Extensions.md b/Documentation/Manual/15-XML-Extensions.md similarity index 92% rename from Documentation/Manual/14-XML-Extensions.md rename to Documentation/Manual/15-XML-Extensions.md index b64f4d6..3af88d8 100644 --- a/Documentation/Manual/14-XML-Extensions.md +++ b/Documentation/Manual/15-XML-Extensions.md @@ -1,4 +1,4 @@ -# 14 — XML Extensions +# 15 — XML Extensions [← Manual Index](00-Index.md) @@ -12,7 +12,7 @@ FCPXML document and element APIs are defined on **protocol types** so the same c - **OFKXMLElement** — element protocol (attributes, children, serialization). On macOS the default wraps Foundation `XMLElement`; on iOS it wraps AEXML. - **OFKXMLFactory** — factory for creating documents and elements. Use **OFKXMLDefaultFactory()** to get the correct backend for the current platform. -All `fcpx*` extensions below apply to `OFKXMLDocument` and `OFKXMLElement`; the concrete type is chosen at runtime. See [16 — Cross-Platform & iOS](16-Cross-Platform-iOS.md) for details. +All `fcpx*` extensions below apply to `OFKXMLDocument` and `OFKXMLElement`; the concrete type is chosen at runtime. See [17 — Cross-Platform & iOS](17-Cross-Platform-iOS.md) for details. --- @@ -83,8 +83,8 @@ let annotations = element.fcpxAnnotations ## Next -- [15 — High-Level Model](15-High-Level-Model.md) — FinalCutPro.FCPXML wrapper. -- [16 — Cross-Platform & iOS](16-Cross-Platform-iOS.md) — XML abstraction, Foundation vs AEXML, iOS support. +- [16 — High-Level Model](16-High-Level-Model.md) — FinalCutPro.FCPXML wrapper. +- [17 — Cross-Platform & iOS](17-Cross-Platform-iOS.md) — XML abstraction, Foundation vs AEXML, iOS support. [← Manual Index](00-Index.md) diff --git a/Documentation/Manual/15-High-Level-Model.md b/Documentation/Manual/16-High-Level-Model.md similarity index 89% rename from Documentation/Manual/15-High-Level-Model.md rename to Documentation/Manual/16-High-Level-Model.md index 9f7d268..7b1ec22 100644 --- a/Documentation/Manual/15-High-Level-Model.md +++ b/Documentation/Manual/16-High-Level-Model.md @@ -1,4 +1,4 @@ -# 15 — High-Level Model +# 16 — High-Level Model [← Manual Index](00-Index.md) @@ -35,13 +35,13 @@ let version = fcpxml.version Bridging with **FCPXMLVersion** (DTD/validation): use `.fcpxmlVersion` and `.dtdVersion` and `init(from:)` converters where provided. -For building Excel or PDF reports from either a project or a standalone compound clip, see [19 — Reporting, Excel & PDF Export](19-Reporting.md). +For building Excel or PDF reports from either a project or a standalone compound clip, see [20 — Reporting, Excel & PDF Export](20-Reporting.md). --- ## Next -- [16 — Cross-Platform & iOS](16-Cross-Platform-iOS.md) — OFKXML abstraction, Foundation vs AEXML, iOS support. +- [17 — Cross-Platform & iOS](17-Cross-Platform-iOS.md) — OFKXML abstraction, Foundation vs AEXML, iOS support. [← Manual Index](00-Index.md) diff --git a/Documentation/Manual/16-Cross-Platform-iOS.md b/Documentation/Manual/17-Cross-Platform-iOS.md similarity index 94% rename from Documentation/Manual/16-Cross-Platform-iOS.md rename to Documentation/Manual/17-Cross-Platform-iOS.md index 68751c0..048237e 100644 --- a/Documentation/Manual/16-Cross-Platform-iOS.md +++ b/Documentation/Manual/17-Cross-Platform-iOS.md @@ -1,4 +1,4 @@ -# 16 — Cross-Platform & iOS +# 17 — Cross-Platform & iOS [← Manual Index](00-Index.md) @@ -51,7 +51,7 @@ let version = root?.stringValue(forAttributeNamed: "version") ## Testing -- The suite uses **Swift Testing** exclusively (`@Suite` / `@Test` / `#expect` / `#require`). **1084** tests are listed in `swift test --list-tests` (**1078** in `OpenFCPXMLKitTests` + **6** optional `ExcelReportTest`). See [Tests/README.md](../../Tests/README.md). +- The suite uses **Swift Testing** exclusively (`@Suite` / `@Test` / `#expect` / `#require`). **1114** tests are listed in `swift test --list-tests` (**1108** in `OpenFCPXMLKitTests` + **6** optional `ExcelReportTest`). See [Tests/README.md](../../Tests/README.md). - Tests run on **macOS** and exercise the Foundation XML backend. Optional fixtures cancel via `Test.cancel` when unset (ExcelReportTest Sample, `OFK_REPORTING_FCPXML_BUNDLE`, Submitted inbox). - Public fixtures: `Tests/FCPXML Samples/FCPXML/` (committed). Private investigation: [Submitted FCPXML](../../Tests/Submitted%20FCPXML/README.md) (`Inbox/` gitignored — never commit private FCPXML to GitHub). - **iOS** is supported for building the library (e.g. iOS Simulator); running the same tests on iOS is not required for CI because they depend on Foundation XML. AEXML parity and structural validation are covered by tests that run on macOS. @@ -66,8 +66,8 @@ let version = root?.stringValue(forAttributeNamed: "version") ## Next -- [17 — Errors & Utilities](17-Errors-Utilities.md) — Error types, ProgressBar, FCPXMLUID. -- [14 — XML Extensions](14-XML-Extensions.md) — FCPXML extensions on OFKXMLElement and OFKXMLDocument. +- [18 — Errors & Utilities](18-Errors-Utilities.md) — Error types, ProgressBar, FCPXMLUID. +- [15 — XML Extensions](15-XML-Extensions.md) — FCPXML extensions on OFKXMLElement and OFKXMLDocument. - [05 — Validation & Cut Detection](05-Validation-CutDetection.md) — Semantic, DTD, and structural validation. [← Manual Index](00-Index.md) diff --git a/Documentation/Manual/17-Errors-Utilities.md b/Documentation/Manual/18-Errors-Utilities.md similarity index 97% rename from Documentation/Manual/17-Errors-Utilities.md rename to Documentation/Manual/18-Errors-Utilities.md index c17baaa..5727ed2 100644 --- a/Documentation/Manual/17-Errors-Utilities.md +++ b/Documentation/Manual/18-Errors-Utilities.md @@ -1,4 +1,4 @@ -# 17 — Errors & Utilities +# 18 — Errors & Utilities [← Manual Index](00-Index.md) @@ -79,7 +79,7 @@ When exporting with **FCPXMLExporter**, pass `eventUid` and `projectUid` (or omi ## Next -- [18 — CLI](18-CLI.md) — Experimental command-line interface. +- [19 — CLI](19-CLI.md) — Experimental command-line interface. [← Manual Index](00-Index.md) diff --git a/Documentation/Manual/18-CLI.md b/Documentation/Manual/19-CLI.md similarity index 97% rename from Documentation/Manual/18-CLI.md rename to Documentation/Manual/19-CLI.md index 90c905c..383cd53 100644 --- a/Documentation/Manual/18-CLI.md +++ b/Documentation/Manual/19-CLI.md @@ -1,4 +1,4 @@ -# 18 — CLI +# 19 — CLI [← Manual Index](00-Index.md) @@ -29,7 +29,7 @@ Use **one** of: `--check-version`, `--convert-version`, `--validate`, `--media-c ### REPORT -Build an Excel (`.xlsx`) report workbook from FCPXML/FCPXMLD, with optional PDF (`.pdf`) export via `--create-pdf`. Works for normal project timelines and for standalone compound-clip exports (event `ref-clip` with no `<project>`). The workbook is written to `<output-dir>`; its file name is derived from the project or compound-clip name. See [19 — Reporting, Excel & PDF Export](19-Reporting.md) for the underlying API. +Build an Excel (`.xlsx`) report workbook from FCPXML/FCPXMLD, with optional PDF (`.pdf`) export via `--create-pdf`. Works for normal project timelines and for standalone compound-clip exports (event `ref-clip` with no `<project>`). The workbook is written to `<output-dir>`; its file name is derived from the project or compound-clip name. See [20 — Reporting, Excel & PDF Export](20-Reporting.md) for the underlying API. | Option | Description | |--------|-------------| @@ -57,7 +57,7 @@ Build an Excel (`.xlsx`) report workbook from FCPXML/FCPXMLD, with optional PDF When `--report` is used without `--report-full` or section flags, the CLI exports role inventory only. Use `--report-full` for every optional sheet, or set individual `--report-*` section flags for a partial export (role inventory is always included). `--report-full` takes precedence when combined with section flags. -Build progress follows **product / workbook order** (Selected Roles Inventory first, then Markers … Media Summary). See [19 — Reporting](19-Reporting.md#progress-callbacks). +Build progress follows **product / workbook order** (Selected Roles Inventory first, then Markers … Media Summary). See [19 — Reporting](20-Reporting.md#progress-callbacks). All REPORT flags except `--report` itself require `--report`. @@ -119,7 +119,7 @@ Common values: | `Source File Path` | Removes Source File Path (and Missing Media on Media Summary) | | `Frame Rate` | Removes Frame Rate/Sample Rate (and related summary metric cells) | -Unknown column names are ignored. See [19 — Reporting, Excel & PDF Export](19-Reporting.md#column-exclusion) for the full **ReportColumn** list and aliases. +Unknown column names are ignored. See [19 — Reporting, Excel & PDF Export](20-Reporting.md#column-exclusion) for the full **ReportColumn** list and aliases. ```bash OpenFCPXMLKit-CLI --report \ @@ -131,7 +131,7 @@ OpenFCPXMLKit-CLI --report \ #### Timecode display format -`--timecode-format` controls how timeline and source time columns are written in Excel and PDF exports (and appends a header suffix when not using default SMPTE frames). See [19 — Reporting, Excel & PDF Export](19-Reporting.md#timecode-display-format). +`--timecode-format` controls how timeline and source time columns are written in Excel and PDF exports (and appends a header suffix when not using default SMPTE frames). See [19 — Reporting, Excel & PDF Export](20-Reporting.md#timecode-display-format). | Value | Cells | Example headers | |-------|-------|-----------------| @@ -215,8 +215,8 @@ For source layout, extending the CLI, and regenerating embedded DTDs, see **[Ope ## Next -- [19 — Reporting, Excel & PDF Export](19-Reporting.md) — the reporting API behind `--report`. -- [20 — Examples](20-Examples.md) — End-to-end workflows and code examples. +- [20 — Reporting, Excel & PDF Export](20-Reporting.md) — the reporting API behind `--report`. +- [21 — Examples](21-Examples.md) — End-to-end workflows and code examples. [← Manual Index](00-Index.md) diff --git a/Documentation/Manual/19-Reporting.md b/Documentation/Manual/20-Reporting.md similarity index 98% rename from Documentation/Manual/19-Reporting.md rename to Documentation/Manual/20-Reporting.md index 38bcd32..99f6a8e 100644 --- a/Documentation/Manual/19-Reporting.md +++ b/Documentation/Manual/20-Reporting.md @@ -1,4 +1,4 @@ -# 19 — Reporting, Excel & PDF Export +# 20 — Reporting, Excel & PDF Export [← Manual Index](00-Index.md) @@ -22,7 +22,7 @@ Everything lives under **`FinalCutPro.FCPXML`**: All **build** APIs are **async**. PDF export is **synchronous** once a `Report` exists. -**Project-once Projection:** When Role Inventory, Markers, Keywords, Titles & Generators, Transitions, Effects, Speed Change Effects, Media Summary, or Summary is enabled, `ReportBuilder` projects the timeline **once** (progress phase `.projecting`) and shares `ReportProjectionContext` across those sections. Markers / Keywords / Titles / Transitions / Effects are Projection-first with Extraction fallback. See [11 — Timeline Projection](11-Timeline-Projection.md). +**Project-once Projection:** When Role Inventory, Markers, Keywords, Titles & Generators, Transitions, Effects, Speed Change Effects, Media Summary, or Summary is enabled, `ReportBuilder` projects the timeline **once** (progress phase `.projecting`) and shares `ReportProjectionContext` across those sections. Markers / Keywords / Titles / Transitions / Effects are Projection-first with Extraction fallback. See [12 — Timeline Projection](12-Timeline-Projection.md). **Configuration parity:** Build the report **once** with `ReportOptions`, then export to Excel, PDF, or both. Section flags, `excludedColumns`, `excludedRoles`, `excludeDisabledClips`, `timecodeFormat`, `copyrightLabel`, `includeMarkersOutsideClipBoundaries`, and `projectName` all apply to both exporters (they shape the shared `Report`). **`protectSheets` is Excel-only** (worksheet edit lock — not encryption). PDF adds presentation-only features (cover page, TOC with sheet colour chips + tint washes, per-sheet content tints, pagination, remaining-column width expansion after exclusions, truncation) on top of the same `Report` data. @@ -255,7 +255,7 @@ Use **RoleInventoryColumnLayout** (internal layout helper) or `RoleClipReportRow By default, markers whose `start` lies outside the host clip’s media range (`[start, start + duration)`) are **omitted** — Final Cut Pro hides them from the timeline and Tags list. Set `includeMarkersOutsideClipBoundaries` (CLI `--include-markers-outside-clip-boundaries`) to include them; the sheet then gains **Hidden** (✓ = outside bounds, ✗ = inside). **Hidden** is not a `ReportColumn` / `--exclude-column` target. -This is **not** the FCPXML 1.13+ empty `hidden-clip-marker` element (see [13 — Typed Models](13-Typed-Models.md#hidden-clip-marker-fcpxml-113)). Boundary helper: `FCPXMLMarkerClipBoundary`; Projection annotations expose `isOutsideClipBoundaries`. +This is **not** the FCPXML 1.13+ empty `hidden-clip-marker` element (see [13 — Typed Models](14-Typed-Models.md#hidden-clip-marker-fcpxml-113)). Boundary helper: `FCPXMLMarkerClipBoundary`; Projection annotations expose `isOutsideClipBoundaries`. **MarkerReportType**: `.standard`, `.incompleteToDo`, `.completedToDo`, `.chapter`. @@ -602,7 +602,7 @@ The same reports are available through **OpenFCPXMLKit-CLI**: | `--exclude-column <name>` | Omit a column from every applicable sheet (repeatable) | | `--timecode-format <format>` | Timeline cell format: `HH:MM:SS:FF` (default), `Frames`, `Feet+Frames`, `HH:MM:SS` | -See [18 — CLI](18-CLI.md#report) for full option reference and matching rules. +See [18 — CLI](19-CLI.md#report) for full option reference and matching rules. ```bash # Role inventory only @@ -642,10 +642,10 @@ For real-world exports that must stay off GitHub, drop them in [Tests/Submitted ## Next -- [20 — Examples](20-Examples.md) — End-to-end workflows and code examples. -- [11 — Timeline Projection](11-Timeline-Projection.md) — windows, options, occupancy, and how report builders consume Projection. -- [10 — Extraction & Media](10-Extraction-Media.md) — Extraction presets and media copy (fallback / discovery). -- [18 — CLI](18-CLI.md) — building reports from the command line. +- [21 — Examples](21-Examples.md) — End-to-end workflows and code examples. +- [12 — Timeline Projection](12-Timeline-Projection.md) — windows, options, occupancy, and how report builders consume Projection. +- [11 — Extraction & Media](11-Extraction-Media.md) — Extraction presets and media copy (fallback / discovery). +- [19 — CLI](19-CLI.md) — building reports from the command line. [← Manual Index](00-Index.md) diff --git a/Documentation/Manual/20-Examples.md b/Documentation/Manual/21-Examples.md similarity index 89% rename from Documentation/Manual/20-Examples.md rename to Documentation/Manual/21-Examples.md index 6e03dc5..e0952ee 100644 --- a/Documentation/Manual/20-Examples.md +++ b/Documentation/Manual/21-Examples.md @@ -1,4 +1,4 @@ -# 20 — Examples +# 21 — Examples [← Manual Index](00-Index.md) @@ -6,7 +6,7 @@ ## Open an FCPXML file -Prefer the cross-platform loader / OFKXML document APIs (see [14 — XML Extensions](14-XML-Extensions.md) and [16 — Cross-Platform & iOS](16-Cross-Platform-iOS.md)). On macOS, Foundation `XMLDocument(contentsOfFCPXML:)` remains available as a convenience. +Prefer the cross-platform loader / OFKXML document APIs (see [15 — XML Extensions](15-XML-Extensions.md) and [17 — Cross-Platform & iOS](17-Cross-Platform-iOS.md)). On macOS, Foundation `XMLDocument(contentsOfFCPXML:)` remains available as a convenience. ```swift let fileURL = URL(fileURLWithPath: "/Users/username/Documents/sample.fcpxml") @@ -314,6 +314,35 @@ OpenFCPXMLKit-CLI --report --report-markers \ --- +## Author a simple project (detached Authoring) + +```swift +typealias Authoring = FinalCutPro.FCPXML.Authoring + +let format = Authoring.Format(id: "r1", frameDuration: "100/2400s", width: 1920, height: 1080) +let asset = Authoring.Asset( + id: "r2", + hasVideo: true, + hasAudio: true, + duration: "10s", + formatID: "r1", + mediaReps: [Authoring.MediaRep(src: "file:///tmp/clip.mov")] +) +let clip = Authoring.AssetClip(ref: "r2", offset: "0s", duration: "5s", name: "Clip", start: "0s") +let document = Authoring.Document.simpleProject( + version: .v1_14, + format: format, + asset: asset, + clip: clip, + sequenceDuration: "5s" +) +let xml = try document.xmlString() +``` + +Full Authoring API: [08 — Detached Authoring](08-Detached-Authoring.md). + +--- + ## Project a timeline (MediaUsageWindow) ```swift @@ -337,7 +366,7 @@ let videoSeconds = FinalCutPro.FCPXML.TimelineOccupancyIndex(windows: windows) print("Union video occupancy:", videoSeconds, "s across", windows.count, "windows") ``` -Full Projection API: [11 — Timeline Projection](11-Timeline-Projection.md). Reporting project-once is automatic inside `buildReport` when sections need windows. +Full Projection API: [12 — Timeline Projection](12-Timeline-Projection.md). Reporting project-once is automatic inside `buildReport` when sections need windows. --- @@ -354,7 +383,7 @@ OpenFCPXMLKit-CLI --report --report-full \ /path/to/project.fcpxmld /path/to/output-dir ``` -See [19 — Reporting, Excel & PDF Export](19-Reporting.md) and [11 — Timeline Projection](11-Timeline-Projection.md) for the full reporting and Projection APIs. +See [20 — Reporting, Excel & PDF Export](20-Reporting.md) and [12 — Timeline Projection](12-Timeline-Projection.md) for the full reporting and Projection APIs. --- diff --git a/Documentation/README.md b/Documentation/README.md index cbdf12e..706a297 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -17,40 +17,42 @@ The manual is split into **chapters** for easier navigation and maintenance: | [03 — Timecode & Timing](Manual/03-Timecode-Timing.md) | SwiftTimecode, FCPXMLTimecode, CMTime, conversions, frame alignment, Projection timing safety | | [04 — Service & Logging](Manual/04-Service-Logging.md) | FCPXMLService, ModularUtilities, logging | | [05 — Validation & Cut Detection](Manual/05-Validation-CutDetection.md) | Semantic and DTD validation, cut detection API | -| [06 — Version Conversion & Export](Manual/06-Version-Conversion-Export.md) | Version conversion, write honesty vs report reads, save as .fcpxml / .fcpxmld | +| [06 — Version Conversion & Export](Manual/06-Version-Conversion-Export.md) | Version conversion, `VersionFeatureGate`, write honesty vs report reads, save as .fcpxml / .fcpxmld | | [07 — Timeline & Export](Manual/07-Timeline-Export.md) | Timeline, TimelineClip, TimelineFormat, custom/preset dimensions and frame rate, zero-clip export, FCPXMLExporter options | -| [08 — Timeline Manipulation](Manual/08-Timeline-Manipulation.md) | Ripple insert, auto lane, clip queries, lane range | -| [09 — Timeline Metadata](Manual/09-Timeline-Metadata.md) | Markers, chapter markers, keywords, ratings, timestamps | -| [10 — Extraction & Media](Manual/10-Extraction-Media.md) | Extraction scope and presets, media extraction and copy | -| [11 — Timeline Projection](Manual/11-Timeline-Projection.md) | `TimelineProjector`, `MediaUsageWindow`, options, occupancy, report project-once | -| [12 — Media Processing](Manual/12-Media-Processing.md) | MIME type, asset validation, silence detection, duration, parallel I/O | -| [13 — Typed Models](Manual/13-Typed-Models.md) | Adjustments, filters, captions/titles, keyframe animation, Live Drawing, collections | -| [14 — XML Extensions](Manual/14-XML-Extensions.md) | OFKXMLDocument and OFKXMLElement FCPXML extensions (cross-platform) | -| [15 — High-Level Model](Manual/15-High-Level-Model.md) | FinalCutPro.FCPXML, Root, events, projects | -| [16 — Cross-Platform & iOS](Manual/16-Cross-Platform-iOS.md) | XML abstraction layer, Foundation vs AEXML, iOS support | -| [17 — Errors & Utilities](Manual/17-Errors-Utilities.md) | Error types, ErrorHandling, ProgressBar, FCPXMLUID | -| [18 — CLI](Manual/18-CLI.md) | Experimental command-line interface (OpenFCPXMLKit-CLI) | -| [19 — Reporting, Excel & PDF Export](Manual/19-Reporting.md) | Report builder, ReportOptions (`copyrightLabel`, `includeMarkersOutsideClipBoundaries`, `protectSheets`, …), Projection-first sections, Excel/PDF export | -| [20 — Examples](Manual/20-Examples.md) | End-to-end workflows and code examples | - -The manual covers the **entire public API** with examples: core operations, async/await, file I/O, validation, timeline creation and manipulation, metadata, media processing, typed models, version conversion, **Timeline Projection**, reporting and Excel/PDF export, CLI, and utilities. - -- **Chapter 11** — Projection (`MediaUsageWindow`, project-once for reports) -- **Chapter 16** — Cross-platform XML abstraction (OFKXML) -- **Chapter 19** — Reporting (Excel & PDF) - -Architecture philosophy: [ARCHITECTURE.md](../ARCHITECTURE.md) §2.7. Hard constraints: [GUARDRAILS.md](../GUARDRAILS.md). - -**Test count (keep in sync):** **1084** listed in `swift test list` — **1078** in `OpenFCPXMLKitTests` + **6** optional `ExcelReportTest` (all Swift Testing `@Test`); **60** sample `.fcpxml` files. Private user exports for local investigation: [Tests/Submitted FCPXML](../Tests/Submitted%20FCPXML/README.md) (gitignored; never commit to GitHub). +| [08 — Detached Authoring](Manual/08-Detached-Authoring.md) | `FinalCutPro.FCPXML.Authoring` value graph, omit-on-write, spine compounds / media resources | +| [09 — Timeline Manipulation](Manual/09-Timeline-Manipulation.md) | Ripple insert, auto lane, clip queries, lane range | +| [10 — Timeline Metadata](Manual/10-Timeline-Metadata.md) | Markers, chapter markers, keywords, ratings, timestamps | +| [11 — Extraction & Media](Manual/11-Extraction-Media.md) | Extraction scope and presets, media extraction and copy | +| [12 — Timeline Projection](Manual/12-Timeline-Projection.md) | `TimelineProjector`, `MediaUsageWindow`, options, occupancy, report project-once | +| [13 — Media Processing](Manual/13-Media-Processing.md) | MIME type, asset validation, silence detection, duration, parallel I/O | +| [14 — Typed Models](Manual/14-Typed-Models.md) | Adjustments (incl. Corners/Panner), filters, captions/titles, keyframe animation, Live Drawing, collections | +| [15 — XML Extensions](Manual/15-XML-Extensions.md) | OFKXMLDocument and OFKXMLElement FCPXML extensions (cross-platform) | +| [16 — High-Level Model](Manual/16-High-Level-Model.md) | FinalCutPro.FCPXML, Root, events, projects | +| [17 — Cross-Platform & iOS](Manual/17-Cross-Platform-iOS.md) | XML abstraction layer, Foundation vs AEXML, iOS support | +| [18 — Errors & Utilities](Manual/18-Errors-Utilities.md) | Error types, ErrorHandling, ProgressBar, FCPXMLUID | +| [19 — CLI](Manual/19-CLI.md) | Experimental command-line interface (OpenFCPXMLKit-CLI) | +| [20 — Reporting, Excel & PDF Export](Manual/20-Reporting.md) | Report builder, ReportOptions (`copyrightLabel`, `includeMarkersOutsideClipBoundaries`, `protectSheets`, …), Projection-first sections, Excel/PDF export | +| [21 — Examples](Manual/21-Examples.md) | End-to-end workflows and code examples | + +The manual covers the **entire public API** with examples: core operations, async/await, file I/O, validation, timeline creation and manipulation, **detached Authoring**, metadata, media processing, typed models, version conversion, **Timeline Projection**, reporting and Excel/PDF export, CLI, and utilities. + +- **Chapter 08** — Detached Authoring (`Authoring.Document`, omit-on-write) +- **Chapter 12** — Projection (`MediaUsageWindow`, project-once for reports) +- **Chapter 17** — Cross-platform XML abstraction (OFKXML) +- **Chapter 20** — Reporting (Excel & PDF) + +Architecture philosophy: [ARCHITECTURE.md](../ARCHITECTURE.md) §2.7. Hard constraints: [GUARDRAILS.md](../GUARDRAILS.md). **Element / layer inventory:** [Coverage.md](Coverage.md) (Model · Authoring · Extraction · Projection · Reporting matrices). + +**Test count (keep in sync):** **1114** listed in `swift test list` — **1108** in `OpenFCPXMLKitTests` + **6** optional `ExcelReportTest` (all Swift Testing `@Test`); **60** sample `.fcpxml` files. Private user exports for local investigation: [Tests/Submitted FCPXML](../Tests/Submitted%20FCPXML/README.md) (gitignored; never commit to GitHub). --- ## Other references +- **[Coverage](Coverage.md)** — Detailed FCPXML coverage matrices (typed Model, Authoring, Extraction, Projection, Reporting, version gates). - **[CLI](../Sources/OpenFCPXMLKitCLI/README.md)** — Full CLI usage, options, building, extending, and regenerating embedded DTDs (`Scripts/generate_embedded_dtds.sh` or `swift run GenerateEmbeddedDTDs`). - **Project [README](../README.md)** — Installation, architecture, requirements. - **[Tests/README.md](../Tests/README.md)** — Test suite layout and categories (including Submitted FCPXML). - **[Submitted FCPXML](../Tests/Submitted%20FCPXML/README.md)** — Private inbox workflow for parsing / reporting edge cases (local only). -- **[ARCHITECTURE.md](../ARCHITECTURE.md)** — Layer stack, Projection, reporting. +- **[ARCHITECTURE.md](../ARCHITECTURE.md)** — Layer stack, Authoring, Projection, reporting. - **[GUARDRAILS.md](../GUARDRAILS.md)** — Must / must-not constraints (layers, naming, tests, reporting honesty). - diff --git a/GUARDRAILS.md b/GUARDRAILS.md index bba646b..6d07784 100644 --- a/GUARDRAILS.md +++ b/GUARDRAILS.md @@ -4,7 +4,25 @@ Hard constraints for contributors and AI agents. Prefer this file when deciding **See also:** [ARCHITECTURE.md](ARCHITECTURE.md), [.cursorrules](.cursorrules), [AGENT.md](AGENT.md), [Tests/README.md](Tests/README.md), [CONTRIBUTING.md](CONTRIBUTING.md). -**Current suite (keep in sync):** **1084** tests listed in `swift test list` — **1078** in `OpenFCPXMLKitTests` + **6** optional `ExcelReportTest` (all Swift Testing `@Test`; no XCTest); **60** public sample `.fcpxml` files. +**Current suite (keep in sync):** **1114** tests listed in `swift test list` — **1108** in `OpenFCPXMLKitTests` + **6** optional `ExcelReportTest` (all Swift Testing `@Test`; no XCTest); **60** public sample `.fcpxml` files. + +--- + +## Table of Contents + +- [How to use this document](#how-to-use-this-document) +- [1. Naming & product identity](#1-naming--product-identity) +- [2. Layer boundaries (non-negotiable)](#2-layer-boundaries-non-negotiable) +- [3. FCPXML compatibility & versions](#3-fcpxml-compatibility--versions) +- [4. Architecture & concurrency](#4-architecture--concurrency) +- [5. Reporting & CLI honesty](#5-reporting--cli-honesty) +- [6. Tests & fixtures](#6-tests--fixtures) +- [7. Documentation & changelog](#7-documentation--changelog) +- [8. Safety & scope](#8-safety--scope) +- [9. Signs (learned constraints)](#9-signs-learned-constraints) + - [Active signs](#active-signs) +- [10. Quick checklist before merge](#10-quick-checklist-before-merge) +- [11. References](#11-references) --- @@ -42,12 +60,15 @@ Extend the engine **bottom-up**. Do not invent FCPXML meaning inside Reporting. XML → Parsing → Model → Extraction → Projection → Reporting ``` +**Authoring** (`Authoring/`) is a **parallel create path** (detached value graph). It must not feed Reporting, and Reporting must not depend on Authoring types. + | Always | Never | |--------|-------| | Put new XML facts in **Model / Parsing** first | Parse or reinterpret FCPXML only inside `Reporting/` builders | | Put occupancy / retiming / channel visibility in **Projection** | Duplicate timeline math, role resolution, or story walks in Excel/PDF exporters | | Keep **Reporting** presentation-thin (rows, columns, colours, sheet layout) | Add report-only ad hoc XML walks when Extraction/Projection can supply the fact | | Prefer **Projection-first** for Markers / Keywords / Titles / Transitions / Effects (Extraction fallback) | Bypass `ReportProjectionContext` / project-once when those sections are enabled | +| Keep **Authoring** omit-on-write honest via `VersionAvailability` / `VersionFeatureGate` | Use Authoring types inside Reporting builders or invent FCPXML meaning only in Authoring when Model should own it | See ARCHITECTURE.md §2.7 for the full “where to put a change” table. @@ -110,7 +131,7 @@ See ARCHITECTURE.md §2.7 for the full “where to put a change” table. | Rule | Detail | |------|--------| | **AGENT ↔ .cursorrules** | When you update one, update the other. Same overview, architecture, test structure, and conventions. | -| **Feature docs** | User-visible behaviour → Manual (esp. [11 Timeline Projection](Documentation/Manual/11-Timeline-Projection.md) / [18 CLI](Documentation/Manual/18-CLI.md) / [19 Reporting](Documentation/Manual/19-Reporting.md) / [20 Examples](Documentation/Manual/20-Examples.md)) and CLI README as needed. Structural boundaries → ARCHITECTURE.md. Hard constraints → this file. | +| **Feature docs** | User-visible behaviour → Manual (esp. [11 Timeline Projection](Documentation/Manual/12-Timeline-Projection.md) / [18 CLI](Documentation/Manual/19-CLI.md) / [19 Reporting](Documentation/Manual/20-Reporting.md) / [20 Examples](Documentation/Manual/21-Examples.md)) and CLI README as needed. Structural boundaries → ARCHITECTURE.md. Hard constraints → this file. | | **CHANGELOG** | Keep a Changelog format. Version heading links to the GitHub release tag. Sections: **✨ New Features**, **🔧 Improvements**, **🐛 Bug Fixes** (empty → “None in this release.”). | | **File headers** | New Swift files use the project header (see ARCHITECTURE.md §5.2): OpenFCPXMLKit URL line, MIT, tabbed purpose block — no `Created by` / extra copyright lines. | @@ -165,9 +186,15 @@ Append new signs when a failure repeats or a design decision must not drift. Kee ### Sign: swift-testing-only - **Trigger:** Adding or changing any test under `Tests/`. - **Instruction:** Use Swift Testing only (`@Suite` / `@Test` / `#expect` / `#require`). Never reintroduce XCTest or mix frameworks in one file. Harness: `tryLoad*` in `FCPXMLTestSampleLoading` (core) and `require*` in `FCPXMLTestingSampleSupport` (`Test.cancel` for optional fixtures; hard fail for missing bundled samples). Performance: `ContinuousClock` sanity budgets, not XCTest `measure`. Update suite counts in Tests/README + agent docs when the suite grows. -- **Reason:** Migration (former Phases 0–7) is complete; the suite is **1084** listed tests, all Swift Testing. Hybrid XCTest + Testing caused skip/cancel confusion and dual harness drift. +- **Reason:** Migration (former Phases 0–7) is complete; the suite is **1114** listed tests, all Swift Testing. Hybrid XCTest + Testing caused skip/cancel confusion and dual harness drift. - **Provenance:** 2026-07-18 — phased migration completed; supersedes prior hybrid-only and cutover-phase Signs. +### Sign: authoring-not-in-reporting +- **Trigger:** Detached Authoring (`FinalCutPro.FCPXML.Authoring`) or report builders. +- **Instruction:** Keep Authoring parallel to live Model / Timeline Export. Do not import Authoring types into Reporting; do not invent FCPXML meaning only in Authoring when Model/Parsing should own it. Omit-on-write must consult `VersionAvailability` / `VersionFeatureGate`. +- **Reason:** Reporting consumes Extraction → Projection only; Authoring is a create/round-trip path. +- **Provenance:** 2026-07-19 — design lock for Authoring layer (3.2.0). + ### Sign: never-commit-submitted-fcpxml - **Trigger:** Debugging with a user-supplied `.fcpxml` / `.fcpxmld`. - **Instruction:** Keep it under `Tests/Submitted FCPXML/` (gitignored). Promote only anonymised minimal public fixtures. @@ -191,6 +218,6 @@ Append new signs when a failure repeats or a design decision must not drift. Kee ## 11. References -- **Internal:** [ARCHITECTURE.md](ARCHITECTURE.md), [AGENT.md](AGENT.md), [.cursorrules](.cursorrules), [Tests/README.md](Tests/README.md), [Tests/Submitted FCPXML/README.md](Tests/Submitted%20FCPXML/README.md), [Documentation/Manual/00-Index.md](Documentation/Manual/00-Index.md). +- **Internal:** [ARCHITECTURE.md](ARCHITECTURE.md), [AGENT.md](AGENT.md), [.cursorrules](.cursorrules), [Tests/README.md](Tests/README.md), [Tests/Submitted FCPXML/README.md](Tests/Submitted%20FCPXML/README.md), [Documentation/Manual/00-Index.md](Documentation/Manual/00-Index.md), [Documentation/Coverage.md](Documentation/Coverage.md). - **External:** [Final Cut Pro XML](https://fcp.cafe/developers/fcpxml/), [Swift API Design Guidelines](https://swift.org/documentation/api-design-guidelines/), [Swift Concurrency](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/). diff --git a/README.md b/README.md index cf2fb9c..af3d234 100755 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A modern Swift 6 framework for working with Final Cut Pro's FCPXML with full con OpenFCPXMLKit provides a type-safe API for parsing, creating, and manipulating FCPXML with async/await, SwiftTimecode, and Excel/PDF reporting. Targets **macOS 26+** and **iOS 26+** (Foundation XML on macOS; AEXML on iOS). -**Tests:** **1084** listed in `swift test list` — **1078** in `OpenFCPXMLKitTests` + **6** optional `ExcelReportTest` (all Swift Testing) — across **60** sample `.fcpxml` files. Private local investigation inbox: [`Tests/Submitted FCPXML/`](Tests/Submitted%20FCPXML/README.md) (gitignored; never commit private FCPXML). +**Tests:** **1114** listed in `swift test list` — **1108** in `OpenFCPXMLKitTests` + **6** optional `ExcelReportTest` (all Swift Testing) — across **60** sample `.fcpxml` files. Private local investigation inbox: [`Tests/Submitted FCPXML/`](Tests/Submitted%20FCPXML/README.md) (gitignored; never commit private FCPXML). OpenFCPXMLKit is currently in an experimental stage. It covers most core FCPXML attributes and parameters and provides a solid foundation for parsing, creation, and manipulation, with room for future expansion and additional feature coverage. @@ -31,6 +31,7 @@ This codebase is developed using AI agents. - [Timecode & timing](#timecode--timing) - [Typed models](#typed-models) - [Timeline](#timeline) + - [Detached Authoring](#detached-authoring) - [Extraction & media](#extraction--media) - [Timeline Projection](#timeline-projection) - [Excel & PDF reporting](#excel--pdf-reporting) @@ -72,7 +73,7 @@ This codebase is developed using AI agents. ### Typed models - Resources, events, clips, projects, transitions, multicam -- Adjustments (crop, transform, volume, EQ, 360, stereo 3D, …) +- Adjustments (crop, corners, transform, volume, panner, EQ, 360, stereo 3D, …) - Filters, captions/titles (`TextStyle`), smart collections, keyword folders - Keyframe animation, Live Drawing (1.11+), HiddenClipMarker / heroEye / mediaReps (1.13+) @@ -81,6 +82,13 @@ This codebase is developed using AI agents. - Ripple insert, auto lane, clip queries, secondary storylines - Markers, keywords, ratings, custom metadata, timestamps +### Detached Authoring +- `FinalCutPro.FCPXML.Authoring` value graph (no live XML ownership) +- Encode/decode limited subset; omit-on-write via `VersionAvailability` / `VersionFeatureGate` +- Spine: asset-clip, gap, title, transition, video/audio, caption, sync/ref/mc-clip, audition +- Resources: format, asset, effect, media (compound + multicam) +- See [Manual 08 — Detached Authoring](Documentation/Manual/08-Detached-Authoring.md) + ### Extraction & media - Extraction presets (Captions, Markers, Roles, Titles, Effects, FrameData) - Media extract / copy; MIME detection; asset validation; silence; duration; parallel I/O @@ -88,8 +96,9 @@ This codebase is developed using AI agents. ### Timeline Projection - Mid-layer between Extraction and Reporting: `TimelineProjector` → `MediaUsageWindow` - Channels, lanes, `timeMap` / conform retiming, multicam / ref-clip / audition unfold +- `RetimingSegment` compose/clip; `TimelineOccupancyIndex` overlap; `.trackAnalysis` options preset - Reports project **once** per timeline; Markers / Keywords / Titles / Transitions / Effects are Projection-first (Extraction fallback) -- See [Manual 11 — Timeline Projection](Documentation/Manual/11-Timeline-Projection.md) +- See [Manual 12 — Timeline Projection](Documentation/Manual/12-Timeline-Projection.md) ### Excel & PDF reporting - Build once with `buildReport(options:)`, then export `.xlsx` (XLKit) and/or `.pdf` (CoreGraphics) @@ -98,7 +107,7 @@ This codebase is developed using AI agents. - Markers: default omits out-of-bounds starts; `--include-markers-outside-clip-boundaries` adds them + **Hidden** column - Excel: `--protect-sheets` / `protectSheets` applies worksheet edit locks (not encryption; PDF unaffected) - CLI: `--report`, `--report-full`, `--create-pdf`, `--media-resolution`, `--timecode-format`, `--protect-sheets`, … -- See [Manual 19 — Reporting](Documentation/Manual/19-Reporting.md) +- See [Manual 20 — Reporting](Documentation/Manual/20-Reporting.md) ### CLI - `OpenFCPXMLKit-CLI`: check / convert / validate / media-copy / create-project / report @@ -141,7 +150,7 @@ let package = Package( .iOS(.v26) ], dependencies: [ - .package(url: "https://github.com/TheAcharya/OpenFCPXMLKit", from: "3.1.2") + .package(url: "https://github.com/TheAcharya/OpenFCPXMLKit", from: "3.2.0") ], targets: [ .target( @@ -204,7 +213,7 @@ sudo rm /usr/local/bin/OpenFCPXMLKit-CLI ### Compiled From Source ```shell -VERSION=3.1.2 # replace this with the git tag of the version you need +VERSION=3.2.0 # replace this with the git tag of the version you need git clone https://github.com/TheAcharya/OpenFCPXMLKit.git cd OpenFCPXMLKit git checkout "tags/$VERSION" @@ -324,10 +333,11 @@ Complete manual, usage guide, and examples are in the [Documentation](Documentat - **[Manual Index](Documentation/Manual/00-Index.md)** — Full chapter list and navigation (start here) - **[Documentation hub](Documentation/README.md)** — Manual overview +- **[Coverage](Documentation/Coverage.md)** — Detailed FCPXML coverage matrices (typed Model, Authoring, Extraction, Projection, Reporting) - **[CLI](Sources/OpenFCPXMLKitCLI/README.md)** — Flags, examples, building and extending - **[ARCHITECTURE.md](ARCHITECTURE.md)** — Layer stack, codebase map, Mermaid diagrams - **[GUARDRAILS.md](GUARDRAILS.md)** — Must / must-not constraints for contributors and agents -- **[Tests/README.md](Tests/README.md)** — Test suite layout (**1084** listed; all Swift Testing) +- **[Tests/README.md](Tests/README.md)** — Test suite layout (**1114** listed; all Swift Testing) - **[AGENT.md](AGENT.md)** — AI agent / contributor briefing ## FCPXML Version Support @@ -348,7 +358,7 @@ OpenFCPXMLKit supports FCPXML versions 1.5 through 1.14. All DTDs for these vers - Protocols define parsing, timecode conversion, document operations, error handling, MIME type detection, asset validation, silence detection, asset duration measurement, and parallel file I/O; each has a default implementation you can swap. FCPXMLService (and FCPXMLUtility) composes these and exposes sync and async APIs. ModularUtilities provides createService, processFCPXML, validateDocument, convertTimecodes, and similar helpers. - FCPXMLFileLoader handles .fcpxml and .fcpxmld (including bundle Info.fcpxml). FCPXMLValidator and FCPXMLDTDValidator handle structural and schema validation (full DTD on macOS; FCPXMLStructuralValidator on iOS when DTD is unavailable); DTDs for 1.5–1.14 are bundled. - A cross-platform XML layer (`Sources/OpenFCPXMLKit/XML/`) provides protocol types (OFKXMLNode, OFKXMLElement, OFKXMLDocument, OFKXMLFactory) with Foundation and AEXML backends. Extensions on CMTime and the XML protocol types offer convenience APIs; use modular overloads with an explicit dependency to inject your own. Error types are explicit (FCPXMLError, FCPXMLLoadError, export and validation errors); you can inject a custom error handler. -- The engine is layered bottom-up — `XML → Parsing → Model → Extraction → Projection → Reporting` — so the CLI, extraction presets, timeline tools, and reports share one foundation. **Projection** emits playable `MediaUsageWindow`s; **Reporting** (Excel via XLKit, PDF via CoreGraphics) maps Projection + Extraction facts into sheets and owns presentation only. See [ARCHITECTURE.md](ARCHITECTURE.md) for the full codebase map and layer boundaries, and [GUARDRAILS.md](GUARDRAILS.md) for hard must / must-not constraints on those layers. +- The engine is layered bottom-up — `XML → Parsing → Model → Extraction → Projection → Reporting` — so the CLI, extraction presets, timeline tools, and reports share one foundation. **Authoring** is a parallel detached create path (not in the Reporting stack). **Projection** emits playable `MediaUsageWindow`s; **Reporting** (Excel via XLKit, PDF via CoreGraphics) maps Projection + Extraction facts into sheets and owns presentation only. See [ARCHITECTURE.md](ARCHITECTURE.md) for the full codebase map and layer boundaries, and [GUARDRAILS.md](GUARDRAILS.md) for hard must / must-not constraints on those layers. See [AGENT.md](AGENT.md) for a detailed breakdown for AI agents and contributors, and [GUARDRAILS.md](GUARDRAILS.md) for hard must / must-not constraints. diff --git a/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredCompoundClips.swift b/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredCompoundClips.swift new file mode 100644 index 0000000..db59aac --- /dev/null +++ b/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredCompoundClips.swift @@ -0,0 +1,698 @@ +// +// FCPXMLAuthoredCompoundClips.swift +// OpenFCPXMLKit • https://github.com/TheAcharya/OpenFCPXMLKit +// © 2026 • Licensed under MIT License +// + + +// +// Detached authoring types for sync-clip, ref-clip, mc-clip, audition, caption, and media. +// + +import Foundation + +extension FinalCutPro.FCPXML.Authoring { + /// Detached `<caption>`. + public struct Caption: Element, Hashable { + public var name: String? + public var offset: String + public var duration: String + public var start: String? + public var lane: Int? + public var role: String? + public var note: String? + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init( + offset: String, + duration: String, + name: String? = nil, + start: String? = nil, + lane: Int? = nil, + role: String? = nil, + note: String? = nil + ) { + self.name = name + self.offset = offset + self.duration = duration + self.start = start + self.lane = lane + self.role = role + self.note = note + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "caption") + if let name { element.addAttribute(name: "name", value: name) } + element.addAttribute(name: "offset", value: offset) + element.addAttribute(name: "duration", value: duration) + if let start { element.addAttribute(name: "start", value: start) } + if let lane { element.addAttribute(name: "lane", value: String(lane)) } + if let role { element.addAttribute(name: "role", value: role) } + if let note { + let noteElement = context.makeElement(name: "note") + noteElement.stringValue = note + element.addChild(noteElement) + } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> Caption? { + guard element.name == "caption", + let offset = element.stringValue(forAttributeNamed: "offset"), + let duration = element.stringValue(forAttributeNamed: "duration") + else { + return nil + } + let lane = element.stringValue(forAttributeNamed: "lane").flatMap(Int.init) + let note = element.firstChildElement(named: "note")?.stringValue + return Caption( + offset: offset, + duration: duration, + name: element.stringValue(forAttributeNamed: "name"), + start: element.stringValue(forAttributeNamed: "start"), + lane: lane, + role: element.stringValue(forAttributeNamed: "role"), + note: note + ) + } + } + + /// Detached `<mc-source>` on an `mc-clip`. + public struct MCSource: Element, Hashable { + public var angleID: String + /// `all` | `audio` | `video` | `none` (DTD default `all`). + public var srcEnable: String? + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init(angleID: String, srcEnable: String? = nil) { + self.angleID = angleID + self.srcEnable = srcEnable + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "mc-source") + element.addAttribute(name: "angleID", value: angleID) + if let srcEnable { element.addAttribute(name: "srcEnable", value: srcEnable) } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> MCSource? { + guard element.name == "mc-source", + let angleID = element.stringValue(forAttributeNamed: "angleID") + else { + return nil + } + return MCSource( + angleID: angleID, + srcEnable: element.stringValue(forAttributeNamed: "srcEnable") + ) + } + } + + /// Detached `<sync-source>` on a `sync-clip`. + public struct SyncSource: Element, Hashable { + /// DTD: `storyline` | `connected`. + public var sourceID: String + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init(sourceID: String) { + self.sourceID = sourceID + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "sync-source") + element.addAttribute(name: "sourceID", value: sourceID) + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> SyncSource? { + guard element.name == "sync-source", + let sourceID = element.stringValue(forAttributeNamed: "sourceID") + else { + return nil + } + return SyncSource(sourceID: sourceID) + } + } + + /// Nested content allowed inside `<sync-clip>`. + public indirect enum SyncClipContent: Element, Hashable { + case spine(Spine) + case item(SpineItem) + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + switch self { + case .spine(let spine): + try spine.encodeIfAvailable(into: parent, context: context) + case .item(let item): + try item.encodeIfAvailable(into: parent, context: context) + } + } + + static func decode(from element: any OFKXMLElement) -> SyncClipContent? { + if element.name == "spine", let spine = try? Spine.decode(from: element) { + return .spine(spine) + } + if let item = SpineItem.decode(from: element) { + return .item(item) + } + return nil + } + } + + /// Detached `<sync-clip>`. + public struct SyncClip: Element, Hashable { + public var name: String? + public var offset: String + public var duration: String + public var start: String? + public var formatID: String? + public var audioStart: String? + public var audioDuration: String? + public var tcStart: String? + public var tcFormat: String? + public var contents: [SyncClipContent] + public var syncSources: [SyncSource] + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init( + offset: String, + duration: String, + name: String? = nil, + start: String? = nil, + formatID: String? = nil, + audioStart: String? = nil, + audioDuration: String? = nil, + tcStart: String? = nil, + tcFormat: String? = nil, + contents: [SyncClipContent] = [], + syncSources: [SyncSource] = [] + ) { + self.name = name + self.offset = offset + self.duration = duration + self.start = start + self.formatID = formatID + self.audioStart = audioStart + self.audioDuration = audioDuration + self.tcStart = tcStart + self.tcFormat = tcFormat + self.contents = contents + self.syncSources = syncSources + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "sync-clip") + if let name { element.addAttribute(name: "name", value: name) } + element.addAttribute(name: "offset", value: offset) + element.addAttribute(name: "duration", value: duration) + if let start { element.addAttribute(name: "start", value: start) } + if let formatID { element.addAttribute(name: "format", value: formatID) } + if let audioStart { element.addAttribute(name: "audioStart", value: audioStart) } + if let audioDuration { element.addAttribute(name: "audioDuration", value: audioDuration) } + if let tcStart { element.addAttribute(name: "tcStart", value: tcStart) } + if let tcFormat { element.addAttribute(name: "tcFormat", value: tcFormat) } + for content in contents { + try content.encodeIfAvailable(into: element, context: context) + } + for source in syncSources { + try source.encodeIfAvailable(into: element, context: context) + } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> SyncClip? { + guard element.name == "sync-clip", + let offset = element.stringValue(forAttributeNamed: "offset"), + let duration = element.stringValue(forAttributeNamed: "duration") + else { + return nil + } + let syncSources = element.childElements.compactMap { SyncSource.decode(from: $0) } + let contents = element.childElements.compactMap { child -> SyncClipContent? in + if child.name == "sync-source" { return nil } + return SyncClipContent.decode(from: child) + } + return SyncClip( + offset: offset, + duration: duration, + name: element.stringValue(forAttributeNamed: "name"), + start: element.stringValue(forAttributeNamed: "start"), + formatID: element.stringValue(forAttributeNamed: "format"), + audioStart: element.stringValue(forAttributeNamed: "audioStart"), + audioDuration: element.stringValue(forAttributeNamed: "audioDuration"), + tcStart: element.stringValue(forAttributeNamed: "tcStart"), + tcFormat: element.stringValue(forAttributeNamed: "tcFormat"), + contents: contents, + syncSources: syncSources + ) + } + } + + /// Detached `<ref-clip>` referencing compound-clip `<media>`. + public struct RefClip: Element, Hashable { + public var ref: String + public var name: String? + public var offset: String + public var duration: String + public var start: String? + /// `all` | `audio` | `video` (DTD default `all`). + public var srcEnable: String? + public var audioStart: String? + public var audioDuration: String? + public var useAudioSubroles: Bool? + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init( + ref: String, + offset: String, + duration: String, + name: String? = nil, + start: String? = nil, + srcEnable: String? = nil, + audioStart: String? = nil, + audioDuration: String? = nil, + useAudioSubroles: Bool? = nil + ) { + self.ref = ref + self.name = name + self.offset = offset + self.duration = duration + self.start = start + self.srcEnable = srcEnable + self.audioStart = audioStart + self.audioDuration = audioDuration + self.useAudioSubroles = useAudioSubroles + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "ref-clip") + element.addAttribute(name: "ref", value: ref) + if let name { element.addAttribute(name: "name", value: name) } + element.addAttribute(name: "offset", value: offset) + element.addAttribute(name: "duration", value: duration) + if let start { element.addAttribute(name: "start", value: start) } + if let srcEnable { element.addAttribute(name: "srcEnable", value: srcEnable) } + if let audioStart { element.addAttribute(name: "audioStart", value: audioStart) } + if let audioDuration { element.addAttribute(name: "audioDuration", value: audioDuration) } + if let useAudioSubroles { + element.addAttribute(name: "useAudioSubroles", value: useAudioSubroles ? "1" : "0") + } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> RefClip? { + guard element.name == "ref-clip", + let ref = element.stringValue(forAttributeNamed: "ref"), + let offset = element.stringValue(forAttributeNamed: "offset"), + let duration = element.stringValue(forAttributeNamed: "duration") + else { + return nil + } + let useAudioSubroles: Bool? + if let raw = element.stringValue(forAttributeNamed: "useAudioSubroles") { + useAudioSubroles = raw == "1" + } else { + useAudioSubroles = nil + } + return RefClip( + ref: ref, + offset: offset, + duration: duration, + name: element.stringValue(forAttributeNamed: "name"), + start: element.stringValue(forAttributeNamed: "start"), + srcEnable: element.stringValue(forAttributeNamed: "srcEnable"), + audioStart: element.stringValue(forAttributeNamed: "audioStart"), + audioDuration: element.stringValue(forAttributeNamed: "audioDuration"), + useAudioSubroles: useAudioSubroles + ) + } + } + + /// Detached `<mc-clip>` referencing multicam `<media>`. + public struct MCClip: Element, Hashable { + public var ref: String + public var name: String? + public var offset: String + public var duration: String + public var start: String? + /// `all` | `audio` | `video` (DTD default `all`). + public var srcEnable: String? + public var audioStart: String? + public var audioDuration: String? + public var sources: [MCSource] + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init( + ref: String, + offset: String, + duration: String, + name: String? = nil, + start: String? = nil, + srcEnable: String? = nil, + audioStart: String? = nil, + audioDuration: String? = nil, + sources: [MCSource] = [] + ) { + self.ref = ref + self.name = name + self.offset = offset + self.duration = duration + self.start = start + self.srcEnable = srcEnable + self.audioStart = audioStart + self.audioDuration = audioDuration + self.sources = sources + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "mc-clip") + element.addAttribute(name: "ref", value: ref) + if let name { element.addAttribute(name: "name", value: name) } + element.addAttribute(name: "offset", value: offset) + element.addAttribute(name: "duration", value: duration) + if let start { element.addAttribute(name: "start", value: start) } + if let srcEnable { element.addAttribute(name: "srcEnable", value: srcEnable) } + if let audioStart { element.addAttribute(name: "audioStart", value: audioStart) } + if let audioDuration { element.addAttribute(name: "audioDuration", value: audioDuration) } + for source in sources { + try source.encodeIfAvailable(into: element, context: context) + } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> MCClip? { + guard element.name == "mc-clip", + let ref = element.stringValue(forAttributeNamed: "ref"), + let offset = element.stringValue(forAttributeNamed: "offset"), + let duration = element.stringValue(forAttributeNamed: "duration") + else { + return nil + } + return MCClip( + ref: ref, + offset: offset, + duration: duration, + name: element.stringValue(forAttributeNamed: "name"), + start: element.stringValue(forAttributeNamed: "start"), + srcEnable: element.stringValue(forAttributeNamed: "srcEnable"), + audioStart: element.stringValue(forAttributeNamed: "audioStart"), + audioDuration: element.stringValue(forAttributeNamed: "audioDuration"), + sources: element.childElements.compactMap { MCSource.decode(from: $0) } + ) + } + } + + /// Candidate story element inside an `<audition>`. + public indirect enum AuditionCandidate: Element, Hashable { + case assetClip(AssetClip) + case video(Video) + case audio(Audio) + case title(Title) + case refClip(RefClip) + case syncClip(SyncClip) + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + switch self { + case .assetClip(let value): try value.encodeIfAvailable(into: parent, context: context) + case .video(let value): try value.encodeIfAvailable(into: parent, context: context) + case .audio(let value): try value.encodeIfAvailable(into: parent, context: context) + case .title(let value): try value.encodeIfAvailable(into: parent, context: context) + case .refClip(let value): try value.encodeIfAvailable(into: parent, context: context) + case .syncClip(let value): try value.encodeIfAvailable(into: parent, context: context) + } + } + + static func decode(from element: any OFKXMLElement) -> AuditionCandidate? { + if let clip = AssetClip.decode(from: element) { return .assetClip(clip) } + if let video = Video.decode(from: element) { return .video(video) } + if let audio = Audio.decode(from: element) { return .audio(audio) } + if let title = Title.decode(from: element) { return .title(title) } + if let refClip = RefClip.decode(from: element) { return .refClip(refClip) } + if let syncClip = SyncClip.decode(from: element) { return .syncClip(syncClip) } + return nil + } + } + + /// Detached `<audition>` (first child is the active candidate). + public struct Audition: Element, Hashable { + public var offset: String? + public var lane: Int? + public var candidates: [AuditionCandidate] + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init( + offset: String? = nil, + lane: Int? = nil, + candidates: [AuditionCandidate] = [] + ) { + self.offset = offset + self.lane = lane + self.candidates = candidates + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "audition") + if let offset { element.addAttribute(name: "offset", value: offset) } + if let lane { element.addAttribute(name: "lane", value: String(lane)) } + for candidate in candidates { + try candidate.encodeIfAvailable(into: element, context: context) + } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> Audition? { + guard element.name == "audition" else { return nil } + let lane = element.stringValue(forAttributeNamed: "lane").flatMap(Int.init) + return Audition( + offset: element.stringValue(forAttributeNamed: "offset"), + lane: lane, + candidates: element.childElements.compactMap { AuditionCandidate.decode(from: $0) } + ) + } + } + + /// Detached `<mc-angle>` inside multicam media. + public struct MCAngle: Element, Hashable { + public var angleID: String + public var name: String? + public var items: [SpineItem] + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init(angleID: String, name: String? = nil, items: [SpineItem] = []) { + self.angleID = angleID + self.name = name + self.items = items + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "mc-angle") + element.addAttribute(name: "angleID", value: angleID) + if let name { element.addAttribute(name: "name", value: name) } + for item in items { + try item.encodeIfAvailable(into: element, context: context) + } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> MCAngle? { + guard element.name == "mc-angle", + let angleID = element.stringValue(forAttributeNamed: "angleID") + else { + return nil + } + return MCAngle( + angleID: angleID, + name: element.stringValue(forAttributeNamed: "name"), + items: element.childElements.compactMap { SpineItem.decode(from: $0) } + ) + } + } + + /// Detached `<multicam>` content inside `<media>`. + public struct Multicam: Element, Hashable { + public var formatID: String + public var duration: String? + public var tcStart: String? + public var tcFormat: String? + public var angles: [MCAngle] + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init( + formatID: String, + duration: String? = nil, + tcStart: String? = nil, + tcFormat: String? = nil, + angles: [MCAngle] = [] + ) { + self.formatID = formatID + self.duration = duration + self.tcStart = tcStart + self.tcFormat = tcFormat + self.angles = angles + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "multicam") + element.addAttribute(name: "format", value: formatID) + if let duration { element.addAttribute(name: "duration", value: duration) } + if let tcStart { element.addAttribute(name: "tcStart", value: tcStart) } + if let tcFormat { element.addAttribute(name: "tcFormat", value: tcFormat) } + for angle in angles { + try angle.encodeIfAvailable(into: element, context: context) + } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> Multicam? { + guard element.name == "multicam", + let formatID = element.stringValue(forAttributeNamed: "format") + else { + return nil + } + return Multicam( + formatID: formatID, + duration: element.stringValue(forAttributeNamed: "duration"), + tcStart: element.stringValue(forAttributeNamed: "tcStart"), + tcFormat: element.stringValue(forAttributeNamed: "tcFormat"), + angles: element.childElements.compactMap { MCAngle.decode(from: $0) } + ) + } + } + + /// Detached compound-clip `<sequence>` inside `<media>` (not a project sequence). + public struct MediaSequence: Element, Hashable { + public var formatID: String + public var duration: String? + public var tcStart: String? + public var tcFormat: String? + public var spine: Spine + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init( + formatID: String, + duration: String? = nil, + tcStart: String? = nil, + tcFormat: String? = nil, + spine: Spine = Spine() + ) { + self.formatID = formatID + self.duration = duration + self.tcStart = tcStart + self.tcFormat = tcFormat + self.spine = spine + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "sequence") + element.addAttribute(name: "format", value: formatID) + if let duration { element.addAttribute(name: "duration", value: duration) } + if let tcStart { element.addAttribute(name: "tcStart", value: tcStart) } + if let tcFormat { element.addAttribute(name: "tcFormat", value: tcFormat) } + try spine.encodeIfAvailable(into: element, context: context) + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> MediaSequence? { + guard element.name == "sequence", + let formatID = element.stringValue(forAttributeNamed: "format") + else { + return nil + } + let spine = element.firstChildElement(named: "spine").flatMap { try? Spine.decode(from: $0) } ?? Spine() + return MediaSequence( + formatID: formatID, + duration: element.stringValue(forAttributeNamed: "duration"), + tcStart: element.stringValue(forAttributeNamed: "tcStart"), + tcFormat: element.stringValue(forAttributeNamed: "tcFormat"), + spine: spine + ) + } + } + + /// Content of a `<media>` resource. + public enum MediaContent: Element, Hashable { + case sequence(MediaSequence) + case multicam(Multicam) + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + switch self { + case .sequence(let sequence): + try sequence.encodeIfAvailable(into: parent, context: context) + case .multicam(let multicam): + try multicam.encodeIfAvailable(into: parent, context: context) + } + } + + static func decode(from element: any OFKXMLElement) -> MediaContent? { + if let sequence = MediaSequence.decode(from: element) { return .sequence(sequence) } + if let multicam = Multicam.decode(from: element) { return .multicam(multicam) } + return nil + } + } + + /// Detached `<media>` resource (compound clip or multicam). + public struct Media: Element, Hashable { + public var id: String + public var name: String? + public var uid: String? + public var content: MediaContent? + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init( + id: String, + name: String? = nil, + uid: String? = nil, + content: MediaContent? = nil + ) { + self.id = id + self.name = name + self.uid = uid + self.content = content + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "media") + element.addAttribute(name: "id", value: id) + if let name { element.addAttribute(name: "name", value: name) } + if let uid { element.addAttribute(name: "uid", value: uid) } + if let content { + try content.encodeIfAvailable(into: element, context: context) + } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> Media? { + guard element.name == "media", + let id = element.stringValue(forAttributeNamed: "id") + else { + return nil + } + let content = element.childElements.compactMap { MediaContent.decode(from: $0) }.first + return Media( + id: id, + name: element.stringValue(forAttributeNamed: "name"), + uid: element.stringValue(forAttributeNamed: "uid"), + content: content + ) + } + } +} diff --git a/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredDocument.swift b/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredDocument.swift new file mode 100644 index 0000000..d23daed --- /dev/null +++ b/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredDocument.swift @@ -0,0 +1,125 @@ +// +// FCPXMLAuthoredDocument.swift +// OpenFCPXMLKit • https://github.com/TheAcharya/OpenFCPXMLKit +// © 2026 • Licensed under MIT License +// + + +// +// Detached FCPXML document root for authoring round-trips. +// + +import Foundation + +extension FinalCutPro.FCPXML.Authoring { + /// Independent FCPXML document value graph (no live XML ownership). + /// + /// Encode with ``makeXMLDocument(factory:)`` / ``xmlString(factory:)``. Decode a limited + /// subset with ``init(xmlDocument:)``. Expand coverage incrementally; do not use this + /// layer inside Reporting. + public struct Document: Sendable, Hashable { + /// Document version written on the root `fcpxml` element. + public var version: FCPXMLVersion + + /// Detached resources. + public var resources: Resources + + /// Detached library (optional for resource-only documents). + public var library: Library? + + public init( + version: FCPXMLVersion = .default, + resources: Resources = Resources(), + library: Library? = nil + ) { + self.version = version + self.resources = resources + self.library = library + } + + /// Builds a convenience document with one format, one asset, and one project clip. + public static func simpleProject( + version: FCPXMLVersion = .default, + projectName: String = "Untitled", + eventName: String = "Event", + format: Format, + asset: Asset, + clip: AssetClip, + sequenceDuration: String + ) -> Document { + Document( + version: version, + resources: Resources(formats: [format], assets: [asset]), + library: Library( + events: [ + Event( + name: eventName, + projects: [ + Project( + name: projectName, + sequence: Sequence( + formatID: format.id, + duration: sequenceDuration, + spine: Spine(assetClips: [clip]) + ) + ) + ] + ) + ] + ) + ) + } + + /// Encodes this value graph into an ``OFKXMLDocument``. + public func makeXMLDocument( + factory: any OFKXMLFactory = OFKXMLDefaultFactory() + ) throws -> any OFKXMLDocument { + let context = Context(version: version, factory: factory) + let root = context.makeElement(name: "fcpxml") + root.addAttribute(name: "version", value: version.stringValue) + try resources.encodeIfAvailable(into: root, context: context) + if let library { + try library.encodeIfAvailable(into: root, context: context) + } + let document = factory.makeDocument() + document.setRootElement(root) + return document + } + + /// Serializes to an XML string. + public func xmlString( + factory: any OFKXMLFactory = OFKXMLDefaultFactory() + ) throws -> String { + let document = try makeXMLDocument(factory: factory) + return document.xmlString + } + + /// Decodes a limited authoring subset from a live XML document. + public init(xmlDocument: any OFKXMLDocument) throws { + guard let root = xmlDocument.rootElement() else { + throw Error.missingRootElement + } + guard root.name == "fcpxml" else { + throw Error.invalidRootName(root.name ?? "") + } + let versionString = root.stringValue(forAttributeNamed: "version") + guard let versionString, + let version = FCPXMLVersion(string: versionString) + else { + throw Error.missingOrInvalidVersion(versionString) + } + guard let resourcesElement = root.firstChildElement(named: "resources"), + let resources = Resources.decode(from: resourcesElement) + else { + throw Error.missingResources + } + let library: Library? + if let libraryElement = root.firstChildElement(named: "library") { + library = try Library.decode(from: libraryElement) + } else { + library = nil + } + self.init(version: version, resources: resources, library: library) + } + } +} diff --git a/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredResources.swift b/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredResources.swift new file mode 100644 index 0000000..d50b237 --- /dev/null +++ b/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredResources.swift @@ -0,0 +1,252 @@ +// +// FCPXMLAuthoredResources.swift +// OpenFCPXMLKit • https://github.com/TheAcharya/OpenFCPXMLKit +// © 2026 • Licensed under MIT License +// + + +// +// Detached resource value types for authoring (format, asset, media-rep). +// + +import Foundation + +extension FinalCutPro.FCPXML.Authoring { + /// Detached `<format>` resource. + public struct Format: Element, Hashable { + public var id: String + public var name: String? + public var frameDuration: String + public var width: Int + public var height: Int + public var colorSpace: String? + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init( + id: String, + frameDuration: String, + width: Int, + height: Int, + name: String? = nil, + colorSpace: String? = nil + ) { + self.id = id + self.name = name + self.frameDuration = frameDuration + self.width = width + self.height = height + self.colorSpace = colorSpace + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "format") + element.addAttribute(name: "id", value: id) + if let name { element.addAttribute(name: "name", value: name) } + element.addAttribute(name: "frameDuration", value: frameDuration) + element.addAttribute(name: "width", value: String(width)) + element.addAttribute(name: "height", value: String(height)) + if let colorSpace { element.addAttribute(name: "colorSpace", value: colorSpace) } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> Format? { + guard element.name == "format", + let id = element.stringValue(forAttributeNamed: "id"), + let frameDuration = element.stringValue(forAttributeNamed: "frameDuration"), + let width = Int(element.stringValue(forAttributeNamed: "width") ?? ""), + let height = Int(element.stringValue(forAttributeNamed: "height") ?? "") + else { + return nil + } + return Format( + id: id, + frameDuration: frameDuration, + width: width, + height: height, + name: element.stringValue(forAttributeNamed: "name"), + colorSpace: element.stringValue(forAttributeNamed: "colorSpace") + ) + } + } + + /// Detached `<media-rep>` under an asset. + public struct MediaRep: Element, Hashable { + public var kind: String + public var src: String + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init(kind: String = "original-media", src: String) { + self.kind = kind + self.src = src + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "media-rep") + element.addAttribute(name: "kind", value: kind) + element.addAttribute(name: "src", value: src) + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> MediaRep? { + guard element.name == "media-rep", + let src = element.stringValue(forAttributeNamed: "src") + else { + return nil + } + return MediaRep( + kind: element.stringValue(forAttributeNamed: "kind") ?? "original-media", + src: src + ) + } + } + + /// Detached `<asset>` resource. + public struct Asset: Element, Hashable { + public var id: String + public var name: String? + public var hasVideo: Bool + public var hasAudio: Bool + public var duration: String? + public var formatID: String? + public var mediaReps: [MediaRep] + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init( + id: String, + name: String? = nil, + hasVideo: Bool = true, + hasAudio: Bool = false, + duration: String? = nil, + formatID: String? = nil, + mediaReps: [MediaRep] = [] + ) { + self.id = id + self.name = name + self.hasVideo = hasVideo + self.hasAudio = hasAudio + self.duration = duration + self.formatID = formatID + self.mediaReps = mediaReps + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "asset") + element.addAttribute(name: "id", value: id) + if let name { element.addAttribute(name: "name", value: name) } + if hasVideo { element.addAttribute(name: "hasVideo", value: "1") } + if hasAudio { element.addAttribute(name: "hasAudio", value: "1") } + if let duration { element.addAttribute(name: "duration", value: duration) } + if let formatID { element.addAttribute(name: "format", value: formatID) } + for rep in mediaReps { + try rep.encodeIfAvailable(into: element, context: context) + } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> Asset? { + guard element.name == "asset", + let id = element.stringValue(forAttributeNamed: "id") + else { + return nil + } + let reps = element.childElements.compactMap { MediaRep.decode(from: $0) } + return Asset( + id: id, + name: element.stringValue(forAttributeNamed: "name"), + hasVideo: (element.stringValue(forAttributeNamed: "hasVideo") ?? "0") == "1", + hasAudio: (element.stringValue(forAttributeNamed: "hasAudio") ?? "0") == "1", + duration: element.stringValue(forAttributeNamed: "duration"), + formatID: element.stringValue(forAttributeNamed: "format"), + mediaReps: reps + ) + } + } + + /// Detached `<effect>` resource (titles, transitions, filters). + public struct Effect: Element, Hashable { + public var id: String + public var name: String? + public var uid: String? + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init(id: String, name: String? = nil, uid: String? = nil) { + self.id = id + self.name = name + self.uid = uid + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "effect") + element.addAttribute(name: "id", value: id) + if let name { element.addAttribute(name: "name", value: name) } + if let uid { element.addAttribute(name: "uid", value: uid) } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> Effect? { + guard element.name == "effect", + let id = element.stringValue(forAttributeNamed: "id") + else { + return nil + } + return Effect( + id: id, + name: element.stringValue(forAttributeNamed: "name"), + uid: element.stringValue(forAttributeNamed: "uid") + ) + } + } + + /// Detached `<resources>` container. + public struct Resources: Element, Hashable { + public var formats: [Format] + public var assets: [Asset] + public var effects: [Effect] + public var media: [Media] + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init( + formats: [Format] = [], + assets: [Asset] = [], + effects: [Effect] = [], + media: [Media] = [] + ) { + self.formats = formats + self.assets = assets + self.effects = effects + self.media = media + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "resources") + for format in formats { + try format.encodeIfAvailable(into: element, context: context) + } + for asset in assets { + try asset.encodeIfAvailable(into: element, context: context) + } + for effect in effects { + try effect.encodeIfAvailable(into: element, context: context) + } + for mediaResource in media { + try mediaResource.encodeIfAvailable(into: element, context: context) + } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> Resources? { + guard element.name == "resources" else { return nil } + return Resources( + formats: element.childElements.compactMap { Format.decode(from: $0) }, + assets: element.childElements.compactMap { Asset.decode(from: $0) }, + effects: element.childElements.compactMap { Effect.decode(from: $0) }, + media: element.childElements.compactMap { Media.decode(from: $0) } + ) + } + } +} diff --git a/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredSpineItems.swift b/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredSpineItems.swift new file mode 100644 index 0000000..f66334b --- /dev/null +++ b/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredSpineItems.swift @@ -0,0 +1,337 @@ +// +// FCPXMLAuthoredSpineItems.swift +// OpenFCPXMLKit • https://github.com/TheAcharya/OpenFCPXMLKit +// © 2026 • Licensed under MIT License +// + + +// +// Detached spine story items (gap, title, transition, video, audio, compounds) + SpineItem. +// + +import Foundation + +extension FinalCutPro.FCPXML.Authoring { + /// Detached volume adjustment (`adjust-volume`). + public struct VolumeAdjustment: Element, Hashable { + /// Amount string as written in FCPXML (e.g. `"0dB"`, `"-3dB"`). + public var amount: String + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init(amount: String = "0dB") { + self.amount = amount + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "adjust-volume") + element.addAttribute(name: "amount", value: amount) + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> VolumeAdjustment? { + guard element.name == "adjust-volume" else { return nil } + return VolumeAdjustment( + amount: element.stringValue(forAttributeNamed: "amount") ?? "0dB" + ) + } + } + + /// Detached `<gap>`. + public struct Gap: Element, Hashable { + public var name: String? + public var offset: String + public var duration: String + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init(offset: String, duration: String, name: String? = nil) { + self.name = name + self.offset = offset + self.duration = duration + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "gap") + if let name { element.addAttribute(name: "name", value: name) } + element.addAttribute(name: "offset", value: offset) + element.addAttribute(name: "duration", value: duration) + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> Gap? { + guard element.name == "gap", + let offset = element.stringValue(forAttributeNamed: "offset"), + let duration = element.stringValue(forAttributeNamed: "duration") + else { + return nil + } + return Gap( + offset: offset, + duration: duration, + name: element.stringValue(forAttributeNamed: "name") + ) + } + } + + /// Detached `<title>` (generator/title effect instance). + public struct Title: Element, Hashable { + public var ref: String + public var name: String? + public var offset: String + public var duration: String + public var start: String? + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init( + ref: String, + offset: String, + duration: String, + name: String? = nil, + start: String? = nil + ) { + self.ref = ref + self.name = name + self.offset = offset + self.duration = duration + self.start = start + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "title") + element.addAttribute(name: "ref", value: ref) + if let name { element.addAttribute(name: "name", value: name) } + element.addAttribute(name: "offset", value: offset) + element.addAttribute(name: "duration", value: duration) + if let start { element.addAttribute(name: "start", value: start) } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> Title? { + guard element.name == "title", + let ref = element.stringValue(forAttributeNamed: "ref"), + let offset = element.stringValue(forAttributeNamed: "offset"), + let duration = element.stringValue(forAttributeNamed: "duration") + else { + return nil + } + return Title( + ref: ref, + offset: offset, + duration: duration, + name: element.stringValue(forAttributeNamed: "name"), + start: element.stringValue(forAttributeNamed: "start") + ) + } + } + + /// Detached `<transition>`. + public struct Transition: Element, Hashable { + public var ref: String + public var name: String? + public var offset: String + public var duration: String + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init( + ref: String, + offset: String, + duration: String, + name: String? = nil + ) { + self.ref = ref + self.name = name + self.offset = offset + self.duration = duration + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "transition") + element.addAttribute(name: "ref", value: ref) + if let name { element.addAttribute(name: "name", value: name) } + element.addAttribute(name: "offset", value: offset) + element.addAttribute(name: "duration", value: duration) + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> Transition? { + guard element.name == "transition", + let ref = element.stringValue(forAttributeNamed: "ref"), + let offset = element.stringValue(forAttributeNamed: "offset"), + let duration = element.stringValue(forAttributeNamed: "duration") + else { + return nil + } + return Transition( + ref: ref, + offset: offset, + duration: duration, + name: element.stringValue(forAttributeNamed: "name") + ) + } + } + + /// Detached `<video>` leaf. + public struct Video: Element, Hashable { + public var ref: String + public var name: String? + public var offset: String + public var duration: String + public var start: String? + public var srcID: String? + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init( + ref: String, + offset: String, + duration: String, + name: String? = nil, + start: String? = nil, + srcID: String? = nil + ) { + self.ref = ref + self.name = name + self.offset = offset + self.duration = duration + self.start = start + self.srcID = srcID + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "video") + element.addAttribute(name: "ref", value: ref) + if let name { element.addAttribute(name: "name", value: name) } + element.addAttribute(name: "offset", value: offset) + element.addAttribute(name: "duration", value: duration) + if let start { element.addAttribute(name: "start", value: start) } + if let srcID { element.addAttribute(name: "srcID", value: srcID) } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> Video? { + guard element.name == "video", + let ref = element.stringValue(forAttributeNamed: "ref"), + let offset = element.stringValue(forAttributeNamed: "offset"), + let duration = element.stringValue(forAttributeNamed: "duration") + else { + return nil + } + return Video( + ref: ref, + offset: offset, + duration: duration, + name: element.stringValue(forAttributeNamed: "name"), + start: element.stringValue(forAttributeNamed: "start"), + srcID: element.stringValue(forAttributeNamed: "srcID") + ) + } + } + + /// Detached `<audio>` leaf. + public struct Audio: Element, Hashable { + public var ref: String + public var name: String? + public var offset: String + public var duration: String + public var start: String? + public var srcID: String? + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init( + ref: String, + offset: String, + duration: String, + name: String? = nil, + start: String? = nil, + srcID: String? = nil + ) { + self.ref = ref + self.name = name + self.offset = offset + self.duration = duration + self.start = start + self.srcID = srcID + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "audio") + element.addAttribute(name: "ref", value: ref) + if let name { element.addAttribute(name: "name", value: name) } + element.addAttribute(name: "offset", value: offset) + element.addAttribute(name: "duration", value: duration) + if let start { element.addAttribute(name: "start", value: start) } + if let srcID { element.addAttribute(name: "srcID", value: srcID) } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> Audio? { + guard element.name == "audio", + let ref = element.stringValue(forAttributeNamed: "ref"), + let offset = element.stringValue(forAttributeNamed: "offset"), + let duration = element.stringValue(forAttributeNamed: "duration") + else { + return nil + } + return Audio( + ref: ref, + offset: offset, + duration: duration, + name: element.stringValue(forAttributeNamed: "name"), + start: element.stringValue(forAttributeNamed: "start"), + srcID: element.stringValue(forAttributeNamed: "srcID") + ) + } + } + + /// Polymorphic spine child for detached authoring. + public indirect enum SpineItem: Element, Hashable { + case assetClip(AssetClip) + case gap(Gap) + case title(Title) + case transition(Transition) + case video(Video) + case audio(Audio) + case caption(Caption) + case syncClip(SyncClip) + case refClip(RefClip) + case mcClip(MCClip) + case audition(Audition) + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + switch self { + case .assetClip(let value): try value.encodeIfAvailable(into: parent, context: context) + case .gap(let value): try value.encodeIfAvailable(into: parent, context: context) + case .title(let value): try value.encodeIfAvailable(into: parent, context: context) + case .transition(let value): try value.encodeIfAvailable(into: parent, context: context) + case .video(let value): try value.encodeIfAvailable(into: parent, context: context) + case .audio(let value): try value.encodeIfAvailable(into: parent, context: context) + case .caption(let value): try value.encodeIfAvailable(into: parent, context: context) + case .syncClip(let value): try value.encodeIfAvailable(into: parent, context: context) + case .refClip(let value): try value.encodeIfAvailable(into: parent, context: context) + case .mcClip(let value): try value.encodeIfAvailable(into: parent, context: context) + case .audition(let value): try value.encodeIfAvailable(into: parent, context: context) + } + } + + static func decode(from element: any OFKXMLElement) -> SpineItem? { + if let clip = AssetClip.decode(from: element) { return .assetClip(clip) } + if let gap = Gap.decode(from: element) { return .gap(gap) } + if let title = Title.decode(from: element) { return .title(title) } + if let transition = Transition.decode(from: element) { return .transition(transition) } + if let video = Video.decode(from: element) { return .video(video) } + if let audio = Audio.decode(from: element) { return .audio(audio) } + if let caption = Caption.decode(from: element) { return .caption(caption) } + if let syncClip = SyncClip.decode(from: element) { return .syncClip(syncClip) } + if let refClip = RefClip.decode(from: element) { return .refClip(refClip) } + if let mcClip = MCClip.decode(from: element) { return .mcClip(mcClip) } + if let audition = Audition.decode(from: element) { return .audition(audition) } + return nil + } + } +} diff --git a/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredStory.swift b/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredStory.swift new file mode 100644 index 0000000..e11f1c6 --- /dev/null +++ b/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoredStory.swift @@ -0,0 +1,333 @@ +// +// FCPXMLAuthoredStory.swift +// OpenFCPXMLKit • https://github.com/TheAcharya/OpenFCPXMLKit +// © 2026 • Licensed under MIT License +// + + +// +// Detached story value types for authoring (library → asset-clip). +// + +import Foundation + +extension FinalCutPro.FCPXML.Authoring { + /// Detached cinematic adjustment (`adjust-cinematic`, FCPXML 1.10+). + /// + /// Omitted automatically when encoding to versions before 1.10. + public struct CinematicAdjustment: Element, Hashable { + public var isEnabled: Bool + public var aperture: String? + + public var availability: FinalCutPro.FCPXML.VersionAvailability { + FinalCutPro.FCPXML.VersionFeatureGate.availability(forElement: "adjust-cinematic") + } + + public init(isEnabled: Bool = true, aperture: String? = nil) { + self.isEnabled = isEnabled + self.aperture = aperture + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "adjust-cinematic") + if !isEnabled { + element.addAttribute(name: "enabled", value: "0") + } + if let aperture { + element.addAttribute(name: "aperture", value: aperture) + } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> CinematicAdjustment? { + guard element.name == "adjust-cinematic" else { return nil } + let enabled = (element.stringValue(forAttributeNamed: "enabled") ?? "1") == "1" + return CinematicAdjustment( + isEnabled: enabled, + aperture: element.stringValue(forAttributeNamed: "aperture") + ) + } + } + + /// Detached `<asset-clip>` on a spine or lane. + public struct AssetClip: Element, Hashable { + public var ref: String + public var name: String? + public var offset: String + public var duration: String + public var start: String? + public var audioStart: String? + public var audioDuration: String? + public var cinematic: CinematicAdjustment? + public var volume: VolumeAdjustment? + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init( + ref: String, + offset: String, + duration: String, + name: String? = nil, + start: String? = nil, + audioStart: String? = nil, + audioDuration: String? = nil, + cinematic: CinematicAdjustment? = nil, + volume: VolumeAdjustment? = nil + ) { + self.ref = ref + self.name = name + self.offset = offset + self.duration = duration + self.start = start + self.audioStart = audioStart + self.audioDuration = audioDuration + self.cinematic = cinematic + self.volume = volume + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "asset-clip") + element.addAttribute(name: "ref", value: ref) + if let name { element.addAttribute(name: "name", value: name) } + element.addAttribute(name: "offset", value: offset) + element.addAttribute(name: "duration", value: duration) + if let start { element.addAttribute(name: "start", value: start) } + if let audioStart { element.addAttribute(name: "audioStart", value: audioStart) } + if let audioDuration { element.addAttribute(name: "audioDuration", value: audioDuration) } + if let cinematic { + try cinematic.encodeIfAvailable(into: element, context: context) + } + if let volume { + try volume.encodeIfAvailable(into: element, context: context) + } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) -> AssetClip? { + guard element.name == "asset-clip", + let ref = element.stringValue(forAttributeNamed: "ref"), + let offset = element.stringValue(forAttributeNamed: "offset"), + let duration = element.stringValue(forAttributeNamed: "duration") + else { + return nil + } + let cinematic = element.childElements + .compactMap { CinematicAdjustment.decode(from: $0) } + .first + let volume = element.childElements + .compactMap { VolumeAdjustment.decode(from: $0) } + .first + return AssetClip( + ref: ref, + offset: offset, + duration: duration, + name: element.stringValue(forAttributeNamed: "name"), + start: element.stringValue(forAttributeNamed: "start"), + audioStart: element.stringValue(forAttributeNamed: "audioStart"), + audioDuration: element.stringValue(forAttributeNamed: "audioDuration"), + cinematic: cinematic, + volume: volume + ) + } + } + + /// Detached `<spine>`. + public struct Spine: Element, Hashable { + /// Ordered spine children (asset-clip, gap, title, transition, video, audio, …). + public var items: [SpineItem] + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init(items: [SpineItem] = []) { + self.items = items + } + + /// Convenience for spines that only contain asset-clips. + public init(assetClips: [AssetClip]) { + self.items = assetClips.map { .assetClip($0) } + } + + /// Asset-clip children only (derived from ``items``). + public var assetClips: [AssetClip] { + items.compactMap { + if case .assetClip(let clip) = $0 { return clip } + return nil + } + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "spine") + for item in items { + try item.encodeIfAvailable(into: element, context: context) + } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) throws -> Spine { + guard element.name == "spine" else { throw Error.missingSpine } + return Spine( + items: element.childElements.compactMap { SpineItem.decode(from: $0) } + ) + } + } + + /// Detached `<sequence>`. + public struct Sequence: Element, Hashable { + public var formatID: String + public var duration: String + public var tcStart: String? + public var spine: Spine + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init( + formatID: String, + duration: String, + tcStart: String? = "0s", + spine: Spine = Spine() + ) { + self.formatID = formatID + self.duration = duration + self.tcStart = tcStart + self.spine = spine + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "sequence") + element.addAttribute(name: "format", value: formatID) + element.addAttribute(name: "duration", value: duration) + if let tcStart { element.addAttribute(name: "tcStart", value: tcStart) } + try spine.encodeIfAvailable(into: element, context: context) + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) throws -> Sequence { + guard element.name == "sequence", + let formatID = element.stringValue(forAttributeNamed: "format"), + let duration = element.stringValue(forAttributeNamed: "duration") + else { + throw Error.missingSequence + } + guard let spineElement = element.firstChildElement(named: "spine") else { + throw Error.missingSpine + } + return Sequence( + formatID: formatID, + duration: duration, + tcStart: element.stringValue(forAttributeNamed: "tcStart"), + spine: try Spine.decode(from: spineElement) + ) + } + } + + /// Detached `<project>`. + public struct Project: Element, Hashable { + public var name: String + public var uid: String? + public var sequence: Sequence + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init(name: String, sequence: Sequence, uid: String? = nil) { + self.name = name + self.uid = uid + self.sequence = sequence + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "project") + element.addAttribute(name: "name", value: name) + if let uid { element.addAttribute(name: "uid", value: uid) } + try sequence.encodeIfAvailable(into: element, context: context) + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) throws -> Project { + guard element.name == "project", + let name = element.stringValue(forAttributeNamed: "name"), + let sequenceElement = element.firstChildElement(named: "sequence") + else { + throw Error.missingProject + } + return Project( + name: name, + sequence: try Sequence.decode(from: sequenceElement), + uid: element.stringValue(forAttributeNamed: "uid") + ) + } + } + + /// Detached `<event>`. + public struct Event: Element, Hashable { + public var name: String + public var uid: String? + public var projects: [Project] + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init(name: String, projects: [Project] = [], uid: String? = nil) { + self.name = name + self.uid = uid + self.projects = projects + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "event") + element.addAttribute(name: "name", value: name) + if let uid { element.addAttribute(name: "uid", value: uid) } + for project in projects { + try project.encodeIfAvailable(into: element, context: context) + } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) throws -> Event { + guard element.name == "event", + let name = element.stringValue(forAttributeNamed: "name") + else { + throw Error.missingLibrary + } + let projects = try element.childElements + .filter { $0.name == "project" } + .map { try Project.decode(from: $0) } + return Event( + name: name, + projects: projects, + uid: element.stringValue(forAttributeNamed: "uid") + ) + } + } + + /// Detached `<library>`. + public struct Library: Element, Hashable { + public var location: String? + public var events: [Event] + + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + public init(events: [Event] = [], location: String? = nil) { + self.location = location + self.events = events + } + + public func encode(into parent: any OFKXMLElement, context: Context) throws { + let element = context.makeElement(name: "library") + if let location { element.addAttribute(name: "location", value: location) } + for event in events { + try event.encodeIfAvailable(into: element, context: context) + } + parent.addChild(element) + } + + static func decode(from element: any OFKXMLElement) throws -> Library { + guard element.name == "library" else { throw Error.missingLibrary } + let events = try element.childElements + .filter { $0.name == "event" } + .map { try Event.decode(from: $0) } + return Library( + events: events, + location: element.stringValue(forAttributeNamed: "location") + ) + } + } +} diff --git a/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoringContext.swift b/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoringContext.swift new file mode 100644 index 0000000..bcb13de --- /dev/null +++ b/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoringContext.swift @@ -0,0 +1,50 @@ +// +// FCPXMLAuthoringContext.swift +// OpenFCPXMLKit • https://github.com/TheAcharya/OpenFCPXMLKit +// © 2026 • Licensed under MIT License +// + + +// +// Encode context for the detached authoring layer. +// + +import Foundation + +extension FinalCutPro.FCPXML { + /// Namespace for detached (non-live) FCPXML document authoring value types. + /// + /// Authoring models do **not** wrap ``OFKXMLElement``. They are independent value + /// graphs that encode into XML via ``Authoring/Document/makeXMLDocument(factory:)`` + /// and optionally decode a limited subset back. Live parse/edit remains in ``Model``. + public enum Authoring {} +} + +extension FinalCutPro.FCPXML.Authoring { + /// Shared encode context: target document version and XML factory. + public struct Context: Sendable { + /// Target FCPXML version written on the root and used for omit-on-write. + public var version: FCPXMLVersion + + /// Factory used to create elements and documents. + public nonisolated(unsafe) var factory: any OFKXMLFactory + + public init( + version: FCPXMLVersion = .default, + factory: any OFKXMLFactory = OFKXMLDefaultFactory() + ) { + self.version = version + self.factory = factory + } + + /// `true` when `availability` allows emission at ``version``. + public func allows(_ availability: FinalCutPro.FCPXML.VersionAvailability) -> Bool { + availability.contains(version) + } + + /// Creates an element with the given name. + public func makeElement(name: String) -> any OFKXMLElement { + factory.makeElement(name: name) + } + } +} diff --git a/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoringElement.swift b/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoringElement.swift new file mode 100644 index 0000000..8aa6ac4 --- /dev/null +++ b/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoringElement.swift @@ -0,0 +1,41 @@ +// +// FCPXMLAuthoringElement.swift +// OpenFCPXMLKit • https://github.com/TheAcharya/OpenFCPXMLKit +// © 2026 • Licensed under MIT License +// + + +// +// Protocol for detached authoring value types that encode into OFKXML. +// + +import Foundation + +extension FinalCutPro.FCPXML.Authoring { + /// A detached authoring value that can emit XML under a versioned context. + /// + /// Implementations must not retain live XML nodes. Encoding is explicit (no reflection). + public protocol Element: Sendable { + /// Versions that may include this element or field when encoding. + var availability: FinalCutPro.FCPXML.VersionAvailability { get } + + /// Appends this value under `parent` when available for ``Context/version``. + /// + /// When unavailable, implementations must no-op (omit) without throwing. + func encode(into parent: any OFKXMLElement, context: FinalCutPro.FCPXML.Authoring.Context) throws + } +} + +extension FinalCutPro.FCPXML.Authoring.Element { + /// Default availability: all supported DTD versions. + public var availability: FinalCutPro.FCPXML.VersionAvailability { .always } + + /// Encodes only when ``availability`` contains the context version. + public func encodeIfAvailable( + into parent: any OFKXMLElement, + context: FinalCutPro.FCPXML.Authoring.Context + ) throws { + guard context.allows(availability) else { return } + try encode(into: parent, context: context) + } +} diff --git a/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoringError.swift b/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoringError.swift new file mode 100644 index 0000000..16c037f --- /dev/null +++ b/Sources/OpenFCPXMLKit/Authoring/FCPXMLAuthoringError.swift @@ -0,0 +1,47 @@ +// +// FCPXMLAuthoringError.swift +// OpenFCPXMLKit • https://github.com/TheAcharya/OpenFCPXMLKit +// © 2026 • Licensed under MIT License +// + + +// +// Errors for detached authoring encode/decode. +// + +import Foundation + +extension FinalCutPro.FCPXML.Authoring { + /// Errors raised while building or parsing authored FCPXML value graphs. + public enum Error: Swift.Error, LocalizedError, Sendable, Equatable { + case missingRootElement + case invalidRootName(String) + case missingOrInvalidVersion(String?) + case missingResources + case missingLibrary + case missingProject + case missingSequence + case missingSpine + + public var errorDescription: String? { + switch self { + case .missingRootElement: + return "Authored document has no root element." + case .invalidRootName(let name): + return "Expected root element 'fcpxml', found '\(name)'." + case .missingOrInvalidVersion(let value): + return "Missing or unsupported FCPXML version attribute: \(value ?? "nil")." + case .missingResources: + return "Authored document is missing a resources element." + case .missingLibrary: + return "Authored document is missing a library element." + case .missingProject: + return "Authored library/event is missing a project element." + case .missingSequence: + return "Authored project is missing a sequence element." + case .missingSpine: + return "Authored sequence is missing a spine element." + } + } + } +} diff --git a/Sources/OpenFCPXMLKit/Authoring/FCPXMLVersionAvailability.swift b/Sources/OpenFCPXMLKit/Authoring/FCPXMLVersionAvailability.swift new file mode 100644 index 0000000..b936e35 --- /dev/null +++ b/Sources/OpenFCPXMLKit/Authoring/FCPXMLVersionAvailability.swift @@ -0,0 +1,70 @@ +// +// FCPXMLVersionAvailability.swift +// OpenFCPXMLKit • https://github.com/TheAcharya/OpenFCPXMLKit +// © 2026 • Licensed under MIT License +// + + +// +// Version range for detached authoring encode/omit decisions. +// + +import Foundation + +extension FinalCutPro.FCPXML { + /// Declares which FCPXML document versions may emit an authored fact. + /// + /// Used by the detached ``Authoring`` layer so fields and elements introduced in later + /// DTDs are omitted when encoding to an older target version (FCPXML 1.5 floor). + /// Shared element/attribute introductions also live in ``VersionFeatureGate``. + public struct VersionAvailability: Sendable, Hashable, Equatable { + /// First version that may include the fact (`nil` = from the earliest supported DTD). + public var introduced: FCPXMLVersion? + + /// Last version that may include the fact (`nil` = through the latest supported DTD). + public var lastSupported: FCPXMLVersion? + + /// Available in every supported DTD version (1.5–1.14). + public static let always = VersionAvailability(introduced: nil, lastSupported: nil) + + /// Available starting at `version` (inclusive) through the latest DTD. + public static func from(_ version: FCPXMLVersion) -> VersionAvailability { + VersionAvailability(introduced: version, lastSupported: nil) + } + + /// Available from the earliest DTD through `version` (inclusive). + public static func upTo(_ version: FCPXMLVersion) -> VersionAvailability { + VersionAvailability(introduced: nil, lastSupported: version) + } + + /// Available in the inclusive range `introduced`…`lastSupported`. + public static func between( + _ introduced: FCPXMLVersion, + and lastSupported: FCPXMLVersion + ) -> VersionAvailability { + VersionAvailability(introduced: introduced, lastSupported: lastSupported) + } + + public init(introduced: FCPXMLVersion? = nil, lastSupported: FCPXMLVersion? = nil) { + self.introduced = introduced + self.lastSupported = lastSupported + } + + /// `true` when `version` lies inside this availability window. + public func contains(_ version: FCPXMLVersion) -> Bool { + if let introduced, !version.isAtLeast(introduced) { + return false + } + if let lastSupported, version.isAtLeast(lastSupported), version != lastSupported { + // version > lastSupported + guard let lastIndex = FCPXMLVersion.allCases.firstIndex(of: lastSupported), + let versionIndex = FCPXMLVersion.allCases.firstIndex(of: version) + else { + return false + } + if versionIndex > lastIndex { return false } + } + return true + } + } +} diff --git a/Sources/OpenFCPXMLKit/Classes/FCPXMLVersionFeatureGate.swift b/Sources/OpenFCPXMLKit/Classes/FCPXMLVersionFeatureGate.swift new file mode 100644 index 0000000..35ea0c2 --- /dev/null +++ b/Sources/OpenFCPXMLKit/Classes/FCPXMLVersionFeatureGate.swift @@ -0,0 +1,107 @@ +// +// FCPXMLVersionFeatureGate.swift +// OpenFCPXMLKit • https://github.com/TheAcharya/OpenFCPXMLKit +// © 2026 • Licensed under MIT License +// + + +// +// First-class DTD-derived version gates for elements and attributes. +// + +import Foundation + +extension FinalCutPro.FCPXML { + /// Shared registry of FCPXML features gated by document version. + /// + /// Derived from public DTD introductions (clean-room). Used by: + /// - Detached ``Authoring`` omit-on-write via ``VersionAvailability`` + /// - ``FCPXMLVersionConverter`` fallback stripping when a DTD allowlist is unavailable + /// + /// Prefer DTD allowlist stripping when embedded/bundle DTDs are present; this registry + /// is the explicit per-feature API and the converter fallback source of truth. + public enum VersionFeatureGate { + /// Element name and the versions that may include it. + public struct ElementFeature: Sendable, Hashable, Equatable { + public var name: String + public var availability: VersionAvailability + + public init(name: String, availability: VersionAvailability) { + self.name = name + self.availability = availability + } + } + + /// Attribute on a named element and the versions that may include it. + public struct AttributeFeature: Sendable, Hashable, Equatable { + public var element: String + public var attribute: String + public var availability: VersionAvailability + + public init(element: String, attribute: String, availability: VersionAvailability) { + self.element = element + self.attribute = attribute + self.availability = availability + } + } + + /// Elements introduced after FCPXML 1.5 (omit when encoding/converting older). + public static let elements: [ElementFeature] = [ + .init(name: "match-usage", availability: .from(.v1_9)), + .init(name: "object-tracker", availability: .from(.v1_10)), + .init(name: "adjust-cinematic", availability: .from(.v1_10)), + .init(name: "match-representation", availability: .from(.v1_10)), + .init(name: "match-markers", availability: .from(.v1_10)), + .init(name: "adjust-colorConform", availability: .from(.v1_11)), + .init(name: "adjust-voiceIsolation", availability: .from(.v1_11)), + .init(name: "live-drawing", availability: .from(.v1_11)), + .init(name: "adjust-stereo-3D", availability: .from(.v1_13)), + .init(name: "hidden-clip-marker", availability: .from(.v1_13)), + .init(name: "match-analysis-type", availability: .from(.v1_14)), + ] + + /// Attributes introduced after FCPXML 1.5. + public static let attributes: [AttributeFeature] = [ + .init(element: "param", attribute: "auxValue", availability: .from(.v1_11)), + .init(element: "keyframe", attribute: "auxValue", availability: .from(.v1_11)), + .init(element: "format", attribute: "heroEye", availability: .from(.v1_13)), + .init(element: "asset", attribute: "heroEyeOverride", availability: .from(.v1_13)), + ] + + /// Availability for a known element name, or ``VersionAvailability/always`` if unlisted. + public static func availability(forElement name: String) -> VersionAvailability { + elements.first { $0.name == name }?.availability ?? .always + } + + /// Availability for a known attribute on an element, or ``VersionAvailability/always``. + public static func availability( + forAttribute attribute: String, + onElement element: String + ) -> VersionAvailability { + attributes.first { + $0.element == element && $0.attribute == attribute + }?.availability ?? .always + } + + /// Element names that must be omitted at `version`. + public static func elementNamesToOmit(at version: FCPXMLVersion) -> Set<String> { + Set( + elements + .filter { !$0.availability.contains(version) } + .map(\.name) + ) + } + + /// Attribute names to omit on `element` at `version`. + public static func attributeNamesToOmit( + onElement element: String, + at version: FCPXMLVersion + ) -> Set<String> { + Set( + attributes + .filter { $0.element == element && !$0.availability.contains(version) } + .map(\.attribute) + ) + } + } +} diff --git a/Sources/OpenFCPXMLKit/Implementations/FCPXMLVersionConverter.swift b/Sources/OpenFCPXMLKit/Implementations/FCPXMLVersionConverter.swift index 26fa2c2..54b6172 100644 --- a/Sources/OpenFCPXMLKit/Implementations/FCPXMLVersionConverter.swift +++ b/Sources/OpenFCPXMLKit/Implementations/FCPXMLVersionConverter.swift @@ -41,52 +41,18 @@ public final class FCPXMLVersionConverter: FCPXMLVersionConverting, Sendable { try _convert(document, to: targetVersion) } - /// Elements introduced after a given version (per DTD). When converting to a target version, - /// any element whose introduced version is greater than the target is stripped. - private static let elementsIntroducedAfter: [(name: String, introducedIn: FCPXMLVersion)] = [ - ("object-tracker", .v1_10), - ("adjust-cinematic", .v1_10), - ("adjust-colorConform", .v1_11), - ("adjust-voiceIsolation", .v1_11), - ("adjust-stereo-3D", .v1_13), - // Smart collection match rules (content model per version) - ("match-usage", .v1_9), - ("match-representation", .v1_10), - ("match-markers", .v1_10), - ("match-analysis-type", .v1_14), - ("hidden-clip-marker", .v1_13), - ] - - /// Attributes introduced after a given version (per DTD). When converting to a target version, - /// any attribute whose introduced version is greater than the target is stripped from the element. - /// Keeps output valid for older DTDs (e.g. 1.5) and backward compatible. - private static let attributesIntroducedAfter: [(element: String, attribute: String, introducedIn: FCPXMLVersion)] = [ - ("format", "heroEye", .v1_13), - ("asset", "heroEyeOverride", .v1_13), - ("keyframe", "auxValue", .v1_11), - ("param", "auxValue", .v1_11), - ] - - /// Element names to remove when converting to `target` (elements not in that version’s DTD). + /// Elements introduced after a given version (per ``FinalCutPro/FCPXML/VersionFeatureGate``). + /// When converting to a target version, any element not available at that version is stripped. private static func elementNamesToStrip(whenConvertingTo target: FCPXMLVersion) -> Set<String> { - var names: Set<String> = [] - for entry in elementsIntroducedAfter { - if target.isOlder(than: entry.introducedIn) { - names.insert(entry.name) - } - } - return names + FinalCutPro.FCPXML.VersionFeatureGate.elementNamesToOmit(at: target) } /// Attribute names to remove from the given element when converting to `target`. private static func attributeNamesToStrip(forElement elementName: String, whenConvertingTo target: FCPXMLVersion) -> Set<String> { - var names: Set<String> = [] - for entry in attributesIntroducedAfter { - if entry.element == elementName && target.isOlder(than: entry.introducedIn) { - names.insert(entry.attribute) - } - } - return names + FinalCutPro.FCPXML.VersionFeatureGate.attributeNamesToOmit( + onElement: elementName, + at: target + ) } private func _convert(_ document: any OFKXMLDocument, to targetVersion: FCPXMLVersion) throws -> any OFKXMLDocument { diff --git a/Sources/OpenFCPXMLKit/Model/Adjustments/FCPXMLAdjustmentCorners.swift b/Sources/OpenFCPXMLKit/Model/Adjustments/FCPXMLAdjustmentCorners.swift new file mode 100644 index 0000000..7414e3d --- /dev/null +++ b/Sources/OpenFCPXMLKit/Model/Adjustments/FCPXMLAdjustmentCorners.swift @@ -0,0 +1,65 @@ +// +// FCPXMLAdjustmentCorners.swift +// OpenFCPXMLKit • https://github.com/TheAcharya/OpenFCPXMLKit +// © 2026 • Licensed under MIT License +// + + +// +// Corners adjustment model (adjust-corners) for four-corner distortion. +// + +import Foundation + +extension FinalCutPro.FCPXML { + /// Distorts the image by independently offsetting each corner. + /// + /// Corresponds to the FCPXML ``adjust-corners`` element (present since early DTD + /// versions; part of intrinsic video parameters). Corner offsets default to + /// `"0 0"` in the DTD when omitted. + /// + /// Optional nested ``param`` children are preserved for forward compatibility. + public struct CornersAdjustment: Sendable, Equatable, Hashable, Codable { + /// Whether the adjustment is enabled (`enabled` attribute; default `true`). + public var isEnabled: Bool + + /// Bottom-left corner offset (DTD `botLeft`). + public var bottomLeft: Point + + /// Top-left corner offset (DTD `topLeft`). + public var topLeft: Point + + /// Top-right corner offset (DTD `topRight`). + public var topRight: Point + + /// Bottom-right corner offset (DTD `botRight`). + public var bottomRight: Point + + /// Nested filter-style parameters, if present. + public var parameters: [FilterParameter] + + /// Creates a corners adjustment. + /// - Parameters: + /// - isEnabled: Whether the adjustment is enabled (default `true`). + /// - bottomLeft: Bottom-left offset (default `.zero`). + /// - topLeft: Top-left offset (default `.zero`). + /// - topRight: Top-right offset (default `.zero`). + /// - bottomRight: Bottom-right offset (default `.zero`). + /// - parameters: Nested `param` children (default empty). + public init( + isEnabled: Bool = true, + bottomLeft: Point = .zero, + topLeft: Point = .zero, + topRight: Point = .zero, + bottomRight: Point = .zero, + parameters: [FilterParameter] = [] + ) { + self.isEnabled = isEnabled + self.bottomLeft = bottomLeft + self.topLeft = topLeft + self.topRight = topRight + self.bottomRight = bottomRight + self.parameters = parameters + } + } +} diff --git a/Sources/OpenFCPXMLKit/Model/Adjustments/FCPXMLAdjustmentPanner.swift b/Sources/OpenFCPXMLKit/Model/Adjustments/FCPXMLAdjustmentPanner.swift new file mode 100644 index 0000000..7ee5e57 --- /dev/null +++ b/Sources/OpenFCPXMLKit/Model/Adjustments/FCPXMLAdjustmentPanner.swift @@ -0,0 +1,107 @@ +// +// FCPXMLAdjustmentPanner.swift +// OpenFCPXMLKit • https://github.com/TheAcharya/OpenFCPXMLKit +// © 2026 • Licensed under MIT License +// + + +// +// Panner adjustment model (adjust-panner) for audio spatial positioning. +// + +import Foundation + +extension FinalCutPro.FCPXML { + /// Audio panner / surround positioning adjustment. + /// + /// Corresponds to the FCPXML ``adjust-panner`` element (intrinsic audio parameters + /// alongside ``adjust-volume``). Attribute names follow the DTD exactly, including + /// mixed-case identifiers such as `LFE_balance`. + /// + /// Optional nested ``param`` children are preserved for forward compatibility. + public struct PannerAdjustment: Sendable, Equatable, Hashable, Codable { + /// Panner mode string when present (DTD `mode`). + public var mode: String? + + /// Primary amount (DTD `amount`; default `0`). + public var amount: Double + + /// Original vs decoded mix (DTD `original_decoded_mix`). + public var originalDecodedMix: Double? + + /// Ambient vs direct mix (DTD `ambient_direct_mix`). + public var ambientDirectMix: Double? + + /// Surround width (DTD `surround_width`). + public var surroundWidth: Double? + + /// Left/right mix (DTD `left_right_mix`). + public var leftRightMix: Double? + + /// Front/back mix (DTD `front_back_mix`). + public var frontBackMix: Double? + + /// LFE balance (DTD `LFE_balance`). + public var lfeBalance: Double? + + /// Rotation (DTD `rotation`). + public var rotation: Double? + + /// Stereo spread (DTD `stereo_spread`). + public var stereoSpread: Double? + + /// Attenuate/collapse mix (DTD `attenuate_collapse_mix`). + public var attenuateCollapseMix: Double? + + /// Center balance (DTD `center_balance`). + public var centerBalance: Double? + + /// Nested filter-style parameters, if present. + public var parameters: [FilterParameter] + + /// Creates a panner adjustment. + /// - Parameters: + /// - mode: Optional mode string. + /// - amount: Primary amount (default `0`). + /// - originalDecodedMix: Optional original/decoded mix. + /// - ambientDirectMix: Optional ambient/direct mix. + /// - surroundWidth: Optional surround width. + /// - leftRightMix: Optional left/right mix. + /// - frontBackMix: Optional front/back mix. + /// - lfeBalance: Optional LFE balance. + /// - rotation: Optional rotation. + /// - stereoSpread: Optional stereo spread. + /// - attenuateCollapseMix: Optional attenuate/collapse mix. + /// - centerBalance: Optional center balance. + /// - parameters: Nested `param` children (default empty). + public init( + mode: String? = nil, + amount: Double = 0, + originalDecodedMix: Double? = nil, + ambientDirectMix: Double? = nil, + surroundWidth: Double? = nil, + leftRightMix: Double? = nil, + frontBackMix: Double? = nil, + lfeBalance: Double? = nil, + rotation: Double? = nil, + stereoSpread: Double? = nil, + attenuateCollapseMix: Double? = nil, + centerBalance: Double? = nil, + parameters: [FilterParameter] = [] + ) { + self.mode = mode + self.amount = amount + self.originalDecodedMix = originalDecodedMix + self.ambientDirectMix = ambientDirectMix + self.surroundWidth = surroundWidth + self.leftRightMix = leftRightMix + self.frontBackMix = frontBackMix + self.lfeBalance = lfeBalance + self.rotation = rotation + self.stereoSpread = stereoSpread + self.attenuateCollapseMix = attenuateCollapseMix + self.centerBalance = centerBalance + self.parameters = parameters + } + } +} diff --git a/Sources/OpenFCPXMLKit/Model/Clips/FCPXMLClip+Adjustments.swift b/Sources/OpenFCPXMLKit/Model/Clips/FCPXMLClip+Adjustments.swift index 3d0fa41..1818262 100644 --- a/Sources/OpenFCPXMLKit/Model/Clips/FCPXMLClip+Adjustments.swift +++ b/Sources/OpenFCPXMLKit/Model/Clips/FCPXMLClip+Adjustments.swift @@ -14,13 +14,18 @@ extension FinalCutPro.FCPXML.Clip { /// Attribute names used when reading/writing adjustment XML (avoids typos and centralizes strings). private enum AttributeName { static let amount = "amount" + static let ambientDirectMix = "ambient_direct_mix" static let anchor = "anchor" static let aperture = "aperture" + static let attenuateCollapseMix = "attenuate_collapse_mix" static let autoOrient = "autoOrient" static let autoOrManual = "autoOrManual" static let autoScale = "autoScale" static let auxValue = "auxValue" + static let botLeft = "botLeft" + static let botRight = "botRight" static let bottom = "bottom" + static let centerBalance = "center_balance" static let conformType = "conformType" static let convergence = "convergence" static let coordinates = "coordinates" @@ -30,14 +35,18 @@ extension FinalCutPro.FCPXML.Clip { static let enabled = "enabled" static let fieldOfView = "fieldOfView" static let frequency = "frequency" + static let frontBackMix = "front_back_mix" static let interaxial = "interaxial" static let key = "key" static let latitude = "latitude" static let left = "left" + static let leftRightMix = "left_right_mix" + static let lfeBalance = "LFE_balance" static let longitude = "longitude" static let mapping = "mapping" static let mode = "mode" static let name = "name" + static let originalDecodedMix = "original_decoded_mix" static let pan = "pan" static let peakNitsOfPQSource = "peakNitsOfPQSource" static let peakNitsOfSDRToPQSource = "peakNitsOfSDRToPQSource" @@ -46,9 +55,13 @@ extension FinalCutPro.FCPXML.Clip { static let roll = "roll" static let rotation = "rotation" static let scale = "scale" + static let stereoSpread = "stereo_spread" + static let surroundWidth = "surround_width" static let swapEyes = "swapEyes" static let tilt = "tilt" static let top = "top" + static let topLeft = "topLeft" + static let topRight = "topRight" static let type = "type" static let uniformity = "uniformity" static let value = "value" @@ -60,6 +73,33 @@ extension FinalCutPro.FCPXML.Clip { static let zPosition = "zPosition" } + /// Parses nested `param` children from an adjustment element. + private static func filterParameters( + from adjustElement: any OFKXMLElement + ) -> [FinalCutPro.FCPXML.FilterParameter] { + Array( + adjustElement.childElements + .filter { $0.name == "param" } + .compactMap { FinalCutPro.FCPXML.FilterParameter(paramElement: $0) } + ) + } + + /// Appends nested `param` children to an adjustment element. + private static func appendFilterParameters( + _ parameters: [FinalCutPro.FCPXML.FilterParameter], + to adjustElement: any OFKXMLElement + ) { + for param in parameters { + let paramElement = OFKXMLDefaultFactory().makeElement(name: "param") + paramElement.addAttribute(name: AttributeName.name, value: param.name) + if let k = param.key { paramElement.addAttribute(name: AttributeName.key, value: k) } + if let v = param.value { paramElement.addAttribute(name: AttributeName.value, value: v) } + if let av = param.auxValue { paramElement.addAttribute(name: AttributeName.auxValue, value: av) } + if !param.isEnabled { paramElement.addAttribute(name: AttributeName.enabled, value: "0") } + adjustElement.addChild(paramElement) + } + } + /// The crop adjustment applied to the clip. public var cropAdjustment: FinalCutPro.FCPXML.CropAdjustment? { get { @@ -161,6 +201,50 @@ extension FinalCutPro.FCPXML.Clip { element.addChild(adjustElement) } } + + /// The corners adjustment applied to the clip (`adjust-corners`). + public var cornersAdjustment: FinalCutPro.FCPXML.CornersAdjustment? { + get { + guard let adjustElement = element.firstChildElement(named: "adjust-corners") else { + return nil + } + let enabledString = adjustElement.stringValue(forAttributeNamed: AttributeName.enabled) ?? "1" + let bottomLeft = FinalCutPro.FCPXML.Point( + fromString: adjustElement.stringValue(forAttributeNamed: AttributeName.botLeft) ?? "0 0" + ) ?? .zero + let topLeft = FinalCutPro.FCPXML.Point( + fromString: adjustElement.stringValue(forAttributeNamed: AttributeName.topLeft) ?? "0 0" + ) ?? .zero + let topRight = FinalCutPro.FCPXML.Point( + fromString: adjustElement.stringValue(forAttributeNamed: AttributeName.topRight) ?? "0 0" + ) ?? .zero + let bottomRight = FinalCutPro.FCPXML.Point( + fromString: adjustElement.stringValue(forAttributeNamed: AttributeName.botRight) ?? "0 0" + ) ?? .zero + return FinalCutPro.FCPXML.CornersAdjustment( + isEnabled: enabledString == "1", + bottomLeft: bottomLeft, + topLeft: topLeft, + topRight: topRight, + bottomRight: bottomRight, + parameters: Self.filterParameters(from: adjustElement) + ) + } + nonmutating set { + element.removeChildren { $0.name == "adjust-corners" } + guard let adjustment = newValue else { return } + let adjustElement = OFKXMLDefaultFactory().makeElement(name: "adjust-corners") + if !adjustment.isEnabled { + adjustElement.addAttribute(name: AttributeName.enabled, value: "0") + } + adjustElement.addAttribute(name: AttributeName.botLeft, value: adjustment.bottomLeft.stringValue) + adjustElement.addAttribute(name: AttributeName.topLeft, value: adjustment.topLeft.stringValue) + adjustElement.addAttribute(name: AttributeName.topRight, value: adjustment.topRight.stringValue) + adjustElement.addAttribute(name: AttributeName.botRight, value: adjustment.bottomRight.stringValue) + Self.appendFilterParameters(adjustment.parameters, to: adjustElement) + element.addChild(adjustElement) + } + } /// The transform adjustment applied to the clip. public var transformAdjustment: FinalCutPro.FCPXML.TransformAdjustment? { @@ -337,6 +421,76 @@ extension FinalCutPro.FCPXML.Clip { element.addChild(adjustElement) } } + + /// The panner adjustment applied to the clip (`adjust-panner`). + public var pannerAdjustment: FinalCutPro.FCPXML.PannerAdjustment? { + get { + guard let adjustElement = element.firstChildElement(named: "adjust-panner") else { + return nil + } + let amount = Double(adjustElement.stringValue(forAttributeNamed: AttributeName.amount) ?? "0") ?? 0 + func optionalDouble(_ name: String) -> Double? { + guard let raw = adjustElement.stringValue(forAttributeNamed: name) else { return nil } + return Double(raw) + } + return FinalCutPro.FCPXML.PannerAdjustment( + mode: adjustElement.stringValue(forAttributeNamed: AttributeName.mode), + amount: amount, + originalDecodedMix: optionalDouble(AttributeName.originalDecodedMix), + ambientDirectMix: optionalDouble(AttributeName.ambientDirectMix), + surroundWidth: optionalDouble(AttributeName.surroundWidth), + leftRightMix: optionalDouble(AttributeName.leftRightMix), + frontBackMix: optionalDouble(AttributeName.frontBackMix), + lfeBalance: optionalDouble(AttributeName.lfeBalance), + rotation: optionalDouble(AttributeName.rotation), + stereoSpread: optionalDouble(AttributeName.stereoSpread), + attenuateCollapseMix: optionalDouble(AttributeName.attenuateCollapseMix), + centerBalance: optionalDouble(AttributeName.centerBalance), + parameters: Self.filterParameters(from: adjustElement) + ) + } + nonmutating set { + element.removeChildren { $0.name == "adjust-panner" } + guard let adjustment = newValue else { return } + let adjustElement = OFKXMLDefaultFactory().makeElement(name: "adjust-panner") + if let mode = adjustment.mode { + adjustElement.addAttribute(name: AttributeName.mode, value: mode) + } + adjustElement.addAttribute(name: AttributeName.amount, value: String(adjustment.amount)) + if let v = adjustment.originalDecodedMix { + adjustElement.addAttribute(name: AttributeName.originalDecodedMix, value: String(v)) + } + if let v = adjustment.ambientDirectMix { + adjustElement.addAttribute(name: AttributeName.ambientDirectMix, value: String(v)) + } + if let v = adjustment.surroundWidth { + adjustElement.addAttribute(name: AttributeName.surroundWidth, value: String(v)) + } + if let v = adjustment.leftRightMix { + adjustElement.addAttribute(name: AttributeName.leftRightMix, value: String(v)) + } + if let v = adjustment.frontBackMix { + adjustElement.addAttribute(name: AttributeName.frontBackMix, value: String(v)) + } + if let v = adjustment.lfeBalance { + adjustElement.addAttribute(name: AttributeName.lfeBalance, value: String(v)) + } + if let v = adjustment.rotation { + adjustElement.addAttribute(name: AttributeName.rotation, value: String(v)) + } + if let v = adjustment.stereoSpread { + adjustElement.addAttribute(name: AttributeName.stereoSpread, value: String(v)) + } + if let v = adjustment.attenuateCollapseMix { + adjustElement.addAttribute(name: AttributeName.attenuateCollapseMix, value: String(v)) + } + if let v = adjustment.centerBalance { + adjustElement.addAttribute(name: AttributeName.centerBalance, value: String(v)) + } + Self.appendFilterParameters(adjustment.parameters, to: adjustElement) + element.addChild(adjustElement) + } + } /// The loudness adjustment applied to the clip. public var loudnessAdjustment: FinalCutPro.FCPXML.LoudnessAdjustment? { diff --git a/Sources/OpenFCPXMLKit/Model/Structure/FCPXMLSmartCollection.swift b/Sources/OpenFCPXMLKit/Model/Structure/FCPXMLSmartCollection.swift index c3242e0..cd75485 100644 --- a/Sources/OpenFCPXMLKit/Model/Structure/FCPXMLSmartCollection.swift +++ b/Sources/OpenFCPXMLKit/Model/Structure/FCPXMLSmartCollection.swift @@ -439,13 +439,22 @@ extension FinalCutPro.FCPXML.SmartCollection { guard let keyString = matchElement.stringValue(forAttributeNamed: "key"), let key = FinalCutPro.FCPXML.MatchProperty.PropertyKey(rawValue: keyString), let ruleString = matchElement.stringValue(forAttributeNamed: "rule"), - let rule = FinalCutPro.FCPXML.SmartCollectionRule(rawValue: ruleString), - let value = matchElement.stringValue(forAttributeNamed: "value") else { + let rule = FinalCutPro.FCPXML.SmartCollectionRule(rawValue: ruleString) else { + return nil + } + let value = matchElement.stringValue(forAttributeNamed: "value") + // `isSet` / `isNotSet` omit value; other rules require it per DTD practice. + if rule != .isSet && rule != .isNotSet && value == nil { return nil } let enabledString = matchElement.stringValue(forAttributeNamed: "enabled") ?? "1" let isEnabled = enabledString == "1" - return FinalCutPro.FCPXML.MatchProperty(key: key, rule: rule, value: value, isEnabled: isEnabled) + return FinalCutPro.FCPXML.MatchProperty( + key: key, + rule: rule, + value: value, + isEnabled: isEnabled + ) } } nonmutating set { @@ -458,7 +467,9 @@ extension FinalCutPro.FCPXML.SmartCollection { matchElement.addAttribute(name: "enabled", value: matchProperty.isEnabled ? "1" : "0") matchElement.addAttribute(name: "key", value: matchProperty.key.rawValue) matchElement.addAttribute(name: "rule", value: matchProperty.rule.rawValue) - matchElement.addAttribute(name: "value", value: matchProperty.value) + if let value = matchProperty.value { + matchElement.addAttribute(name: "value", value: value) + } element.addChild(matchElement) } } diff --git a/Sources/OpenFCPXMLKit/Model/Structure/FCPXMLSmartCollectionMatchTypes.swift b/Sources/OpenFCPXMLKit/Model/Structure/FCPXMLSmartCollectionMatchTypes.swift index c4371c5..b8370eb 100644 --- a/Sources/OpenFCPXMLKit/Model/Structure/FCPXMLSmartCollectionMatchTypes.swift +++ b/Sources/OpenFCPXMLKit/Model/Structure/FCPXMLSmartCollectionMatchTypes.swift @@ -160,6 +160,8 @@ extension FinalCutPro.FCPXML { /// A property match criterion for a smart collection. public struct MatchProperty: Sendable, Equatable, Hashable, Codable { /// Specifies the possible property keys to match. + /// + /// Keys `projection`, `stereoscopic`, and `cinematic` are defined in FCPXML 1.11+. public enum PropertyKey: String, Sendable, Equatable, Hashable, Codable { case reel case scene @@ -170,6 +172,12 @@ extension FinalCutPro.FCPXML { case audioSampleRate case cameraName case cameraAngle + /// FCPXML 1.11+. + case projection + /// FCPXML 1.11+. + case stereoscopic + /// FCPXML 1.11+. + case cinematic } /// A Boolean value indicating whether the property match is enabled. @@ -182,7 +190,10 @@ extension FinalCutPro.FCPXML { public var rule: SmartCollectionRule /// The property value to match. - public var value: String + /// + /// Absent when the rule is ``SmartCollectionRule/isSet`` or ``SmartCollectionRule/isNotSet`` + /// (FCPXML 1.11+ DTD: `value` is `#IMPLIED`). + public var value: String? private enum CodingKeys: String, CodingKey { case isEnabled = "enabled" @@ -193,9 +204,14 @@ extension FinalCutPro.FCPXML { /// - Parameters: /// - key: The property key to match. /// - rule: The rule to use for the property match (default: `.includes`). - /// - value: The property value to match. + /// - value: The property value to match (omit for `isSet` / `isNotSet`). /// - isEnabled: Whether the match is enabled (default: `true`). - public init(key: PropertyKey, rule: SmartCollectionRule = .includes, value: String, isEnabled: Bool = true) { + public init( + key: PropertyKey, + rule: SmartCollectionRule = .includes, + value: String? = nil, + isEnabled: Bool = true + ) { self.key = key self.rule = rule self.value = value diff --git a/Sources/OpenFCPXMLKit/Model/Structure/FCPXMLSmartCollectionRule.swift b/Sources/OpenFCPXMLKit/Model/Structure/FCPXMLSmartCollectionRule.swift index 048df53..3f959a4 100644 --- a/Sources/OpenFCPXMLKit/Model/Structure/FCPXMLSmartCollectionRule.swift +++ b/Sources/OpenFCPXMLKit/Model/Structure/FCPXMLSmartCollectionRule.swift @@ -48,6 +48,12 @@ extension FinalCutPro.FCPXML { /// Matches if the value is not exactly equal to the specified value. case isNot = "isNot" + + /// Matches when the property is set (FCPXML 1.11+ `match-property` rule). + case isSet + + /// Matches when the property is not set (FCPXML 1.11+ `match-property` rule). + case isNotSet /// Matches if the value starts with the specified text. case startsWith diff --git a/Sources/OpenFCPXMLKit/Projection/Retiming/AudioSplitRetiming.swift b/Sources/OpenFCPXMLKit/Projection/Retiming/AudioSplitRetiming.swift index d74f345..d2ce0df 100644 --- a/Sources/OpenFCPXMLKit/Projection/Retiming/AudioSplitRetiming.swift +++ b/Sources/OpenFCPXMLKit/Projection/Retiming/AudioSplitRetiming.swift @@ -23,24 +23,36 @@ extension FinalCutPro.FCPXML { } /// `true` when audio timeline occupancy differs from the video clip span. + /// + /// A split is detected when either: + /// - `audioStart` differs from the clip `start` (J/L lead-in), or + /// - `audioDuration` differs from `videoDuration` (unequal A/V lengths). + /// + /// When only `audioStart` is present, `audioDuration` is treated as + /// `videoDuration` for detection and emission (DTD both attributes are optional). static func hasSplitEdit( videoStart: Fraction?, videoDuration: Fraction, audioStart: Fraction?, audioDuration: Fraction? ) -> Bool { - guard let audioDuration else { return false } let clipStart = videoStart ?? .zero - let resolvedAudioStart = audioStart ?? clipStart - return resolvedAudioStart != clipStart || audioDuration != videoDuration + if let audioStart, audioStart != clipStart { + return true + } + if let audioDuration, audioDuration != videoDuration { + return true + } + return false } /// Builds channel-specific segments for an asset-clip placement. /// /// - Video uses ``ClipRetiming`` on the video timeline span (`absoluteStart` + /// `videoDuration` / `videoMediaStart`). - /// - When a split edit is present, audio uses an identity window on - /// `audioTimelineStart`…+`audioDuration` reading media from `audioStart`. + /// - When a split edit is present, audio uses ``ClipRetiming`` on the audio + /// timeline occupancy (including any `timeMap`), with media origin at + /// `audioStart` (defaulting to clip `start`). /// - Without a split, audio reuses the video segments (including any `timeMap`). /// /// - Parameter clipStartAttribute: The clip's `start` attribute (local media origin @@ -66,25 +78,23 @@ extension FinalCutPro.FCPXML { videoDuration: videoDuration, audioStart: audioStart, audioDuration: audioDuration - ), - let audioDuration - else { + ) else { return ChannelSegments(video: video, audio: video) } let clipStart = clipStartAttribute ?? .zero let resolvedAudioStart = audioStart ?? clipStart + let effectiveAudioDuration = audioDuration ?? videoDuration let audioTimelineStart = ProjectionTiming.adding( absoluteStart, ProjectionTiming.subtracting(resolvedAudioStart, clipStart) ) - let audio = [ - RetimingSegment.identity( - timelineStart: audioTimelineStart, - duration: audioDuration, - mediaStart: resolvedAudioStart - ) - ] + let audio = ClipRetiming.segments( + timeMap: timeMap, + clipOffset: audioTimelineStart, + clipDuration: effectiveAudioDuration, + mediaStart: resolvedAudioStart + ) return ChannelSegments(video: video, audio: audio) } } diff --git a/Sources/OpenFCPXMLKit/Projection/Retiming/RetimingSegment.swift b/Sources/OpenFCPXMLKit/Projection/Retiming/RetimingSegment.swift index 4e69050..ae3e211 100644 --- a/Sources/OpenFCPXMLKit/Projection/Retiming/RetimingSegment.swift +++ b/Sources/OpenFCPXMLKit/Projection/Retiming/RetimingSegment.swift @@ -58,6 +58,61 @@ extension FinalCutPro.FCPXML { self.isReversed = isReversed } + /// Forward timeline occupancy length in seconds (`max(0, timelineEnd − timelineStart)`). + public var timelineDuration: Double { + max(0, timelineEnd.doubleValue - timelineStart.doubleValue) + } + + /// Absolute media span length in seconds (`abs(mediaEnd − mediaStart)`). + /// + /// Hold / freeze segments approach `0` even when timeline occupancy is positive. + public var mediaDuration: Double { + abs(mediaEnd.doubleValue - mediaStart.doubleValue) + } + + /// `true` when media does not advance over a positive timeline span (hold / freeze). + public var isHold: Bool { + timelineDuration > .ulpOfOne && mediaDuration <= .ulpOfOne + } + + /// `true` when `timeline` lies in the half-open occupancy `[timelineStart, timelineEnd)`. + public func containsTimeline(_ timeline: Fraction) -> Bool { + let t = timeline.doubleValue + return t >= timelineStart.doubleValue && t < timelineEnd.doubleValue + } + + /// `true` when this segment’s timeline occupancy overlaps `[start, end)`. + public func intersectsTimeline(start: Fraction, end: Fraction) -> Bool { + let queryStart = min(start.doubleValue, end.doubleValue) + let queryEnd = max(start.doubleValue, end.doubleValue) + return timelineStart.doubleValue < queryEnd && queryStart < timelineEnd.doubleValue + } + + /// Returns a copy clipped to the overlapping timeline range `[start, end)`, remapping + /// media endpoints through ``mediaPoint(forTimeline:)``. + /// + /// Returns `nil` when there is no positive overlap. + public func clipped(toTimelineStart start: Fraction, timelineEnd end: Fraction) -> RetimingSegment? { + let queryStart = min(start.doubleValue, end.doubleValue) + let queryEnd = max(start.doubleValue, end.doubleValue) + let overlapLo = max(timelineStart.doubleValue, queryStart) + let overlapHi = min(timelineEnd.doubleValue, queryEnd) + guard overlapHi > overlapLo + .ulpOfOne else { return nil } + + let clippedStart = Fraction(double: overlapLo) + let clippedEnd = Fraction(double: overlapHi) + let clippedMediaStart = mediaPoint(forTimeline: clippedStart) + let clippedMediaEnd = mediaPoint(forTimeline: clippedEnd) + return RetimingSegment( + timelineStart: clippedStart, + timelineEnd: clippedEnd, + mediaStart: clippedMediaStart, + mediaEnd: clippedMediaEnd, + scale: scale, + isReversed: clippedMediaEnd.doubleValue < clippedMediaStart.doubleValue + ) + } + /// Identity mapping: clip occupies `[timelineStart, timelineStart + duration)` and /// reads media `[mediaStart, mediaStart + duration)`. public static func identity( @@ -152,5 +207,18 @@ extension FinalCutPro.FCPXML { } return current } + + /// Composes every child through the parent chain (outermost → innermost). + /// + /// Useful when both a container and a nested clip expose multi-point ``TimeMap`` + /// segments: each child is composed independently, then results are concatenated. + public static func composing( + parents: [RetimingSegment], + children: [RetimingSegment] + ) -> [RetimingSegment] { + guard !children.isEmpty else { return [] } + guard !parents.isEmpty else { return children } + return children.flatMap { composing(parents: parents, child: $0) } + } } } diff --git a/Sources/OpenFCPXMLKit/Projection/TimelineOccupancyIndex.swift b/Sources/OpenFCPXMLKit/Projection/TimelineOccupancyIndex.swift index b02a5fc..b57b058 100644 --- a/Sources/OpenFCPXMLKit/Projection/TimelineOccupancyIndex.swift +++ b/Sources/OpenFCPXMLKit/Projection/TimelineOccupancyIndex.swift @@ -17,6 +17,10 @@ extension FinalCutPro.FCPXML { /// /// Built once from a ``ReportProjectionContext`` (or any window list). Used by Summary /// for overlap-aware occupied media time and available to other report builders. + /// + /// Overlap queries use a start-sorted interval list with binary search (O(log n + k) + /// candidates) rather than a linear scan of every window. ``occupiedDuration(kind:matching:)`` + /// and ``unionDuration(_:)`` semantics are unchanged. public struct TimelineOccupancyIndex: Sendable, Equatable { /// Contiguous half-open timeline interval `[start, end)`. public struct Interval: Hashable, Sendable, Equatable { @@ -37,19 +41,36 @@ extension FinalCutPro.FCPXML { } } + /// One indexed window placed on the timeline for overlap search. + private struct IndexedInterval: Sendable, Equatable { + var windowIndex: Int + var start: Double + var end: Double + } + /// Source windows (stable order preserved from input). public var windows: [MediaUsageWindow] + /// Start-sorted sidecar for overlap queries (derived from ``windows``). + private var indexedByStart: [IndexedInterval] + public init(windows: [MediaUsageWindow]) { self.windows = windows + self.indexedByStart = Self.makeIndexedIntervals(from: windows) } /// Convenience from a report projection context. public init(projection: ReportProjectionContext) { - self.windows = projection.windows + self.init(windows: projection.windows) + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.windows == rhs.windows } /// Windows whose timeline range overlaps `[start, end)`. + /// + /// Results preserve the original ``windows`` order. public func windows( overlapping start: Fraction, end: Fraction @@ -58,18 +79,18 @@ extension FinalCutPro.FCPXML { } /// Windows whose timeline range overlaps `[start, end)`. + /// + /// Results preserve the original ``windows`` order. public func windows( overlapping start: Double, end: Double ) -> [MediaUsageWindow] { let queryStart = min(start, end) let queryEnd = max(start, end) - return windows.filter { window in - Interval( - start: window.timelineIn.doubleValue, - end: window.timelineOut.doubleValue - ).overlaps(start: queryStart, end: queryEnd) - } + guard queryEnd > queryStart else { return [] } + + let hitIndices = overlappingWindowIndices(queryStart: queryStart, queryEnd: queryEnd) + return hitIndices.map { windows[$0] } } /// Union length (seconds) of timeline occupancy for matching windows. @@ -127,5 +148,57 @@ extension FinalCutPro.FCPXML { total += current.duration return total } + + // MARK: - Indexed overlap + + private static func makeIndexedIntervals( + from windows: [MediaUsageWindow] + ) -> [IndexedInterval] { + var entries: [IndexedInterval] = [] + entries.reserveCapacity(windows.count) + for (index, window) in windows.enumerated() { + let start = min(window.timelineIn.doubleValue, window.timelineOut.doubleValue) + let end = max(window.timelineIn.doubleValue, window.timelineOut.doubleValue) + guard end > start + .ulpOfOne else { continue } + entries.append(IndexedInterval(windowIndex: index, start: start, end: end)) + } + entries.sort { lhs, rhs in + if lhs.start != rhs.start { return lhs.start < rhs.start } + return lhs.windowIndex < rhs.windowIndex + } + return entries + } + + /// Returns overlapping window indices in ascending original order. + private func overlappingWindowIndices( + queryStart: Double, + queryEnd: Double + ) -> [Int] { + guard !indexedByStart.isEmpty else { return [] } + + // First entry whose start could still be < queryEnd. + var low = 0 + var high = indexedByStart.count + while low < high { + let mid = (low + high) / 2 + if indexedByStart[mid].start < queryEnd { + low = mid + 1 + } else { + high = mid + } + } + let upperExclusive = low + + var hits: [Int] = [] + hits.reserveCapacity(min(8, upperExclusive)) + for entry in indexedByStart[..<upperExclusive] { + // entry.start < queryEnd already; need entry.end > queryStart. + if entry.end > queryStart { + hits.append(entry.windowIndex) + } + } + hits.sort() + return hits + } } } diff --git a/Sources/OpenFCPXMLKit/Projection/TimelineProjectionOptions.swift b/Sources/OpenFCPXMLKit/Projection/TimelineProjectionOptions.swift index 6a8772f..47437d2 100644 --- a/Sources/OpenFCPXMLKit/Projection/TimelineProjectionOptions.swift +++ b/Sources/OpenFCPXMLKit/Projection/TimelineProjectionOptions.swift @@ -63,5 +63,21 @@ extension FinalCutPro.FCPXML { mcClipAngles: .active, excludeFullyOccluded: true ) + + /// Active-audition / active-angle track occupancy analysis. + /// + /// Unfolds only the active audition leaf and active multicam angles, expands all + /// A/V source channels, and keeps disabled clips. Matches the common “what is + /// playable on the active mix” policy used for track-usage analysis (distinct from + /// report inventory/summary, which may use ``Audition/AuditionMask/all`` / + /// ``MCClip/AngleMask/all`` via ``forReport(excludeDisabledClips:auditions:mcClipAngles:includeAnnotations:expandAllSourceChannels:)``). + public static let trackAnalysis = TimelineProjectionOptions( + includeDisabled: true, + auditions: .active, + mcClipAngles: .active, + excludeFullyOccluded: false, + includeAnnotations: false, + expandAllSourceChannels: true + ) } } diff --git a/Sources/OpenFCPXMLKitCLI/README.md b/Sources/OpenFCPXMLKitCLI/README.md index f8087a4..e9721fe 100644 --- a/Sources/OpenFCPXMLKitCLI/README.md +++ b/Sources/OpenFCPXMLKitCLI/README.md @@ -137,14 +137,14 @@ OpenFCPXMLKit-CLI --quiet --media-copy /path/to/project.fcpxml /path/to/media | `--exclude-disabled-clips` | Omit disabled clips (`enabled="0"`) from all timeline-based report sections (requires `--report`). | | `--include-markers-outside-clip-boundaries` | Include markers whose start is outside the host clip’s media range (hidden in FCP timeline/Tags) and add a **Hidden** column (✓/✗) on the Markers sheet (requires `--report`). Default omits those markers and does not show Hidden. Not available via `--exclude-column`. | | `--protect-sheets` | Protect every sheet in the Excel workbook against casual edits (requires `--report`). Cover + all content sheets. **Edit lock only** — not file-open encryption; Excel still opens freely and protection can be turned off. PDF export is unaffected (use Preview → Encrypt for a PDF open password). | -| `--exclude-column <column>` | Exclude a report column from every applicable Excel/PDF sheet (repeatable; requires `--report`). Case-insensitive; includes **`Row`** / `Row Numbers` (omits the 1-based Row index on all tabular sheets and suppresses PDF multi-page Row injection). See [19 — Reporting](../../Documentation/Manual/19-Reporting.md#column-exclusion) for accepted names. **Hidden** is not an exclude-column target. | -| `--timecode-format <format>` | Timeline time display format for Excel and PDF report cells (requires `--report`). Values: `HH:MM:SS:FF` (default; SMPTE with frames; `;` before frames for drop-frame), `Frames`, `Feet+Frames`, `HH:MM:SS`. Non-default formats append a suffix to timecode column headers (e.g. `Timeline In (frames)`). See [19 — Reporting](../../Documentation/Manual/19-Reporting.md#timecode-display-format). | +| `--exclude-column <column>` | Exclude a report column from every applicable Excel/PDF sheet (repeatable; requires `--report`). Case-insensitive; includes **`Row`** / `Row Numbers` (omits the 1-based Row index on all tabular sheets and suppresses PDF multi-page Row injection). See [19 — Reporting](../../Documentation/Manual/20-Reporting.md#column-exclusion) for accepted names. **Hidden** is not an exclude-column target. | +| `--timecode-format <format>` | Timeline time display format for Excel and PDF report cells (requires `--report`). Values: `HH:MM:SS:FF` (default; SMPTE with frames; `;` before frames for drop-frame), `Frames`, `Feet+Frames`, `HH:MM:SS`. Non-default formats append a suffix to timecode column headers (e.g. `Timeline In (frames)`). See [19 — Reporting](../../Documentation/Manual/20-Reporting.md#timecode-display-format). | When `--report` is used without `--report-full` or section flags, the CLI exports role inventory only. Use `--report-full` for every optional sheet, or set individual `--report-*` section flags for a partial export (role inventory is always included). `--report-full` takes precedence when combined with section flags. -Report building uses **Timeline Projection** once per timeline when inventory, markers, keywords, titles, transitions, effects, speed-change, media summary, or summary sections are enabled (Markers / Keywords / Titles / Transitions / Effects are Projection-first with Extraction fallback). See [11 — Timeline Projection](../../Documentation/Manual/11-Timeline-Projection.md). +Report building uses **Timeline Projection** once per timeline when inventory, markers, keywords, titles, transitions, effects, speed-change, media summary, or summary sections are enabled (Markers / Keywords / Titles / Transitions / Effects are Projection-first with Extraction fallback). See [11 — Timeline Projection](../../Documentation/Manual/12-Timeline-Projection.md). -Build progress follows **Projecting Timeline** (when Projection is needed), then **product / workbook order** (Selected Roles Inventory → Markers → Keywords → Titles & Generators → Transitions → Video & Audio Effects → Speed Change Effects → Summary → Media Summary), then **Saving Workbook**, and **Saving PDF** when `--create-pdf` is set. See [19 — Progress callbacks](../../Documentation/Manual/19-Reporting.md#progress-callbacks). +Build progress follows **Projecting Timeline** (when Projection is needed), then **product / workbook order** (Selected Roles Inventory → Markers → Keywords → Titles & Generators → Transitions → Video & Audio Effects → Speed Change Effects → Summary → Media Summary), then **Saving Workbook**, and **Saving PDF** when `--create-pdf` is set. See [19 — Progress callbacks](../../Documentation/Manual/20-Reporting.md#progress-callbacks). --- diff --git a/Tests/ExcelReportTest/Output/README.md b/Tests/ExcelReportTest/Output/README.md index 6a3ecd0..4c5e536 100644 --- a/Tests/ExcelReportTest/Output/README.md +++ b/Tests/ExcelReportTest/Output/README.md @@ -1,6 +1,6 @@ # Excel and PDF report test output -This folder holds **generated** `.xlsx` workbooks and `.pdf` reports from the `ExcelReportTest` target (**6** optional Swift Testing integration tests; part of the **1084**-test public suite). It is gitignored; files here are produced on your machine when you run the export tests. Without a local fixture, those tests **cancel** via `Test.cancel` and nothing is written. +This folder holds **generated** `.xlsx` workbooks and `.pdf` reports from the `ExcelReportTest` target (**6** optional Swift Testing integration tests; part of the **1114**-test public suite). It is gitignored; files here are produced on your machine when you run the export tests. Without a local fixture, those tests **cancel** via `Test.cancel` and nothing is written. --- diff --git a/Tests/ExcelReportTest/README.md b/Tests/ExcelReportTest/README.md index 0ecf36f..77c4d52 100644 --- a/Tests/ExcelReportTest/README.md +++ b/Tests/ExcelReportTest/README.md @@ -5,7 +5,7 @@ Optional integration tests that build real `.xlsx` workbooks and `.pdf` reports **Target:** `ExcelReportTest` (Swift Testing) **Depends on:** `OpenFCPXMLKit`, `XLKit` **Tests:** **6** `@Test` methods in `@Suite("Excel report export")` / `ExcelReportExportTests` -**Public suite (keep in sync):** **1084** listed (`1078` OpenFCPXMLKitTests + **6** ExcelReportTest; all Swift Testing); **60** public samples +**Public suite (keep in sync):** **1114** listed (`1108` OpenFCPXMLKitTests + **6** ExcelReportTest; all Swift Testing); **60** public samples Unit-level reporting behaviour (universal **Row** on all tabular sheets, Summary title in **B1**, column layout, column exclusion including `ReportColumn.row`, disabled-clip filtering, timecode formats / DF·NDF, format-aware headers, build-phase order including `.projecting`, workbook cell formatting, optional `copyrightLabel` cover/footer branding, `includeMarkersOutsideClipBoundaries` / Markers **Hidden** column, `protectSheets` worksheet protection, `ReportMediaResolutionPolicy` / Media Summary proxy-original distinction, PDF cover notes / black header + `info.circle`, TOC colour chips, column-width expansion after exclusions, pagination, shared row colours, **standalone compound-clip timeline resolution**, **Projection-first** Markers/Keywords/Titles/Transitions/Effects) lives in **`OpenFCPXMLKitTests`** — see [Tests/README.md](../README.md#reporting--excelpdf-export) (`FCPXMLCompoundClipReportTests`, `FCPXMLTimelineProjectionTests`, `FCPXMLReportObligationCorpusTests`, `FCPXMLMarkersReportTests`, `FCPXMLReportPDFExportTests`, `FCPXMLReportPDFSheetPlanTests`, `FCPXMLReportPDFTableLayoutTests`, `FCPXMLReportColumnExclusionTests`, `FCPXMLReportExcelExportTests`, and related files). @@ -120,7 +120,7 @@ try await FinalCutPro.FCPXML.ReportExcelExport.export(report, to: xlsxURL) try FinalCutPro.FCPXML.ReportPDFExport.export(report, to: pdfURL) ``` -See [Documentation/Manual/19-Reporting.md](../../Documentation/Manual/19-Reporting.md) and [11 — Timeline Projection](../../Documentation/Manual/11-Timeline-Projection.md) for the full API (`ReportPDFExport`, `ReportTimecodeFormat`, progress phases, column exclusion, Projection). +See [Documentation/Manual/20-Reporting.md](../../Documentation/Manual/20-Reporting.md) and [11 — Timeline Projection](../../Documentation/Manual/12-Timeline-Projection.md) for the full API (`ReportPDFExport`, `ReportTimecodeFormat`, progress phases, column exclusion, Projection). --- diff --git a/Tests/OpenFCPXMLKitTests/FCPXMLAdjustmentTests.swift b/Tests/OpenFCPXMLKitTests/FCPXMLAdjustmentTests.swift index 87e69ff..ca092b4 100644 --- a/Tests/OpenFCPXMLKitTests/FCPXMLAdjustmentTests.swift +++ b/Tests/OpenFCPXMLKitTests/FCPXMLAdjustmentTests.swift @@ -554,6 +554,101 @@ struct FCPXMLAdjustmentTests { #expect(adjustEl?.stringValue(forAttributeNamed: "type") == "fill") } + // MARK: - CornersAdjustment Tests + + @Test("CornersAdjustment initialization defaults") + func cornersAdjustmentDefaults() { + let corners = FinalCutPro.FCPXML.CornersAdjustment() + #expect(corners.isEnabled) + #expect(corners.bottomLeft == .zero) + #expect(corners.topLeft == .zero) + #expect(corners.topRight == .zero) + #expect(corners.bottomRight == .zero) + #expect(corners.parameters.isEmpty) + } + + @Test("CornersAdjustment Codable round-trip") + func cornersAdjustmentCodable() throws { + var corners = FinalCutPro.FCPXML.CornersAdjustment( + isEnabled: false, + bottomLeft: .init(x: 1, y: 2), + topLeft: .init(x: 3, y: 4), + topRight: .init(x: 5, y: 6), + bottomRight: .init(x: 7, y: 8) + ) + corners.parameters = [] + let data = try JSONEncoder().encode(corners) + let decoded = try JSONDecoder().decode(FinalCutPro.FCPXML.CornersAdjustment.self, from: data) + #expect(decoded == corners) + } + + @Test("Clip corners adjustment round-trip") + func clipCornersAdjustmentRoundTrip() throws { + let clipEl = FoundationXMLFactory().makeElement(name: "clip") + clipEl.addAttribute(name: "ref", value: "r1") + let videoEl = FoundationXMLFactory().makeElement(name: "video") + clipEl.addChild(videoEl) + let clip = try #require(FinalCutPro.FCPXML.Clip(element: clipEl)) + let corners = FinalCutPro.FCPXML.CornersAdjustment( + bottomLeft: .init(x: -10, y: -20), + topRight: .init(x: 10, y: 20) + ) + clip.cornersAdjustment = corners + #expect(clip.cornersAdjustment?.bottomLeft.x == -10) + #expect(clip.cornersAdjustment?.topRight.y == 20) + let adjustEl = clip.element.firstChildElement(named: "adjust-corners") + #expect(adjustEl?.stringValue(forAttributeNamed: "botLeft") == "-10 -20") + #expect(adjustEl?.stringValue(forAttributeNamed: "topRight") == "10 20") + } + + // MARK: - PannerAdjustment Tests + + @Test("PannerAdjustment initialization defaults") + func pannerAdjustmentDefaults() { + let panner = FinalCutPro.FCPXML.PannerAdjustment() + #expect(panner.amount == 0) + #expect(panner.mode == nil) + #expect(panner.lfeBalance == nil) + #expect(panner.parameters.isEmpty) + } + + @Test("PannerAdjustment Codable round-trip") + func pannerAdjustmentCodable() throws { + let panner = FinalCutPro.FCPXML.PannerAdjustment( + mode: "surround", + amount: 0.5, + leftRightMix: -0.25, + lfeBalance: 0.1 + ) + let data = try JSONEncoder().encode(panner) + let decoded = try JSONDecoder().decode(FinalCutPro.FCPXML.PannerAdjustment.self, from: data) + #expect(decoded == panner) + } + + @Test("Clip panner adjustment round-trip") + func clipPannerAdjustmentRoundTrip() throws { + let clipEl = FoundationXMLFactory().makeElement(name: "clip") + clipEl.addAttribute(name: "ref", value: "r1") + let videoEl = FoundationXMLFactory().makeElement(name: "video") + clipEl.addChild(videoEl) + let clip = try #require(FinalCutPro.FCPXML.Clip(element: clipEl)) + let panner = FinalCutPro.FCPXML.PannerAdjustment( + mode: "stereo", + amount: 0.25, + stereoSpread: 0.8, + centerBalance: 0.1 + ) + clip.pannerAdjustment = panner + #expect(clip.pannerAdjustment?.mode == "stereo") + #expect(clip.pannerAdjustment?.amount == 0.25) + #expect(clip.pannerAdjustment?.stereoSpread == 0.8) + let adjustEl = clip.element.firstChildElement(named: "adjust-panner") + #expect(adjustEl?.stringValue(forAttributeNamed: "mode") == "stereo") + #expect(adjustEl?.stringValue(forAttributeNamed: "amount") == "0.25") + #expect(adjustEl?.stringValue(forAttributeNamed: "stereo_spread") == "0.8") + #expect(adjustEl?.stringValue(forAttributeNamed: "LFE_balance") == nil) + } + // MARK: - Equatable Tests @Test("Adjustment equality") diff --git a/Tests/OpenFCPXMLKitTests/FCPXMLAuthoringTests.swift b/Tests/OpenFCPXMLKitTests/FCPXMLAuthoringTests.swift new file mode 100644 index 0000000..1c60353 --- /dev/null +++ b/Tests/OpenFCPXMLKitTests/FCPXMLAuthoringTests.swift @@ -0,0 +1,571 @@ +// +// FCPXMLAuthoringTests.swift +// OpenFCPXMLKit • https://github.com/TheAcharya/OpenFCPXMLKit +// © 2026 • Licensed under MIT License +// + + +// +// Detached authoring layer: round-trip and version omit-on-write. +// + +import Testing +@testable import OpenFCPXMLKit + +@Suite("Detached authoring") +struct FCPXMLAuthoringTests { + private typealias Authoring = FinalCutPro.FCPXML.Authoring + + private func sampleDocument( + version: FCPXMLVersion = .v1_14, + includeCinematic: Bool = true + ) -> Authoring.Document { + let format = Authoring.Format( + id: "r1", + frameDuration: "100/2400s", + width: 1920, + height: 1080, + name: "FFVideoFormat1080p24" + ) + let asset = Authoring.Asset( + id: "r2", + name: "Clip", + hasVideo: true, + hasAudio: true, + duration: "10s", + formatID: "r1", + mediaReps: [ + Authoring.MediaRep(src: "file:///tmp/clip.mov") + ] + ) + var clip = Authoring.AssetClip( + ref: "r2", + offset: "0s", + duration: "5s", + name: "Clip", + start: "0s" + ) + if includeCinematic { + clip.cinematic = Authoring.CinematicAdjustment(aperture: "wide") + } + return Authoring.Document.simpleProject( + version: version, + projectName: "Demo", + eventName: "Event", + format: format, + asset: asset, + clip: clip, + sequenceDuration: "5s" + ) + } + + @Test("VersionAvailability from and upTo") + func versionAvailabilityRanges() { + let from10 = FinalCutPro.FCPXML.VersionAvailability.from(.v1_10) + #expect(from10.contains(.v1_10)) + #expect(from10.contains(.v1_14)) + #expect(!from10.contains(.v1_9)) + + let upTo9 = FinalCutPro.FCPXML.VersionAvailability.upTo(.v1_9) + #expect(upTo9.contains(.v1_5)) + #expect(upTo9.contains(.v1_9)) + #expect(!upTo9.contains(.v1_10)) + + #expect(FinalCutPro.FCPXML.VersionAvailability.always.contains(.v1_5)) + #expect(FinalCutPro.FCPXML.VersionAvailability.always.contains(.v1_14)) + } + + @Test("Authored document round-trips through XML") + func authoredDocumentRoundTrip() throws { + let original = sampleDocument(version: .v1_14, includeCinematic: true) + let xml = try original.xmlString() + let parsedLive = try OFKXMLDefaultFactory().makeDocument( + xmlString: xml, + options: .fcpxmlDefaults + ) + let decoded = try Authoring.Document(xmlDocument: parsedLive) + + #expect(decoded.version == .v1_14) + #expect(decoded.resources.formats.count == 1) + #expect(decoded.resources.assets.count == 1) + #expect(decoded.resources.assets[0].mediaReps.count == 1) + let project = try #require(decoded.library?.events.first?.projects.first) + #expect(project.name == "Demo") + #expect(project.sequence.spine.assetClips.count == 1) + #expect(project.sequence.spine.assetClips[0].cinematic?.aperture == "wide") + } + + @Test("Cinematic omitted when encoding to FCPXML 1.5") + func cinematicOmittedForVersion1_5() throws { + let document = sampleDocument(version: .v1_5, includeCinematic: true) + let xml = try document.xmlString() + #expect(!xml.contains("adjust-cinematic")) + #expect(xml.contains("asset-clip")) + #expect(xml.contains("version=\"1.5\"")) + + let parsed = try OFKXMLDefaultFactory().makeDocument( + xmlString: xml, + options: .fcpxmlDefaults + ) + let decoded = try Authoring.Document(xmlDocument: parsed) + let clip = try #require(decoded.library?.events.first?.projects.first?.sequence.spine.assetClips.first) + #expect(clip.cinematic == nil) + } + + @Test("Cinematic retained when encoding to FCPXML 1.10+") + func cinematicRetainedForVersion1_10() throws { + let document = sampleDocument(version: .v1_10, includeCinematic: true) + let xml = try document.xmlString() + #expect(xml.contains("adjust-cinematic")) + #expect(xml.contains("aperture=\"wide\"")) + } + + @Test("Authored XML parses as FinalCutPro.FCPXML") + func authoredXMLParsesAsLiveFCPXML() throws { + let document = sampleDocument(version: .v1_11, includeCinematic: false) + let xml = try document.xmlString() + let data = try #require(xml.data(using: .utf8)) + let fcpxml = try FinalCutPro.FCPXML(fileContent: data) + #expect(fcpxml.allProjects().count == 1) + #expect(fcpxml.allReportTimelineSources().count == 1) + } + + @Test("Mixed spine items round-trip") + func mixedSpineItemsRoundTrip() throws { + let format = Authoring.Format( + id: "r1", + frameDuration: "100/2400s", + width: 1920, + height: 1080 + ) + let asset = Authoring.Asset( + id: "r2", + name: "Clip", + hasVideo: true, + hasAudio: true, + duration: "30s", + formatID: "r1", + mediaReps: [Authoring.MediaRep(src: "file:///tmp/clip.mov")] + ) + let titleEffect = Authoring.Effect(id: "r3", name: "Basic Title", uid: "…/Titles.localized/Basic Title") + let transitionEffect = Authoring.Effect(id: "r4", name: "Cross Dissolve") + + let document = Authoring.Document( + version: .v1_11, + resources: Authoring.Resources( + formats: [format], + assets: [asset], + effects: [titleEffect, transitionEffect] + ), + library: Authoring.Library( + events: [ + Authoring.Event( + name: "Event", + projects: [ + Authoring.Project( + name: "Mixed", + sequence: Authoring.Sequence( + formatID: "r1", + duration: "20s", + spine: Authoring.Spine(items: [ + .assetClip( + Authoring.AssetClip( + ref: "r2", + offset: "0s", + duration: "5s", + name: "A", + start: "0s", + volume: Authoring.VolumeAdjustment(amount: "-3dB") + ) + ), + .transition( + Authoring.Transition( + ref: "r4", + offset: "4s", + duration: "1s", + name: "XD" + ) + ), + .gap(Authoring.Gap(offset: "5s", duration: "2s", name: "Hold")), + .title( + Authoring.Title( + ref: "r3", + offset: "7s", + duration: "3s", + name: "Lower Third", + start: "3600s" + ) + ), + .video( + Authoring.Video( + ref: "r2", + offset: "10s", + duration: "2s", + name: "VLeaf", + start: "0s", + srcID: "1" + ) + ), + .audio( + Authoring.Audio( + ref: "r2", + offset: "12s", + duration: "2s", + name: "ALeaf", + start: "0s", + srcID: "1" + ) + ), + ]) + ) + ) + ] + ) + ] + ) + ) + + let xml = try document.xmlString() + #expect(xml.contains("<gap ")) + #expect(xml.contains("<title ")) + #expect(xml.contains("<transition ")) + #expect(xml.contains("<video ")) + #expect(xml.contains("<audio ")) + #expect(xml.contains("adjust-volume")) + #expect(xml.contains("-3dB")) + + let parsed = try OFKXMLDefaultFactory().makeDocument(xmlString: xml, options: .fcpxmlDefaults) + let decoded = try Authoring.Document(xmlDocument: parsed) + let items = try #require(decoded.library?.events.first?.projects.first?.sequence.spine.items) + #expect(items.count == 6) + #expect(decoded.resources.effects.count == 2) + + if case .assetClip(let clip) = items[0] { + #expect(clip.volume?.amount == "-3dB") + } else { + Issue.record("Expected asset-clip at index 0") + } + if case .transition(let transition) = items[1] { + #expect(transition.ref == "r4") + } else { + Issue.record("Expected transition at index 1") + } + if case .gap(let gap) = items[2] { + #expect(gap.duration == "2s") + } else { + Issue.record("Expected gap at index 2") + } + if case .title(let title) = items[3] { + #expect(title.name == "Lower Third") + } else { + Issue.record("Expected title at index 3") + } + if case .video(let video) = items[4] { + #expect(video.srcID == "1") + } else { + Issue.record("Expected video at index 4") + } + if case .audio(let audio) = items[5] { + #expect(audio.name == "ALeaf") + } else { + Issue.record("Expected audio at index 5") + } + } + + @Test("Authored J/L asset-clip preserves audioStart attributes") + func authoredJLAttributesRoundTrip() throws { + let clip = Authoring.AssetClip( + ref: "r2", + offset: "2s", + duration: "5s", + name: "JL", + start: "10s", + audioStart: "9s", + audioDuration: "7s" + ) + let format = Authoring.Format(id: "r1", frameDuration: "100/2400s", width: 1920, height: 1080) + let asset = Authoring.Asset( + id: "r2", + hasVideo: true, + hasAudio: true, + duration: "60s", + formatID: "r1", + mediaReps: [Authoring.MediaRep(src: "file:///tmp/a.mov")] + ) + let document = Authoring.Document.simpleProject( + version: .v1_11, + format: format, + asset: asset, + clip: clip, + sequenceDuration: "10s" + ) + let decoded = try Authoring.Document( + xmlDocument: try OFKXMLDefaultFactory().makeDocument( + xmlString: try document.xmlString(), + options: .fcpxmlDefaults + ) + ) + let roundTrip = try #require(decoded.library?.events.first?.projects.first?.sequence.spine.assetClips.first) + #expect(roundTrip.audioStart == "9s") + #expect(roundTrip.audioDuration == "7s") + } + + @Test("Authored sync-clip / ref-clip / mc-clip / audition / caption round-trip") + func authoredCompoundStoryItemsRoundTrip() throws { + let format = Authoring.Format(id: "r1", frameDuration: "100/2400s", width: 1920, height: 1080) + let asset = Authoring.Asset( + id: "r2", + hasVideo: true, + hasAudio: true, + duration: "60s", + formatID: "r1", + mediaReps: [Authoring.MediaRep(src: "file:///tmp/a.mov")] + ) + let compoundMedia = Authoring.Media( + id: "r3", + name: "Compound", + content: .sequence( + Authoring.MediaSequence( + formatID: "r1", + duration: "4s", + spine: Authoring.Spine(items: [ + .assetClip( + Authoring.AssetClip( + ref: "r2", + offset: "0s", + duration: "4s", + name: "Inner", + start: "0s" + ) + ) + ]) + ) + ) + ) + let multicamMedia = Authoring.Media( + id: "r4", + name: "Multicam", + content: .multicam( + Authoring.Multicam( + formatID: "r1", + duration: "8s", + angles: [ + Authoring.MCAngle( + angleID: "angleA", + name: "Cam A", + items: [ + .assetClip( + Authoring.AssetClip( + ref: "r2", + offset: "0s", + duration: "8s", + name: "A", + start: "0s" + ) + ) + ] + ), + Authoring.MCAngle( + angleID: "angleB", + name: "Cam B", + items: [ + .assetClip( + Authoring.AssetClip( + ref: "r2", + offset: "0s", + duration: "8s", + name: "B", + start: "0s" + ) + ) + ] + ), + ] + ) + ) + ) + + let document = Authoring.Document( + version: .v1_11, + resources: Authoring.Resources( + formats: [format], + assets: [asset], + media: [compoundMedia, multicamMedia] + ), + library: Authoring.Library( + events: [ + Authoring.Event( + name: "Event", + projects: [ + Authoring.Project( + name: "Compounds", + sequence: Authoring.Sequence( + formatID: "r1", + duration: "30s", + spine: Authoring.Spine(items: [ + .syncClip( + Authoring.SyncClip( + offset: "0s", + duration: "5s", + name: "Synced", + start: "0s", + formatID: "r1", + contents: [ + .item( + .assetClip( + Authoring.AssetClip( + ref: "r2", + offset: "0s", + duration: "5s", + name: "SyncLeaf", + start: "0s" + ) + ) + ) + ], + syncSources: [ + Authoring.SyncSource(sourceID: "storyline") + ] + ) + ), + .refClip( + Authoring.RefClip( + ref: "r3", + offset: "5s", + duration: "4s", + name: "Ref", + start: "0s", + useAudioSubroles: true + ) + ), + .mcClip( + Authoring.MCClip( + ref: "r4", + offset: "9s", + duration: "8s", + name: "MC", + start: "0s", + sources: [ + Authoring.MCSource(angleID: "angleA", srcEnable: "all"), + Authoring.MCSource(angleID: "angleB", srcEnable: "audio"), + ] + ) + ), + .audition( + Authoring.Audition( + offset: "17s", + candidates: [ + .assetClip( + Authoring.AssetClip( + ref: "r2", + offset: "17s", + duration: "3s", + name: "Active", + start: "0s" + ) + ), + .assetClip( + Authoring.AssetClip( + ref: "r2", + offset: "17s", + duration: "3s", + name: "Alt", + start: "10s" + ) + ), + ] + ) + ), + .caption( + Authoring.Caption( + offset: "0s", + duration: "2s", + name: "Hello", + lane: 1, + role: "iTT?caption", + note: "spoken" + ) + ), + ]) + ) + ) + ] + ) + ] + ) + ) + + let xml = try document.xmlString() + #expect(xml.contains("<sync-clip ")) + #expect(xml.contains("<sync-source ")) + #expect(xml.contains("sourceID=\"storyline\"")) + #expect(xml.contains("<ref-clip ")) + #expect(xml.contains("useAudioSubroles=\"1\"")) + #expect(xml.contains("<mc-clip ")) + #expect(xml.contains("<mc-source ")) + #expect(xml.contains("<audition ")) + #expect(xml.contains("<caption ")) + #expect(xml.contains("<media ")) + #expect(xml.contains("<multicam ")) + #expect(xml.contains("<mc-angle ")) + + let decoded = try Authoring.Document( + xmlDocument: try OFKXMLDefaultFactory().makeDocument( + xmlString: xml, + options: .fcpxmlDefaults + ) + ) + #expect(decoded.resources.media.count == 2) + if case .sequence(let sequence) = decoded.resources.media[0].content { + #expect(sequence.spine.items.count == 1) + } else { + Issue.record("Expected compound media sequence") + } + if case .multicam(let multicam) = decoded.resources.media[1].content { + #expect(multicam.angles.count == 2) + #expect(multicam.angles[0].angleID == "angleA") + } else { + Issue.record("Expected multicam media") + } + + let items = try #require(decoded.library?.events.first?.projects.first?.sequence.spine.items) + #expect(items.count == 5) + + if case .syncClip(let sync) = items[0] { + #expect(sync.name == "Synced") + #expect(sync.syncSources.first?.sourceID == "storyline") + #expect(sync.contents.count == 1) + } else { + Issue.record("Expected sync-clip") + } + if case .refClip(let ref) = items[1] { + #expect(ref.ref == "r3") + #expect(ref.useAudioSubroles == true) + } else { + Issue.record("Expected ref-clip") + } + if case .mcClip(let mc) = items[2] { + #expect(mc.sources.count == 2) + #expect(mc.sources[1].srcEnable == "audio") + } else { + Issue.record("Expected mc-clip") + } + if case .audition(let audition) = items[3] { + #expect(audition.candidates.count == 2) + if case .assetClip(let active) = audition.candidates[0] { + #expect(active.name == "Active") + } else { + Issue.record("Expected active audition asset-clip") + } + } else { + Issue.record("Expected audition") + } + if case .caption(let caption) = items[4] { + #expect(caption.role == "iTT?caption") + #expect(caption.note == "spoken") + #expect(caption.lane == 1) + } else { + Issue.record("Expected caption") + } + } +} diff --git a/Tests/OpenFCPXMLKitTests/FCPXMLProjectionCoverageTests.swift b/Tests/OpenFCPXMLKitTests/FCPXMLProjectionCoverageTests.swift index f9477d8..9e69790 100644 --- a/Tests/OpenFCPXMLKitTests/FCPXMLProjectionCoverageTests.swift +++ b/Tests/OpenFCPXMLKitTests/FCPXMLProjectionCoverageTests.swift @@ -129,6 +129,118 @@ struct FCPXMLProjectionCoverageTests { #expect(try #require(composed.first).isReversed) } + @Test("RetimingSegment timeline clip remaps media endpoints") + func retimingSegmentClippedRemapsMedia() throws { + let segment = FinalCutPro.FCPXML.RetimingSegment( + timelineStart: Fraction(0, 1), + timelineEnd: Fraction(10, 1), + mediaStart: Fraction(100, 1), + mediaEnd: Fraction(200, 1), + scale: 10, + isReversed: false + ) + let clipped = try #require( + segment.clipped(toTimelineStart: Fraction(2, 1), timelineEnd: Fraction(4, 1)) + ) + #expect(abs(clipped.timelineStart.doubleValue - 2) < 0.001) + #expect(abs(clipped.timelineEnd.doubleValue - 4) < 0.001) + #expect(abs(clipped.mediaStart.doubleValue - 120) < 0.001) + #expect(abs(clipped.mediaEnd.doubleValue - 140) < 0.001) + #expect(!clipped.isReversed) + #expect(segment.intersectsTimeline(start: Fraction(3, 1), end: Fraction(7, 2))) + #expect(segment.containsTimeline(Fraction(0, 1))) + #expect(!segment.containsTimeline(Fraction(10, 1))) + } + + @Test("RetimingSegment hold detection and durations") + func retimingSegmentHoldAndDurations() { + let hold = FinalCutPro.FCPXML.RetimingSegment( + timelineStart: Fraction(0, 1), + timelineEnd: Fraction(2, 1), + mediaStart: Fraction(5, 1), + mediaEnd: Fraction(5, 1), + scale: 0, + isReversed: false + ) + #expect(hold.isHold) + #expect(abs(hold.timelineDuration - 2) < 0.001) + #expect(hold.mediaDuration < 0.001) + + let reverse = FinalCutPro.FCPXML.RetimingSegment( + timelineStart: Fraction(0, 1), + timelineEnd: Fraction(4, 1), + mediaStart: Fraction(8, 1), + mediaEnd: Fraction(0, 1), + scale: 2, + isReversed: true + ) + #expect(!reverse.isHold) + #expect(abs(reverse.mediaDuration - 8) < 0.001) + } + + @Test("RetimingSegment composing parents against multiple children") + func retimingSegmentComposingParentsAgainstChildren() { + let parent = FinalCutPro.FCPXML.RetimingSegment( + timelineStart: Fraction(0, 1), + timelineEnd: Fraction(4, 1), + mediaStart: Fraction(0, 1), + mediaEnd: Fraction(8, 1), + scale: 0.5, + isReversed: false + ) + let children = [ + FinalCutPro.FCPXML.RetimingSegment( + timelineStart: Fraction(0, 1), + timelineEnd: Fraction(2, 1), + mediaStart: Fraction(10, 1), + mediaEnd: Fraction(12, 1), + scale: 1, + isReversed: false + ), + FinalCutPro.FCPXML.RetimingSegment( + timelineStart: Fraction(4, 1), + timelineEnd: Fraction(6, 1), + mediaStart: Fraction(20, 1), + mediaEnd: Fraction(22, 1), + scale: 1, + isReversed: false + ), + ] + let composed = FinalCutPro.FCPXML.RetimingSegment.composing( + parents: [parent], + children: children + ) + #expect(composed.count == 2) + #expect(abs(composed[0].timelineStart.doubleValue - 0) < 0.001) + #expect(abs(composed[0].timelineEnd.doubleValue - 1) < 0.001) + #expect(abs(composed[1].timelineStart.doubleValue - 2) < 0.001) + #expect(abs(composed[1].timelineEnd.doubleValue - 3) < 0.001) + } + + @Test("TimelineOccupancyIndex overlap preserves window order") + func timelineOccupancyIndexOverlapPreservesOrder() { + let channel = FinalCutPro.FCPXML.MediaChannel( + resourceID: "r1", + kind: .video, + sourceIndex: 1 + ) + // Insert later-starting window first so order ≠ start sort order. + let late = FinalCutPro.FCPXML.MediaUsageWindow( + channel: channel, + retiming: .identity(timelineStart: Fraction(5, 1), duration: Fraction(2, 1), mediaStart: .zero), + clipDisplayName: "Late" + ) + let early = FinalCutPro.FCPXML.MediaUsageWindow( + channel: channel, + retiming: .identity(timelineStart: Fraction(0, 1), duration: Fraction(10, 1), mediaStart: .zero), + clipDisplayName: "Early" + ) + let index = FinalCutPro.FCPXML.TimelineOccupancyIndex(windows: [late, early]) + let hits = index.windows(overlapping: Fraction(11, 2), end: Fraction(6, 1)) + #expect(hits.map(\.clipDisplayName) == ["Late", "Early"]) + #expect(abs(index.occupiedDuration(kind: .video) - 10) < 0.001) + } + @Test("Nested ref-clip timeMap composes inner identity") func nestedRefClipTimeMapComposesInnerIdentity() async throws { let fcpxml = try parseInlineFCPXML(nestedRefClipWithOuterTimeMapXML) diff --git a/Tests/OpenFCPXMLKitTests/FCPXMLProjectionEdgeCaseCorpusTests.swift b/Tests/OpenFCPXMLKitTests/FCPXMLProjectionEdgeCaseCorpusTests.swift new file mode 100644 index 0000000..e7951ae --- /dev/null +++ b/Tests/OpenFCPXMLKitTests/FCPXMLProjectionEdgeCaseCorpusTests.swift @@ -0,0 +1,215 @@ +// +// FCPXMLProjectionEdgeCaseCorpusTests.swift +// OpenFCPXMLKit • https://github.com/TheAcharya/OpenFCPXMLKit +// © 2026 • Licensed under MIT License +// + + +// +// Projection edge-case corpus: J/L cuts, timeMap, nested ref/spine. +// + +import Testing +import SwiftTimecode +@testable import OpenFCPXMLKit + +@Suite("Projection edge-case corpus") +struct FCPXMLProjectionEdgeCaseCorpusTests { + private let projector = FinalCutPro.FCPXML.TimelineProjector() + + private func parseInlineFCPXML(_ xml: String) throws -> FinalCutPro.FCPXML { + let data = try #require(xml.data(using: .utf8)) + return try FinalCutPro.FCPXML(fileContent: data) + } + + // MARK: - J/L + timeMap + + @Test("J/L cut with timeMap scales audio occupancy independently") + func jlCutWithTimeMapScalesAudioIndependently() async throws { + // Video occupancy normalized onto duration=5s at offset=2s → [2,7). + // timeMap 0→10 remapped / 0→20 media → scale 2 on video. + // Audio: audioStart=9s audioDuration=7s → timeline [1,8), also timeMap-normalized. + let fcpxml = try parseInlineFCPXML(""" + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE fcpxml> + <fcpxml version="1.11"> + <resources> + <format id="r1" frameDuration="100/2400s" width="1920" height="1080"/> + <asset id="r2" name="ClipA" hasVideo="1" hasAudio="1" videoSources="1" audioSources="1" duration="60s"> + <media-rep kind="original-media" src="file:///tmp/a.mov"/> + </asset> + </resources> + <library> + <event name="E"> + <project name="P"> + <sequence format="r1" duration="20s" tcStart="0s"> + <spine> + <asset-clip ref="r2" offset="2s" name="JLRetime" start="10s" duration="5s" + audioStart="9s" audioDuration="7s"> + <timeMap> + <timept time="0s" value="0s" interp="linear"/> + <timept time="10s" value="20s" interp="linear"/> + </timeMap> + </asset-clip> + </spine> + </sequence> + </project> + </event> + </library> + </fcpxml> + """) + + let source = try #require(fcpxml.allReportTimelineSources().first) + let windows = try await projector.project(from: source, fcpxml: fcpxml, options: .trackAnalysis) + let video = try #require(windows.first { $0.channel.kind == .video }) + let audio = try #require(windows.first { $0.channel.kind == .audio }) + + #expect(abs(video.timelineIn.doubleValue - 2) < 0.001) + #expect(abs(video.timelineOut.doubleValue - 7) < 0.001) + #expect(abs(video.retiming.scale - 2) < 0.05) + + #expect(abs(audio.timelineIn.doubleValue - 1) < 0.001) + #expect(abs(audio.timelineOut.doubleValue - 8) < 0.001) + #expect(abs(audio.retiming.scale - 2) < 0.05) + #expect(video.timelineIn != audio.timelineIn) + } + + @Test("AudioStart-only J-cut from XML emits earlier audio window") + func audioStartOnlyJCutFromXML() async throws { + let fcpxml = try parseInlineFCPXML(""" + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE fcpxml> + <fcpxml version="1.11"> + <resources> + <format id="r1" frameDuration="100/2400s" width="1920" height="1080"/> + <asset id="r2" name="ClipA" hasVideo="1" hasAudio="1" videoSources="1" audioSources="1" duration="60s"> + <media-rep kind="original-media" src="file:///tmp/a.mov"/> + </asset> + </resources> + <library> + <event name="E"> + <project name="P"> + <sequence format="r1" duration="10s" tcStart="0s"> + <spine> + <asset-clip ref="r2" offset="2s" name="JOnly" start="10s" duration="5s" + audioStart="9s"/> + </spine> + </sequence> + </project> + </event> + </library> + </fcpxml> + """) + + let source = try #require(fcpxml.allReportTimelineSources().first) + let windows = try await projector.project(from: source, fcpxml: fcpxml, options: .init()) + let video = try #require(windows.first { $0.channel.kind == .video }) + let audio = try #require(windows.first { $0.channel.kind == .audio }) + + #expect(video.timelineIn == Fraction(2, 1)) + #expect(video.timelineOut == Fraction(7, 1)) + #expect(audio.timelineIn == Fraction(1, 1)) + #expect(audio.timelineOut == Fraction(6, 1)) + #expect(audio.mediaIn == Fraction(9, 1)) + } + + // MARK: - Nested ref + timeMap / JL + + @Test("Nested ref-clip with outer timeMap and inner J/L cut") + func nestedRefClipOuterTimeMapInnerJL() async throws { + let fcpxml = try parseInlineFCPXML(""" + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE fcpxml> + <fcpxml version="1.11"> + <resources> + <format id="r1" frameDuration="100/2400s" width="1920" height="1080"/> + <asset id="r2" name="Leaf" hasVideo="1" hasAudio="1" videoSources="1" audioSources="1" duration="60s"> + <media-rep kind="original-media" src="file:///tmp/leaf.mov"/> + </asset> + <media id="r3" name="Compound"> + <sequence format="r1" duration="10s" tcStart="0s"> + <spine> + <asset-clip ref="r2" offset="0s" name="Inner" start="10s" duration="5s" + audioStart="9s" audioDuration="7s"/> + </spine> + </sequence> + </media> + </resources> + <library> + <event name="E"> + <project name="P"> + <sequence format="r1" duration="10s" tcStart="0s"> + <spine> + <ref-clip ref="r3" offset="0s" name="Outer" duration="4s"> + <timeMap> + <timept time="0s" value="0s" interp="linear"/> + <timept time="4s" value="8s" interp="linear"/> + </timeMap> + </ref-clip> + </spine> + </sequence> + </project> + </event> + </library> + </fcpxml> + """) + + let source = try #require(fcpxml.allReportTimelineSources().first) + let windows = try await projector.project(from: source, fcpxml: fcpxml, options: .trackAnalysis) + let video = windows.filter { $0.channel.kind == .video } + let audio = windows.filter { $0.channel.kind == .audio } + #expect(!video.isEmpty) + #expect(!audio.isEmpty) + // Outer 2x map compresses nested occupancy onto ~4s timeline. + #expect(video.contains { $0.timelineOut.doubleValue <= 4.1 + 0.05 }) + #expect(audio.contains { abs($0.timelineIn.doubleValue - $0.timelineOut.doubleValue) > 0.01 }) + } + + @Test("Nested secondary spine with timeMap child") + func nestedSecondarySpineWithTimeMapChild() async throws { + let fcpxml = try parseInlineFCPXML(""" + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE fcpxml> + <fcpxml version="1.11"> + <resources> + <format id="r1" frameDuration="100/2400s" width="1920" height="1080"/> + <asset id="r2" name="A" hasVideo="1" videoSources="1" duration="60s"> + <media-rep kind="original-media" src="file:///tmp/a.mov"/> + </asset> + <asset id="r3" name="B" hasVideo="1" videoSources="1" duration="60s"> + <media-rep kind="original-media" src="file:///tmp/b.mov"/> + </asset> + </resources> + <library> + <event name="E"> + <project name="P"> + <sequence format="r1" duration="20s" tcStart="0s"> + <spine> + <asset-clip ref="r2" offset="0s" name="Primary" start="0s" duration="10s"> + <spine lane="1" offset="2s"> + <asset-clip ref="r3" offset="0s" name="NestedRetime" start="0s" duration="4s"> + <timeMap> + <timept time="0s" value="0s" interp="linear"/> + <timept time="4s" value="8s" interp="linear"/> + </timeMap> + </asset-clip> + </spine> + </asset-clip> + </spine> + </sequence> + </project> + </event> + </library> + </fcpxml> + """) + + let source = try #require(fcpxml.allReportTimelineSources().first) + let windows = try await projector.project(from: source, fcpxml: fcpxml, options: .init()) + let nested = try #require(windows.first { $0.clipDisplayName == "NestedRetime" }) + #expect(nested.lanePath.components == [1]) + // Nested clip at parent abs 0+2=2, duration 4 → [2,6), scale ~2. + #expect(abs(nested.timelineIn.doubleValue - 2) < 0.05) + #expect(abs(nested.timelineOut.doubleValue - 6) < 0.05) + #expect(abs(nested.retiming.scale - 2) < 0.1) + } +} diff --git a/Tests/OpenFCPXMLKitTests/FCPXMLSmartCollectionTests.swift b/Tests/OpenFCPXMLKitTests/FCPXMLSmartCollectionTests.swift index e77a1ed..5499630 100644 --- a/Tests/OpenFCPXMLKitTests/FCPXMLSmartCollectionTests.swift +++ b/Tests/OpenFCPXMLKitTests/FCPXMLSmartCollectionTests.swift @@ -143,6 +143,32 @@ struct FCPXMLSmartCollectionTests { #expect(FinalCutPro.FCPXML.MatchProperty.PropertyKey.reel.rawValue == "reel") #expect(FinalCutPro.FCPXML.MatchProperty.PropertyKey.scene.rawValue == "scene") #expect(FinalCutPro.FCPXML.MatchProperty.PropertyKey.take.rawValue == "take") + #expect(FinalCutPro.FCPXML.MatchProperty.PropertyKey.projection.rawValue == "projection") + #expect(FinalCutPro.FCPXML.MatchProperty.PropertyKey.stereoscopic.rawValue == "stereoscopic") + #expect(FinalCutPro.FCPXML.MatchProperty.PropertyKey.cinematic.rawValue == "cinematic") + } + + @Test("MatchProperty isSet omits value") + func matchPropertyIsSetOmitsValue() throws { + let smartEl = OFKXMLDefaultFactory().makeElement(name: "smart-collection") + smartEl.addAttribute(name: "name", value: "Cinematic") + let smart = try #require(FinalCutPro.FCPXML.SmartCollection(element: smartEl)) + smart.matchProperties = [ + FinalCutPro.FCPXML.MatchProperty(key: .cinematic, rule: .isSet, value: nil) + ] + let matchEl = try #require(smart.element.firstChildElement(named: "match-property")) + #expect(matchEl.stringValue(forAttributeNamed: "key") == "cinematic") + #expect(matchEl.stringValue(forAttributeNamed: "rule") == "isSet") + #expect(matchEl.stringValue(forAttributeNamed: "value") == nil) + #expect(smart.matchProperties.count == 1) + #expect(smart.matchProperties[0].rule == .isSet) + #expect(smart.matchProperties[0].value == nil) + } + + @Test("SmartCollectionRule isSet and isNotSet raw values") + func smartCollectionRuleIsSetRawValues() { + #expect(FinalCutPro.FCPXML.SmartCollectionRule.isSet.rawValue == "isSet") + #expect(FinalCutPro.FCPXML.SmartCollectionRule.isNotSet.rawValue == "isNotSet") } // MARK: - MatchTime Tests diff --git a/Tests/OpenFCPXMLKitTests/FCPXMLTimelineProjectionTests.swift b/Tests/OpenFCPXMLKitTests/FCPXMLTimelineProjectionTests.swift index 6eef352..656c227 100644 --- a/Tests/OpenFCPXMLKitTests/FCPXMLTimelineProjectionTests.swift +++ b/Tests/OpenFCPXMLKitTests/FCPXMLTimelineProjectionTests.swift @@ -567,6 +567,42 @@ struct FCPXMLTimelineProjectionTests { #expect(segments.video[0].timelineStart == Fraction(2, 1)) } + @Test("AudioStart-only split defaults audioDuration to video duration") + func audioSplitRetiming_AudioStartOnly_DefaultsDuration() throws { + // video start=10s duration=5s at offset=2s → [2,7) media [10,15) + // audioStart=9s only → audio timeline starts 1s earlier, duration remains 5s → [1,6) + let segments = FinalCutPro.FCPXML.AudioSplitRetiming.segments( + timeMap: nil, + absoluteStart: Fraction(2, 1), + videoDuration: Fraction(5, 1), + videoMediaStart: Fraction(10, 1), + clipStartAttribute: Fraction(10, 1), + audioStart: Fraction(9, 1), + audioDuration: nil + ) + #expect(FinalCutPro.FCPXML.AudioSplitRetiming.hasSplitEdit( + videoStart: Fraction(10, 1), + videoDuration: Fraction(5, 1), + audioStart: Fraction(9, 1), + audioDuration: nil + )) + #expect(segments.audio.count == 1) + #expect(segments.audio[0].timelineStart == Fraction(1, 1)) + #expect(segments.audio[0].timelineEnd == Fraction(6, 1)) + #expect(segments.audio[0].mediaStart == Fraction(9, 1)) + #expect(segments.audio[0].mediaEnd == Fraction(14, 1)) + } + + @Test("trackAnalysis preset uses active audition and multicam") + func trackAnalysisPreset_UsesActiveMasks() { + let options = FinalCutPro.FCPXML.TimelineProjectionOptions.trackAnalysis + #expect(options.auditions == .active) + #expect(options.mcClipAngles == .active) + #expect(options.expandAllSourceChannels) + #expect(options.includeDisabled) + #expect(!options.excludeFullyOccluded) + } + // MARK: - Multicam, ref-clip, audition, video/audio leaves @Test("MC-clip active angle only emits from active angle") diff --git a/Tests/OpenFCPXMLKitTests/FCPXMLVersionFeatureGateTests.swift b/Tests/OpenFCPXMLKitTests/FCPXMLVersionFeatureGateTests.swift new file mode 100644 index 0000000..6410b50 --- /dev/null +++ b/Tests/OpenFCPXMLKitTests/FCPXMLVersionFeatureGateTests.swift @@ -0,0 +1,57 @@ +// +// FCPXMLVersionFeatureGateTests.swift +// OpenFCPXMLKit • https://github.com/TheAcharya/OpenFCPXMLKit +// © 2026 • Licensed under MIT License +// + + +// +// VersionFeatureGate registry: omit sets and Authoring/converter alignment. +// + +import Testing +@testable import OpenFCPXMLKit + +@Suite("Version feature gate") +struct FCPXMLVersionFeatureGateTests { + @Test("Element omit set for 1.5 includes post-1.5 features") + func elementOmitSetFor1_5() { + let omitted = FinalCutPro.FCPXML.VersionFeatureGate.elementNamesToOmit(at: .v1_5) + #expect(omitted.contains("adjust-cinematic")) + #expect(omitted.contains("match-usage")) + #expect(omitted.contains("live-drawing")) + #expect(omitted.contains("hidden-clip-marker")) + #expect(omitted.contains("match-analysis-type")) + #expect(!omitted.contains("asset-clip")) + } + + @Test("Element omit set for 1.14 is empty for gated features") + func elementOmitSetFor1_14Empty() { + let omitted = FinalCutPro.FCPXML.VersionFeatureGate.elementNamesToOmit(at: .v1_14) + #expect(omitted.isEmpty) + } + + @Test("Attribute omit set for heroEye before 1.13") + func attributeOmitHeroEyeBefore1_13() { + let at112 = FinalCutPro.FCPXML.VersionFeatureGate.attributeNamesToOmit( + onElement: "format", + at: .v1_12 + ) + #expect(at112.contains("heroEye")) + let at113 = FinalCutPro.FCPXML.VersionFeatureGate.attributeNamesToOmit( + onElement: "format", + at: .v1_13 + ) + #expect(at113.isEmpty) + } + + @Test("Cinematic availability matches feature gate") + func cinematicAvailabilityMatchesGate() { + let gate = FinalCutPro.FCPXML.VersionFeatureGate.availability(forElement: "adjust-cinematic") + #expect(gate.contains(.v1_10)) + #expect(!gate.contains(.v1_9)) + #expect( + FinalCutPro.FCPXML.Authoring.CinematicAdjustment().availability == gate + ) + } +} diff --git a/Tests/README.md b/Tests/README.md index aa49727..fa4eca2 100644 --- a/Tests/README.md +++ b/Tests/README.md @@ -2,7 +2,7 @@ This directory contains the test suite for OpenFCPXMLKit, a Swift 6 framework for Final Cut Pro FCPXML processing with SwiftTimecode integration. The suite runs on **macOS** (Foundation XML backend). The library also supports **iOS 26+** (AEXML backend); CI builds for iOS Simulator; the same tests are not run on iOS because they rely on Foundation XML. -- **Test count:** **1084** tests listed in `swift test list` — **1078** in `OpenFCPXMLKitTests` + **6** in `ExcelReportTest` (all Swift Testing `@Test`; no XCTest remaining; ExcelReportTest cancels without a local fixture) +- **Test count:** **1114** tests listed in `swift test list` — **1108** in `OpenFCPXMLKitTests` + **6** in `ExcelReportTest` (all Swift Testing `@Test`; no XCTest remaining; ExcelReportTest cancels without a local fixture) - **Scope:** Parsing, timecode, document operations, file loading, timeline export, validation (semantic, DTD, structural), timeline manipulation, media processing, typed models (adjustments, filters, captions/titles, keyframe animation), CMTime Codable, collections, Live Drawing (1.11+), HiddenClipMarker (1.13+), Format/Asset 1.13+ (heroEye, heroEyeOverride, mediaReps), SmartCollection match rules, 360 video (projection, stereoscopic), auditions, conform-rate, still images, multicam, secondary storylines, audio keyframes, keyword collections/folders, empty timeline creation at different sizes and frame rates, project-creation export at different sizes and frame rates (with DTD validation), FCPXMLExporter clip-level metadata export (markers, chapter-markers, keywords, ratings, metadata as asset-clip children; DTD and xmllint-compatible XML declaration), cross-platform XML (AEXML serialization parity, DTD validator behaviour, structural validator), Timeline Projection (`TimelineProjector` / `MediaUsageWindow` / `ReportProjectionContext`, project-once for report sections), Excel and PDF reporting (universal **Row** column on all tabular sheets via `ensuringRowColumn` / `allowsInjectedRowColumn`, role inventory columns, Summary sheet with project title in **B1**, Media Summary sheets, configurable `ReportTimecodeFormat` / DF·NDF notation, format-aware headers, Frames/Feet+Frames sort order, inventory-first `ReportBuildPhase` progress, global column exclusion, disabled-clip filtering, Markers out-of-bounds filter / optional **Hidden** column (`includeMarkersOutsideClipBoundaries`), Excel `protectSheets` worksheet protection, workbook export and cell formatting, PDF cover with black “About This PDF Export” header + `info.circle`, TOC with accent colour chips + content-tint washes keyed to sheet `colorIndex`, remaining columns expanded to fill A4 landscape width after exclusions, section pagination, shared `FCPXMLReportRowColorPolicy`, standalone compound-clip timelines via `allReportTimelineSources()` / `FCPXMLCompoundClipReportTests`), and all supported FCPXML versions and frame rates - **Layout:** Shared utilities for sample paths; file tests per sample; logic/parsing tests for model types and structure; validation and cross-platform XML tests; optional Excel/PDF report integration tests under `ExcelReportTest/`; private investigation inbox under `Submitted FCPXML/` (gitignored contents) @@ -198,8 +198,8 @@ swift test --filter OpenFCPXMLKitTests # By pattern To verify the documented test counts: ```bash -swift test list 2>/dev/null | grep -c '\.' # 1084 -swift test list 2>/dev/null | grep -c 'OpenFCPXMLKitTests\.' # 1078 +swift test list 2>/dev/null | grep -c '\.' # 1114 +swift test list 2>/dev/null | grep -c 'OpenFCPXMLKitTests\.' # 1108 swift test list 2>/dev/null | grep -c 'ExcelReportTest\.' # 6 ``` @@ -278,6 +278,9 @@ Tests are discovered automatically by Swift PM. Run `swift test` (Swift Testing - **FCPXMLCutDetectionTests** — Edit points (hardCut, transition, gapCut); source relationship (sameClip, differentClips); empty spine; single clip; same ref transitions; different refs; CutSample.fcpxml file test. EditPoint, CutDetectionResult. - **FCPXMLTimelineProjectionTests** — Timeline Projection: identity/`timeMap`; nested lanes; J/L cuts; multicam active/all + split angles; ref-clip unfold; audition mask; video/audio leaves; SyncClip/24 sample regression; Role Inventory / Markers / Keywords / Titles / Transitions / Effects / Speed Change / Media Summary / Summary project-once; occupancy index; disabled filtering; streaming parity. +- **FCPXMLProjectionEdgeCaseCorpusTests** — JL+timeMap, audioStart-only, nested ref+JL, nested spine+timeMap corpus. +- **FCPXMLAuthoringTests** — Detached Authoring round-trip, cinematic omit-on-write, mixed spine + compound clips (sync/ref/mc/audition/caption). +- **FCPXMLVersionFeatureGateTests** — Shared version feature registry availability / omit sets. **Typed models** @@ -291,7 +294,7 @@ Tests are discovered automatically by Swift PM. Run `swift test` (Swift Testing - **FCPXMLCMTimeCodableTests** — CMTime encode/decode as FCPXML time strings; round-trip; edge cases. - **FCPXMLCollectionTests** — CollectionFolder, KeywordCollection; nested folders; Codable. -**Reporting & Excel/PDF export** (see [19 — Reporting, Excel & PDF Export](../Documentation/Manual/19-Reporting.md)) +**Reporting & Excel/PDF export** (see [19 — Reporting, Excel & PDF Export](../Documentation/Manual/20-Reporting.md)) - **FCPXMLCompoundClipReportTests** — Standalone compound-clip FCPXML (event `ref-clip` → `media`/`sequence`, no `<project>`): `allReportTimelineSources()`, role inventory / markers / summary via `buildReport`, project-name filter, and regression that normal project reports still resolve. - **FCPXMLRoleInventoryReportTests** — Role inventory section: Selected Roles Inventory rows and per-role sheets, categories, columns. @@ -323,7 +326,7 @@ Tests are discovered automatically by Swift PM. Run `swift test` (Swift Testing - **FCPXMLExtractionScopeTests** — ExtractionScope behaviour (main-timeline visibility, occlusion, depth/type filters). - **FCPXMLExtractionNestFidelityTests** / **FCPXMLRoleInheritanceMatrixTests** / **FCPXMLExtractionProjectionPolicyTests** — Extraction fidelity (preset nests, role inheritance matrix, Extraction↔Projection occlusion/`excludeDisabledClips` policy). - **FCPXMLProjectionCoverageTests** — Projection geometry (annotations, per-src, nested retiming compose, sync-in-mc, Photoshop multi-src, Summary overlap-aware durations). -- **FCPXMLReportObligationCorpusTests** — Reporting contracts: fail-soft vs fail-loud (`ReportMediaResolutionPolicy`), Media Summary proxy/original distinction, near-zero-miss obligation corpus on in-repo samples (BasicMarkers, Keywords, TitlesRoles, RolesList, TransitionMarkers1, Complex). Sheet obligation contracts are documented in Manual 19. +- **FCPXMLReportObligationCorpusTests** — Reporting contracts: fail-soft vs fail-loud (`ReportMediaResolutionPolicy`), Media Summary proxy/original distinction, near-zero-miss obligation corpus on in-repo samples (BasicMarkers, Keywords, TitlesRoles, RolesList, TransitionMarkers1, Complex). Sheet obligation contracts are documented in Manual 20. - **FCPXMLEngineHygieneTests** — Engine hygiene: ReportBuilder project-once, version-strip honesty (1.13+ attrs omitted on 1.5 convert), Complex projection soft 30s smoke budget. - **FCPXMLMarkersKeywordsProjectionTests** — Markers/Keywords report builders prefer Projection `ProjectedClipAnnotations` (BasicMarkers title markers, Keywords sample); Extraction fallback when annotations absent. - **FCPXMLTitlesProjectionTests** — Titles & Generators report builder prefers Projection `WindowTitleAnnotation` (TitlesRoles, BasicMarkers, DisabledClips); Extraction fallback when annotations absent. @@ -552,11 +555,11 @@ Add tests for new behaviour or edge cases; place them in the right file and MARK - **OpenFCPXMLKit README** (project root) — overview and API usage - **[ARCHITECTURE.md](../ARCHITECTURE.md)** — layer stack, Mermaid codebase map - **[GUARDRAILS.md](../GUARDRAILS.md)** — must / must-not (incl. never commit private FCPXML) -- **Documentation/Manual** — full manual; [19 — Reporting, Excel & PDF Export](../Documentation/Manual/19-Reporting.md) for report API; [16 — Cross-Platform & iOS](../Documentation/Manual/16-Cross-Platform-iOS.md) for XML abstraction and iOS support; [11 — Timeline Projection](../Documentation/Manual/11-Timeline-Projection.md) +- **Documentation/Manual** — full manual; [19 — Reporting, Excel & PDF Export](../Documentation/Manual/20-Reporting.md) for report API; [16 — Cross-Platform & iOS](../Documentation/Manual/17-Cross-Platform-iOS.md) for XML abstraction and iOS support; [11 — Timeline Projection](../Documentation/Manual/12-Timeline-Projection.md) - **Final Cut Pro XML (FCPXML)** — [fcp.cafe](https://fcp.cafe) for format reference - **SwiftTimecode** (GitHub) — timecode and frame rate types -**Keep counts in sync:** `swift test list` → **1084** total (**1078** OpenFCPXMLKitTests + **6** ExcelReportTest; all Swift Testing); **60** public samples. +**Keep counts in sync:** `swift test list` → **1114** total (**1108** OpenFCPXMLKitTests + **6** ExcelReportTest; all Swift Testing); **60** public samples. --- diff --git a/Tests/Submitted FCPXML/README.md b/Tests/Submitted FCPXML/README.md index b475c4d..0edbff9 100644 --- a/Tests/Submitted FCPXML/README.md +++ b/Tests/Submitted FCPXML/README.md @@ -4,7 +4,7 @@ Local-only drop zone for **private / user-supplied** FCPXML exports used when in **These files are never committed.** Contents are gitignored; only this README (and `.gitkeep`) is tracked. See [GUARDRAILS.md](../../GUARDRAILS.md) (Sign: never-commit-submitted-fcpxml) and [ARCHITECTURE.md](../../ARCHITECTURE.md) §8. -**Public suite counts (keep in sync):** **1084** tests listed (`1078` OpenFCPXMLKitTests + `6` ExcelReportTest; **all Swift Testing**); **60** public samples under `Tests/FCPXML Samples/FCPXML/` (e.g. `HiddenMarkers.fcpxml` was promoted from this workflow). +**Public suite counts (keep in sync):** **1114** tests listed (`1108` OpenFCPXMLKitTests + `6` ExcelReportTest; **all Swift Testing**); **60** public samples under `Tests/FCPXML Samples/FCPXML/` (e.g. `HiddenMarkers.fcpxml` was promoted from this workflow). ---