Shaka Packager SDK
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 "packager/base/base64.h"
10 #include "packager/base/strings/stringprintf.h"
11 #include "packager/media/base/rcheck.h"
12 
13 namespace shaka {
14 namespace media {
15 namespace ttml {
16 
17 namespace {
18 
19 constexpr const char* kRegionIdPrefix = "_shaka_region_";
20 
21 std::string ToTtmlTime(int64_t time, uint32_t timescale) {
22  int64_t remaining = time * 1000 / timescale;
23 
24  const int ms = remaining % 1000;
25  remaining /= 1000;
26  const int sec = remaining % 60;
27  remaining /= 60;
28  const int min = remaining % 60;
29  remaining /= 60;
30  const int hr = remaining;
31 
32  return base::StringPrintf("%02d:%02d:%02d.%02d", hr, min, sec, ms);
33 }
34 
35 std::string ToTtmlSize(const TextNumber& x, const TextNumber& y) {
36  const char* kSuffixMap[] = {"px", "em", "%"};
37  return base::StringPrintf("%.0f%s %.0f%s", x.value,
38  kSuffixMap[static_cast<int>(x.type)], y.value,
39  kSuffixMap[static_cast<int>(y.type)]);
40 }
41 
42 } // namespace
43 
44 const char* TtmlGenerator::kTtNamespace = "http://www.w3.org/ns/ttml";
45 
46 TtmlGenerator::TtmlGenerator() {}
47 
48 TtmlGenerator::~TtmlGenerator() {}
49 
50 void TtmlGenerator::Initialize(const std::map<std::string, TextRegion>& regions,
51  const std::string& language,
52  uint32_t time_scale) {
53  regions_ = regions;
54  language_ = language;
55  time_scale_ = time_scale;
56 }
57 
58 void TtmlGenerator::AddSample(const TextSample& sample) {
59  samples_.emplace_back(sample);
60 }
61 
62 void TtmlGenerator::Reset() {
63  samples_.clear();
64 }
65 
66 bool TtmlGenerator::Dump(std::string* result) const {
67  xml::XmlNode root("tt");
68  RCHECK(root.SetStringAttribute("xmlns", kTtNamespace));
69  RCHECK(root.SetStringAttribute("xmlns:tts",
70  "http://www.w3.org/ns/ttml#styling"));
71 
72  bool did_log = false;
73  xml::XmlNode head("head");
74  RCHECK(root.SetStringAttribute("xml:lang", language_));
75  for (const auto& pair : regions_) {
76  if (!did_log && (pair.second.region_anchor_x.value != 0 &&
77  pair.second.region_anchor_y.value != 0)) {
78  LOG(WARNING) << "TTML doesn't support non-0 region anchor";
79  did_log = true;
80  }
81 
82  xml::XmlNode region("region");
83  const auto origin =
84  ToTtmlSize(pair.second.window_anchor_x, pair.second.window_anchor_y);
85  const auto extent = ToTtmlSize(pair.second.width, pair.second.height);
86  RCHECK(region.SetStringAttribute("xml:id", pair.first));
87  RCHECK(region.SetStringAttribute("tts:origin", origin));
88  RCHECK(region.SetStringAttribute("tts:extent", extent));
89  RCHECK(head.AddChild(std::move(region)));
90  }
91  RCHECK(root.AddChild(std::move(head)));
92 
93  size_t image_count = 0;
94  xml::XmlNode metadata("metadata");
95  xml::XmlNode body("body");
96  xml::XmlNode div("div");
97  for (const auto& sample : samples_) {
98  RCHECK(AddSampleToXml(sample, &div, &metadata, &image_count));
99  }
100  RCHECK(body.AddChild(std::move(div)));
101  if (image_count > 0) {
102  RCHECK(root.SetStringAttribute(
103  "xmlns:smpte", "http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt"));
104  RCHECK(root.AddChild(std::move(metadata)));
105  }
106  RCHECK(root.AddChild(std::move(body)));
107 
108  *result = root.ToString(/* comment= */ "");
109  return true;
110 }
111 
112 bool TtmlGenerator::AddSampleToXml(const TextSample& sample,
113  xml::XmlNode* body,
114  xml::XmlNode* metadata,
115  size_t* image_count) const {
116  xml::XmlNode p("p");
117  RCHECK(p.SetStringAttribute("xml:space", "preserve"));
118  RCHECK(p.SetStringAttribute("begin",
119  ToTtmlTime(sample.start_time(), time_scale_)));
120  RCHECK(
121  p.SetStringAttribute("end", ToTtmlTime(sample.EndTime(), time_scale_)));
122  RCHECK(ConvertFragmentToXml(sample.body(), &p, metadata, image_count));
123  if (!sample.id().empty())
124  RCHECK(p.SetStringAttribute("xml:id", sample.id()));
125 
126  const auto& settings = sample.settings();
127  if (settings.line || settings.position || settings.width || settings.height) {
128  // TTML positioning needs to be from a region.
129  if (!settings.region.empty()) {
130  LOG(WARNING)
131  << "Using both text regions and positioning isn't supported in TTML";
132  }
133 
134  const auto origin = ToTtmlSize(
135  settings.position.value_or(TextNumber(0, TextUnitType::kPixels)),
136  settings.line.value_or(TextNumber(0, TextUnitType::kPixels)));
137  const auto extent = ToTtmlSize(
138  settings.width.value_or(TextNumber(100, TextUnitType::kPercent)),
139  settings.height.value_or(TextNumber(100, TextUnitType::kPercent)));
140 
141  const std::string id = kRegionIdPrefix + std::to_string(region_id_++);
142  xml::XmlNode region("region");
143  RCHECK(region.SetStringAttribute("xml:id", id));
144  RCHECK(region.SetStringAttribute("tts:origin", origin));
145  RCHECK(region.SetStringAttribute("tts:extent", extent));
146  RCHECK(p.SetStringAttribute("region", id));
147  RCHECK(body->AddChild(std::move(region)));
148  } else if (!settings.region.empty()) {
149  RCHECK(p.SetStringAttribute("region", settings.region));
150  }
151 
152  if (settings.writing_direction != WritingDirection::kHorizontal) {
153  const char* dir =
154  settings.writing_direction == WritingDirection::kVerticalGrowingLeft
155  ? "tbrl"
156  : "tblr";
157  RCHECK(p.SetStringAttribute("tts:writingMode", dir));
158  }
159  if (settings.text_alignment != TextAlignment::kStart) {
160  switch (settings.text_alignment) {
161  case TextAlignment::kStart: // To avoid compiler warning.
162  case TextAlignment::kCenter:
163  RCHECK(p.SetStringAttribute("tts:textAlign", "center"));
164  break;
165  case TextAlignment::kEnd:
166  RCHECK(p.SetStringAttribute("tts:textAlign", "end"));
167  break;
168  case TextAlignment::kLeft:
169  RCHECK(p.SetStringAttribute("tts:textAlign", "left"));
170  break;
171  case TextAlignment::kRight:
172  RCHECK(p.SetStringAttribute("tts:textAlign", "right"));
173  break;
174  }
175  }
176 
177  RCHECK(body->AddChild(std::move(p)));
178  return true;
179 }
180 
181 bool TtmlGenerator::ConvertFragmentToXml(const TextFragment& body,
182  xml::XmlNode* parent,
183  xml::XmlNode* metadata,
184  size_t* image_count) const {
185  if (body.newline) {
186  xml::XmlNode br("br");
187  return parent->AddChild(std::move(br));
188  }
189 
190  // If we have new styles, add a new <span>.
191  xml::XmlNode span("span");
192  xml::XmlNode* node = parent;
193  if (body.style.bold || body.style.italic || body.style.underline) {
194  node = &span;
195  if (body.style.bold) {
196  RCHECK(span.SetStringAttribute("tts:fontWeight",
197  *body.style.bold ? "bold" : "normal"));
198  }
199  if (body.style.italic) {
200  RCHECK(span.SetStringAttribute("tts:fontStyle",
201  *body.style.italic ? "italic" : "normal"));
202  }
203  if (body.style.underline) {
204  RCHECK(span.SetStringAttribute(
205  "tts:textDecoration",
206  *body.style.underline ? "underline" : "noUnderline"));
207  }
208  }
209 
210  if (!body.body.empty()) {
211  node->AddContent(body.body);
212  } else if (!body.image.empty()) {
213  std::string image_data(body.image.begin(), body.image.end());
214  std::string base64_data;
215  base::Base64Encode(image_data, &base64_data);
216  std::string id = "img_" + std::to_string(++*image_count);
217 
218  xml::XmlNode image_xml("smpte:image");
219  RCHECK(image_xml.SetStringAttribute("imageType", "PNG"));
220  RCHECK(image_xml.SetStringAttribute("encoding", "Base64"));
221  RCHECK(image_xml.SetStringAttribute("xml:id", id));
222  image_xml.SetContent(base64_data);
223  RCHECK(metadata->AddChild(std::move(image_xml)));
224 
225  RCHECK(node->SetStringAttribute("smpte:backgroundImage", "#" + id));
226  } else {
227  for (const auto& frag : body.sub_fragments) {
228  if (!ConvertFragmentToXml(frag, node, metadata, image_count))
229  return false;
230  }
231  }
232 
233  if (body.style.bold || body.style.italic || body.style.underline)
234  RCHECK(parent->AddChild(std::move(span)));
235  return true;
236 }
237 
238 } // namespace ttml
239 } // namespace media
240 } // namespace shaka
All the methods that are virtual are virtual for mocking.