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