718 | | vector<CC608Text*>::iterator i = textlist->buffers.begin(); |
719 | | bool teletextmode = (*i)->teletextmode; |
720 | | int xscale = teletextmode ? 40 : 36; |
721 | | int yscale = teletextmode ? 25 : 17; |
722 | | gTextSubFont->GetFace()->setPixelSize(m_safeArea.height() / (yscale * 1.2)); |
723 | | QBrush bgfill = QBrush(QColor(0, 0, 0), Qt::SolidPattern); |
724 | | |
725 | | for (; i != textlist->buffers.end(); ++i) |
726 | | { |
727 | | CC608Text *cc = (*i); |
728 | | int color = 0; |
729 | | bool isItalic = false, isUnderline = false; |
730 | | bool first = true; |
731 | | bool showedNonControl = false; |
732 | | int x = 0, width = 0; |
733 | | QString text(cc->text); |
734 | | |
735 | | for (int chunk = 0; text != QString::null; first = false, chunk++) |
736 | | { |
737 | | QString captionText = |
738 | | extract_cc608(text, cc->teletextmode, |
739 | | color, isItalic, isUnderline, |
740 | | showedNonControl); |
741 | | gTextSubFont->GetFace()->setItalic(isItalic); |
742 | | gTextSubFont->GetFace()->setUnderline(isUnderline); |
743 | | gTextSubFont->SetColor(clr[min(max(0, color), 7)]); |
744 | | QFontMetrics font(*(gTextSubFont->GetFace())); |
745 | | // XXX- could there be different heights across the same line? |
746 | | int height = font.height() * (1 + PAD_HEIGHT); |
747 | | if (first) |
748 | | { |
749 | | x = teletextmode ? cc->y : (cc->x + 3); |
750 | | x = (int)(((float)x / (float)xscale) * |
751 | | (float)m_safeArea.width()); |
752 | | } |
753 | | else |
754 | | { |
755 | | x += width; // bump x by the previous width |
756 | | } |
757 | | |
758 | | int pad_width = font.maxWidth() * PAD_WIDTH; |
759 | | width = font.width(captionText) + pad_width; |
760 | | int y = teletextmode ? cc->x : cc->y; |
761 | | y = (int)(((float)y / (float)yscale) * (float)m_safeArea.height()); |
762 | | // Sometimes a line of caption text begins with a mid-row |
763 | | // format control like italics or a color change. The |
764 | | // spec says the mid-row control also includes a space |
765 | | // character. But this looks clumsy when using a |
766 | | // background, so we suppress it after the placement is |
767 | | // calculated. |
768 | | if (!showedNonControl) |
769 | | continue; |
770 | | QRect rect(x, y, width, height); |
771 | | |
772 | | if (!teletextmode && m_useBackground) |
773 | | { |
774 | | MythUIShape *shape = new MythUIShape(this, |
775 | | QString("cc608bg%1%2%3").arg(cc->x).arg(cc->y).arg(width)); |
776 | | shape->SetFillBrush(bgfill); |
777 | | QRect bgrect(x - pad_width, y, width + pad_width, height); |
778 | | shape->SetArea(MythRect(bgrect)); |
779 | | } |
780 | | |
781 | | new MythUISimpleText(captionText, *gTextSubFont, rect, |
782 | | Qt::AlignLeft, (MythUIType*)this, |
783 | | QString("cc608txt%1%2%3%4") |
784 | | .arg(cc->x).arg(cc->y) |
785 | | .arg(width).arg(chunk)); |
786 | | |
787 | | m_refreshArea = true; |
788 | | |
789 | | LOG(VB_VBI, LOG_INFO, |
790 | | QString("x %1 y %2 uline=%4 ital=%5 " |
791 | | "color=%6 coord=%7,%8 String: '%3'") |
792 | | .arg(cc->x).arg(cc->y).arg(captionText) |
793 | | .arg(isUnderline).arg(isItalic).arg(color).arg(x).arg(y)); |
794 | | } |
795 | | } |
| 622 | FormattedTextSubtitle fsub(m_safeArea); |
| 623 | fsub.InitFromCC608(textlist->buffers); |
| 624 | fsub.Draw(this); |
| 625 | m_refreshArea = true; |
| 1022 | void FormattedTextChunk::GetSize(int pixelSize, int &width, int &height) const |
| 1023 | { |
| 1024 | QFont *font = gTextSubFont->GetFace(); |
| 1025 | font->setItalic(isItalic); |
| 1026 | font->setBold(isBold); |
| 1027 | font->setUnderline(isUnderline); |
| 1028 | font->setPixelSize(pixelSize); |
| 1029 | QFontMetrics fm(*font); |
| 1030 | width = fm.width(text) + fm.maxWidth() * PAD_WIDTH; |
| 1031 | height = fm.height() * (1 + PAD_HEIGHT); |
| 1032 | } |
| 1033 | |
| 1034 | void FormattedTextSubtitle::InitFromCC608(vector<CC608Text*> &buffers) |
| 1035 | { |
| 1036 | static const QColor clr[8] = |
| 1037 | { |
| 1038 | Qt::white, Qt::green, Qt::blue, Qt::cyan, |
| 1039 | Qt::red, Qt::yellow, Qt::magenta, Qt::white, |
| 1040 | }; |
| 1041 | |
| 1042 | if (buffers.empty()) |
| 1043 | return; |
| 1044 | vector<CC608Text*>::iterator i = buffers.begin(); |
| 1045 | bool teletextmode = (*i)->teletextmode; |
| 1046 | m_useBackground = m_useBackground && !teletextmode; |
| 1047 | int xscale = teletextmode ? 40 : 36; |
| 1048 | int yscale = teletextmode ? 25 : 17; |
| 1049 | m_pixelSize = m_safeArea.height() / (yscale * 1.2); |
| 1050 | |
| 1051 | for (; i != buffers.end(); ++i) |
| 1052 | { |
| 1053 | CC608Text *cc = (*i); |
| 1054 | int color = 0; |
| 1055 | bool isItalic = false, isUnderline = false; |
| 1056 | const bool isBold = false; |
| 1057 | QString text(cc->text); |
| 1058 | |
| 1059 | int orig_x = teletextmode ? cc->y : (cc->x + 3); |
| 1060 | int x = (int)(((float)orig_x / (float)xscale) * (float)m_safeArea.width()); |
| 1061 | int orig_y = teletextmode ? cc->x : cc->y; |
| 1062 | int y = (int)(((float)orig_y / (float)yscale) * (float)m_safeArea.height()); |
| 1063 | FormattedTextLine line(x, y); |
| 1064 | for (int chunk = 0; text != QString::null; chunk++) |
| 1065 | { |
| 1066 | QString captionText = |
| 1067 | extract_cc608(text, cc->teletextmode, |
| 1068 | color, isItalic, isUnderline); |
| 1069 | FormattedTextChunk chunk(captionText, |
| 1070 | isItalic, isBold, isUnderline, |
| 1071 | clr[min(max(0, color), 7)]); |
| 1072 | line.chunks += chunk; |
| 1073 | LOG(VB_VBI, LOG_INFO, |
| 1074 | QString("Adding cc608 chunk: x=%1 y=%2 " |
| 1075 | "uline=%3 ital=%4 color=%5 text='%6'") |
| 1076 | .arg(cc->x).arg(cc->y) |
| 1077 | .arg(isUnderline).arg(isItalic).arg(color).arg(captionText)); |
| 1078 | } |
| 1079 | m_lines += line; |
| 1080 | } |
| 1081 | } |
| 1082 | |
| 1083 | void FormattedTextSubtitle::InitFromSRT(QStringList &subs, int textFontZoom) |
| 1084 | { |
| 1085 | // Does a simplistic parsing of HTML tags from the strings. |
| 1086 | // Nesting is not implemented. Stray whitespace may cause |
| 1087 | // legitimate tags to be ignored. All other HTML tags are |
| 1088 | // stripped and ignored. |
| 1089 | // |
| 1090 | // <i> - enable italics |
| 1091 | // </i> - disable italics |
| 1092 | // <b> - enable boldface |
| 1093 | // </b> - disable boldface |
| 1094 | // <u> - enable underline |
| 1095 | // </u> - disable underline |
| 1096 | // <font color="#xxyyzz"> - change font color |
| 1097 | // </font> - reset font color to white |
| 1098 | |
| 1099 | m_pixelSize = (m_safeArea.height() * textFontZoom) / 1800; |
| 1100 | |
| 1101 | bool isItalic = false; |
| 1102 | bool isBold = false; |
| 1103 | bool isUnderline = false; |
| 1104 | QColor color(Qt::white); |
| 1105 | QRegExp htmlTag("</?.+>"); |
| 1106 | QString htmlPrefix("<font color=\""), htmlSuffix("\">"); |
| 1107 | htmlTag.setMinimal(true); |
| 1108 | foreach (QString subtitle, subs) |
| 1109 | { |
| 1110 | FormattedTextLine line; |
| 1111 | QString text(subtitle); |
| 1112 | while (!text.isEmpty()) |
| 1113 | { |
| 1114 | int pos = text.indexOf(htmlTag); |
| 1115 | if (pos != 0) // don't add a zero-length string |
| 1116 | { |
| 1117 | FormattedTextChunk chunk(text.left(pos), |
| 1118 | isItalic, isBold, isUnderline, color); |
| 1119 | line.chunks += chunk; |
| 1120 | text = (pos < 0 ? "" : text.mid(pos)); |
| 1121 | LOG(VB_VBI, LOG_INFO, QString("Adding SRT chunk '%1'").arg(chunk.text)); |
| 1122 | } |
| 1123 | if (pos >= 0) |
| 1124 | { |
| 1125 | int htmlLen = htmlTag.matchedLength(); |
| 1126 | QString html = text.left(htmlLen).toLower(); |
| 1127 | text = text.mid(htmlLen); |
| 1128 | if (html == "<i>") |
| 1129 | isItalic = true; |
| 1130 | else if (html == "</i>") |
| 1131 | isItalic = false; |
| 1132 | else if (html.startsWith(htmlPrefix) && |
| 1133 | html.endsWith(htmlSuffix)) |
| 1134 | { |
| 1135 | int colorLen = html.length() - |
| 1136 | (htmlPrefix.length() + htmlSuffix.length()); |
| 1137 | QString colorString(html.mid(htmlPrefix.length(), colorLen)); |
| 1138 | QColor newColor(colorString); |
| 1139 | if (newColor.isValid()) |
| 1140 | color = newColor; |
| 1141 | else |
| 1142 | LOG(VB_VBI, LOG_INFO, |
| 1143 | QString("Ignoring invalid SRT color specification: " |
| 1144 | "'%1'").arg(colorString)); |
| 1145 | } |
| 1146 | else if (html == "</font>") |
| 1147 | color = Qt::white; |
| 1148 | else if (html == "<b>") |
| 1149 | isBold = true; |
| 1150 | else if (html == "</b>") |
| 1151 | isBold = false; |
| 1152 | else if (html == "<u>") |
| 1153 | isUnderline = true; |
| 1154 | else if (html == "</u>") |
| 1155 | isUnderline = false; |
| 1156 | else |
| 1157 | LOG(VB_VBI, LOG_INFO, |
| 1158 | QString("Ignoring unknown SRT formatting: '%1'").arg(html)); |
| 1159 | |
| 1160 | LOG(VB_VBI, LOG_INFO, |
| 1161 | QString("SRT formatting change '%1', " |
| 1162 | "new ital=%2 bold=%3 uline=%4 color=#%5%6%7)") |
| 1163 | .arg(html).arg(isItalic).arg(isBold).arg(isUnderline) |
| 1164 | .arg(color.red(), 2, 16, QLatin1Char('0')) |
| 1165 | .arg(color.green(), 2, 16, QLatin1Char('0')) |
| 1166 | .arg(color.blue(), 2, 16, QLatin1Char('0'))); |
| 1167 | } |
| 1168 | } |
| 1169 | m_lines += line; |
| 1170 | } |
| 1171 | } |
| 1172 | |
| 1173 | bool FormattedTextChunk::Split(FormattedTextChunk &newChunk) |
| 1174 | { |
| 1175 | LOG(VB_VBI, LOG_INFO, |
| 1176 | QString("Attempting to split chunk '%1'").arg(text)); |
| 1177 | int lastSpace = text.lastIndexOf(' ', -2); // -2 to ignore trailing space |
| 1178 | if (lastSpace < 0) |
| 1179 | { |
| 1180 | LOG(VB_VBI, LOG_INFO, |
| 1181 | QString("Failed to split chunk '%1'").arg(text)); |
| 1182 | return false; |
| 1183 | } |
| 1184 | newChunk.isItalic = isItalic; |
| 1185 | newChunk.isBold = isBold; |
| 1186 | newChunk.isUnderline = isUnderline; |
| 1187 | newChunk.color = color; |
| 1188 | newChunk.text = text.mid(lastSpace + 1).trimmed() + ' '; |
| 1189 | text = text.left(lastSpace).trimmed(); |
| 1190 | LOG(VB_VBI, LOG_INFO, |
| 1191 | QString("Split chunk into '%1' + '%2'").arg(text).arg(newChunk.text)); |
| 1192 | return true; |
| 1193 | } |
| 1194 | |
| 1195 | void FormattedTextSubtitle::WrapLongLines(void) |
| 1196 | { |
| 1197 | int maxWidth = m_safeArea.width(); |
| 1198 | for (int i = 0; i < m_lines.size(); i++) |
| 1199 | { |
| 1200 | int width = m_lines[i].Width(m_pixelSize); |
| 1201 | // Move entire chunks to the next line as necessary. Leave at |
| 1202 | // least one chunk on the current line. |
| 1203 | while (width > maxWidth && m_lines[i].chunks.size() > 1) |
| 1204 | { |
| 1205 | width -= m_lines[i].chunks.back().Width(m_pixelSize); |
| 1206 | // Make sure there's a next line to wrap into. |
| 1207 | if (m_lines.size() == i + 1) |
| 1208 | m_lines += FormattedTextLine(m_lines[i].x_indent, |
| 1209 | m_lines[i].y_indent); |
| 1210 | m_lines[i+1].chunks.prepend(m_lines[i].chunks.takeLast()); |
| 1211 | LOG(VB_VBI, LOG_INFO, |
| 1212 | QString("Wrapping chunk to next line: '%1'") |
| 1213 | .arg(m_lines[i+1].chunks[0].text)); |
| 1214 | } |
| 1215 | // Split the last chunk until width is small enough or until |
| 1216 | // no more splits are possible. |
| 1217 | bool isSplitPossible = true; |
| 1218 | while (width > maxWidth && isSplitPossible) |
| 1219 | { |
| 1220 | FormattedTextChunk newChunk; |
| 1221 | isSplitPossible = m_lines[i].chunks.back().Split(newChunk); |
| 1222 | if (isSplitPossible) |
| 1223 | { |
| 1224 | // Make sure there's a next line to split into. |
| 1225 | if (m_lines.size() == i + 1) |
| 1226 | m_lines += FormattedTextLine(m_lines[i].x_indent, |
| 1227 | m_lines[i].y_indent); |
| 1228 | m_lines[i+1].chunks.prepend(newChunk); |
| 1229 | width = m_lines[i].Width(m_pixelSize); |
| 1230 | } |
| 1231 | } |
| 1232 | } |
| 1233 | } |
| 1234 | |
| 1235 | void FormattedTextSubtitle::Draw(SubtitleScreen *parent, |
| 1236 | uint64_t start, uint64_t duration) const |
| 1237 | { |
| 1238 | bool useBackground = m_useBackground && parent->GetUseBackground(); |
| 1239 | gTextSubFont->GetFace()->setPixelSize(m_pixelSize); |
| 1240 | QFontMetrics font(*(gTextSubFont->GetFace())); |
| 1241 | int pad_width = font.maxWidth() * PAD_WIDTH; |
| 1242 | QBrush bgfill = QBrush(QColor(0, 0, 0), Qt::SolidPattern); |
| 1243 | |
| 1244 | for (int i = 0; i < m_lines.size(); i++) |
| 1245 | { |
| 1246 | int x = m_lines[i].x_indent; |
| 1247 | if (x < 0) // centering |
| 1248 | x = (m_safeArea.width() - m_lines[i].Width(m_pixelSize)) / 2; |
| 1249 | int y = m_lines[i].y_indent; |
| 1250 | if (y < 0) // stack lines at bottom |
| 1251 | y = m_safeArea.height() - |
| 1252 | (m_lines.size() - i) * m_lines[i].Height(m_pixelSize) * 1.2; |
| 1253 | |
| 1254 | if (useBackground) |
| 1255 | { |
| 1256 | QRect bgrect(x - pad_width, y, |
| 1257 | m_lines[i].Width(m_pixelSize) + pad_width, |
| 1258 | m_lines[i].Height(m_pixelSize)); |
| 1259 | // Special case. If the first chunk is entirely |
| 1260 | // whitespace, don't put a black background behind that |
| 1261 | // chunk. |
| 1262 | // |
| 1263 | // This often happens when a line of cc608 caption text |
| 1264 | // begins with a mid-row format control like italics or a |
| 1265 | // color change. The spec says the mid-row control |
| 1266 | // implies a space character, which needs to be preserved |
| 1267 | // so that the rest of the text is accurately laid out. A |
| 1268 | // leading space looks clumsy against the black |
| 1269 | // background, so we adjust the background accordingly. |
| 1270 | if (!m_lines[i].chunks.isEmpty() && |
| 1271 | m_lines[i].chunks[0].text.trimmed().isEmpty()) |
| 1272 | { |
| 1273 | bgrect.setLeft(bgrect.left() + |
| 1274 | m_lines[i].chunks[0].Width(m_pixelSize)); |
| 1275 | } |
| 1276 | MythUIShape *shape = |
| 1277 | new MythUIShape(parent, QString("subbg%1_%2").arg(x).arg(y)); |
| 1278 | shape->SetFillBrush(bgfill); |
| 1279 | shape->SetArea(MythRect(bgrect)); |
| 1280 | if (duration > 0) |
| 1281 | parent->RegisterExpiration(shape, start + duration); |
| 1282 | } |
| 1283 | QList<FormattedTextChunk>::const_iterator chunk; |
| 1284 | for (chunk = m_lines[i].chunks.constBegin(); |
| 1285 | chunk != m_lines[i].chunks.constEnd(); |
| 1286 | ++chunk) |
| 1287 | { |
| 1288 | // If the chunk starts with whitespace, the leading |
| 1289 | // whitespace ultimately gets lost due to the |
| 1290 | // text.trimmed() operation in the MythUISimpleText |
| 1291 | // constructor. To compensate, we manually indent the |
| 1292 | // chunk accordingly. |
| 1293 | int count = 0; |
| 1294 | while (count < (*chunk).text.length() && |
| 1295 | (*chunk).text.at(count) == ' ') |
| 1296 | ++count; |
| 1297 | int x_adjust = count * font.width(" "); |
| 1298 | gTextSubFont->GetFace()->setItalic((*chunk).isItalic); |
| 1299 | gTextSubFont->GetFace()->setBold((*chunk).isBold); |
| 1300 | gTextSubFont->GetFace()->setUnderline((*chunk).isUnderline); |
| 1301 | gTextSubFont->SetColor((*chunk).color); |
| 1302 | QRect rect(x + x_adjust, y, |
| 1303 | (*chunk).Width(m_pixelSize) - x_adjust, |
| 1304 | (*chunk).Height(m_pixelSize)); |
| 1305 | |
| 1306 | MythUISimpleText *text = |
| 1307 | new MythUISimpleText((*chunk).text, *gTextSubFont, rect, |
| 1308 | Qt::AlignLeft, (MythUIType*)parent, |
| 1309 | QString("subtxt%1x%2@%3,%4") |
| 1310 | .arg((*chunk).Width(m_pixelSize)) |
| 1311 | .arg((*chunk).Height(m_pixelSize)) |
| 1312 | .arg(x).arg(y)); |
| 1313 | if (duration > 0) |
| 1314 | parent->RegisterExpiration(text, start + duration); |
| 1315 | |
| 1316 | LOG(VB_VBI, LOG_INFO, |
| 1317 | QString("Drawing chunk at (%1,%2) with " |
| 1318 | "ital=%3 bold=%4 uline=%5 color=#%6%7%8 " |
| 1319 | "text='%9'") |
| 1320 | .arg(x).arg(y) |
| 1321 | .arg((*chunk).isItalic) |
| 1322 | .arg((*chunk).isBold) |
| 1323 | .arg((*chunk).isUnderline) |
| 1324 | .arg((*chunk).color.red(), 2, 16, QLatin1Char('0')) |
| 1325 | .arg((*chunk).color.green(), 2, 16, QLatin1Char('0')) |
| 1326 | .arg((*chunk).color.blue(), 2, 16, QLatin1Char('0')) |
| 1327 | .arg((*chunk).text)); |
| 1328 | |
| 1329 | x += (*chunk).Width(m_pixelSize); |
| 1330 | } |
| 1331 | } |
| 1332 | } |
| 1333 | |