File indexing completed on 2024-12-22 04:40:09

0001 /*
0002     SPDX-FileCopyrightText: 2017-2022 Mladen Milinkovic <max@smoothware.net>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "vobsubinputprocessdialog.h"
0008 #include "ui_vobsubinputprocessdialog.h"
0009 
0010 #include "core/richtext/richdocument.h"
0011 
0012 #include <functional>
0013 
0014 #include <QDebug>
0015 #include <QPainter>
0016 #include <QKeyEvent>
0017 
0018 #include <KMessageBox>
0019 
0020 #include <QStringBuilder>
0021 #include <QDataStream>
0022 #include <QFile>
0023 #include <QSaveFile>
0024 #include <QStringView>
0025 
0026 using namespace SubtitleComposer;
0027 
0028 // Private helper classes
0029 class VobSubInputProcessDialog::Frame : public QSharedData
0030 {
0031 public:
0032     Frame() {}
0033     Frame(const Frame &other) : QSharedData(other) {}
0034     ~Frame() {}
0035 
0036     bool processPieces();
0037 
0038     static QMap<qint32, qint32> spaceStats;
0039 
0040     quint32 index;
0041     QImage subImage;
0042     Time subShowTime;
0043     Time subHideTime;
0044     QList<PiecePtr> pieces;
0045 };
0046 
0047 class VobSubInputProcessDialog::Piece : public QSharedData
0048 {
0049 public:
0050     Piece()
0051         : line(nullptr),
0052           top(0),
0053           left(0),
0054           bottom(0),
0055           right(0),
0056           symbolCount(1) { }
0057     Piece(int x, int y)
0058         : line(nullptr),
0059           top(y),
0060           left(x),
0061           bottom(y),
0062           right(x),
0063           symbolCount(1) { }
0064     Piece(const Piece &other)
0065         : QSharedData(other),
0066           line(other.line),
0067           top(other.top),
0068           left(other.left),
0069           bottom(other.bottom),
0070           right(other.right),
0071           symbolCount(other.symbolCount),
0072           pixels(other.pixels) { }
0073     ~Piece() { }
0074 
0075     inline int height() {
0076         return bottom - top + 1;
0077     }
0078 
0079     inline bool operator<(const Piece &other) const;
0080     inline bool operator==(const Piece &other) const;
0081     inline Piece & operator+=(const Piece &other);
0082 
0083 
0084     inline void normalize();
0085 
0086     LinePtr line;
0087     qint32 top, left, bottom, right;
0088     qint32 symbolCount;
0089     RichString text;
0090     QVector<QPoint> pixels;
0091 };
0092 
0093 class VobSubInputProcessDialog::Line : public QSharedData {
0094 public:
0095     Line(int top, int bottom)
0096         : top(top),
0097           bottom(bottom) { }
0098     Line(const Line &other)
0099         : QSharedData(other),
0100           top(other.top),
0101           bottom(other.bottom) { }
0102 
0103     inline int height() { return bottom - top; }
0104 
0105     inline bool contains(PiecePtr piece) { return top <= piece->bottom && piece->top <= bottom; }
0106     inline bool intersects(LinePtr line) { return top <= line->bottom && line->top <= bottom; }
0107     inline void extend(int top, int bottom) {
0108         if(top < this->top)
0109             this->top = top;
0110         if(bottom > this->bottom)
0111             this->bottom = bottom;
0112     }
0113 
0114     qint32 top, bottom;
0115     qint16 baseline;
0116 };
0117 
0118 QMap<qint32, qint32> VobSubInputProcessDialog::Frame::spaceStats;
0119 
0120 bool
0121 VobSubInputProcessDialog::Frame::processPieces()
0122 {
0123     QImage pieceBitmap = subImage;
0124     const int width = pieceBitmap.width();
0125     const int height = pieceBitmap.height();
0126     PiecePtr piece;
0127 
0128     QVector<int> ignoredColors = {pieceBitmap.pixelIndex(0, 0)};
0129     int maxAlpha = 0;
0130     for(int i = 0; i < pieceBitmap.colorCount(); i++) {
0131         const int alpha = qAlpha(pieceBitmap.color(i));
0132         if(maxAlpha < alpha)
0133             maxAlpha = alpha;
0134     }
0135     for(int i = 0; i < pieceBitmap.colorCount(); i++) {
0136         if(i == ignoredColors.at(0))
0137             continue;
0138         const QRgb color = pieceBitmap.color(i);
0139         if(qAlpha(color) < maxAlpha || qGray(color) <= 127)
0140             ignoredColors.append(i);
0141     }
0142 
0143     pieces.clear();
0144 
0145     // build piece by searching non-diagonal adjacent pixels, assigned pixels are
0146     // removed from pieceBitmap
0147     std::function<void(int,int)> cutPiece = [&](int x, int y){
0148         if(piece->top > y)
0149             piece->top = y;
0150         if(piece->bottom < y)
0151             piece->bottom = y;
0152 
0153         if(piece->left > x)
0154             piece->left = x;
0155         if(piece->right < x)
0156             piece->right = x;
0157 
0158         piece->pixels.append(QPoint(x, y));
0159         pieceBitmap.setPixel(x, y, ignoredColors.at(0));
0160 
0161         if(x < width - 1 && !ignoredColors.contains(pieceBitmap.pixelIndex(x + 1, y)))
0162             cutPiece(x + 1, y);
0163         if(x > 0 && !ignoredColors.contains(pieceBitmap.pixelIndex(x - 1, y)))
0164             cutPiece(x - 1, y);
0165         if(y < height - 1 && !ignoredColors.contains(pieceBitmap.pixelIndex(x, y + 1)))
0166             cutPiece(x, y + 1);
0167         if(y > 0 && !ignoredColors.contains(pieceBitmap.pixelIndex(x, y - 1)))
0168             cutPiece(x, y - 1);
0169     };
0170 
0171     // search pieces from top left
0172     for(int y = 0; y < height; y++) {
0173         for(int x = 0; x < width; x++) {
0174             if(!ignoredColors.contains(pieceBitmap.pixelIndex(x, y))) {
0175                 piece = new Piece(x, y);
0176                 cutPiece(x, y);
0177                 pieces.append(piece);
0178             }
0179         }
0180     }
0181 
0182     if(pieces.empty())
0183         return false;
0184 
0185     // figure out where the lines are
0186     int maxLineHeight = 0;
0187     QVector<LinePtr> lines;
0188     foreach(piece, pieces) {
0189         foreach(LinePtr line, lines) {
0190             if(line->contains(piece)) {
0191                 piece->line = line;
0192                 line->extend(piece->top, piece->bottom);
0193                 break;
0194             }
0195         }
0196         if(!piece->line) {
0197             piece->line = new Line(piece->top, piece->bottom);
0198             lines.append(piece->line);
0199         }
0200         if(maxLineHeight < piece->line->height())
0201             maxLineHeight = piece->line->height();
0202     }
0203 
0204     // fix accents of characters going into their own line, merge short lines
0205     // that are close to next line with next line
0206     LinePtr lastLine;
0207     foreach(LinePtr line, lines) {
0208         if(lastLine && line->top - lastLine->bottom < maxLineHeight / 3 && lastLine->height() < maxLineHeight / 3) {
0209             foreach(piece, pieces) {
0210                 if(piece->line == lastLine)
0211                     piece->line = line;
0212             }
0213         }
0214         lastLine = line;
0215     }
0216 
0217     // find out where the symbol baseline is, using most frequent bottom coordinate,
0218     // otherwise comma and apostrophe could be recognized as same character
0219     QHash<LinePtr, QHash<qint16, qint16>> bottomCount;
0220     foreach(piece, pieces)
0221         bottomCount[piece->line][piece->bottom]++;
0222     foreach(LinePtr line, lines) {
0223         qint16 max = 0;
0224         for(auto i = bottomCount[line].cbegin(); i != bottomCount[line].cend(); ++i) {
0225             if(i.value() > max) {
0226                 max = i.value();
0227                 line->baseline = i.key();
0228             }
0229         }
0230     }
0231     foreach(piece, pieces) {
0232         if(piece->bottom < piece->line->baseline)
0233             piece->bottom = piece->line->baseline;
0234     }
0235 
0236     // sort pieces, line by line, left to right, comparison is done in Piece::operator<()
0237     std::sort(pieces.begin(), pieces.end(), [](const PiecePtr &a, const PiecePtr &b)->bool{
0238         return *a < *b;
0239     });
0240 
0241     PiecePtr prevPiece;
0242     foreach(piece, pieces) {
0243         if(prevPiece && prevPiece->line == piece->line)
0244             spaceStats[piece->left - prevPiece->right]++;
0245         prevPiece = piece;
0246     }
0247 
0248     return true;
0249 }
0250 
0251 inline bool
0252 VobSubInputProcessDialog::Piece::operator<(const Piece &other) const
0253 {
0254     if(line->top < other.line->top)
0255         return true;
0256     if(line->intersects(other.line) && left < other.left)
0257         return true;
0258     return false;
0259 }
0260 
0261 inline bool
0262 VobSubInputProcessDialog::Piece::operator==(const Piece &other) const
0263 {
0264     if(bottom - top != other.bottom - other.top)
0265         return false;
0266     if(right - left != other.right - other.left)
0267         return false;
0268     if(symbolCount != other.symbolCount)
0269         return false;
0270     // we assume pixel vectors contain QPoint elements ordered exactly the same
0271     return pixels == other.pixels;
0272 }
0273 
0274 inline VobSubInputProcessDialog::Piece &
0275 VobSubInputProcessDialog::Piece::operator+=(const Piece &other)
0276 {
0277     if(top > other.top)
0278         top = other.top;
0279     if(bottom < other.bottom)
0280         bottom = other.bottom;
0281 
0282     if(left > other.left)
0283         left = other.left;
0284     if(right < other.right)
0285         right = other.right;
0286 
0287     pixels.append(other.pixels);
0288 
0289     return *this;
0290 }
0291 
0292 // write to QDataStream
0293 inline QDataStream &
0294 operator<<(QDataStream &stream, const SubtitleComposer::VobSubInputProcessDialog::Line &line) {
0295     stream << line.top << line.bottom;
0296     return stream;
0297 }
0298 
0299 inline QDataStream &
0300 operator<<(QDataStream &stream, const SubtitleComposer::VobSubInputProcessDialog::Piece &piece) {
0301     stream << *piece.line;
0302     stream << piece.top << piece.left << piece.bottom << piece.right;
0303     stream << piece.symbolCount;
0304     stream << piece.text;
0305     stream << piece.pixels;
0306     return stream;
0307 }
0308 
0309 // read from QDataStream
0310 inline QDataStream &
0311 operator>>(QDataStream &stream, SubtitleComposer::VobSubInputProcessDialog::Line &line) {
0312     stream >> line.top >> line.bottom;
0313     return stream;
0314 }
0315 
0316 inline QDataStream &
0317 operator>>(QDataStream &stream, SubtitleComposer::VobSubInputProcessDialog::Piece &piece) {
0318     piece.line = new VobSubInputProcessDialog::Line(0, 0);
0319     stream >> *piece.line;
0320     stream >> piece.top >> piece.left >> piece.bottom >> piece.right;
0321     stream >> piece.symbolCount;
0322     stream >> piece.text;
0323     stream >> piece.pixels;
0324     return stream;
0325 }
0326 
0327 inline void
0328 VobSubInputProcessDialog::Piece::normalize()
0329 {
0330     if(top == 0 && left == 0)
0331         return;
0332 
0333     for(auto i = pixels.begin(); i != pixels.end(); ++i) {
0334         i->rx() -= left;
0335         i->ry() -= top;
0336     }
0337 
0338     right -= left;
0339     bottom -= top;
0340     top = left = 0;
0341 }
0342 
0343 inline uint
0344 qHash(const VobSubInputProcessDialog::Piece &piece)
0345 {
0346     // ignore top and left since this is used on normalized pieces
0347     return 1000 * piece.right * piece.bottom + piece.pixels.length();
0348 }
0349 
0350 
0351 
0352 
0353 // VobSubInputProcessDialog
0354 VobSubInputProcessDialog::VobSubInputProcessDialog(Subtitle *subtitle, QWidget *parent) :
0355     QDialog(parent),
0356     ui(new Ui::VobSubInputProcessDialog),
0357     m_subtitle(subtitle),
0358     m_recognizedPiecesMaxSymbolLength(0)
0359 {
0360     ui->setupUi(this);
0361 
0362     connect(ui->btnOk, &QPushButton::clicked, this, &VobSubInputProcessDialog::onOkClicked);
0363     connect(ui->btnAbort, &QPushButton::clicked, this, &VobSubInputProcessDialog::onAbortClicked);
0364 
0365     connect(ui->styleBold, &QPushButton::toggled, this, [this](bool checked){
0366         QFont font = ui->lineEdit->font();
0367         font.setBold(checked);
0368         ui->lineEdit->setFont(font);
0369     });
0370     connect(ui->styleItalic, &QPushButton::toggled, this, [this](bool checked){
0371         QFont font = ui->lineEdit->font();
0372         font.setItalic(checked);
0373         ui->lineEdit->setFont(font);
0374     });
0375     connect(ui->styleUnderline, &QPushButton::toggled, this, [this](bool checked){
0376         QFont font = ui->lineEdit->font();
0377         font.setUnderline(checked);
0378         ui->lineEdit->setFont(font);
0379     });
0380 
0381     connect(ui->symbolCount, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &VobSubInputProcessDialog::onSymbolCountChanged);
0382 
0383     connect(ui->btnPrevSymbol, &QPushButton::clicked, this, &VobSubInputProcessDialog::onPrevSymbolClicked);
0384     connect(ui->btnNextSymbol, &QPushButton::clicked, this, &VobSubInputProcessDialog::onNextSymbolClicked);
0385     connect(ui->btnPrevImage, &QPushButton::clicked, this, &VobSubInputProcessDialog::onPrevImageClicked);
0386     connect(ui->btnNextImage, &QPushButton::clicked, this, &VobSubInputProcessDialog::onNextImageClicked);
0387 
0388     ui->lineEdit->installEventFilter(this);
0389     ui->lineEdit->setFocus();
0390 }
0391 
0392 VobSubInputProcessDialog::~VobSubInputProcessDialog()
0393 {
0394     delete ui;
0395 }
0396 
0397 bool
0398 VobSubInputProcessDialog::symFileOpen(const QString &filename)
0399 {
0400     QFile file(filename);
0401     if(!file.open(QIODevice::ReadOnly))
0402         return false;
0403 
0404     QTextStream stream(&file);
0405     if(stream.readLine() != QStringLiteral("SubtitleComposer Symbol Matrix v1.0"))
0406         return false;
0407 
0408     m_recognizedPieces.clear();
0409     m_recognizedPiecesMaxSymbolLength = 0;
0410 
0411     RichString text;
0412     Piece piece;
0413     QString line;
0414     QChar ch;
0415     while(stream.readLineInto(&line)) {
0416         if(line.startsWith(QStringLiteral(".s "))) {
0417             text.setRichString(QStringView(line).mid(3).trimmed().toString());
0418         } else if(line.startsWith(QStringLiteral(".d "))) {
0419             QTextStream data(QStringView(line).mid(3).trimmed().toUtf8());
0420 
0421             // read piece data
0422             data >> piece.right;
0423             do { data >> ch; } while(ch != QLatin1Char(','));
0424             data >> piece.bottom;
0425             do { data >> ch; } while(ch != QLatin1Char(','));
0426             data >> piece.symbolCount;
0427 
0428             // skip to point data
0429             do { data >> ch; } while(ch != QLatin1Char(':'));
0430             data.skipWhiteSpace();
0431 
0432             // read point data
0433             piece.pixels.clear();
0434             const QByteArray pixelData(qUncompress(QByteArray::fromBase64(data.readAll().toUtf8(), QByteArray::Base64Encoding | QByteArray::OmitTrailingEquals)));
0435             QDataStream pixelDataStream(pixelData);
0436             while(!pixelDataStream.atEnd()) {
0437                 int x, y;
0438                 pixelDataStream >> x >> y;
0439                 piece.pixels.append(QPoint(x, y));
0440             }
0441 
0442             // save piece
0443             if(piece.symbolCount > m_recognizedPiecesMaxSymbolLength)
0444                 m_recognizedPiecesMaxSymbolLength = piece.symbolCount;
0445             m_recognizedPieces[piece] = text;
0446 
0447             text.clear();
0448         }
0449     }
0450 
0451     file.close();
0452 
0453     return true;
0454 }
0455 
0456 bool
0457 VobSubInputProcessDialog::symFileSave(const QString &filename)
0458 {
0459     QSaveFile file(filename);
0460     if(!file.open(QIODevice::WriteOnly))
0461         return false;
0462 
0463     QTextStream stream(&file);
0464     stream << QStringLiteral("SubtitleComposer Symbol Matrix v1.0\n");
0465     for(auto i = m_recognizedPieces.cbegin(); i != m_recognizedPieces.cend(); ++i) {
0466         if(!i.value().length())
0467             continue;
0468         Piece piece = i.key();
0469         stream << "\n.s " << i.value().richString();
0470         stream << QString::asprintf("\n.d %d, %d, %d: ", i.key().right, i.key().bottom, i.key().symbolCount);
0471         QByteArray pixelData;
0472         QDataStream pixelDataStream(&pixelData, QIODevice::WriteOnly);
0473         foreach(QPoint p, i.key().pixels)
0474             pixelDataStream << p.x() << p.y();
0475         stream << qCompress(pixelData).toBase64(QByteArray::Base64Encoding | QByteArray::OmitTrailingEquals) << '\n';
0476     }
0477     return file.commit();
0478 }
0479 
0480 /*virtual*/ bool
0481 VobSubInputProcessDialog::eventFilter(QObject *obj, QEvent *event) /*override*/
0482 {
0483     if(event->type() == QEvent::FocusOut) {
0484         ui->lineEdit->setFocus();
0485         return true;
0486     }
0487 
0488     if(event->type() == QEvent::KeyPress) {
0489         QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
0490         switch(keyEvent->key()) {
0491         case Qt::Key_Up:
0492             ui->symbolCount->setValue(ui->symbolCount->value() + ui->symbolCount->singleStep());
0493             return true;
0494 
0495         case Qt::Key_Down:
0496             ui->symbolCount->setValue(ui->symbolCount->value() - ui->symbolCount->singleStep());
0497             return true;
0498 
0499         case Qt::Key_Left:
0500             if((keyEvent->modifiers() & Qt::ControlModifier) == 0)
0501                 break;
0502             if((keyEvent->modifiers() & Qt::ShiftModifier) == 0)
0503                 QMetaObject::invokeMethod(this, "onPrevSymbolClicked", Qt::QueuedConnection);
0504             else
0505                 QMetaObject::invokeMethod(this, "onPrevImageClicked", Qt::QueuedConnection);
0506             return true;
0507 
0508         case Qt::Key_Right:
0509             if((keyEvent->modifiers() & Qt::ControlModifier) == 0)
0510                 break;
0511             if((keyEvent->modifiers() & Qt::ShiftModifier) == 0)
0512                 QMetaObject::invokeMethod(this, "onNextSymbolClicked", Qt::QueuedConnection);
0513             else
0514                 QMetaObject::invokeMethod(this, "onNextImageClicked", Qt::QueuedConnection);
0515             return true;
0516 
0517         case Qt::Key_Space:
0518         case Qt::Key_Escape:
0519             return true;
0520 
0521         case Qt::Key_B:
0522             if((keyEvent->modifiers() & Qt::ControlModifier) == 0)
0523                 break;
0524             ui->styleBold->toggle();
0525             return true;
0526 
0527         case Qt::Key_I:
0528             if((keyEvent->modifiers() & Qt::ControlModifier) == 0)
0529                 break;
0530             ui->styleItalic->toggle();
0531             return true;
0532 
0533         case Qt::Key_U:
0534             if((keyEvent->modifiers() & Qt::ControlModifier) == 0)
0535                 break;
0536             ui->styleUnderline->toggle();
0537             return true;
0538         }
0539     }
0540 
0541     return QDialog::eventFilter(obj, event);
0542 }
0543 
0544 void
0545 VobSubInputProcessDialog::processFrames(StreamProcessor *streamProcessor)
0546 {
0547     connect(streamProcessor, &StreamProcessor::streamError, this, &VobSubInputProcessDialog::onStreamError);
0548     connect(streamProcessor, &StreamProcessor::streamFinished, this, &VobSubInputProcessDialog::onStreamFinished);
0549     connect(streamProcessor, &StreamProcessor::imageDataAvailable, this, &VobSubInputProcessDialog::onStreamData, Qt::BlockingQueuedConnection);
0550 
0551     streamProcessor->start();
0552 
0553     Frame::spaceStats.clear();
0554 
0555     ui->progressBar->setMinimum(0);
0556     ui->progressBar->setValue(0);
0557 
0558     ui->grpText->setDisabled(true);
0559     ui->grpNavButtons->setDisabled(true);
0560 }
0561 
0562 void
0563 VobSubInputProcessDialog::onStreamData(const QImage &image, quint64 msecStart, quint64 msecDuration)
0564 {
0565     FramePtr frame(new Frame());
0566     frame->subShowTime.setMillisTime(double(msecStart));
0567     frame->subHideTime.setMillisTime(double(msecStart + msecDuration));
0568     frame->subImage = image;
0569 
0570     ui->subtitleView->setPixmap(QPixmap::fromImage(frame->subImage));
0571     QCoreApplication::processEvents();
0572 
0573     if(frame->processPieces()) {
0574         frame->index = m_frames.length();
0575         ui->progressBar->setMaximum(m_frames.length());
0576         m_frames.append(frame);
0577     }
0578 }
0579 
0580 void
0581 VobSubInputProcessDialog::onStreamError(int /*code*/, const QString &message, const QString &debug)
0582 {
0583     QString text = message % QStringLiteral("\n") % debug;
0584     KMessageBox::error(this, text, i18n("VobSub Error"));
0585 }
0586 
0587 void
0588 VobSubInputProcessDialog::onStreamFinished()
0589 {
0590     m_frameCurrent = m_frames.begin() - 1;
0591 
0592     // average word length in english is 5.1 chars
0593     const double avgWordLength = 4;
0594 
0595     if(!Frame::spaceStats.empty()) {
0596         auto itChar = Frame::spaceStats.begin(); // shorter spaces on start
0597         auto itWord = std::prev(Frame::spaceStats.end()); // longer spaces near end
0598         qint64 charSpacingSum = itChar.key() * itChar.value();
0599         quint64 charSpacingCount = itChar.value();
0600         qint64 wordSpacingSum = itWord.key() * itWord.value();
0601         quint64 wordSpacingCount = itWord.value();
0602 
0603         while(itChar != itWord) {
0604             if(charSpacingCount < avgWordLength * wordSpacingCount) {
0605                 // sum up chars
0606                 ++itChar;
0607                 charSpacingSum += itChar.key() * itChar.value();
0608                 charSpacingCount += itChar.value();
0609             } else {
0610                 // sum up words
0611                 --itWord;
0612                 wordSpacingSum += itWord.key() * itWord.value();
0613                 wordSpacingCount += itWord.value();
0614             }
0615         }
0616         m_spaceWidth = wordSpacingSum / wordSpacingCount;
0617     } else {
0618         m_spaceWidth = 100;
0619     }
0620 
0621     ui->grpText->setDisabled(true);
0622     ui->grpNavButtons->setDisabled(true);
0623     QMetaObject::invokeMethod(this, "processNextImage", Qt::QueuedConnection);
0624 }
0625 
0626 void
0627 VobSubInputProcessDialog::processNextImage()
0628 {
0629     if(++m_frameCurrent == m_frames.end()) {
0630         accept();
0631         return;
0632     }
0633 
0634     ui->progressBar->setValue((*m_frameCurrent)->index + 1);
0635 
0636     ui->subtitleView->setPixmap(QPixmap::fromImage((*m_frameCurrent)->subImage));
0637 
0638     m_pieces = (*m_frameCurrent)->pieces;
0639     m_pieceCurrent = m_pieces.begin();
0640 
0641     recognizePiece();
0642 }
0643 
0644 void
0645 VobSubInputProcessDialog::processCurrentPiece()
0646 {
0647     if(m_pieceCurrent == m_pieces.end())
0648         return;
0649 
0650     ui->grpText->setDisabled(false);
0651     ui->grpNavButtons->setDisabled(false);
0652 
0653     QPixmap pixmap = QPixmap::fromImage((*m_frameCurrent)->subImage);
0654     QPainter p(&pixmap);
0655 
0656     QList<PiecePtr>::iterator i = m_pieceCurrent;
0657     p.setPen(QColor(255, 255, 255, 64));
0658     p.drawLine(0, (*i)->line->baseline, pixmap.width(), (*i)->line->baseline);
0659 
0660     p.setPen(QColor(255, 0, 0, 200));
0661     QRect rcVisible(QPoint((*i)->left, (*i)->top), QPoint((*i)->right, (*i)->bottom));
0662     int n = (*i)->symbolCount;
0663     for(; n-- && i != m_pieces.end(); ++i) {
0664         rcVisible |= QRect(QPoint((*i)->left, (*i)->top), QPoint((*i)->right, (*i)->bottom));
0665         foreach(QPoint pix, (*i)->pixels)
0666             p.drawPoint(pix);
0667     }
0668     rcVisible.adjust((ui->subtitleView->minimumWidth() - rcVisible.width()) / -2, (ui->subtitleView->minimumHeight() - rcVisible.height()) / -2, 0, 0);
0669     rcVisible.setBottomRight(QPoint(pixmap.width(), pixmap.height()));
0670     ui->subtitleView->setPixmap(pixmap.copy(rcVisible));
0671 
0672     ui->lineEdit->setFocus();
0673 
0674     ui->symbolCount->setMaximum(m_pieces.end() - m_pieceCurrent);
0675 }
0676 
0677 void
0678 VobSubInputProcessDialog::processNextPiece()
0679 {
0680     m_pieceCurrent += (*m_pieceCurrent)->symbolCount;
0681 
0682     ui->lineEdit->clear();
0683     ui->symbolCount->setValue(1);
0684 
0685     if(m_pieceCurrent == m_pieces.end()) {
0686         RichString subText;
0687         PiecePtr piecePrev;
0688         foreach(PiecePtr piece, m_pieces) {
0689             if(piecePrev) {
0690                 if(!piecePrev->line->intersects(piece->line))
0691                     subText.append(QChar(QChar::LineFeed));
0692                 else if(piece->left - piecePrev->right > m_spaceWidth)
0693                     subText.append(QChar(QChar::Space));
0694             }
0695 
0696             subText += piece->text;
0697             piecePrev = piece;
0698         }
0699 
0700         SubtitleLine *l = m_subtitle->line((*m_frameCurrent)->index);
0701         if(!l) {
0702             l = new SubtitleLine((*m_frameCurrent)->subShowTime, (*m_frameCurrent)->subHideTime);
0703             m_subtitle->insertLine(l);
0704         }
0705         l->primaryDoc()->setRichText(subText);
0706 
0707         ui->grpText->setDisabled(true);
0708         ui->grpNavButtons->setDisabled(true);
0709         QMetaObject::invokeMethod(this, "processNextImage", Qt::QueuedConnection);
0710         return;
0711     }
0712 
0713     recognizePiece();
0714 }
0715 
0716 void
0717 VobSubInputProcessDialog::recognizePiece()
0718 {
0719     for(int len = m_recognizedPiecesMaxSymbolLength; len > 0; len--) {
0720         PiecePtr normal = currentNormalizedPiece(len);
0721         if(len != normal->symbolCount)
0722             continue;
0723         if(m_recognizedPieces.contains(*normal)) {
0724             const RichString text = m_recognizedPieces.value(*normal);
0725             (*m_pieceCurrent)->text = text;
0726             currentSymbolCountSet(len);
0727             processNextPiece();
0728             return;
0729         }
0730     }
0731 
0732     processCurrentPiece();
0733 }
0734 
0735 RichString
0736 VobSubInputProcessDialog::currentText()
0737 {
0738     int style = 0;
0739     if(ui->styleBold->isChecked())
0740         style |= RichString::Bold;
0741     if(ui->styleItalic->isChecked())
0742         style |= RichString::Italic;
0743     if(ui->styleUnderline->isChecked())
0744         style |= RichString::Underline;
0745     return RichString(ui->lineEdit->text(), style);
0746 }
0747 
0748 VobSubInputProcessDialog::PiecePtr
0749 VobSubInputProcessDialog::currentNormalizedPiece(int symbolCount)
0750 {
0751     PiecePtr normal(new Piece(**m_pieceCurrent));
0752     normal->symbolCount = 1;
0753     for(auto piece = m_pieceCurrent; --symbolCount && ++piece != m_pieces.end(); ) {
0754         *normal += **piece;
0755         normal->symbolCount++;
0756     }
0757 
0758     normal->normalize();
0759 
0760     return normal;
0761 }
0762 
0763 void
0764 VobSubInputProcessDialog::currentSymbolCountSet(int symbolCount)
0765 {
0766     if(m_pieceCurrent == m_pieces.end())
0767         return;
0768 
0769     int n = (*m_pieceCurrent)->symbolCount;
0770     QList<PiecePtr>::iterator piece = m_pieceCurrent;
0771     while(--n && ++piece != m_pieces.end())
0772         (*piece)->symbolCount = 1;
0773 
0774     piece = m_pieceCurrent;
0775     (*piece)->symbolCount = symbolCount;
0776     while(--symbolCount && ++piece != m_pieces.end())
0777         (*piece)->symbolCount = 0;
0778 }
0779 
0780 void
0781 VobSubInputProcessDialog::onSymbolCountChanged(int symbolCount)
0782 {
0783     currentSymbolCountSet(symbolCount);
0784 
0785     processCurrentPiece();
0786 }
0787 
0788 void
0789 VobSubInputProcessDialog::onOkClicked()
0790 {
0791     if((*m_pieceCurrent)->symbolCount > m_recognizedPiecesMaxSymbolLength)
0792         m_recognizedPiecesMaxSymbolLength = (*m_pieceCurrent)->symbolCount;
0793 
0794     (*m_pieceCurrent)->text = currentText();
0795 
0796     PiecePtr normal = currentNormalizedPiece((*m_pieceCurrent)->symbolCount);
0797     m_recognizedPieces[*normal] = (*m_pieceCurrent)->text;
0798 
0799     processNextPiece();
0800 }
0801 
0802 void
0803 VobSubInputProcessDialog::onAbortClicked()
0804 {
0805     reject();
0806 }
0807 
0808 void
0809 VobSubInputProcessDialog::onPrevImageClicked()
0810 {
0811     if(m_frameCurrent == m_frames.begin())
0812         return;
0813 
0814     --m_frameCurrent;
0815 
0816     ui->progressBar->setValue((*m_frameCurrent)->index + 1);
0817 
0818     m_pieces = (*m_frameCurrent)->pieces;
0819     m_pieceCurrent = m_pieces.end();
0820 
0821     onPrevSymbolClicked();
0822 }
0823 
0824 void
0825 VobSubInputProcessDialog::onNextImageClicked()
0826 {
0827     if(m_frameCurrent == m_frames.end() - 1)
0828         return;
0829 
0830     ++m_frameCurrent;
0831 
0832     ui->progressBar->setValue((*m_frameCurrent)->index + 1);
0833 
0834     m_pieces = (*m_frameCurrent)->pieces;
0835     m_pieceCurrent = m_pieces.begin() - 1;
0836 
0837     onNextSymbolClicked();
0838 }
0839 
0840 void
0841 VobSubInputProcessDialog::onPrevSymbolClicked()
0842 {
0843     do {
0844         if(m_pieceCurrent == m_pieces.begin())
0845             return onPrevImageClicked();
0846         --m_pieceCurrent;
0847     } while((*m_pieceCurrent)->symbolCount == 0);
0848 
0849     updateCurrentPiece();
0850 }
0851 
0852 void
0853 VobSubInputProcessDialog::onNextSymbolClicked()
0854 {
0855     do {
0856         if(m_pieceCurrent >= m_pieces.end() - 1)
0857             return onNextImageClicked();
0858         ++m_pieceCurrent;
0859     } while((*m_pieceCurrent)->symbolCount == 0);
0860 
0861     updateCurrentPiece();
0862 }
0863 
0864 void
0865 VobSubInputProcessDialog::updateCurrentPiece()
0866 {
0867     processCurrentPiece();
0868 
0869     ui->lineEdit->setText((*m_pieceCurrent)->text.string());
0870     ui->lineEdit->selectAll();
0871 
0872     int style = (*m_pieceCurrent)->text.styleFlagsAt(0);
0873     ui->styleBold->setChecked((style & RichString::Bold) != 0);
0874     ui->styleItalic->setChecked((style & RichString::Italic) != 0);
0875     ui->styleUnderline->setChecked((style & RichString::Underline) != 0);
0876 
0877     ui->symbolCount->setValue((*m_pieceCurrent)->symbolCount);
0878 }