mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
craft-captions: MCC export can write a packet on every frame (#951)
* craft-captions: TTML cue text collapses only XML whitespace and keeps edge line breaks Ported from storytold/filmcraft#3145. char::is_whitespace also matched U+00A0, U+2009 and U+3000, so no-break spaces became ordinary spaces on import, and a leading or trailing <br/> was trimmed away. * craft-captions: TTML/DFXP export drops characters XML 1.0 forbids Ported from storytold/filmcraft#4467. A control character in cue text made the exported file unreadable. The filter also covers U+FFFE and U+FFFF, which the review of that pull request asked for. * craft-captions: WebVTT cue-text arrow escape is reversible Ported from storytold/filmcraft#4437. A literal --> in cue text came back as --> after a write and parse; the writer now adds one amp; level and the reader removes exactly one. * craft-captions: a WebVTT speaker name with > no longer spills into the cue text Ported from storytold/filmcraft#4470. The writer puts the lookalike U+203A in place of > inside the voice tag. Also from that pull request's review: a line break in a name is written as a space, and a name ending in - is kept apart from the tag's closing >. * craft-captions: an SRT text line that looks like a timing line survives a round trip Ported from storytold/filmcraft#4448. The writer marks such a line with a leading zero-width space and the reader removes it, so the cue is not split. * craft-captions: back-to-back SCC captions with the same text stay separate cues Ported from storytold/filmcraft#3808. Every End of Caption now closes the open cue. MCC's CEA-608 path uses the same decoder. * craft-captions: an SCC pop-on load may run across the previous caption's erase Ported from storytold/filmcraft#1837 (filmcraft#1791). A caption that closely follows another popped on about a second late, because its load had to be one unbroken run of free frames. Doubled codes stay adjacent; pauses are allowed between them. * craft-captions: CEA-708 export starts each caption at its window's top left Ported from storytold/filmcraft#4043 (filmcraft#4042). Each window load now sends DeleteWindows before DefineWindow in place of the ClearWindows after it: a window made anew is empty with the pen at its top left in any decoder, for the same number of bytes, so cue timing is unchanged. * craft-captions: TTML/DFXP export keeps literal < and > text Ported from storytold/filmcraft#2830. The writer treated every < with a later > as a tag and wrote nothing for unknown names, so "1 < 2 and 3 > 2" lost its middle. Only a well-formed tag is markup now; a > inside a quoted attribute value does not end the tag. * craft-captions: TTML/DFXP export keeps cue IDs unique and valid Unicode IDs as written Ported from the TTML commit of storytold/filmcraft#1839. A cue named s1 could collide with the style ID, and a generated c1 could take the ID of a later cue. Valid explicit IDs are reserved first; missing, invalid or repeated ones get a replacement that is in no one's way. IDs are checked as XML NCNames. * craft-captions: compare the craft-xml and roxmltree views of TTML documents The TTML reader moved from roxmltree to craft-xml. This dev-only test checks that both hand it the same elements, attributes and text, by namespace, on TTML documents and on the writer's output. roxmltree becomes a dev-dependency, which the cue ID tests use too. * craft-captions: use craft-timeline's exact time instead of a private copy craft-captions carried a 290-line subset of the tick, frame rate and timecode field code because craft-libs had none when it was added. craft-timeline now has the whole module with the same tick unit. craft_captions::time is a re-export of craft_timeline::time, so every public path still resolves and the types are now the same ones craft-timeline's callers hold. * craft-captions: SCC and MCC time codes count from the document's start timecode Ported from the caption-format part of storytold/filmcraft#4131 (filmcraft#4130). Document::start_timecode is the label of time zero; the SCC and MCC writers count from the 29.97 frame with the same label, so the label carries across rates. Readers note the rate and drop-frame mode of a file's labels in Document::labels, and Document::count_from counts cues back from a start time code. charset_warnings names the time codes the file has, which that pull request's review found missing. * craft-captions: MCC export can write a packet on every frame Ported from the caption-format part of storytold/filmcraft#4037 (filmcraft#4024). WriteOptions::every_frame_until fills every 29.97 fps frame before that time that has no caption data with a packet of CEA-608 nulls and CEA-708 padding, as SDI and frame-wrapped MXF carry one per frame. Off by default, at most 24 hours (mcc::MAX_PADDED_FRAMES), and write_to streams the file line by line. * craft-captions: README has its MCC writer section, with the every-frame option The section's text was cut off when the crate was added; only a stray heading marker was left. --------- Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: SeeRay11 <searay112023@gmail.com> Co-authored-by: jnohclee-rgb <336857373+jnohclee-rgb@users.noreply.github.com> Co-authored-by: JonRC <jonarc06@gmail.com> Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
This commit is contained in:
3 files changed
+148
-17
No files matched your search
@@ -51,7 +51,7 @@ lists at most 20 characters per stream.
|
||||
| SubRip `.srt` | yes | yes | BOM, UTF-16, Windows-1252 fallback, CRLF/CR, missing or wrong indexes, `,` or `.` milliseconds, 1–9 fraction digits, missing hours, SSA coordinates, cues not separated by blank lines. Inline tags are kept as text. |
|
||||
| WebVTT `.vtt` | yes | yes | Cue identifiers, cue settings (verbatim), a leading `<v Speaker>` voice span (as the caption's speaker) and STYLE / REGION / NOTE blocks are kept. A missing `WEBVTT` signature is tolerated. |
|
||||
| Scenarist SCC `.scc` | yes | yes | CEA-608 at 29.97 fps, drop-frame or not. Reads pop-on, roll-up and paint-on (channel 1); writes pop-on. |
|
||||
| MacCaption MCC `.mcc` | yes | yes | One SMPTE ST 334-2 caption distribution packet (CDP) per frame carrying caption data, omitting empty frames, in MacCaption's hex text with letter abbreviations. Writes 29.97 fps (`30DF` / `30`) with CEA-608 field 1 (the SCC schedule) **and** CEA-708 service 1 (two-window pop-on emulation). Reads CEA-608 field 1 when present, else CEA-708 service 1; time code rates 30DF, 30, 60DF, 60, 25, 24, 50. |
|
||||
| MacCaption MCC `.mcc` | yes | yes | One SMPTE ST 334-2 caption distribution packet (CDP) per frame carrying caption data, omitting empty frames (or on every frame, on request), in MacCaption's hex text with letter abbreviations. Writes 29.97 fps (`30DF` / `30`) with CEA-608 field 1 (the SCC schedule) **and** CEA-708 service 1 (two-window pop-on emulation). Reads CEA-608 field 1 when present, else CEA-708 service 1; time code rates 30DF, 30, 60DF, 60, 25, 24, 50. |
|
||||
| EBU STL `.stl` | yes | yes | EBU Tech 3264-E: GSI block + 128-byte TTI blocks, `STL25.01` (25 fps) or `STL30.01` (read/written as 29.97 fps, non-drop labels), ISO/IEC 6937 Latin text (accents as diacritic + letter), italics / underline codes as `<i>` / `<u>`, extension blocks for long subtitles, start-of-programme (TCP) subtracted. |
|
||||
| TTML `.ttml` (IMSC1) | yes | yes | Writes the IMSC1 Text profile (TTML2 namespaces) with the document's language as `xml:lang` (read back on import): `<p>` per cue, `<br/>`, styled `<span>`s for `<i>` / `<b>` / `<u>`, `ttm:agent` speakers, exact frame times (`123f`) at the sequence rate. Reads any TTML (TTML1, TTML2, the 2006 `ttaf1` drafts): clock / frame / offset / tick times, `dur`, nested time containers (`body` / `div` begins offset and ends clip their contents; `seq` containers are read as `par` with a warning), timed spans, referenced styles. |
|
||||
| DFXP `.dfxp` | yes | yes | TTML1 with the DFXP presentation profile; clock times in milliseconds (frame-exact after `snap_to_frames`). |
|
||||
@@ -117,4 +117,19 @@ character, as the standard requires); a few have a close stand-in (’ as ', –
|
||||
dropped. `charset_warnings` names each dropped or replaced character. If there is no room to load a caption
|
||||
before its in point (for example a caption at 00:00:00:00), it appears as soon as it is loaded.
|
||||
|
||||
#
|
||||
### MCC writer
|
||||
|
||||
The writer merges the scheduled CEA-608 and CEA-708 frames in timestamp order. Frames without
|
||||
caption data are omitted, including during a displayed caption; its explicit erase/hide command
|
||||
still lands at its out point. CDP sequence counters advance per emitted packet, wrapping at 16 bits.
|
||||
CEA-708 text uses the G0 and G1 (Latin-1) sets and part of G2 (… ‘ ’ “ ” • ™ and a few more);
|
||||
other characters are sent as `?`, and `charset_warnings` names them.
|
||||
Export work and output size therefore follow the scheduled caption data, not the time between
|
||||
cues or their duration.
|
||||
|
||||
For consumers that need a packet on every video frame (SDI, frame-wrapped MXF with a SMPTE 436M
|
||||
track), `WriteOptions::every_frame_until` also writes one on every frame without caption data,
|
||||
from the start to that time: CEA-608 null pairs and CEA-708 padding. The packets are 29.97 fps,
|
||||
so they line up with the video only at that rate; the caller decides whether to offer it. It is
|
||||
off by default, covers at most 24 hours (`mcc::MAX_PADDED_FRAMES`), and can be streamed
|
||||
(`write_to`).
|
||||
@@ -310,11 +310,16 @@ pub struct WriteOptions {
|
||||
pub rate: Option<FrameRate>,
|
||||
/// MCC / EBU STL: the name written as the creating program ([`PROGRAM`] by default).
|
||||
pub program: &'static str,
|
||||
/// MCC: a packet on every frame from the start to this time (the end of the programme), with
|
||||
/// no caption data where there is none, as SDI and frame-wrapped MXF carry one per frame.
|
||||
/// The frames are MCC's 29.97 fps ones, so this is one packet per video frame only for
|
||||
/// 29.97 fps video. None (the default) writes only the frames that carry caption data.
|
||||
pub every_frame_until: Option<Tick>,
|
||||
}
|
||||
|
||||
impl Default for WriteOptions {
|
||||
fn default() -> Self {
|
||||
Self { drop_frame: true, rate: None, program: PROGRAM }
|
||||
Self { drop_frame: true, rate: None, program: PROGRAM, every_frame_until: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,7 +329,7 @@ pub fn write(doc: &Document, format: Format, opts: WriteOptions) -> Vec<u8> {
|
||||
Format::Srt => srt::write(doc).into_bytes(),
|
||||
Format::WebVtt => vtt::write(doc).into_bytes(),
|
||||
Format::Scc => scc::write(doc, opts.drop_frame).into_bytes(),
|
||||
Format::Mcc => mcc::write_with(doc, opts.drop_frame, true, true, opts.program).into_bytes(),
|
||||
Format::Mcc => mcc::write_padded(doc, opts.drop_frame, true, true, mcc_pad_frames(opts), opts.program).into_bytes(),
|
||||
Format::Stl => stl::write(doc, opts.rate, opts.program),
|
||||
Format::Ttml => ttml::write(doc, ttml::Flavor::Imsc1, opts.rate, doc.language.as_deref().unwrap_or("")).into_bytes(),
|
||||
Format::Dfxp => ttml::write(doc, ttml::Flavor::Dfxp, opts.rate, doc.language.as_deref().unwrap_or("")).into_bytes(),
|
||||
@@ -385,6 +390,24 @@ fn lossy_chars(doc: &Document, stream: &str, shown_as: impl Fn(char) -> Option<c
|
||||
}
|
||||
}
|
||||
|
||||
/// [`write`] into `out`. MCC goes out line by line, so a long `every_frame_until` export is never
|
||||
/// held in memory; the other formats are small and written whole.
|
||||
pub fn write_to(out: &mut dyn std::io::Write, doc: &Document, format: Format, opts: WriteOptions) -> std::io::Result<()> {
|
||||
match format {
|
||||
Format::Mcc => mcc::write_padded_to(out, doc, opts.drop_frame, true, true, mcc_pad_frames(opts), opts.program),
|
||||
_ => out.write_all(&write(doc, format, opts)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The MCC frames `opts.every_frame_until` asks for: those that start before it, so a last
|
||||
/// partial frame gets its packet too.
|
||||
fn mcc_pad_frames(opts: WriteOptions) -> i64 {
|
||||
opts.every_frame_until.map_or(0, |end| {
|
||||
let f = mcc::RATE.frame_at(end);
|
||||
if mcc::RATE.tick_of(f) < end { f.saturating_add(1) } else { f }
|
||||
})
|
||||
}
|
||||
|
||||
/// Decode text bytes: UTF-8 (BOM stripped), UTF-16 with BOM, else Windows-1252. Line endings are
|
||||
/// normalised to `\n`.
|
||||
pub fn decode_text(bytes: &[u8]) -> String {
|
||||
@@ -588,4 +611,17 @@ mod tests {
|
||||
let r: Vec<(i64, i64)> = d.cues.iter().map(|c| (c.start.0, c.end.0)).collect();
|
||||
assert_eq!(r, vec![(0, 10), (10, 12), (12, 20)]);
|
||||
}
|
||||
|
||||
/// `write_to` streams the same bytes `write` returns, for every format and a padded MCC.
|
||||
#[test]
|
||||
fn write_to_matches_write() {
|
||||
let cue = Cue { start: Tick(TICKS_PER_SECOND), end: Tick(2 * TICKS_PER_SECOND), text: "Hello".into(), ..Default::default() };
|
||||
let doc = Document { cues: vec![cue], ..Default::default() };
|
||||
let padded = WriteOptions { every_frame_until: Some(Tick(3 * TICKS_PER_SECOND)), ..Default::default() };
|
||||
for (format, opts) in Format::ALL.map(|f| (f, WriteOptions::default())).into_iter().chain([(Format::Mcc, padded)]) {
|
||||
let mut out = Vec::new();
|
||||
write_to(&mut out, &doc, format, opts).unwrap();
|
||||
assert_eq!(out, write(&doc, format, opts), "{format:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+93
-13
@@ -581,6 +581,10 @@ impl Dec708 {
|
||||
// File
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
/// The most frames [`write_padded`] fills: 24 hours of 29.97 fps drop-frame time code, as far
|
||||
/// as an MCC time code counts.
|
||||
pub const MAX_PADDED_FRAMES: i64 = 24 * 107_892;
|
||||
|
||||
/// Write `doc` as MCC: CEA-608 field 1 and CEA-708 service 1 (see the module docs), with
|
||||
/// [`crate::PROGRAM`] as the creation program.
|
||||
pub fn write(doc: &Document, drop_frame: bool) -> String {
|
||||
@@ -600,46 +604,82 @@ fn header_value(program: &str) -> String {
|
||||
/// An empty document or two disabled streams produces a header with no data lines.
|
||||
/// `program` is named as the file's creation program.
|
||||
pub fn write_with(doc: &Document, drop_frame: bool, cea608: bool, cea708: bool, program: &str) -> String {
|
||||
write_padded(doc, drop_frame, cea608, cea708, 0, program)
|
||||
}
|
||||
|
||||
/// [`write_with`], plus a packet with no caption data (CEA-608 nulls, CEA-708 padding) on every
|
||||
/// frame before `pad_frames` that has none, as SDI and frame-wrapped MXF carry one packet per
|
||||
/// video frame. Frames with caption data after `pad_frames` are written as usual. At most
|
||||
/// [`MAX_PADDED_FRAMES`] are filled.
|
||||
pub fn write_padded(doc: &Document, drop_frame: bool, cea608: bool, cea708: bool, pad_frames: i64, program: &str) -> String {
|
||||
let mut out = Vec::new();
|
||||
// writing into a Vec cannot fail, and the text is UTF-8 (ASCII but for the program name)
|
||||
let _ = write_padded_to(&mut out, doc, drop_frame, cea608, cea708, pad_frames, program);
|
||||
String::from_utf8(out).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned())
|
||||
}
|
||||
|
||||
/// [`write_padded`] into `out` line by line, so a long padded export is never held in memory.
|
||||
pub fn write_padded_to(
|
||||
out: &mut dyn std::io::Write,
|
||||
doc: &Document,
|
||||
drop_frame: bool,
|
||||
cea608: bool,
|
||||
cea708: bool,
|
||||
pad_frames: i64,
|
||||
program: &str,
|
||||
) -> std::io::Result<()> {
|
||||
let program = header_value(program);
|
||||
let pad = pad_frames.clamp(0, MAX_PADDED_FRAMES);
|
||||
let f1 = if cea608 { scc::schedule(doc) } else { BTreeMap::new() };
|
||||
let dtv = if cea708 { schedule_708(doc) } else { BTreeMap::new() };
|
||||
let origin = crate::label_frame(doc.start_timecode, RATE, drop_frame); // time codes count from the start time code
|
||||
let mut out = String::from("File Format=MacCaption_MCC V1.0\n\n");
|
||||
out.push_str("///////////////////////////////////////////////////////////////////////////////////\n");
|
||||
out.push_str(&format!("// Closed captions written by {program}: SMPTE ST 334-2 caption distribution packets\n"));
|
||||
out.push_str("// carrying CEA-608 field 1 and CEA-708 service 1 data; empty frames omitted.\n");
|
||||
out.push_str("///////////////////////////////////////////////////////////////////////////////////\n\n");
|
||||
out.write_all(b"File Format=MacCaption_MCC V1.0\n\n")?;
|
||||
out.write_all(b"///////////////////////////////////////////////////////////////////////////////////\n")?;
|
||||
writeln!(out, "// Closed captions written by {program}: SMPTE ST 334-2 caption distribution packets")?;
|
||||
let frames: &[u8] = if pad > 0 {
|
||||
b"// carrying CEA-608 field 1 and CEA-708 service 1 data; a packet on every frame.\n"
|
||||
} else {
|
||||
b"// carrying CEA-608 field 1 and CEA-708 service 1 data; empty frames omitted.\n"
|
||||
};
|
||||
out.write_all(frames)?;
|
||||
out.write_all(b"///////////////////////////////////////////////////////////////////////////////////\n\n")?;
|
||||
let n = doc.cues.len() as u64;
|
||||
let h = doc
|
||||
.cues
|
||||
.iter()
|
||||
.fold(0xcbf2_9ce4_8422_2325u64 ^ n, |h, c| (h ^ c.start.0 as u64 ^ (c.end.0 as u64).rotate_left(17)).wrapping_mul(0x100_0000_01b3));
|
||||
out.push_str(&format!(
|
||||
"UUID={:08X}-{:04X}-4{:03X}-8{:03X}-{:012X}\n",
|
||||
writeln!(
|
||||
out,
|
||||
"UUID={:08X}-{:04X}-4{:03X}-8{:03X}-{:012X}",
|
||||
h >> 32,
|
||||
(h >> 16) & 0xffff,
|
||||
h & 0xfff,
|
||||
(h >> 20) & 0xfff,
|
||||
h.rotate_left(29) & 0xffff_ffff_ffff
|
||||
));
|
||||
out.push_str(&format!("Creation Program={program}\n"));
|
||||
out.push_str(&format!("Time Code Rate={}\n\n", if drop_frame { "30DF" } else { "30" }));
|
||||
)?;
|
||||
writeln!(out, "Creation Program={program}")?;
|
||||
writeln!(out, "Time Code Rate={}\n", if drop_frame { "30DF" } else { "30" })?;
|
||||
// Merge the sparse schedules without walking (or allocating for) input-derived time gaps.
|
||||
// Erase/hide commands are scheduled data too, so omitting padding preserves cue end times.
|
||||
// Padding walks only the frames before `pad`, which the caller chose and is capped.
|
||||
let mut f1 = f1.into_iter().peekable();
|
||||
let mut dtv = dtv.into_iter().peekable();
|
||||
let mut seq = 0u16;
|
||||
while let Some(f) = f1.peek().map(|(f, _)| *f).into_iter().chain(dtv.peek().map(|(f, _)| *f)).min() {
|
||||
let mut unpadded = 0i64; // the first frame with no packet yet, while it is before `pad`
|
||||
loop {
|
||||
let data = f1.peek().map(|(f, _)| *f).into_iter().chain(dtv.peek().map(|(f, _)| *f)).min();
|
||||
let Some(f) = data.into_iter().chain((unpadded < pad).then_some(unpadded)).min() else { break };
|
||||
unpadded = unpadded.max(f.saturating_add(1));
|
||||
let data = FrameData {
|
||||
f1: f1.next_if(|(frame, _)| *frame == f).map(|(_, pair)| pair),
|
||||
dtvcc: dtv.next_if(|(frame, _)| *frame == f).map(|(_, pairs)| pairs).unwrap_or_default(),
|
||||
};
|
||||
let (_, hh, mm, ss, ff) = frames_to_fields(f.saturating_add(origin), RATE, drop_frame);
|
||||
let sep = if drop_frame { ';' } else { ':' };
|
||||
out.push_str(&format!("{hh:02}:{mm:02}:{ss:02}{sep}{ff:02}\t{}\n", compress(&build_cdp(seq, &data))));
|
||||
writeln!(out, "{hh:02}:{mm:02}:{ss:02}{sep}{ff:02}\t{}", compress(&build_cdp(seq, &data)))?;
|
||||
seq = seq.wrapping_add(1);
|
||||
}
|
||||
out
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tc_rate(v: &str) -> Option<(FrameRate, bool)> {
|
||||
@@ -893,6 +933,46 @@ mod tests {
|
||||
assert!(!write(&Document::default(), true).contains('\t'));
|
||||
}
|
||||
|
||||
/// With padding, every frame to the end has one packet in time code order, numbered and
|
||||
/// checksummed, empty frames carry no caption data, and the captions read back unchanged.
|
||||
#[test]
|
||||
fn padded_export_has_a_packet_on_every_frame() {
|
||||
let doc = Document { cues: vec![cue(60, 120, "First"), cue(300, 330, "Last")], ..Default::default() };
|
||||
let empty = compress(&build_cdp(0, &FrameData::default()));
|
||||
for drop_frame in [false, true] {
|
||||
for (cea608, cea708) in [(true, true), (true, false), (false, true)] {
|
||||
let text = write_padded(&doc, drop_frame, cea608, cea708, 2_000, "test");
|
||||
assert!(text.contains("a packet on every frame"));
|
||||
let lines: Vec<(&str, &str)> = text.lines().filter_map(|line| line.split_once('\t')).collect();
|
||||
assert_eq!(lines.len(), 2_000);
|
||||
for (f, (tc, data)) in lines.iter().enumerate() {
|
||||
let (_, hh, mm, ss, ff) = frames_to_fields(f as i64, RATE, drop_frame);
|
||||
let sep = if drop_frame { ';' } else { ':' };
|
||||
assert_eq!(*tc, format!("{hh:02}:{mm:02}:{ss:02}{sep}{ff:02}"));
|
||||
let bytes = expand(data).unwrap();
|
||||
assert_eq!(u16::from_be_bytes([bytes[8], bytes[9]]), f as u16);
|
||||
assert_eq!(bytes[3..76].iter().map(|&b| b as u32).sum::<u32>() % 256, 0);
|
||||
}
|
||||
// nothing is scheduled at frame 0 (the first load comes later), nor after the last erase
|
||||
assert_eq!(lines[0].1, empty);
|
||||
assert_eq!(lines[1_999].1, compress(&build_cdp(1_999, &FrameData::default())));
|
||||
assert_eq!(parse(&text).unwrap().cues, doc.cues);
|
||||
}
|
||||
}
|
||||
// caption data after the padded span is still written; no padding is the sparse writer
|
||||
let sparse = write_with(&doc, true, true, true, "test");
|
||||
let tcs = |text: &str| text.lines().filter_map(|line| line.split_once('\t')).map(|(tc, _)| tc.to_string()).collect::<Vec<_>>();
|
||||
let first_unpadded = scc::format_timecode(200, true);
|
||||
let later = |tcs: Vec<String>| tcs.into_iter().filter(|tc| *tc >= first_unpadded).collect::<Vec<_>>();
|
||||
let short = tcs(&write_padded(&doc, true, true, true, 200, "test"));
|
||||
assert!(!later(short.clone()).is_empty());
|
||||
assert_eq!(short.len(), 200 + later(short.clone()).len());
|
||||
assert_eq!(later(short), later(tcs(&sparse)));
|
||||
for pad in [0, -5, i64::MIN] {
|
||||
assert_eq!(write_padded(&doc, true, true, true, pad, "test"), sparse);
|
||||
}
|
||||
}
|
||||
|
||||
/// Prevent CEA-708 text and line breaks from overflowing a window's declared dimensions.
|
||||
#[test]
|
||||
fn decoder_keeps_708_text_within_declared_window_dimensions() {
|
||||
|
||||
Reference in new issue
Block a user