mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +00:00
* 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. * craft-captions: export warnings name STL's ? and text cut to fit Ported from storytold/filmcraft#4157 (filmcraft#4155, filmcraft#4156). charset_warnings now names the characters EBU STL writes as ? (from the writer's own encoder, at the STL file's time code) and each caption that CEA-608's 15 rows or the CEA-708 window's 12 rows cut, with how much it lost. The CEA-708 writer loads only the 12 rows its window holds. The time codes count from the document's start time code, as the files do. * craft-captions: TTML import keeps inherited and reset text styles Ported from the TTML style part of storytold/filmcraft#1723 (filmcraft#1721). Italic, bold and underline set on <body> or on a timed <span> cue root now reach the cue text; normal, noUnderline and inherit reset or restore the parent value; styling is read from the TTML styling namespaces (modern and 2006 drafts) with unqualified attributes as a fallback, and foreign namespaces are ignored. The inline text collector is iterative. Line breaks at the start or end of a cue are kept, as #3145's port does, so this differs from the source pull request there. * craft-captions: literal < and & stay text beside cue style tags Ported from the literal-text part of storytold/filmcraft#1723. The TTML and EBU STL readers keep a literal < as < (and an & that would read as an entity as &), as a WebVTT import already keeps its entities, so text like "<b>" in a TTML file never becomes a bold tag. The TTML and STL writers decode those entities, so a cue read from WebVTT with "1 < 2" is no longer written as "1 &lt; 2". entity_at and decode_entities are shared with plain_text. --------- 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> Co-authored-by: nuclearspike <liquidmonkey@gmail.com>
150 lines
7.4 KiB
Rust
150 lines
7.4 KiB
Rust
//! Styling identity must not depend on XML attribute order or foreign extensions.
|
|
|
|
use craft_captions::{Format, parse};
|
|
use craft_captions::{TICKS_PER_SECOND, Tick};
|
|
|
|
#[test]
|
|
fn recognized_styling_namespaces_have_fixed_priority() {
|
|
let namespaces = [
|
|
"http://www.w3.org/ns/ttml#styling",
|
|
"http://www.w3.org/2006/10/ttaf1#style",
|
|
"http://www.w3.org/2006/10/ttaf1#styling",
|
|
"http://www.w3.org/2006/04/ttaf1#style",
|
|
"http://www.w3.org/2006/04/ttaf1#styling",
|
|
];
|
|
for (property, positive, reset, tag) in
|
|
[("fontStyle", "italic", "normal", "i"), ("fontWeight", "bold", "normal", "b"), ("textDecoration", "underline", "noUnderline", "u")]
|
|
{
|
|
for (i, high) in namespaces.iter().enumerate() {
|
|
for low in namespaces.iter().skip(i + 1) {
|
|
for attrs in [
|
|
format!(r#"lo:{property}="{reset}" hi:{property}="{positive}""#),
|
|
format!(r#"hi:{property}="{positive}" lo:{property}="{reset}""#),
|
|
] {
|
|
check(
|
|
"",
|
|
&format!(r#"<div><p xmlns:hi="{high}" xmlns:lo="{low}" {attrs} begin="1s" end="2s">Hello</p></div>"#),
|
|
&format!("<{tag}>Hello</{tag}>"),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn check(head: &str, body: &str, expected: &str) {
|
|
let xml = format!(
|
|
r#"<tt xmlns="http://www.w3.org/ns/ttml" xmlns:tts="http://www.w3.org/ns/ttml#styling" xmlns:app="urn:example">{head}<body>{body}</body></tt>"#
|
|
);
|
|
for format in [Format::Ttml, Format::Dfxp] {
|
|
let doc = parse(xml.as_bytes(), format).unwrap();
|
|
assert!(doc.warnings.is_empty(), "{xml}");
|
|
assert_eq!(doc.cues.len(), 1, "{xml}");
|
|
assert_eq!((doc.cues[0].start, doc.cues[0].end), (Tick(TICKS_PER_SECOND), Tick(2 * TICKS_PER_SECOND)));
|
|
assert_eq!(doc.cues[0].text, expected, "{format:?}, {xml}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn genuine_styling_wins_over_foreign_and_unqualified_properties() {
|
|
for (property, positive, reset, tag) in
|
|
[("fontStyle", "italic", "normal", "i"), ("fontWeight", "bold", "normal", "b"), ("textDecoration", "underline", "noUnderline", "u")]
|
|
{
|
|
for other in ["app:", ""] {
|
|
for reversed in [false, true] {
|
|
let actual = format!(r#"tts:{property}="{positive}""#);
|
|
let collision = format!(r#"{other}{property}="{reset}""#);
|
|
let attrs = if reversed { format!("{actual} {collision}") } else { format!("{collision} {actual}") };
|
|
for body in [
|
|
format!(r#"<div {attrs}><p begin="1s" end="2s">Hello</p></div>"#),
|
|
format!(r#"<div><p {attrs} begin="1s" end="2s">Hello</p></div>"#),
|
|
format!(r#"<div><p><span {attrs} begin="1s" end="2s">Hello</span></p></div>"#),
|
|
] {
|
|
check("", &body, &format!("<{tag}>Hello</{tag}>"));
|
|
}
|
|
let head = format!(r#"<head><styling><style xml:id="emphasis" {attrs}/></styling></head>"#);
|
|
check(&head, r#"<div style="emphasis"><p begin="1s" end="2s">Hello</p></div>"#, &format!("<{tag}>Hello</{tag}>"));
|
|
let actual_reset = format!(r#"tts:{property}="{reset}""#);
|
|
let foreign_positive = format!(r#"app:{property}="{positive}""#);
|
|
let reset_attrs = if reversed { format!("{actual_reset} {foreign_positive}") } else { format!("{foreign_positive} {actual_reset}") };
|
|
check("", &format!(r#"<div tts:{property}="{positive}"><p {reset_attrs} begin="1s" end="2s">Hello</p></div>"#), "Hello");
|
|
}
|
|
}
|
|
check("", &format!(r#"<div><p app:{property}="{positive}" begin="1s" end="2s">Hello</p></div>"#), "Hello");
|
|
check(
|
|
"",
|
|
&format!(r#"<div tts:{property}="{positive}"><p app:{property}="{reset}" begin="1s" end="2s">Hello</p></div>"#),
|
|
&format!("<{tag}>Hello</{tag}>"),
|
|
);
|
|
// Body styling resolves before the cue walk, so namespace priority must hold on that path too.
|
|
let xml = format!(
|
|
r#"<tt xmlns="http://www.w3.org/ns/ttml" xmlns:tts="http://www.w3.org/ns/ttml#styling" xmlns:app="urn:example"><body app:{property}="{reset}" tts:{property}="{positive}"><div><p begin="1s" end="2s">Hello</p></div></body></tt>"#
|
|
);
|
|
for format in [Format::Ttml, Format::Dfxp] {
|
|
let doc = parse(xml.as_bytes(), format).unwrap();
|
|
assert_eq!(doc.cues.len(), 1);
|
|
assert_eq!(doc.cues[0].text, format!("<{tag}>Hello</{tag}>"));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn foreign_style_references_and_definitions_are_ignored() {
|
|
let real = r#"<style xml:id="emphasis" tts:fontStyle="italic"/>"#;
|
|
let foreign = r#"<app:style xml:id="emphasis" tts:fontStyle="normal"/>"#;
|
|
for definitions in [format!("{real}{foreign}"), format!("{foreign}{real}")] {
|
|
let head = format!("<head><styling>{definitions}</styling></head>");
|
|
for attrs in [r#"app:style="missing" style="emphasis""#, r#"style="emphasis" app:style="missing""#] {
|
|
check(&head, &format!(r#"<div><p {attrs} begin="1s" end="2s">Hello</p></div>"#), "<i>Hello</i>");
|
|
}
|
|
check(&head, r#"<div><p app:style="emphasis" begin="1s" end="2s">Hello</p></div>"#, "Hello");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn genuine_style_ids_win_over_foreign_and_legacy_ids() {
|
|
for attrs in [
|
|
r#"app:id="wrong" xml:id="emphasis""#,
|
|
r#"xml:id="emphasis" app:id="wrong""#,
|
|
r#"id="wrong" xml:id="emphasis""#,
|
|
r#"xml:id="emphasis" id="wrong""#,
|
|
r#"id="emphasis""#,
|
|
] {
|
|
check(
|
|
&format!(r#"<head><styling><style {attrs} tts:fontStyle="italic"/></styling></head>"#),
|
|
r#"<div><p style="emphasis" begin="1s" end="2s">Hello</p></div>"#,
|
|
"<i>Hello</i>",
|
|
);
|
|
}
|
|
check(
|
|
r#"<head><styling><style app:id="emphasis" tts:fontStyle="italic"/></styling></head>"#,
|
|
r#"<div><p style="emphasis" begin="1s" end="2s">Hello</p></div>"#,
|
|
"Hello",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn modern_draft_and_unqualified_styling_remain_supported() {
|
|
for (tt, tts, id) in [
|
|
("http://www.w3.org/ns/ttml", "http://www.w3.org/ns/ttml#styling", "xml:id"),
|
|
("http://www.w3.org/2006/04/ttaf1", "http://www.w3.org/2006/04/ttaf1#style", "id"),
|
|
("http://www.w3.org/2006/10/ttaf1", "http://www.w3.org/2006/10/ttaf1#style", "xml:id"),
|
|
("http://www.w3.org/2006/04/ttaf1", "http://www.w3.org/2006/04/ttaf1#styling", "id"),
|
|
("http://www.w3.org/2006/10/ttaf1", "http://www.w3.org/2006/10/ttaf1#styling", "xml:id"),
|
|
("", "", "id"),
|
|
] {
|
|
let ns = if tt.is_empty() { String::new() } else { format!(r#"xmlns="{tt}" xmlns:tts="{tts}""#) };
|
|
let prefix = if tts.is_empty() { "" } else { "tts:" };
|
|
let xml = format!(
|
|
r#"<tt {ns}><head><styling><style {id}="emphasis" {prefix}fontStyle="italic"/></styling></head><body style="emphasis"><div><p {prefix}fontWeight="bold" begin="1s" end="2s">Hello</p></div></body></tt>"#
|
|
);
|
|
for format in [Format::Ttml, Format::Dfxp] {
|
|
let doc = parse(xml.as_bytes(), format).unwrap();
|
|
assert_eq!(doc.cues.len(), 1);
|
|
assert!(doc.warnings.is_empty());
|
|
assert_eq!((doc.cues[0].start, doc.cues[0].end), (Tick(TICKS_PER_SECOND), Tick(2 * TICKS_PER_SECOND)));
|
|
assert_eq!(doc.cues[0].text, "<i><b>Hello</b></i>", "{format:?}, {xml}");
|
|
}
|
|
}
|
|
}
|