Releases: SteliyanH/kadr-captions
Release list
v0.8.0 — iOS 17 platform floor
Mechanical platform-floor bump to iOS 17 / macOS 14 / tvOS 17 / visionOS 1 so KadrCaptions can depend on Kadr ≥ 0.15.0 (an iOS 16 package can't depend on an iOS 17 one). Part of the coordinated ecosystem iOS 17 move. No source or behavior change — 0 @available annotations, 0 API-call migrations. Consumers needing the iOS 16 floor stay on 0.7.x. 200 tests pass.
Full diff: v0.7.0...v0.8.0
v0.7.0 — VobSub .sub bitmaps + caption merge / split / frame snap
Reopened cycle. Closes the bitmap-decode gap v0.6 explicitly deferred and bundles three caption-utility helpers consumers were reimplementing as one-liners. Pure additive; every v0.6 call site compiles unchanged. Kadr floor stays at ≥ 0.9.2.
Added — VobSub .sub SPU bitmap extraction (Tier 1)
CaptionParser.extractVobSubBitmaps(idx:sub:)async — decodes the SPU packets referenced by eachVobSubCue.fileOffsetin the paired.subblob and returns[VobSubBitmap]. Each entry pairs the cue's start with the SPU's stop-display end (the.idxalone can't tell you duration — that's encoded inside the SPU control sequence) and the rendered RGBACGImage.VobSubBitmapvalue type —start: CMTime,end: CMTime,image: CGImage.- Handles both bare-SPU
.sublayouts and MPEG-2 PS / PES-wrapped.subfiles. Bad cues drop silently — dropping one beats throwing the whole batch. - Free per the locked premium scope; AI / OCR stays in kadr-pro.
Added — Caption-utility helpers (Tier 2)
Caption.split(at: CMTime) -> (Caption, Caption)— cleave a cue at an absolute timestamp.Caption.snappedToFrameRate(_: Double)+ array sugar — roundstart/endto the nearest frame at the given rate (23.976 / 24 / 25 / 29.97 / 30 / 60). Non-positive / non-finite rates no-op.Array<Caption>.merged(within: CMTime)— collapse adjacent / overlapping cues whose gap is ≤ threshold. Text joins on\n.
Tests
27 new tests across VobSubBitmapTests (13) and CaptionTransformsTests (14). Full suite green.
Notes
extractVobSubBitmapsis async only because real call sites read the.suboff disk; the decode itself is synchronous.- The renderer interleaves top / bottom RLE fields per the SPU spec; alpha is scaled from the SPU's 4-bit field to 8-bit (×17) so palette index 0 with alpha 0 is fully transparent.
merged(within:)treats overlapping cues (negative gap) as merge candidates — they share screen time anyway.
Full changelog: CHANGELOG.md.
v0.6.0 — Format extensions
Format extensions. Reopened cycle (v0.5 was marked feature-complete) to close the niche-but-real ingest gaps that have come up in downstream pipelines and bring the API in line with the modern VTT spec. Three additions, all additive — every v0.5 call site compiles unchanged.
Added — VobSub .idx parser (Tier 1)
VobSubIndex+VobSubCueSendable structs — language + 16-entry palette + array of cues (start time + byte offset into the paired.subfile).CaptionParser.parseVobSubIndex(_:)+ pure helpers (parseVobSubLanguage,parseVobSubPalette,parseVobSubTimestamp). Timestamp usesHH:MM:SS:mmmcolon format — strictly rejects SRT-comma / VTT-dot variants.Caption.fromVobSubIndex(_:placeholderText:trailingDuration:)— bridge that renders the index as[Caption]with placeholder text. Cue duration inferred from gap to next cue; last cue uses caller-supplied trailing duration (the format has no end-time field).- Bitmap extraction from the paired
.subfile is a v0.7 follow-up — v0.6 surfaces the index timing only so consumers can mark subtitle existence in the timeline.
Added — WebVTT cue regions (Tier 2)
CaptionRegion+RegionScrollModetypes — id, widthPercent, lines, region/viewport anchors (normalized0...1per axis), scroll mode. Defaults align with WebVTT spec.StyledCaption.region: CaptionRegion?— additive, default nil. v0.5 styled-caption call sites compile unchanged.parseStyledVTT(_:)now collects REGION blocks (instead of skipping) and resolvesregion:NAMEcue references. Unknown ids resolve to nil — tolerates authoring tools that strip regions mid-pipeline.- Pure helpers:
parseStyledVTTRegionBlock(_:)+parseVTTRegionID(_:). - Plain
parseVTT(_:)unchanged — regions stay styled-only.
Added — EBU-TT-D parser (Tier 3)
CaptionParser.parseEBUTTD(_:)+Caption.load(ebuTTD:)— string + disk parsers for the European broadcast TTML profile (EBU Tech 3380).CaptionParser.parseEBUTTDTime(_:)pure time helper. AcceptsHH:MM:SSandHH:MM:SS.mmmonly; rejects iTT's frame-count form (HH:MM:SS:FF) and TTML's suffix forms (1.5s,1500ms). EBU profile mandates clock-form timing.- Plain text only — styled EBU-TT-D's region-nested layout is a future-cycle candidate.
<br/>inserts newlines;<ebuttm:documentMetadata>and<ebutts:style>head blocks silently skipped. - No
Caption.load(_:)extension switch entry —.xml/.ttmlare ambiguous with other TTML profiles. Consumers callload(ebuTTD:)directly.
Tests
39 new tests across the cycle. Highlights: cross-format strictness pins (VobSub rejects SRT-comma timing; EBU-TT-D rejects iTT frame-count timing), region resolution edge cases (referenced / unreferenced / ghost id / multi-region), <br/> newline insertion, head metadata skipping. XCTest suite: 43 → 82.
Dependencies
No floor bumps. Still requires kadr ≥ 0.9.2.
v0.5.0 — Styled ASS/SSA + time utilities
KadrCaptions v0.5.0. Pure additive — Kadr floor stays at 0.9.2.
Highlights
Caption.shifted(by:)/Caption.scaled(by:)— pure value-type transforms for syncing cues to a re-encoded / sped-up video.[Caption]array sugar mirrors both.CaptionParser.parseStyledASS(_:)/parseStyledSSA(_:)+Caption.loadStyled(ass:)/loadStyled(ssa:)— round-trip the most-load-bearing ASS / SSA styling: foreground color (\1c&HBBGGRR&/\c&HBBGGRR&), bold / italic / underline flags, and alignment (\an<N>numpad + legacy\a<N>SSA bitmask) into[StyledCaption]. ASS alpha flipped on import.StyledCaption.color: String?— new optional#RRGGBB/#RRGGBBAAhex field (defaultnil); thetoTextOverlay(...)bridge applies it toKadr.TextStyle.colorvia the newStyledCaption.platformColor(forHex:)helper.
Compatibility
KadrCaptions 0.5.0 requires Kadr ≥ 0.9.2 (unchanged from v0.1).
41 new tests across the cycle. Cycle considered feature-complete pending kadr v1.0.
See CHANGELOG for the full entry.
v0.4.0 — ASS / SSA
Adds Advanced SubStation Alpha (.ass) and SubStation Alpha (.ssa). Heavily used in anime / fansub / streaming pipelines. Completes the every-common-subtitle-format matrix: SRT + VTT + iTT + ASS + SSA.
What's new
import Kadr
import KadrCaptions
let cues = try await Caption.load(subtitleURL) // .srt / .vtt / .itt / .ass / .ssa
let video = Video {
VideoClip(url: footage)
}
.captions(cues)
try await CaptionAuthor.writeASS(cues, to: outputASS)- `Caption.load(ass:)` + `CaptionParser.parseASS(_:)` — async + pure synchronous parsers for Advanced SubStation Alpha.
- `Caption.load(ssa:)` + `CaptionParser.parseSSA(_:)` — same for SubStation Alpha.
- `CaptionAuthor.writeASS(_:to:)` + `writeSSA(_:to:)` + `renderASS(_:)` + `renderSSA(_:)` — async writers + pure render helpers.
- Pure helpers: `parseASSTimestamp`, `formatASSTimestamp`, `splitASSDialogue`, `stripASSOverrides`, `renderASSCueText`.
- `Caption.load(_:)` auto-detect now recognizes `.ass` / `.ssa` (case-insensitive).
Behavior
- Reads `[Events]` `Format:` line for column ordering — finds Start / End / Text indices.
- `Comment:` events skipped silently; `;` and `!:` comment lines also skipped.
- CSV row split preserves commas in the trailing Text field.
- Style override blocks (`{\\b1}`, `{\\c&HFFFFFF&}`, etc.) and karaoke timing tags stripped — styled output flows through v0.3's `TextOverlay` bridge if needed.
- `\\N` / `\\n` line breaks → `\\n`; `\\h` (hard space) → space.
- ASS timestamps parsed as `H:MM:SS.cc` (centisecond precision).
Compatibility
- Requires kadr ≥ 0.9.2 (unchanged).
- iOS 16+ / macOS 13+ / tvOS 16+ / visionOS 1+, Swift 6.0.
- Pure additive — every v0.3 composition compiles unchanged.
Tests
Suite: 155 → 200 (+45).
Documented limitation
Literal `{` / `}` characters in cue text don't round-trip — the parser treats them as override-block markers.
v0.4 closes the cycle
Every common subtitle format is now supported: SRT, VTT, iTT, ASS, SSA. The styled-ASS surface remains the next forward edge if anyone needs it.
Full notes in CHANGELOG.md.
v0.3.0 — Styled captions → TextOverlay
The biggest leap of the cycle. A parser path that preserves VTT cue settings and inline styling, plus a bridge that maps a styled cue onto kadr v0.8's `TextOverlay` + `textAnimation`. Consumers can now render captions as styled, animated overlays burned into the export — not just `AVMetadataItem` cues for the OS picker.
What's new
import Kadr
import KadrCaptions
let cues = try await Caption.loadStyled(vtt: vttURL)
let video = Video {
VideoClip(url: footage)
}
.styledCaptions(cues, animation: .fadeIn(duration: 0.3))
try await video.export(to: outputURL) // captions baked as styled overlays- `StyledCaption` value type — text + timeRange + alignment + line + position + bold / italic / underline flags + speaker + classes.
- `StyledCaptionAlignment` (`.start` / `.center` / `.end`) and `StyledCaptionLine` (`.auto` / `.top` / `.bottom` / `.percent(Double)`).
- `CaptionParser.parseStyledVTT(_:)` + `Caption.loadStyled(vtt:)` — preserve `align:` / `line:` / `position:` cue settings and inline tags.
- `StyledCaption.toTextOverlay(baseStyle:animation:)` — bridge to `Kadr.TextOverlay` with per-cue `visibilityRange`.
- `Video.styledCaptions(_:baseStyle:animation:)` — convenience modifier.
Tag handling
"Any tag of this kind appeared in the cue → per-cue flag set":
- `` / `` / `` toggle the respective flag.
- `` extracts the speaker name.
- `<c.foo.bar>` accumulates classnames.
- `<00:00:01.500>` timed-text markers stripped.
Bridge mapping
- Alignment → `TextStyle.Alignment` (`.leading` / `.center` / `.trailing`).
- Bold → `Weight.bold`. Italic → `fontName = "Helvetica-Oblique"` (documented limitation).
- Line position → y-coordinate + anchor row; alignment → x-anchor.
Critical contract
The plain-text part of every `StyledCaption` equals what `parseVTT` would produce for the same input. Verified by round-trip test.
Compatibility
Pure additive — every v0.2 composition compiles unchanged. Same platform floor: iOS 16+ / macOS 13+ / tvOS 16+ / visionOS 1+, Swift 6.0.
Tests
Suite: 98 → 155 (+57).
What's next
- v0.4.0 — ASS / SSA support (Advanced SubStation Alpha)
- v0.3.x — `<STYLE>`-block parser to apply `<c.classname>` styling
Full notes in CHANGELOG.md.
v0.2.0 — iTT parser + writer
Adds the third common caption format. Parses and authors iTunes Timed Text (.itt) — Apple's flavor of TTML 1.0, used by Final Cut, iTunes / Apple TV submission, and broadcast deliverables.
What's new
import Kadr
import KadrCaptions
let cues = try await Caption.load(ittURL) // auto-detect now handles .srt / .vtt / .itt
let video = Video {
VideoClip(url: footage)
}
.captions(cues)
try await CaptionAuthor.writeITT(cues, to: outputITT)- `Caption.load(itt:)` + `CaptionParser.parseITT(_:)` — Foundation `XMLParser`-based; no third-party deps.
- `CaptionAuthor.writeITT(_:to:)` + `renderITT(_:)` — minimal valid TTML 1.0 with Apple-compatible header (`xmlns` + `ttp:frameRate=30` + `ttp:tickRate=1000`).
- `Caption.load(_:)` auto-detect now recognizes `.itt` alongside `.srt` / `.vtt`.
- `CaptionParser.parseITTTime(_:frameRate:)` — handles `HH:MM:SS.mmm`, `HH:MM:SS:FF` (frame-count, approximated at the document's declared `ttp:frameRate` or 30 fps fallback), `1.5s`, `1500ms`, plain seconds.
- New error case `CaptionParseError.malformedXML(localizedDescription:)` for XML structural failures.
Cue mapping
- Each `
` inside `
` → one `Caption` - `
` → `\n` - Inline `` styling flattened to plain text (the v0.3 `TextOverlay` bridge handles styled output)
- Multiple `` blocks concatenate
- `tt:` prefix on `begin` / `end` attributes tolerated
Compatibility
- Requires kadr ≥ 0.9.2 (unchanged).
- iOS 16+ / macOS 13+ / tvOS 16+ / visionOS 1+, Swift 6.0.
- No third-party dependencies. Pure Swift + Foundation + CoreMedia.
- Pure additive — every v0.1 composition compiles unchanged.
Tests
Suite: 63 → 98 (+35).
What's next
- v0.3.0 — Styled captions → `TextOverlay` + `textAnimation` bridge
- v0.4.0 — ASS / SSA support
Full notes in CHANGELOG.md.
v0.1.0 — SRT + VTT
First release of kadr-captions — caption file parsing and authoring for kadr. Adapter package consuming kadr v0.9.2's `Caption` value type and `Video.captions(_:)` modifier.
What's new
import Kadr
import KadrCaptions
let cues = try await Caption.load(subtitleURL) // auto-detect .srt / .vtt
let video = Video {
VideoClip(url: footage)
}
.captions(cues)
try await video.export(to: outputURL) // captions baked as AVMetadataItem at export- `Caption.load(_:)` — auto-detect by file extension. Dispatches to `load(srt:)` / `load(vtt:)`.
- `Caption.load(srt:)` / `load(vtt:)` — async file loaders. UTF-8 default with Windows-1252 fallback.
- `CaptionParser.parseSRT(_:)` / `parseVTT(_:)` — pure synchronous string parsers.
- `CaptionAuthor.writeSRT(_:to:)` / `writeVTT(_:to:)` — async writers.
- `CaptionAuthor.renderSRT(_:)` / `renderVTT(_:)` — pure render helpers.
- `CaptionParseError` — typed errors with source-line metadata.
Real-world tolerance
- CRLF / LF / mixed line endings
- UTF-8 BOM stripped
- UTF-8 default + Windows-1252 fallback for legacy SRT files
- Lenient SRT cue indexing (non-sequential / missing indices accepted)
- Multi-line cues
- Dot- and comma-separated milliseconds in both formats
- VTT `WEBVTT` header (bare or with description); missing → `.missingHeader`
- VTT `NOTE` / `REGION` / `STYLE` blocks tolerated and skipped
- VTT cue identifiers ignored
- VTT cue settings (`align:`, `position:`, `line:`, `region:`) stripped
- VTT inline tags stripped to plain text: `<c.classname>`, ``/``/``, ``, timed markers
- VTT short-form `MM:SS.mmm` accepted alongside `HH:MM:SS.mmm`
Compatibility
- Requires kadr ≥ 0.9.2.
- iOS 16+ / macOS 13+ / tvOS 16+ / visionOS 1+, Swift 6.0, strict concurrency.
- No third-party dependencies. Pure Swift + Foundation + CoreMedia.
Tests
63 passing across SRT (26), VTT (31), auto-detect (6).
What's next
- v0.2.0 — iTT (iTunes Timed Text) parser + writer
- v0.3.0+ — styled / animated captions mapped onto kadr v0.8 `TextOverlay` + `textAnimation`
Full notes in CHANGELOG.md.