Skip to content

Releases: SteliyanH/kadr-captions

v0.8.0 — iOS 17 platform floor

Choose a tag to compare

@SteliyanH SteliyanH released this 02 Jul 13:09

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

Choose a tag to compare

@SteliyanH SteliyanH released this 02 Jun 14:42

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 each VobSubCue.fileOffset in the paired .sub blob and returns [VobSubBitmap]. Each entry pairs the cue's start with the SPU's stop-display end (the .idx alone can't tell you duration — that's encoded inside the SPU control sequence) and the rendered RGBA CGImage.
  • VobSubBitmap value type — start: CMTime, end: CMTime, image: CGImage.
  • Handles both bare-SPU .sub layouts and MPEG-2 PS / PES-wrapped .sub files. 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 — round start / end to 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

  • extractVobSubBitmaps is async only because real call sites read the .sub off 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

Choose a tag to compare

@SteliyanH SteliyanH released this 21 May 22:24

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 + VobSubCue Sendable structs — language + 16-entry palette + array of cues (start time + byte offset into the paired .sub file).
  • CaptionParser.parseVobSubIndex(_:) + pure helpers (parseVobSubLanguage, parseVobSubPalette, parseVobSubTimestamp). Timestamp uses HH:MM:SS:mmm colon 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 .sub file 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 + RegionScrollMode types — id, widthPercent, lines, region/viewport anchors (normalized 0...1 per 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 resolves region:NAME cue 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. Accepts HH:MM:SS and HH:MM:SS.mmm only; 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 / .ttml are ambiguous with other TTML profiles. Consumers call load(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

Choose a tag to compare

@SteliyanH SteliyanH released this 03 May 15:19

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 / #RRGGBBAA hex field (default nil); the toTextOverlay(...) bridge applies it to Kadr.TextStyle.color via the new StyledCaption.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

Choose a tag to compare

@SteliyanH SteliyanH released this 30 Apr 09:20

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

Choose a tag to compare

@SteliyanH SteliyanH released this 30 Apr 07:44

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

Choose a tag to compare

@SteliyanH SteliyanH released this 30 Apr 06:52

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

Choose a tag to compare

@SteliyanH SteliyanH released this 30 Apr 05:45

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.