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