2 Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
4 This file is part of DCP-o-matic.
6 DCP-o-matic is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 DCP-o-matic is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
22 #include "compose.hpp"
24 #include "text_content.h"
25 #include "text_decoder.h"
27 #include <sub/subtitle.h>
28 #include <boost/algorithm/string.hpp>
35 using std::shared_ptr;
38 using boost::optional;
39 using namespace dcpomatic;
42 TextDecoder::TextDecoder (
44 shared_ptr<const TextContent> content
46 : DecoderPart (parent)
53 /** Called by subclasses when an image subtitle is starting.
54 * @param from From time of the subtitle.
55 * @param image Subtitle image.
56 * @param rect Area expressed as a fraction of the video frame that this subtitle
57 * is for (e.g. a width of 0.5 means the width of the subtitle is half the width
61 TextDecoder::emit_bitmap_start (ContentBitmapText const& bitmap)
64 maybe_set_position(bitmap.from());
70 set_forced_appearance(shared_ptr<const TextContent> content, StringText& subtitle)
72 if (content->colour()) {
73 subtitle.set_colour(*content->colour());
75 if (content->effect_colour()) {
76 subtitle.set_effect_colour(*content->effect_colour());
78 if (content->effect()) {
79 subtitle.set_effect(*content->effect());
81 if (content->fade_in()) {
82 subtitle.set_fade_up_time(dcp::Time(content->fade_in()->seconds(), 1000));
84 if (content->fade_out()) {
85 subtitle.set_fade_down_time (dcp::Time(content->fade_out()->seconds(), 1000));
91 TextDecoder::remove_invalid_characters_for_xml(string text)
95 /* https://www.w3.org/TR/REC-xml/#charsets says that XML may only contain 0x9, 0xa, 0xd below 0x32.
96 * Not sure if we should be doing direct UTF-8 manipulation here.
98 for (size_t i = 0; i < text.length(); ++i) {
99 auto const c = text[i];
100 if ((c & 0xe0) == 0xc0) {
101 // start of 2-byte code point
103 output += text[i + 1];
105 } else if ((c & 0xf0) == 0xe0) {
106 // start of 3-byte code point
108 output += text[i + 1];
109 output += text[i + 2];
111 } else if ((c & 0xf8) == 0xf0) {
112 // start of 4-byte code point
114 output += text[i + 1];
115 output += text[i + 2];
116 output += text[i + 3];
119 if (c >= 0x20 || c == 0x9 || c == 0xa || c == 0xd) {
130 TextDecoder::emit_plain_start(ContentTime from, vector<dcp::SubtitleString> subtitles, dcp::SubtitleStandard valign_standard)
132 vector<StringText> string_texts;
134 for (auto& subtitle: subtitles) {
135 auto font = content()->get_font(subtitle.font().get_value_or(""));
136 DCPOMATIC_ASSERT(font);
137 auto string_text = StringText(subtitle, content()->outline_width(), font, valign_standard);
138 string_text.set_text(remove_invalid_characters_for_xml(string_text.text()));
139 set_forced_appearance(content(), string_text);
140 string_texts.push_back(string_text);
143 PlainStart(ContentStringText(from, string_texts));
144 maybe_set_position(from);
149 TextDecoder::emit_plain_start (ContentTime from, sub::Subtitle const & sub_subtitle)
151 /* See if our next subtitle needs to be vertically placed on screen by us */
152 bool needs_placement = false;
153 optional<int> bottom_line;
154 for (auto line: sub_subtitle.lines) {
155 if (!line.vertical_position.reference || (line.vertical_position.line && !line.vertical_position.lines) || line.vertical_position.reference.get() == sub::TOP_OF_SUBTITLE) {
156 needs_placement = true;
157 if (!bottom_line || bottom_line.get() < line.vertical_position.line.get()) {
158 bottom_line = line.vertical_position.line.get();
163 /* Find the lowest proportional position */
164 optional<float> lowest_proportional;
165 for (auto line: sub_subtitle.lines) {
166 if (line.vertical_position.proportional) {
167 if (!lowest_proportional) {
168 lowest_proportional = line.vertical_position.proportional;
170 lowest_proportional = min(lowest_proportional.get(), line.vertical_position.proportional.get());
175 vector<StringText> string_texts;
176 for (auto line: sub_subtitle.lines) {
177 for (auto block: line.blocks) {
179 if (!block.font_size.specified()) {
180 /* Fallback default font size if no other has been specified */
181 block.font_size.set_points (48);
186 if (needs_placement) {
187 DCPOMATIC_ASSERT (line.vertical_position.line);
188 double const multiplier = 1.2 * content()->line_spacing() * content()->y_scale() * block.font_size.proportional (72 * 11);
189 switch (line.vertical_position.reference.get_value_or(sub::BOTTOM_OF_SCREEN)) {
190 case sub::BOTTOM_OF_SCREEN:
191 case sub::TOP_OF_SUBTITLE:
192 /* This 0.1 is an arbitrary value to lift the bottom sub off the bottom
193 of the screen a bit to a pleasing degree.
196 (1 + bottom_line.get() - line.vertical_position.line.get()) * multiplier;
198 /* Align our subtitles to the bottom of the screen, because if we are making a SMPTE
199 * DCP and the projection system uses the wrong standard to interpret vertical position,
200 * a bottom-aligned subtitle will be less wrong than a top-aligned one. This is because
201 * in the top-aligned case the difference will be the distance between bbox top an
202 * baseline, but in the bottom-aligned case the difference will be between bbox bottom
203 * and baseline (which is shorter).
205 v_align = dcp::VAlign::BOTTOM;
207 case sub::TOP_OF_SCREEN:
208 /* This 0.1 is another fudge factor to bring the top line away from the top of the screen a little */
209 v_position = 0.12 + line.vertical_position.line.get() * multiplier;
210 v_align = dcp::VAlign::TOP;
212 case sub::VERTICAL_CENTRE_OF_SCREEN:
213 v_position = line.vertical_position.line.get() * multiplier;
214 v_align = dcp::VAlign::CENTER;
218 DCPOMATIC_ASSERT (line.vertical_position.reference);
219 if (line.vertical_position.proportional) {
220 v_position = line.vertical_position.proportional.get();
222 DCPOMATIC_ASSERT (line.vertical_position.line);
223 DCPOMATIC_ASSERT (line.vertical_position.lines);
224 v_position = float(*line.vertical_position.line) / *line.vertical_position.lines;
227 if (lowest_proportional) {
228 /* Adjust line spacing */
229 v_position = ((v_position - lowest_proportional.get()) * content()->line_spacing()) + lowest_proportional.get();
232 switch (line.vertical_position.reference.get()) {
233 case sub::TOP_OF_SCREEN:
234 v_align = dcp::VAlign::TOP;
236 case sub::VERTICAL_CENTRE_OF_SCREEN:
237 v_align = dcp::VAlign::CENTER;
239 case sub::BOTTOM_OF_SCREEN:
240 v_align = dcp::VAlign::BOTTOM;
243 v_align = dcp::VAlign::TOP;
249 float h_position = line.horizontal_position.proportional;
250 switch (line.horizontal_position.reference) {
251 case sub::LEFT_OF_SCREEN:
252 h_align = dcp::HAlign::LEFT;
253 h_position = max(h_position, 0.05f);
255 case sub::HORIZONTAL_CENTRE_OF_SCREEN:
256 h_align = dcp::HAlign::CENTER;
258 case sub::RIGHT_OF_SCREEN:
259 h_align = dcp::HAlign::RIGHT;
260 h_position = max(h_position, 0.05f);
263 h_align = dcp::HAlign::CENTER;
267 /* The idea here (rightly or wrongly) is that we set the appearance based on the
268 values in the libsub objects, and these are overridden with values from the
269 content by the other emit_plain_start() above.
272 auto dcp_colour = [](sub::Colour const& c) {
273 return dcp::Colour(lrintf(c.r * 255), lrintf(c.g * 255), lrintf(c.b * 255));
276 auto dcp_subtitle = dcp::SubtitleString(
281 dcp_colour(block.colour),
282 block.font_size.points (72 * 11),
284 dcp::Time (from.seconds(), 1000),
285 /* XXX: hmm; this is a bit ugly (we don't know the to time yet) */
293 remove_invalid_characters_for_xml(block.text),
295 dcp_colour(block.effect_colour.get_value_or(sub::Colour(0, 0, 0))),
296 /* Hack: we should use subtitle.fade_up and subtitle.fade_down here
297 but the times of these often don't have a frame rate associated
298 with them so the sub::Time won't convert them to milliseconds without
299 throwing an exception. Since only DCP subs fill those in (and we don't
300 use libsub for DCP subs) we can cheat by just putting 0 in here.
305 std::vector<dcp::Ruby>()
308 auto font = content()->get_font(block.font.get_value_or(""));
309 DCPOMATIC_ASSERT(font);
311 auto string_text = StringText(
313 content()->outline_width(),
315 dcp::SubtitleStandard::SMPTE_2014
317 set_forced_appearance(content(), string_text);
318 string_texts.push_back(string_text);
322 PlainStart(ContentStringText(from, string_texts));
323 maybe_set_position(from);
328 TextDecoder::emit_stop (ContentTime to)
335 TextDecoder::emit_plain(ContentTimePeriod period, vector<dcp::SubtitleString> subtitles, dcp::SubtitleStandard valign_standard)
337 emit_plain_start (period.from, subtitles, valign_standard);
338 emit_stop (period.to);
343 TextDecoder::emit_plain (ContentTimePeriod period, sub::Subtitle const& subtitles)
345 emit_plain_start (period.from, subtitles);
346 emit_stop (period.to);
350 /* @param rect Area expressed as a fraction of the video frame that this subtitle
351 * is for (e.g. a width of 0.5 means the width of the subtitle is half the width
352 * of the video frame)
355 TextDecoder::emit_bitmap (ContentTimePeriod period, shared_ptr<const Image> image, dcpomatic::Rect<double> rect)
357 emit_bitmap_start ({ period.from, image, rect });
358 emit_stop (period.to);
365 _position = ContentTime ();
370 TextDecoder::maybe_set_position (dcpomatic::ContentTime position)
372 if (!_position || position > *_position) {
373 _position = position;