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.