1 /*
2     SPDX-FileCopyrightText: 2017-2018 Mladen Milinkovic <max@smoothware.net>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "vobsubinputprocessdialog.h"
8 #include "ui_vobsubinputprocessdialog.h"
9 
10 #include "core/richdocument.h"
11 
12 #include <functional>
13 
14 #include <QDebug>
15 #include <QPainter>
16 #include <QKeyEvent>
17 
18 #include <KMessageBox>
19 
20 #include <QStringBuilder>
21 #include <QDataStream>
22 #include <QFile>
23 #include <QSaveFile>
24 
25 using namespace SubtitleComposer;
26 
27 // Private helper classes
28 class VobSubInputProcessDialog::Frame : public QSharedData
29 {
30 public:
Frame()31 	Frame() {}
Frame(const Frame & other)32 	Frame(const Frame &other) : QSharedData(other) {}
~Frame()33 	~Frame() {}
34 
35 	bool processPieces();
36 
37 	static QMap<qint32, qint32> spaceStats;
38 
39 	quint32 index;
40 	QImage subImage;
41 	Time subShowTime;
42 	Time subHideTime;
43 	QList<PiecePtr> pieces;
44 };
45 
46 class VobSubInputProcessDialog::Piece : public QSharedData
47 {
48 public:
Piece()49 	Piece()
50 		: line(nullptr),
51 		  top(0),
52 		  left(0),
53 		  bottom(0),
54 		  right(0),
55 		  symbolCount(1) { }
Piece(int x,int y)56 	Piece(int x, int y)
57 		: line(nullptr),
58 		  top(y),
59 		  left(x),
60 		  bottom(y),
61 		  right(x),
62 		  symbolCount(1) { }
Piece(const Piece & other)63 	Piece(const Piece &other)
64 		: QSharedData(other),
65 		  line(other.line),
66 		  top(other.top),
67 		  left(other.left),
68 		  bottom(other.bottom),
69 		  right(other.right),
70 		  symbolCount(other.symbolCount),
71 		  pixels(other.pixels) { }
~Piece()72 	~Piece() { }
73 
height()74 	inline int height() {
75 		return bottom - top + 1;
76 	}
77 
78 	inline bool operator<(const Piece &other) const;
79 	inline bool operator==(const Piece &other) const;
80 	inline Piece & operator+=(const Piece &other);
81 
82 
83 	inline void normalize();
84 
85 	LinePtr line;
86 	qint32 top, left, bottom, right;
87 	qint32 symbolCount;
88 	SString text;
89 	QVector<QPoint> pixels;
90 };
91 
92 class VobSubInputProcessDialog::Line : public QSharedData {
93 public:
Line(int top,int bottom)94 	Line(int top, int bottom)
95 		: top(top),
96 		  bottom(bottom) { }
Line(const Line & other)97 	Line(const Line &other)
98 		: QSharedData(other),
99 		  top(other.top),
100 		  bottom(other.bottom) { }
101 
height()102 	inline int height() { return bottom - top; }
103 
contains(PiecePtr piece)104 	inline bool contains(PiecePtr piece) { return top <= piece->bottom && piece->top <= bottom; }
intersects(LinePtr line)105 	inline bool intersects(LinePtr line) { return top <= line->bottom && line->top <= bottom; }
extend(int top,int bottom)106 	inline void extend(int top, int bottom) {
107 		if(top < this->top)
108 			this->top = top;
109 		if(bottom > this->bottom)
110 			this->bottom = bottom;
111 	}
112 
113 	qint32 top, bottom;
114 	qint16 baseline;
115 };
116 
117 QMap<qint32, qint32> VobSubInputProcessDialog::Frame::spaceStats;
118 
119 bool
processPieces()120 VobSubInputProcessDialog::Frame::processPieces()
121 {
122 	QImage pieceBitmap = subImage;
123 	const int width = pieceBitmap.width();
124 	const int height = pieceBitmap.height();
125 	PiecePtr piece;
126 
127 	QVector<int> ignoredColors = {pieceBitmap.pixelIndex(0, 0)};
128 	int maxAlpha = 0;
129 	for(int i = 0; i < pieceBitmap.colorCount(); i++) {
130 		const int alpha = qAlpha(pieceBitmap.color(i));
131 		if(maxAlpha < alpha)
132 			maxAlpha = alpha;
133 	}
134 	for(int i = 0; i < pieceBitmap.colorCount(); i++) {
135 		if(i == ignoredColors.at(0))
136 			continue;
137 		const QRgb color = pieceBitmap.color(i);
138 		if(qAlpha(color) < maxAlpha || qGray(color) <= 127)
139 			ignoredColors.append(i);
140 	}
141 
142 	pieces.clear();
143 
144 	// build piece by searching non-diagonal adjacent pixels, assigned pixels are
145 	// removed from pieceBitmap
146 	std::function<void(int,int)> cutPiece = [&](int x, int y){
147 		if(piece->top > y)
148 			piece->top = y;
149 		if(piece->bottom < y)
150 			piece->bottom = y;
151 
152 		if(piece->left > x)
153 			piece->left = x;
154 		if(piece->right < x)
155 			piece->right = x;
156 
157 		piece->pixels.append(QPoint(x, y));
158 		pieceBitmap.setPixel(x, y, ignoredColors.at(0));
159 
160 		if(x < width - 1 && !ignoredColors.contains(pieceBitmap.pixelIndex(x + 1, y)))
161 			cutPiece(x + 1, y);
162 		if(x > 0 && !ignoredColors.contains(pieceBitmap.pixelIndex(x - 1, y)))
163 			cutPiece(x - 1, y);
164 		if(y < height - 1 && !ignoredColors.contains(pieceBitmap.pixelIndex(x, y + 1)))
165 			cutPiece(x, y + 1);
166 		if(y > 0 && !ignoredColors.contains(pieceBitmap.pixelIndex(x, y - 1)))
167 			cutPiece(x, y - 1);
168 	};
169 
170 	// search pieces from top left
171 	for(int y = 0; y < height; y++) {
172 		for(int x = 0; x < width; x++) {
173 			if(!ignoredColors.contains(pieceBitmap.pixelIndex(x, y))) {
174 				piece = new Piece(x, y);
175 				cutPiece(x, y);
176 				pieces.append(piece);
177 			}
178 		}
179 	}
180 
181 	if(pieces.empty())
182 		return false;
183 
184 	// figure out where the lines are
185 	int maxLineHeight = 0;
186 	QVector<LinePtr> lines;
187 	foreach(piece, pieces) {
188 		foreach(LinePtr line, lines) {
189 			if(line->contains(piece)) {
190 				piece->line = line;
191 				line->extend(piece->top, piece->bottom);
192 				break;
193 			}
194 		}
195 		if(!piece->line) {
196 			piece->line = new Line(piece->top, piece->bottom);
197 			lines.append(piece->line);
198 		}
199 		if(maxLineHeight < piece->line->height())
200 			maxLineHeight = piece->line->height();
201 	}
202 
203 	// fix accents of characters going into their own line, merge short lines
204 	// that are close to next line with next line
205 	LinePtr lastLine;
206 	foreach(LinePtr line, lines) {
207 		if(lastLine && line->top - lastLine->bottom < maxLineHeight / 3 && lastLine->height() < maxLineHeight / 3) {
208 			foreach(piece, pieces) {
209 				if(piece->line == lastLine)
210 					piece->line = line;
211 			}
212 		}
213 		lastLine = line;
214 	}
215 
216 	// find out where the symbol baseline is, using most frequent bottom coordinate,
217 	// otherwise comma and apostrophe could be recognized as same character
218 	QHash<LinePtr, QHash<qint16, qint16>> bottomCount;
219 	foreach(piece, pieces)
220 		bottomCount[piece->line][piece->bottom]++;
221 	foreach(LinePtr line, lines) {
222 		qint16 max = 0;
223 		for(auto i = bottomCount[line].cbegin(); i != bottomCount[line].cend(); ++i) {
224 			if(i.value() > max) {
225 				max = i.value();
226 				line->baseline = i.key();
227 			}
228 		}
229 	}
230 	foreach(piece, pieces) {
231 		if(piece->bottom < piece->line->baseline)
232 			piece->bottom = piece->line->baseline;
233 	}
234 
235 	// sort pieces, line by line, left to right, comparison is done in Piece::operator<()
236 	std::sort(pieces.begin(), pieces.end(), [](const PiecePtr &a, const PiecePtr &b)->bool{
237 		return *a < *b;
238 	});
239 
240 	PiecePtr prevPiece;
241 	foreach(piece, pieces) {
242 		if(prevPiece && prevPiece->line == piece->line)
243 			spaceStats[piece->left - prevPiece->right]++;
244 		prevPiece = piece;
245 	}
246 
247 	return true;
248 }
249 
250 inline bool
operator <(const VobSubInputProcessDialog::LinePtr & a,const VobSubInputProcessDialog::LinePtr & b)251 operator<(const VobSubInputProcessDialog::LinePtr &a, const VobSubInputProcessDialog::LinePtr &b)
252 {
253 	return a->top < b->top;
254 }
255 
256 inline bool
operator <(const Piece & other) const257 VobSubInputProcessDialog::Piece::operator<(const Piece &other) const
258 {
259 	if(line->top < other.line->top)
260 		return true;
261 	if(line->intersects(other.line) && left < other.left)
262 		return true;
263 	return false;
264 }
265 
266 inline bool
operator ==(const Piece & other) const267 VobSubInputProcessDialog::Piece::operator==(const Piece &other) const
268 {
269 	if(bottom - top != other.bottom - other.top)
270 		return false;
271 	if(right - left != other.right - other.left)
272 		return false;
273 	if(symbolCount != other.symbolCount)
274 		return false;
275 	// we assume pixel vectors contain QPoint elements ordered exactly the same
276 	return pixels == other.pixels;
277 }
278 
279 inline VobSubInputProcessDialog::Piece &
operator +=(const Piece & other)280 VobSubInputProcessDialog::Piece::operator+=(const Piece &other)
281 {
282 	if(top > other.top)
283 		top = other.top;
284 	if(bottom < other.bottom)
285 		bottom = other.bottom;
286 
287 	if(left > other.left)
288 		left = other.left;
289 	if(right < other.right)
290 		right = other.right;
291 
292 	pixels.append(other.pixels);
293 
294 	return *this;
295 }
296 
297 // write to QDataStream
298 inline QDataStream &
operator <<(QDataStream & stream,const SubtitleComposer::VobSubInputProcessDialog::Line & line)299 operator<<(QDataStream &stream, const SubtitleComposer::VobSubInputProcessDialog::Line &line) {
300 	stream << line.top << line.bottom;
301 	return stream;
302 }
303 
304 inline QDataStream &
operator <<(QDataStream & stream,const SubtitleComposer::VobSubInputProcessDialog::Piece & piece)305 operator<<(QDataStream &stream, const SubtitleComposer::VobSubInputProcessDialog::Piece &piece) {
306 	stream << *piece.line;
307 	stream << piece.top << piece.left << piece.bottom << piece.right;
308 	stream << piece.symbolCount;
309 	stream << piece.text;
310 	stream << piece.pixels;
311 	return stream;
312 }
313 
314 // read from QDataStream
315 inline QDataStream &
operator >>(QDataStream & stream,SubtitleComposer::VobSubInputProcessDialog::Line & line)316 operator>>(QDataStream &stream, SubtitleComposer::VobSubInputProcessDialog::Line &line) {
317 	stream >> line.top >> line.bottom;
318 	return stream;
319 }
320 
321 inline QDataStream &
operator >>(QDataStream & stream,SubtitleComposer::VobSubInputProcessDialog::Piece & piece)322 operator>>(QDataStream &stream, SubtitleComposer::VobSubInputProcessDialog::Piece &piece) {
323 	piece.line = new VobSubInputProcessDialog::Line(0, 0);
324 	stream >> *piece.line;
325 	stream >> piece.top >> piece.left >> piece.bottom >> piece.right;
326 	stream >> piece.symbolCount;
327 	stream >> piece.text;
328 	stream >> piece.pixels;
329 	return stream;
330 }
331 
332 inline void
normalize()333 VobSubInputProcessDialog::Piece::normalize()
334 {
335 	if(top == 0 && left == 0)
336 		return;
337 
338 	for(auto i = pixels.begin(); i != pixels.end(); ++i) {
339 		i->rx() -= left;
340 		i->ry() -= top;
341 	}
342 
343 	right -= left;
344 	bottom -= top;
345 	top = left = 0;
346 }
347 
348 inline uint
qHash(const VobSubInputProcessDialog::Piece & piece)349 qHash(const VobSubInputProcessDialog::Piece &piece)
350 {
351 	// ignore top and left since this is used on normalized pieces
352 	return 1000 * piece.right * piece.bottom + piece.pixels.length();
353 }
354 
355 
356 
357 
358 // VobSubInputProcessDialog
VobSubInputProcessDialog(Subtitle * subtitle,QWidget * parent)359 VobSubInputProcessDialog::VobSubInputProcessDialog(Subtitle *subtitle, QWidget *parent) :
360 	QDialog(parent),
361 	ui(new Ui::VobSubInputProcessDialog),
362 	m_subtitle(subtitle),
363 	m_recognizedPiecesMaxSymbolLength(0)
364 {
365 	ui->setupUi(this);
366 
367 	connect(ui->btnOk, &QPushButton::clicked, this, &VobSubInputProcessDialog::onOkClicked);
368 	connect(ui->btnAbort, &QPushButton::clicked, this, &VobSubInputProcessDialog::onAbortClicked);
369 
370 	connect(ui->styleBold, &QPushButton::toggled, [this](bool checked){
371 		QFont font = ui->lineEdit->font();
372 		font.setBold(checked);
373 		ui->lineEdit->setFont(font);
374 	});
375 	connect(ui->styleItalic, &QPushButton::toggled, [this](bool checked){
376 		QFont font = ui->lineEdit->font();
377 		font.setItalic(checked);
378 		ui->lineEdit->setFont(font);
379 	});
380 	connect(ui->styleUnderline, &QPushButton::toggled, [this](bool checked){
381 		QFont font = ui->lineEdit->font();
382 		font.setUnderline(checked);
383 		ui->lineEdit->setFont(font);
384 	});
385 
386 	connect(ui->symbolCount, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &VobSubInputProcessDialog::onSymbolCountChanged);
387 
388 	connect(ui->btnPrevSymbol, &QPushButton::clicked, this, &VobSubInputProcessDialog::onPrevSymbolClicked);
389 	connect(ui->btnNextSymbol, &QPushButton::clicked, this, &VobSubInputProcessDialog::onNextSymbolClicked);
390 	connect(ui->btnPrevImage, &QPushButton::clicked, this, &VobSubInputProcessDialog::onPrevImageClicked);
391 	connect(ui->btnNextImage, &QPushButton::clicked, this, &VobSubInputProcessDialog::onNextImageClicked);
392 
393 	ui->lineEdit->installEventFilter(this);
394 	ui->lineEdit->setFocus();
395 }
396 
~VobSubInputProcessDialog()397 VobSubInputProcessDialog::~VobSubInputProcessDialog()
398 {
399 	delete ui;
400 }
401 
402 bool
symFileOpen(const QString & filename)403 VobSubInputProcessDialog::symFileOpen(const QString &filename)
404 {
405 	QFile file(filename);
406 	if(!file.open(QIODevice::ReadOnly))
407 		return false;
408 
409 	QTextStream stream(&file);
410 	if(stream.readLine() != QStringLiteral("SubtitleComposer Symbol Matrix v1.0"))
411 		return false;
412 
413 	m_recognizedPieces.clear();
414 	m_recognizedPiecesMaxSymbolLength = 0;
415 
416 	SString text;
417 	Piece piece;
418 	QString line;
419 	QChar ch;
420 	while(stream.readLineInto(&line)) {
421 		if(line.startsWith(QStringLiteral(".s "))) {
422 			text.setRichString(line.midRef(3).trimmed().toString());
423 		} else if(line.startsWith(QStringLiteral(".d "))) {
424 			QTextStream data(line.midRef(3).trimmed().toUtf8());
425 
426 			// read piece data
427 			data >> piece.right;
428 			do { data >> ch; } while(ch != QLatin1Char(','));
429 			data >> piece.bottom;
430 			do { data >> ch; } while(ch != QLatin1Char(','));
431 			data >> piece.symbolCount;
432 
433 			// skip to point data
434 			do { data >> ch; } while(ch != QLatin1Char(':'));
435 			data.skipWhiteSpace();
436 
437 			// read point data
438 			piece.pixels.clear();
439 			const QByteArray pixelData(qUncompress(QByteArray::fromBase64(data.readAll().toUtf8(), QByteArray::Base64Encoding | QByteArray::OmitTrailingEquals)));
440 			QDataStream pixelDataStream(pixelData);
441 			while(!pixelDataStream.atEnd()) {
442 				int x, y;
443 				pixelDataStream >> x >> y;
444 				piece.pixels.append(QPoint(x, y));
445 			}
446 
447 			// save piece
448 			if(piece.symbolCount > m_recognizedPiecesMaxSymbolLength)
449 				m_recognizedPiecesMaxSymbolLength = piece.symbolCount;
450 			m_recognizedPieces[piece] = text;
451 
452 			text.clear();
453 		}
454 	}
455 
456 	file.close();
457 
458 	return true;
459 }
460 
461 bool
symFileSave(const QString & filename)462 VobSubInputProcessDialog::symFileSave(const QString &filename)
463 {
464 	QSaveFile file(filename);
465 	if(!file.open(QIODevice::WriteOnly))
466 		return false;
467 
468 	QTextStream stream(&file);
469 	stream << QStringLiteral("SubtitleComposer Symbol Matrix v1.0\n");
470 	for(auto i = m_recognizedPieces.cbegin(); i != m_recognizedPieces.cend(); ++i) {
471 		if(!i.value().length())
472 			continue;
473 		Piece piece = i.key();
474 		stream << "\n.s " << i.value().richString();
475 		stream << QString::asprintf("\n.d %d, %d, %d: ", i.key().right, i.key().bottom, i.key().symbolCount);
476 		QByteArray pixelData;
477 		QDataStream pixelDataStream(&pixelData, QIODevice::WriteOnly);
478 		foreach(QPoint p, i.key().pixels)
479 			pixelDataStream << p.x() << p.y();
480 		stream << qCompress(pixelData).toBase64(QByteArray::Base64Encoding | QByteArray::OmitTrailingEquals) << '\n';
481 	}
482 	return file.commit();
483 }
484 
485 /*virtual*/ bool
eventFilter(QObject * obj,QEvent * event)486 VobSubInputProcessDialog::eventFilter(QObject *obj, QEvent *event) /*override*/
487 {
488 	if(event->type() == QEvent::FocusOut) {
489 		ui->lineEdit->setFocus();
490 		return true;
491 	}
492 
493 	if(event->type() == QEvent::KeyPress) {
494 		QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
495 		switch(keyEvent->key()) {
496 		case Qt::Key_Up:
497 			ui->symbolCount->setValue(ui->symbolCount->value() + ui->symbolCount->singleStep());
498 			return true;
499 
500 		case Qt::Key_Down:
501 			ui->symbolCount->setValue(ui->symbolCount->value() - ui->symbolCount->singleStep());
502 			return true;
503 
504 		case Qt::Key_Left:
505 			if((keyEvent->modifiers() & Qt::ControlModifier) == 0)
506 				break;
507 			if((keyEvent->modifiers() & Qt::ShiftModifier) == 0)
508 				QMetaObject::invokeMethod(this, "onPrevSymbolClicked", Qt::QueuedConnection);
509 			else
510 				QMetaObject::invokeMethod(this, "onPrevImageClicked", Qt::QueuedConnection);
511 			return true;
512 
513 		case Qt::Key_Right:
514 			if((keyEvent->modifiers() & Qt::ControlModifier) == 0)
515 				break;
516 			if((keyEvent->modifiers() & Qt::ShiftModifier) == 0)
517 				QMetaObject::invokeMethod(this, "onNextSymbolClicked", Qt::QueuedConnection);
518 			else
519 				QMetaObject::invokeMethod(this, "onNextImageClicked", Qt::QueuedConnection);
520 			return true;
521 
522 		case Qt::Key_Space:
523 		case Qt::Key_Escape:
524 			return true;
525 
526 		case Qt::Key_B:
527 			if((keyEvent->modifiers() & Qt::ControlModifier) == 0)
528 				break;
529 			ui->styleBold->toggle();
530 			return true;
531 
532 		case Qt::Key_I:
533 			if((keyEvent->modifiers() & Qt::ControlModifier) == 0)
534 				break;
535 			ui->styleItalic->toggle();
536 			return true;
537 
538 		case Qt::Key_U:
539 			if((keyEvent->modifiers() & Qt::ControlModifier) == 0)
540 				break;
541 			ui->styleUnderline->toggle();
542 			return true;
543 		}
544 	}
545 
546 	return QDialog::eventFilter(obj, event);
547 }
548 
549 void
processFrames(StreamProcessor * streamProcessor)550 VobSubInputProcessDialog::processFrames(StreamProcessor *streamProcessor)
551 {
552 	connect(streamProcessor, &StreamProcessor::streamError, this, &VobSubInputProcessDialog::onStreamError);
553 	connect(streamProcessor, &StreamProcessor::streamFinished, this, &VobSubInputProcessDialog::onStreamFinished);
554 	connect(streamProcessor, &StreamProcessor::imageDataAvailable, this, &VobSubInputProcessDialog::onStreamData, Qt::BlockingQueuedConnection);
555 
556 	streamProcessor->start();
557 
558 	Frame::spaceStats.clear();
559 
560 	ui->progressBar->setMinimum(0);
561 	ui->progressBar->setValue(0);
562 
563 	ui->grpText->setDisabled(true);
564 	ui->grpNavButtons->setDisabled(true);
565 }
566 
567 void
onStreamData(const QImage & image,quint64 msecStart,quint64 msecDuration)568 VobSubInputProcessDialog::onStreamData(const QImage &image, quint64 msecStart, quint64 msecDuration)
569 {
570 	FramePtr frame(new Frame());
571 	frame->subShowTime.setMillisTime(double(msecStart));
572 	frame->subHideTime.setMillisTime(double(msecStart + msecDuration));
573 	frame->subImage = image;
574 
575 	ui->subtitleView->setPixmap(QPixmap::fromImage(frame->subImage));
576 	QCoreApplication::processEvents();
577 
578 	if(frame->processPieces()) {
579 		frame->index = m_frames.length();
580 		ui->progressBar->setMaximum(m_frames.length());
581 		m_frames.append(frame);
582 	}
583 }
584 
585 void
onStreamError(int,const QString & message,const QString & debug)586 VobSubInputProcessDialog::onStreamError(int /*code*/, const QString &message, const QString &debug)
587 {
588 	QString text = message % QStringLiteral("\n") % debug;
589 	KMessageBox::error(this, text, i18n("VobSub Error"));
590 }
591 
592 void
onStreamFinished()593 VobSubInputProcessDialog::onStreamFinished()
594 {
595 	m_frameCurrent = m_frames.begin() - 1;
596 
597 	// average word length in english is 5.1 chars
598 	const double avgWordLength = 4;
599 
600 	if(!Frame::spaceStats.empty()) {
601 		auto itChar = Frame::spaceStats.begin(); // shorter spaces on start
602 		auto itWord = Frame::spaceStats.end() - 1; // longer spaces near end
603 		qint64 charSpacingSum = itChar.key() * itChar.value();
604 		quint64 charSpacingCount = itChar.value();
605 		qint64 wordSpacingSum = itWord.key() * itWord.value();
606 		quint64 wordSpacingCount = itWord.value();
607 
608 		while(itChar != itWord) {
609 			if(charSpacingCount < avgWordLength * wordSpacingCount) {
610 				// sum up chars
611 				++itChar;
612 				charSpacingSum += itChar.key() * itChar.value();
613 				charSpacingCount += itChar.value();
614 			} else {
615 				// sum up words
616 				--itWord;
617 				wordSpacingSum += itWord.key() * itWord.value();
618 				wordSpacingCount += itWord.value();
619 			}
620 		}
621 		m_spaceWidth = wordSpacingSum / wordSpacingCount;
622 	} else {
623 		m_spaceWidth = 100;
624 	}
625 
626 	ui->grpText->setDisabled(true);
627 	ui->grpNavButtons->setDisabled(true);
628 	QMetaObject::invokeMethod(this, "processNextImage", Qt::QueuedConnection);
629 }
630 
631 void
processNextImage()632 VobSubInputProcessDialog::processNextImage()
633 {
634 	if(++m_frameCurrent == m_frames.end()) {
635 		accept();
636 		return;
637 	}
638 
639 	ui->progressBar->setValue((*m_frameCurrent)->index + 1);
640 
641 	ui->subtitleView->setPixmap(QPixmap::fromImage((*m_frameCurrent)->subImage));
642 
643 	m_pieces = (*m_frameCurrent)->pieces;
644 	m_pieceCurrent = m_pieces.begin();
645 
646 	recognizePiece();
647 }
648 
649 void
processCurrentPiece()650 VobSubInputProcessDialog::processCurrentPiece()
651 {
652 	if(m_pieceCurrent == m_pieces.end())
653 		return;
654 
655 	ui->grpText->setDisabled(false);
656 	ui->grpNavButtons->setDisabled(false);
657 
658 	QPixmap pixmap = QPixmap::fromImage((*m_frameCurrent)->subImage);
659 	QPainter p(&pixmap);
660 
661 	QList<PiecePtr>::iterator i = m_pieceCurrent;
662 	p.setPen(QColor(255, 255, 255, 64));
663 	p.drawLine(0, (*i)->line->baseline, pixmap.width(), (*i)->line->baseline);
664 
665 	p.setPen(QColor(255, 0, 0, 200));
666 	QRect rcVisible(QPoint((*i)->left, (*i)->top), QPoint((*i)->right, (*i)->bottom));
667 	int n = (*i)->symbolCount;
668 	for(; n-- && i != m_pieces.end(); ++i) {
669 		rcVisible |= QRect(QPoint((*i)->left, (*i)->top), QPoint((*i)->right, (*i)->bottom));
670 		foreach(QPoint pix, (*i)->pixels)
671 			p.drawPoint(pix);
672 	}
673 	rcVisible.adjust((ui->subtitleView->minimumWidth() - rcVisible.width()) / -2, (ui->subtitleView->minimumHeight() - rcVisible.height()) / -2, 0, 0);
674 	rcVisible.setBottomRight(QPoint(pixmap.width(), pixmap.height()));
675 	ui->subtitleView->setPixmap(pixmap.copy(rcVisible));
676 
677 	ui->lineEdit->setFocus();
678 
679 	ui->symbolCount->setMaximum(m_pieces.end() - m_pieceCurrent);
680 }
681 
682 void
processNextPiece()683 VobSubInputProcessDialog::processNextPiece()
684 {
685 	m_pieceCurrent += (*m_pieceCurrent)->symbolCount;
686 
687 	ui->lineEdit->clear();
688 	ui->symbolCount->setValue(1);
689 
690 	if(m_pieceCurrent == m_pieces.end()) {
691 		QString subText;
692 		PiecePtr piecePrev;
693 		foreach(PiecePtr piece, m_pieces) {
694 			if(piecePrev) {
695 				if(!piecePrev->line->intersects(piece->line))
696 					subText.append(QChar(QChar::LineFeed));
697 				else if(piece->left - piecePrev->right > m_spaceWidth)
698 					subText.append(QChar(QChar::Space));
699 			}
700 
701 			subText += piece->text;
702 			piecePrev = piece;
703 		}
704 
705 		SubtitleLine *l = new SubtitleLine((*m_frameCurrent)->subShowTime, (*m_frameCurrent)->subHideTime);
706 		l->primaryDoc()->setPlainText(subText);
707 		m_subtitle->insertLine(l);
708 
709 		ui->grpText->setDisabled(true);
710 		ui->grpNavButtons->setDisabled(true);
711 		QMetaObject::invokeMethod(this, "processNextImage", Qt::QueuedConnection);
712 		return;
713 	}
714 
715 	recognizePiece();
716 }
717 
718 void
recognizePiece()719 VobSubInputProcessDialog::recognizePiece()
720 {
721 	for(int len = m_recognizedPiecesMaxSymbolLength; len > 0; len--) {
722 		PiecePtr normal = currentNormalizedPiece(len);
723 		if(len != normal->symbolCount)
724 			continue;
725 		if(m_recognizedPieces.contains(*normal)) {
726 			const SString text = m_recognizedPieces.value(*normal);
727 			(*m_pieceCurrent)->text = text;
728 			currentSymbolCountSet(len);
729 			processNextPiece();
730 			return;
731 		}
732 	}
733 
734 	processCurrentPiece();
735 }
736 
737 SString
currentText()738 VobSubInputProcessDialog::currentText()
739 {
740 	int style = 0;
741 	if(ui->styleBold->isChecked())
742 		style |= SString::Bold;
743 	if(ui->styleItalic->isChecked())
744 		style |= SString::Italic;
745 	if(ui->styleUnderline->isChecked())
746 		style |= SString::Underline;
747 	return SString(ui->lineEdit->text(), style);
748 }
749 
750 VobSubInputProcessDialog::PiecePtr
currentNormalizedPiece(int symbolCount)751 VobSubInputProcessDialog::currentNormalizedPiece(int symbolCount)
752 {
753 	PiecePtr normal(new Piece(**m_pieceCurrent));
754 	normal->symbolCount = 1;
755 	for(auto piece = m_pieceCurrent; --symbolCount && ++piece != m_pieces.end(); ) {
756 		*normal += **piece;
757 		normal->symbolCount++;
758 	}
759 
760 	normal->normalize();
761 
762 	return normal;
763 }
764 
765 void
currentSymbolCountSet(int symbolCount)766 VobSubInputProcessDialog::currentSymbolCountSet(int symbolCount)
767 {
768 	if(m_pieceCurrent == m_pieces.end())
769 		return;
770 
771 	int n = (*m_pieceCurrent)->symbolCount;
772 	QList<PiecePtr>::iterator piece = m_pieceCurrent;
773 	while(--n && ++piece != m_pieces.end())
774 		(*piece)->symbolCount = 1;
775 
776 	piece = m_pieceCurrent;
777 	(*piece)->symbolCount = symbolCount;
778 	while(--symbolCount && ++piece != m_pieces.end())
779 		(*piece)->symbolCount = 0;
780 }
781 
782 void
onSymbolCountChanged(int symbolCount)783 VobSubInputProcessDialog::onSymbolCountChanged(int symbolCount)
784 {
785 	currentSymbolCountSet(symbolCount);
786 
787 	processCurrentPiece();
788 }
789 
790 void
onOkClicked()791 VobSubInputProcessDialog::onOkClicked()
792 {
793 	if((*m_pieceCurrent)->symbolCount > m_recognizedPiecesMaxSymbolLength)
794 		m_recognizedPiecesMaxSymbolLength = (*m_pieceCurrent)->symbolCount;
795 
796 	(*m_pieceCurrent)->text = currentText();
797 
798 	PiecePtr normal = currentNormalizedPiece((*m_pieceCurrent)->symbolCount);
799 	m_recognizedPieces[*normal] = (*m_pieceCurrent)->text;
800 
801 	processNextPiece();
802 }
803 
804 void
onAbortClicked()805 VobSubInputProcessDialog::onAbortClicked()
806 {
807 	reject();
808 }
809 
810 void
onPrevImageClicked()811 VobSubInputProcessDialog::onPrevImageClicked()
812 {
813 	if(m_frameCurrent == m_frames.begin())
814 		return;
815 
816 	--m_frameCurrent;
817 	if(m_subtitle->lastIndex() >= 0)
818 		m_subtitle->removeLines(RangeList(Range(m_subtitle->lastIndex())), Both);
819 
820 	ui->progressBar->setValue((*m_frameCurrent)->index + 1);
821 
822 	m_pieces = (*m_frameCurrent)->pieces;
823 	m_pieceCurrent = m_pieces.end();
824 
825 	onPrevSymbolClicked();
826 }
827 
828 void
onNextImageClicked()829 VobSubInputProcessDialog::onNextImageClicked()
830 {
831 	if(m_frameCurrent == m_frames.end() - 1)
832 		return;
833 
834 	++m_frameCurrent;
835 
836 	ui->progressBar->setValue((*m_frameCurrent)->index + 1);
837 
838 	m_pieces = (*m_frameCurrent)->pieces;
839 	m_pieceCurrent = m_pieces.begin() - 1;
840 
841 	onNextSymbolClicked();
842 }
843 
844 void
onPrevSymbolClicked()845 VobSubInputProcessDialog::onPrevSymbolClicked()
846 {
847 	do {
848 		if(m_pieceCurrent == m_pieces.begin())
849 			return onPrevImageClicked();
850 		--m_pieceCurrent;
851 	} while((*m_pieceCurrent)->symbolCount == 0);
852 
853 	updateCurrentPiece();
854 }
855 
856 void
onNextSymbolClicked()857 VobSubInputProcessDialog::onNextSymbolClicked()
858 {
859 	do {
860 		if(m_pieceCurrent >= m_pieces.end() - 1)
861 			return onNextImageClicked();
862 		++m_pieceCurrent;
863 	} while((*m_pieceCurrent)->symbolCount == 0);
864 
865 	updateCurrentPiece();
866 }
867 
868 void
updateCurrentPiece()869 VobSubInputProcessDialog::updateCurrentPiece()
870 {
871 	processCurrentPiece();
872 
873 	ui->lineEdit->setText((*m_pieceCurrent)->text.string());
874 	ui->lineEdit->selectAll();
875 
876 	int style = (*m_pieceCurrent)->text.styleFlagsAt(0);
877 	ui->styleBold->setChecked((style & SString::Bold) != 0);
878 	ui->styleItalic->setChecked((style & SString::Italic) != 0);
879 	ui->styleUnderline->setChecked((style & SString::Underline) != 0);
880 
881 	ui->symbolCount->setValue((*m_pieceCurrent)->symbolCount);
882 }
883