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 string_text = StringText(
137 content()->outline_width(),
138 content()->get_font(subtitle.font().get_value_or("")),
141 string_text.set_text(remove_invalid_characters_for_xml(string_text.text()));
142 set_forced_appearance(content(), string_text);
143 string_texts.push_back(string_text);
146 PlainStart(ContentStringText(from, string_texts));
147 maybe_set_position(from);
152 TextDecoder::emit_plain_start (ContentTime from, sub::Subtitle const & sub_subtitle)
154 /* See if our next subtitle needs to be vertically placed on screen by us */
155 bool needs_placement = false;
156 optional<int> bottom_line;
157 for (auto line: sub_subtitle.lines) {
158 if (!line.vertical_position.reference || (line.vertical_position.line && !line.vertical_position.lines) || line.vertical_position.reference.get() == sub::TOP_OF_SUBTITLE) {
159 needs_placement = true;
160 if (!bottom_line || bottom_line.get() < line.vertical_position.line.get()) {
161 bottom_line = line.vertical_position.line.get();
166 /* Find the lowest proportional position */
167 optional<float> lowest_proportional;
168 for (auto line: sub_subtitle.lines) {
169 if (line.vertical_position.proportional) {
170 if (!lowest_proportional) {
171 lowest_proportional = line.vertical_position.proportional;
173 lowest_proportional = min(lowest_proportional.get(), line.vertical_position.proportional.get());
178 vector<StringText> string_texts;
179 for (auto line: sub_subtitle.lines) {
180 for (auto block: line.blocks) {
182 if (!block.font_size.specified()) {
183 /* Fallback default font size if no other has been specified */
184 block.font_size.set_points (48);
189 if (needs_placement) {
190 DCPOMATIC_ASSERT (line.vertical_position.line);
191 double const multiplier = 1.2 * content()->line_spacing() * content()->y_scale() * block.font_size.proportional (72 * 11);
192 switch (line.vertical_position.reference.get_value_or(sub::BOTTOM_OF_SCREEN)) {
193 case sub::BOTTOM_OF_SCREEN:
194 case sub::TOP_OF_SUBTITLE:
195 /* This 0.1 is an arbitrary value to lift the bottom sub off the bottom
196 of the screen a bit to a pleasing degree.
199 (1 + bottom_line.get() - line.vertical_position.line.get()) * multiplier;
201 /* Align our subtitles to the bottom of the screen, because if we are making a SMPTE
202 * DCP and the projection system uses the wrong standard to interpret vertical position,
203 * a bottom-aligned subtitle will be less wrong than a top-aligned one. This is because
204 * in the top-aligned case the difference will be the distance between bbox top an
205 * baseline, but in the bottom-aligned case the difference will be between bbox bottom
206 * and baseline (which is shorter).
208 v_align = dcp::VAlign::BOTTOM;
210 case sub::TOP_OF_SCREEN:
211 /* This 0.1 is another fudge factor to bring the top line away from the top of the screen a little */
212 v_position = 0.12 + line.vertical_position.line.get() * multiplier;
213 v_align = dcp::VAlign::TOP;
215 case sub::VERTICAL_CENTRE_OF_SCREEN:
216 v_position = line.vertical_position.line.get() * multiplier;
217 v_align = dcp::VAlign::CENTER;
221 DCPOMATIC_ASSERT (line.vertical_position.reference);
222 if (line.vertical_position.proportional) {
223 v_position = line.vertical_position.proportional.get();
225 DCPOMATIC_ASSERT (line.vertical_position.line);
226 DCPOMATIC_ASSERT (line.vertical_position.lines);
227 v_position = float(*line.vertical_position.line) / *line.vertical_position.lines;
230 if (lowest_proportional) {
231 /* Adjust line spacing */
232 v_position = ((v_position - lowest_proportional.get()) * content()->line_spacing()) + lowest_proportional.get();
235 switch (line.vertical_position.reference.get()) {
236 case sub::TOP_OF_SCREEN:
237 v_align = dcp::VAlign::TOP;
239 case sub::VERTICAL_CENTRE_OF_SCREEN:
240 v_align = dcp::VAlign::CENTER;
242 case sub::BOTTOM_OF_SCREEN:
243 v_align = dcp::VAlign::BOTTOM;
246 v_align = dcp::VAlign::TOP;
252 float h_position = line.horizontal_position.proportional;
253 switch (line.horizontal_position.reference) {
254 case sub::LEFT_OF_SCREEN:
255 h_align = dcp::HAlign::LEFT;
256 h_position = max(h_position, 0.05f);
258 case sub::HORIZONTAL_CENTRE_OF_SCREEN:
259 h_align = dcp::HAlign::CENTER;
261 case sub::RIGHT_OF_SCREEN:
262 h_align = dcp::HAlign::RIGHT;
263 h_position = max(h_position, 0.05f);
266 h_align = dcp::HAlign::CENTER;
270 /* The idea here (rightly or wrongly) is that we set the appearance based on the
271 values in the libsub objects, and these are overridden with values from the
272 content by the other emit_plain_start() above.
275 auto dcp_colour = [](sub::Colour const& c) {
276 return dcp::Colour(lrintf(c.r * 255), lrintf(c.g * 255), lrintf(c.b * 255));
279 auto dcp_subtitle = dcp::SubtitleString(
284 dcp_colour(block.colour),
285 block.font_size.points (72 * 11),
287 dcp::Time (from.seconds(), 1000),
288 /* XXX: hmm; this is a bit ugly (we don't know the to time yet) */
296 remove_invalid_characters_for_xml(block.text),
298 dcp_colour(block.effect_colour.get_value_or(sub::Colour(0, 0, 0))),
299 /* Hack: we should use subtitle.fade_up and subtitle.fade_down here
300 but the times of these often don't have a frame rate associated
301 with them so the sub::Time won't convert them to milliseconds without
302 throwing an exception. Since only DCP subs fill those in (and we don't
303 use libsub for DCP subs) we can cheat by just putting 0 in here.
310 auto string_text = StringText(
312 content()->outline_width(),
313 content()->get_font(block.font.get_value_or("")),
314 dcp::SubtitleStandard::SMPTE_2014
316 set_forced_appearance(content(), string_text);
317 string_texts.push_back(string_text);
321 PlainStart(ContentStringText(from, string_texts));
322 maybe_set_position(from);
327 TextDecoder::emit_stop (ContentTime to)
334 TextDecoder::emit_plain(ContentTimePeriod period, vector<dcp::SubtitleString> subtitles, dcp::SubtitleStandard valign_standard)
336 emit_plain_start (period.from, subtitles, valign_standard);
337 emit_stop (period.to);
342 TextDecoder::emit_plain (ContentTimePeriod period, sub::Subtitle const& subtitles)
344 emit_plain_start (period.from, subtitles);
345 emit_stop (period.to);
349 /* @param rect Area expressed as a fraction of the video frame that this subtitle
350 * is for (e.g. a width of 0.5 means the width of the subtitle is half the width
351 * of the video frame)
354 TextDecoder::emit_bitmap (ContentTimePeriod period, shared_ptr<const Image> image, dcpomatic::Rect<double> rect)
356 emit_bitmap_start ({ period.from, image, rect });
357 emit_stop (period.to);
364 _position = ContentTime ();
369 TextDecoder::maybe_set_position (dcpomatic::ContentTime position)
371 if (!_position || position > *_position) {
372 _position = position;