Shaka Packager SDK
Loading...
Searching...
No Matches
ttml_generator.cc
1// Copyright 2020 Google LLC. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd
6
7#include <packager/media/formats/ttml/ttml_generator.h>
8
9#include <absl/strings/escaping.h>
10#include <absl/strings/str_format.h>
11
12#include <packager/media/base/rcheck.h>
13
14namespace shaka {
15namespace media {
16namespace ttml {
17
18namespace {
19
20constexpr const char* kRegionIdPrefix = "_shaka_region_";
21constexpr const char* kRegionTeletextPrefix = "ttx_";
22
23std::string ToTtmlTime(int64_t time, int32_t timescale) {
24 int64_t remaining = time * 1000 / timescale;
25
26 const int ms = remaining % 1000;
27 remaining /= 1000;
28 const int sec = remaining % 60;
29 remaining /= 60;
30 const int min = remaining % 60;
31 remaining /= 60;
32 const int hr = remaining;
33
34 return absl::StrFormat("%02d:%02d:%02d.%03d", hr, min, sec, ms);
35}
36
37std::string ToTtmlSize(const TextNumber& x, const TextNumber& y) {
38 const char* kSuffixMap[] = {"px", "em", "%"};
39 return absl::StrFormat("%.0f%s %.0f%s", x.value,
40 kSuffixMap[static_cast<int>(x.type)], y.value,
41 kSuffixMap[static_cast<int>(y.type)]);
42}
43
44} // namespace
45
46const char* TtmlGenerator::kTtNamespace = "http://www.w3.org/ns/ttml";
47
48TtmlGenerator::TtmlGenerator() {}
49
50TtmlGenerator::~TtmlGenerator() {}
51
52void TtmlGenerator::Initialize(const std::map<std::string, TextRegion>& regions,
53 const std::string& language,
54 int32_t time_scale) {
55 regions_ = regions;
56 language_ = language;
57 time_scale_ = time_scale;
58 // Add ebu_tt_d_regions
59 float step = 74.1f / 11;
60 for (int i = 0; i < 12; i++) {
61 TextRegion region;
62 float verPos = 10.0 + int(float(step) * float(i));
63 region.width = TextNumber(80, TextUnitType::kPercent);
64 region.height = TextNumber(15, TextUnitType::kPercent);
65 region.window_anchor_x = TextNumber(10, TextUnitType::kPercent);
66 region.window_anchor_y = TextNumber(verPos, TextUnitType::kPercent);
67 const std::string id = kRegionTeletextPrefix + std::to_string(i);
68 regions_.emplace(id, region);
69 }
70}
71
72void TtmlGenerator::AddSample(const TextSample& sample) {
73 samples_.emplace_back(sample);
74}
75
76void TtmlGenerator::Reset() {
77 samples_.clear();
78}
79
80bool TtmlGenerator::Dump(std::string* result) const {
81 xml::XmlNode root("tt");
82 bool ebuTTDFormat = isEbuTTTD();
83 RCHECK(root.SetStringAttribute("xmlns", kTtNamespace));
84 RCHECK(root.SetStringAttribute("xmlns:tts",
85 "http://www.w3.org/ns/ttml#styling"));
86 RCHECK(root.SetStringAttribute("xmlns:tts",
87 "http://www.w3.org/ns/ttml#styling"));
88 RCHECK(root.SetStringAttribute("xml:lang", language_));
89
90 if (ebuTTDFormat) {
91 RCHECK(root.SetStringAttribute("xmlns:ttp",
92 "http://www.w3.org/ns/ttml#parameter"));
93 RCHECK(root.SetStringAttribute("xmlns:ttm",
94 "http://www.w3.org/ns/ttml#metadata"));
95 RCHECK(root.SetStringAttribute("xmlns:ebuttm", "urn:ebu:tt:metadata"));
96 RCHECK(root.SetStringAttribute("xmlns:ebutts", "urn:ebu:tt:style"));
97 RCHECK(root.SetStringAttribute("xml:space", "default"));
98 RCHECK(root.SetStringAttribute("ttp:timeBase", "media"));
99 RCHECK(root.SetStringAttribute("ttp:cellResolution", "32 15"));
100 }
101
102 xml::XmlNode head("head");
103 xml::XmlNode styling("styling");
104 xml::XmlNode metadata("metadata");
105 xml::XmlNode layout("layout");
106 RCHECK(addRegions(layout));
107
108 xml::XmlNode body("body");
109 if (ebuTTDFormat) {
110 RCHECK(body.SetStringAttribute("style", "default"));
111 }
112 size_t image_count = 0;
113 std::unordered_set<std::string> fragmentStyles;
114 xml::XmlNode div("div");
115 for (const auto& sample : samples_) {
116 RCHECK(
117 AddSampleToXml(sample, &div, &metadata, fragmentStyles, &image_count));
118 }
119 if (image_count > 0) {
120 RCHECK(root.SetStringAttribute(
121 "xmlns:smpte", "http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt"));
122 }
123 RCHECK(body.AddChild(std::move(div)));
124 RCHECK(head.AddChild(std::move(metadata)));
125 RCHECK(addStyling(styling, fragmentStyles));
126 RCHECK(head.AddChild(std::move(styling)));
127 RCHECK(head.AddChild(std::move(layout)));
128 RCHECK(root.AddChild(std::move(head)));
129
130 RCHECK(root.AddChild(std::move(body)));
131
132 *result = root.ToString(/* comment= */ "");
133 return true;
134}
135
136bool TtmlGenerator::AddSampleToXml(
137 const TextSample& sample,
138 xml::XmlNode* body,
139 xml::XmlNode* metadata,
140 std::unordered_set<std::string>& fragmentStyles,
141 size_t* image_count) const {
142 xml::XmlNode p("p");
143 if (!isEbuTTTD()) {
144 RCHECK(p.SetStringAttribute("xml:space", "preserve"));
145 }
146 RCHECK(p.SetStringAttribute("begin",
147 ToTtmlTime(sample.start_time(), time_scale_)));
148 RCHECK(
149 p.SetStringAttribute("end", ToTtmlTime(sample.EndTime(), time_scale_)));
150 RCHECK(ConvertFragmentToXml(sample.body(), &p, metadata, fragmentStyles,
151 image_count));
152 if (!sample.id().empty())
153 RCHECK(p.SetStringAttribute("xml:id", sample.id()));
154
155 const auto& settings = sample.settings();
156 bool regionFound = false;
157 if (!settings.region.empty()) {
158 auto reg = regions_.find(settings.region);
159 if (reg != regions_.end()) {
160 regionFound = true;
161 RCHECK(p.SetStringAttribute("region", settings.region));
162 }
163 }
164
165 if (!regionFound && (settings.line || settings.position || settings.width ||
166 settings.height)) {
167 // TTML positioning needs to be from a region.
168 const auto origin = ToTtmlSize(
169 settings.position.value_or(TextNumber(0, TextUnitType::kPixels)),
170 settings.line.value_or(TextNumber(0, TextUnitType::kPixels)));
171 const auto extent = ToTtmlSize(
172 settings.width.value_or(TextNumber(100, TextUnitType::kPercent)),
173 settings.height.value_or(TextNumber(100, TextUnitType::kPercent)));
174
175 const std::string id = kRegionIdPrefix + std::to_string(region_id_++);
176 xml::XmlNode region("region");
177 RCHECK(region.SetStringAttribute("xml:id", id));
178 RCHECK(region.SetStringAttribute("tts:origin", origin));
179 RCHECK(region.SetStringAttribute("tts:extent", extent));
180 RCHECK(p.SetStringAttribute("region", id));
181 RCHECK(body->AddChild(std::move(region)));
182 }
183
184 if (settings.writing_direction != WritingDirection::kHorizontal) {
185 const char* dir =
186 settings.writing_direction == WritingDirection::kVerticalGrowingLeft
187 ? "tbrl"
188 : "tblr";
189 RCHECK(p.SetStringAttribute("tts:writingMode", dir));
190 }
191 if (settings.text_alignment != TextAlignment::kStart) {
192 switch (settings.text_alignment) {
193 case TextAlignment::kStart: // To avoid compiler warning.
194 case TextAlignment::kCenter:
195 RCHECK(p.SetStringAttribute("tts:textAlign", "center"));
196 break;
197 case TextAlignment::kEnd:
198 RCHECK(p.SetStringAttribute("tts:textAlign", "end"));
199 break;
200 case TextAlignment::kLeft:
201 RCHECK(p.SetStringAttribute("tts:textAlign", "left"));
202 break;
203 case TextAlignment::kRight:
204 RCHECK(p.SetStringAttribute("tts:textAlign", "right"));
205 break;
206 }
207 }
208
209 RCHECK(body->AddChild(std::move(p)));
210 return true;
211}
212
213bool TtmlGenerator::ConvertFragmentToXml(
214 const TextFragment& body,
215 xml::XmlNode* parent,
216 xml::XmlNode* metadata,
217 std::unordered_set<std::string>& fragmentStyles,
218 size_t* image_count) const {
219 if (body.newline) {
220 xml::XmlNode br("br");
221 return parent->AddChild(std::move(br));
222 }
223 xml::XmlNode span("span");
224 xml::XmlNode* node = parent;
225 bool useSpan =
226 (body.style.bold || body.style.italic || body.style.underline ||
227 !body.style.color.empty() || !body.style.backgroundColor.empty());
228 if (useSpan) {
229 node = &span;
230 if (body.style.bold) {
231 RCHECK(span.SetStringAttribute("tts:fontWeight",
232 *body.style.bold ? "bold" : "normal"));
233 }
234 if (body.style.italic) {
235 RCHECK(span.SetStringAttribute("tts:fontStyle",
236 *body.style.italic ? "italic" : "normal"));
237 }
238 if (body.style.underline) {
239 RCHECK(span.SetStringAttribute(
240 "tts:textDecoration",
241 *body.style.underline ? "underline" : "noUnderline"));
242 }
243 std::string color = "white";
244 std::string backgroundColor = "black";
245
246 if (!body.style.color.empty()) {
247 color = body.style.color;
248 }
249
250 if (!body.style.backgroundColor.empty()) {
251 backgroundColor = body.style.backgroundColor;
252 }
253
254 const std::string fragStyle = color + "_" + backgroundColor;
255 fragmentStyles.insert(fragStyle);
256 RCHECK(span.SetStringAttribute("style", fragStyle));
257 }
258
259 if (!body.body.empty()) {
260 node->AddContent(body.body);
261 } else if (!body.image.empty()) {
262 std::string image_data(body.image.begin(), body.image.end());
263 std::string base64_data;
264 absl::Base64Escape(image_data, &base64_data);
265 std::string id = "img_" + std::to_string(++*image_count);
266
267 xml::XmlNode image_xml("smpte:image");
268 RCHECK(image_xml.SetStringAttribute("imageType", "PNG"));
269 RCHECK(image_xml.SetStringAttribute("encoding", "Base64"));
270 RCHECK(image_xml.SetStringAttribute("xml:id", id));
271 image_xml.SetContent(base64_data);
272 RCHECK(metadata->AddChild(std::move(image_xml)));
273
274 RCHECK(node->SetStringAttribute("smpte:backgroundImage", "#" + id));
275 } else {
276 for (const auto& frag : body.sub_fragments) {
277 if (!ConvertFragmentToXml(frag, node, metadata, fragmentStyles,
278 image_count))
279 return false;
280 }
281 }
282
283 if (useSpan)
284 RCHECK(parent->AddChild(std::move(span)));
285 return true;
286}
287
288std::vector<std::string> TtmlGenerator::usedRegions() const {
289 std::vector<std::string> uRegions;
290 for (const auto& sample : samples_) {
291 if (!sample.settings().region.empty()) {
292 uRegions.push_back(sample.settings().region);
293 }
294 }
295 return uRegions;
296}
297
298bool TtmlGenerator::addRegions(xml::XmlNode& layout) const {
299 auto regNames = usedRegions();
300 for (const auto& r : regions_) {
301 bool used = false;
302 for (const auto& name : regNames) {
303 if (r.first == name) {
304 used = true;
305 }
306 }
307 if (used) {
308 xml::XmlNode region("region");
309 const auto origin =
310 ToTtmlSize(r.second.window_anchor_x, r.second.window_anchor_y);
311 const auto extent = ToTtmlSize(r.second.width, r.second.height);
312 RCHECK(region.SetStringAttribute("xml:id", r.first));
313 RCHECK(region.SetStringAttribute("tts:origin", origin));
314 RCHECK(region.SetStringAttribute("tts:extent", extent));
315 RCHECK(region.SetStringAttribute("tts:overflow", "visible"));
316 RCHECK(layout.AddChild(std::move(region)));
317 }
318 }
319 return true;
320}
321
322bool TtmlGenerator::addStyling(
323 xml::XmlNode& styling,
324 const std::unordered_set<std::string>& fragmentStyles) const {
325 if (fragmentStyles.empty()) {
326 return true;
327 }
328 // Add default style
329 xml::XmlNode defaultStyle("style");
330 RCHECK(defaultStyle.SetStringAttribute("xml:id", "default"));
331 RCHECK(defaultStyle.SetStringAttribute("tts:fontStyle", "normal"));
332 RCHECK(defaultStyle.SetStringAttribute("tts:fontFamily", "sansSerif"));
333 RCHECK(defaultStyle.SetStringAttribute("tts:fontSize", "100%"));
334 RCHECK(defaultStyle.SetStringAttribute("tts:lineHeight", "normal"));
335 RCHECK(defaultStyle.SetStringAttribute("tts:textAlign", "center"));
336 RCHECK(defaultStyle.SetStringAttribute("ebutts:linePadding", "0.5c"));
337 RCHECK(styling.AddChild(std::move(defaultStyle)));
338
339 for (const auto& name : fragmentStyles) {
340 auto pos = name.find('_');
341 auto color = name.substr(0, pos);
342 auto backgroundColor = name.substr(pos + 1, name.size());
343 xml::XmlNode fragStyle("style");
344 RCHECK(fragStyle.SetStringAttribute("xml:id", name));
345 RCHECK(
346 fragStyle.SetStringAttribute("tts:backgroundColor", backgroundColor));
347 RCHECK(fragStyle.SetStringAttribute("tts:color", color));
348 RCHECK(styling.AddChild(std::move(fragStyle)));
349 }
350 return true;
351}
352
353bool TtmlGenerator::isEbuTTTD() const {
354 for (const auto& sample : samples_) {
355 if (sample.settings().region.rfind(kRegionTeletextPrefix, 0) == 0) {
356 return true;
357 }
358 }
359 return false;
360}
361
362} // namespace ttml
363} // namespace media
364} // namespace shaka
All the methods that are virtual are virtual for mocking.