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>
125 lines
6.4 KiB
Rust
125 lines
6.4 KiB
Rust
//! The TTML reader used to sit on `roxmltree` and now sits on `craft-xml`. Its logic is the same;
|
|
//! what changed is the tree it is handed. This compares the two trees on TTML documents: element
|
|
//! names by namespace, attributes by namespace, and character data, in document order.
|
|
|
|
use craft_captions::{Cue, Document, Format, FrameRate, WriteOptions, write};
|
|
|
|
const NAMESPACES: [(&str, &str); 11] = [
|
|
("tt", "http://www.w3.org/ns/ttml"),
|
|
("tt10", "http://www.w3.org/2006/10/ttaf1"),
|
|
("tt04", "http://www.w3.org/2006/04/ttaf1"),
|
|
("ttm", "http://www.w3.org/ns/ttml#metadata"),
|
|
("ttm10", "http://www.w3.org/2006/10/ttaf1#metadata"),
|
|
("ttm04", "http://www.w3.org/2006/04/ttaf1#metadata"),
|
|
("tts", "http://www.w3.org/ns/ttml#styling"),
|
|
("tts10", "http://www.w3.org/2006/10/ttaf1#style"),
|
|
("tts10s", "http://www.w3.org/2006/10/ttaf1#styling"),
|
|
("tts04", "http://www.w3.org/2006/04/ttaf1#style"),
|
|
("tts04s", "http://www.w3.org/2006/04/ttaf1#styling"),
|
|
];
|
|
|
|
/// A name the way the reader's canonical names spell it.
|
|
fn name(namespace: Option<&str>, local: &str) -> String {
|
|
match namespace {
|
|
None => local.to_string(),
|
|
Some("http://www.w3.org/XML/1998/namespace") => format!("xml:{local}"),
|
|
Some(uri) => format!("{}:{local}", NAMESPACES.iter().find(|(_, u)| *u == uri).map_or("?", |(p, _)| p)),
|
|
}
|
|
}
|
|
|
|
fn dump_rox(n: roxmltree::Node, out: &mut Vec<String>) {
|
|
let mut attrs: Vec<String> = n.attributes().map(|a| format!("@{}={}", name(a.namespace(), a.name()), a.value())).collect();
|
|
attrs.sort();
|
|
out.push(format!("<{} {}", name(n.tag_name().namespace(), n.tag_name().name()), attrs.join(" ")));
|
|
let mut text = String::new();
|
|
for c in n.children() {
|
|
if c.is_text() {
|
|
text.push_str(c.text().unwrap_or(""));
|
|
} else if c.is_element() {
|
|
out.push(format!("#{}", std::mem::take(&mut text)));
|
|
dump_rox(c, out);
|
|
}
|
|
}
|
|
out.push(format!("#{text}"));
|
|
out.push(">".into());
|
|
}
|
|
|
|
fn dump_craft(n: &craft_xml::El, out: &mut Vec<String>) {
|
|
let mut attrs: Vec<String> = n.attrs.iter().map(|(k, v)| format!("@{k}={v}")).collect();
|
|
attrs.sort();
|
|
out.push(format!("<{} {}", n.name, attrs.join(" ")));
|
|
let mut text = String::new();
|
|
for c in &n.children {
|
|
match c {
|
|
craft_xml::Node::Text(t) | craft_xml::Node::CData(t) => text.push_str(t),
|
|
craft_xml::Node::El(e) => {
|
|
out.push(format!("#{}", std::mem::take(&mut text)));
|
|
dump_craft(e, out);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
out.push(format!("#{text}"));
|
|
out.push(">".into());
|
|
}
|
|
|
|
fn documents() -> Vec<String> {
|
|
let mut docs: Vec<String> = [
|
|
r#"<tt xmlns="http://www.w3.org/ns/ttml"><body><div><p begin="1s" end="2s">Hi</p></div></body></tt>"#,
|
|
// prefixed content, styling and metadata; xml:lang, xml:id, xml:space
|
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<x:tt xmlns:x="http://www.w3.org/ns/ttml" xmlns:s="http://www.w3.org/ns/ttml#styling" xmlns:m="http://www.w3.org/ns/ttml#metadata"
|
|
xmlns:p="http://www.w3.org/ns/ttml#parameter" xml:lang="pt-BR" p:frameRate="30" p:frameRateMultiplier="1000 1001">
|
|
<x:head><x:metadata><m:agent xml:id="a1" type="person"><m:name type="full" xml:space="preserve"> Ann Lee </m:name></m:agent></x:metadata>
|
|
<x:styling><x:style xml:id="it" s:fontStyle="italic"/></x:styling></x:head>
|
|
<x:body begin="10s"><x:div end="20s"><x:p xml:id="c1" begin="1s" dur="2s" m:agent="a1" style="it">one<x:br/>two <x:span s:fontWeight="bold">three</x:span> four</x:p></x:div></x:body>
|
|
</x:tt>"#,
|
|
// the 2006 drafts
|
|
r#"<tt xmlns="http://www.w3.org/2006/10/ttaf1" xmlns:ttm="http://www.w3.org/2006/10/ttaf1#metadata" xmlns:tts="http://www.w3.org/2006/10/ttaf1#style">
|
|
<head><ttm:agent xml:id="b"><ttm:name>Bo</ttm:name></ttm:agent></head><body><div><p begin="00:00:01:12" end="00:00:02.5" ttm:agent="b" tts:textDecoration="underline">x</p></div></body></tt>"#,
|
|
r#"<tt xmlns="http://www.w3.org/2006/04/ttaf1" xmlns:ttm="http://www.w3.org/2006/04/ttaf1#metadata"><head><ttm:agent id="b"><ttm:name type="full">Bo</ttm:name></ttm:agent></head>
|
|
<body><div><p begin="1s" end="2s" ttm:agent="b">x</p></div></body></tt>"#,
|
|
// no namespace at all, an unknown namespace, and a foreign element inside
|
|
r#"<tt><body timeContainer="seq"><div><p begin="1s" end="2s" agent="_Some_Name">x</p></div></body></tt>"#,
|
|
r#"<tt xmlns="urn:example:other" xmlns:o="urn:example:more"><head><o:thing o:k="v" k="w"/></head><body><div><p begin="1s" end="2s">x</p></div></body></tt>"#,
|
|
// character data: references, CDATA, comments, processing instructions, mixed whitespace
|
|
"<tt xmlns=\"http://www.w3.org/ns/ttml\"><body><div><p begin=\"1s\" end=\"2s\" title=\"a & b A \">\n a <b> & © 😀<![CDATA[ <raw> & ]]>tail<!-- note -->\u{a0}more<?pi data?> end\r\n</p></div></body></tt>",
|
|
// a namespace redeclared in an inner scope
|
|
r#"<tt xmlns="http://www.w3.org/ns/ttml" xmlns:a="http://www.w3.org/ns/ttml#metadata"><head><a:agent xml:id="x" xmlns:a="urn:example:shadow"><a:name>no</a:name></a:agent></head><body/></tt>"#,
|
|
]
|
|
.into_iter()
|
|
.map(str::to_string)
|
|
.collect();
|
|
// and what the writer produces, in both flavours
|
|
let rate = FrameRate::FPS_25;
|
|
let cue = |f: i64, text: &str, speaker: Option<&str>| Cue {
|
|
start: rate.tick_of(f),
|
|
end: rate.tick_of(f + 25),
|
|
text: text.into(),
|
|
speaker: speaker.map(str::to_string),
|
|
..Default::default()
|
|
};
|
|
let doc = Document {
|
|
cues: vec![cue(25, "<i>Olá</i> & <b>tchau</b>\n1 < 2", Some("Ana \"A\" <x>")), cue(75, "<u>second</u>\u{a0}line", None)],
|
|
language: Some("pt".into()),
|
|
..Default::default()
|
|
};
|
|
for format in [Format::Ttml, Format::Dfxp] {
|
|
docs.push(String::from_utf8(write(&doc, format, WriteOptions { rate: Some(rate), ..Default::default() })).unwrap());
|
|
}
|
|
docs
|
|
}
|
|
|
|
#[test]
|
|
fn craft_xml_hands_the_ttml_reader_the_tree_roxmltree_did() {
|
|
let canonical = craft_xml::Canonical::new(&NAMESPACES);
|
|
for text in documents() {
|
|
let rox = roxmltree::Document::parse(&text).unwrap();
|
|
let craft = craft_xml::Options::canonical(&canonical).strict().parse(text.as_bytes()).unwrap();
|
|
let (mut a, mut b) = (Vec::new(), Vec::new());
|
|
dump_rox(rox.root_element(), &mut a);
|
|
dump_craft(&craft, &mut b);
|
|
assert_eq!(a, b, "{text}");
|
|
}
|
|
}
|