oxideav-ass
Pure-Rust ASS / SSA subtitle codec and container — parser and writer
for Advanced SubStation Alpha (.ass) and SubStation Alpha (.ssa)
text subtitle files. Zero C dependencies.
Part of the oxideav framework but usable standalone.
Installation
[]
= "0.1"
= "0.1"
= "0.1"
= "0.0"
= "0.0"
Quick use
ASS is a text format rather than a bitstream, so "decode" means parsing
event lines + style metadata and "encode" means formatting back out to
the same text form. The container opens one .ass / .ssa file and
emits one packet per Dialogue: event; the codec on either side
converts packets to the shared SubtitleCue IR (from oxideav-core).
use CodecRegistry;
use ContainerRegistry;
use Frame;
let mut codecs = new;
let mut containers = new;
register_codecs;
register_containers;
let input: = Boxnew;
let mut dmx = containers.open?;
let stream = &dmx.streams;
let mut dec = codecs.make_decoder?;
loop
# Ok::
Direct parse / write
If you just want the text format without the codec+container pipeline:
let track = parse?;
let out_bytes = write;
Format conversion
Direct ASS / SRT and ASS / WebVTT conversion helpers are exposed — they parse into the shared IR and re-emit in the target format.
let ass = srt_to_ass?;
let srt = ass_to_srt?;
let vtt = ass_to_webvtt?;
let ass = webvtt_to_ass?;
Feature coverage
What the parser understands and preserves on round-trip:
[Script Info]— header key/value pairs captured as track metadata; comment lines (;/!) preserved inside extradata.- Unknown sections preserved — editor-private blocks like
[Aegisub Project Garbage],[Aegisub Extradata],[Aegisub Style Storage],[Fonts],[Graphics], and any other named section not modelled by the parser have their body lines kept verbatim throughextradata, so a parse → write round-trip emits them back unchanged (no dangling section headers, no lost editor state, no lost UU-encoded attachments). [V4+ Styles]and[V4 Styles]—Format:-aware per-Style:decode of name, font, size, primary / outline / back colours (&HAABBGGRRwith ASS alpha inversion), bold / italic / underline / strikeout flags (including SSA's-1for true), alignment (both ASS\anand legacy SSA numpad schemes), margins, outline, and shadow widths.[Events]—Format:-aware;Dialogue:lines decode toSubtitleCuewith start, end, style reference, and styled segments.Comment:events are dropped.- Override tags inside dialogue text —
\b,\i,\u,\s,\cand\1c(primary colour),\2c/\3c/\4c(secondary / outline / shadow colour),\alphaand\1a/\2a/\3a/\4a(per-component alpha — ASS convention: 0 = opaque, 255 = transparent),\fn,\fs,\pos(x,y),\an,\k/\kf/\ko(karaoke timing markers), and\r(reset inline state). Unknown tags survive parsing as opaque pass-through so round-trip keeps them intact, even when mixed with tags the parser does interpret. - Animated tags —
\fad(t1,t2),\fade(7-arg),\pos(x,y)(static line position; non-moving counterpart of\move, writes the sametranslatefield),\move(...),\frz,\frx,\fry,\org(x,y),\blur,\be,\bord,\xbord,\ybord,\shad,\xshad,\yshad,\fax,\fay,\fsp(letter spacing, animatable),\fscx/\fscy,\1c/\2c/\3c/\4c(primary / secondary / outline / shadow colour),\alphaand\1a/\2a/\3a/\4a(per-component alpha),\clip(rect),\clip(drawing),\iclip(rect),\iclip(drawing),\q(line wrap-style override; static per spec),\an<1..=9>(numpad alignment) plus the legacy\a<pos>form (converted to the same numpad surface), and\t(...)wrapping any of the animatable ones. These are exposed via theanimatemodule: calloxideav_ass::extract_cue_animation(&cue)to get a typedCueAnimation, thenevaluate_at(t_ms, dur_ms)to sample the resultingRenderState(alpha multiplier,Transform2D, optional clip + inverse-clip rect or drawing path, blur sigma,\bestrength separate from\blur, per-axis border + shadow widths,(fax, fay)shear factors, additive letter spacing, line wrap style, line alignment as a numpad code, primary / secondary / outline / shadow colours, per-channel alphas independent of the\fadenvelope, pivot, per-axis rotations) at any timestamp. The textual round-trip continues to emit the original tags verbatim. - Karaoke timing — the
\kfamily (\kinstant fill /\kfand the identical uppercase\Kleft-to-right sweep /\kooutline reveal) is extracted as typedAnimatedTag::Karaoke { kind, cs }markers (KaraokeKind+ centisecond duration). Because karaoke is a per-syllable timeline rather than a per-frame state,CueAnimation::karaoke_spans()resolves the in-order markers into cumulativeKaraokeSpans (start_ms/end_msfrom cue start), andKaraokeSpan::progress(t)gives the0.0..=1.0highlight position (the wipe fraction for a sweep syllable; the started/not-started boundary for the instant kinds). The evaluator leavesRenderStateuntouched for karaoke — renderers walk the spans.\ktis not modelled (undocumented per the Aegisub reference); it round-trips verbatim. Note: when karaoke is recovered through the base parser's collapsedSegment::Karaokemarkers the family member is reported as the conservativeFilldefault (the core marker keeps only the duration); the fullKaraokeKindsurvives when parsing raw override text directly viaparse_overrides. - Drawing-mode parser — the
\clip(drawing)and\pmini language (m/n/l/b/s/p/c) is parsed viaoxideav_ass::parse_drawing(s, scale_exp)into anoxideav_core::Path, ready to feedoxideav-raster's clip stack. - Animated rasterisation (
rendercargo feature, default-on) —oxideav_ass::AnimatedRenderedDecoderwraps another ASS subtitle decoder and produces RGBAFrame::Videos sampled at a caller-controlled cue-relative time;set_offset_ms(t)betweenreceive_framecalls steps the animation forward. Internally it composes the evaluatedRenderState(translate / scale / 3D rotations around\org/ clip path / opacity) onto aVectorFrameof shaped glyphs and rasterises throughoxideav-raster. Opt out viadefault-features = false. \Nhard line break,\hhard space,\nsoft break.- ASS timestamp format
H:MM:SS.cc(centiseconds). - Commas inside the
Textfield are preserved (the CSV splitter stops at the per-format column count).
Out of scope for this crate:
[Fonts]/[Graphics]UU-encoded attachment payloads are kept as opaque bytes (round-tripped verbatim via extradata) — the parser does not decode the embedded font / image data into typed objects.- Gaussian blur (
\blur) post-step is not applied by theAnimatedRenderedDecoder—RenderState::blur_sigmais exposed, feed it intooxideav-image-filter::blurif you need the visual effect. - 3D
\frx/\fryrotations are reduced to a 2D affine via the orthographic small-angle approximation (axis-alignedcos(α)shrink), not a full perspective camera. Most subtitle use rotates <90° so the visual difference is small; consumers needing strict 3D should bake their own perspective transform ontoRenderState::rotate_x_radians/rotate_y_radians. - Free-form
\pdrawing-mode rendering (the rasterisation of drawing blocks as decorative shapes) is parser-only — useparse_drawingto lift the path into your own scene.
Codec / container IDs
- Codec:
"ass"; media typeSubtitle, intra-only, lossless. - Container:
"ass", matches.assand.ssaby extension and probes the[Script Info]header magic.
License
MIT — see LICENSE.