File indexing completed on 2025-02-23 04:35:31

0001 /*
0002     SPDX-FileCopyrightText: 2020-2022 Mladen Milinkovic <max@smoothware.net>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "richdocumentlayout.h"
0008 
0009 #include "helpers/common.h"
0010 #include "helpers/debug.h"
0011 #include "core/richtext/richcss.h"
0012 #include "core/richtext/richdocument.h"
0013 
0014 #include <climits>
0015 
0016 #include <QBasicTimer>
0017 #include <QFont>
0018 #include <QFontMetrics>
0019 #include <QGuiApplication>
0020 #include <QPainter>
0021 #include <QSet>
0022 #include <QStringBuilder>
0023 #include <QtMath>
0024 #include <QTextBlock>
0025 #include <QTextBlockFormat>
0026 #include <QTextFrame>
0027 #include <QTextLayout>
0028 
0029 
0030 using namespace SubtitleComposer;
0031 
0032 Q_GUI_EXPORT int qt_defaultDpiY();
0033 
0034 RichDocumentLayout::RichDocumentLayout(RichDocument *doc)
0035     : QAbstractTextDocumentLayout(doc),
0036       m_doc(doc),
0037       m_layoutPosition(0)
0038 {
0039 }
0040 
0041 QTextCharFormat
0042 RichDocumentLayout::applyCSS(const QTextCharFormat &format) const
0043 {
0044     QTextCharFormat fmt(format);
0045     const RichCSS *css = m_doc->stylesheet();
0046     if(!css)
0047         return fmt;
0048 
0049     QSet<QString> selectors;
0050     if(format.fontWeight() == QFont::Bold)
0051         selectors << $("b");
0052     if(format.fontItalic())
0053         selectors << $("i");
0054     if(format.fontUnderline())
0055         selectors << $("u");
0056     if(format.fontStrikeOut())
0057         selectors << $("s");
0058     if(format.hasProperty(RichDocument::Class)) {
0059         selectors << $("c");
0060         const QSet<QString> cl = format.property(RichDocument::Class).value<QSet<QString>>();
0061         for(const QString &c: cl)
0062             selectors << QChar('.') % c;
0063     }
0064     if(format.hasProperty(RichDocument::Voice)) {
0065         selectors << $("v");
0066         selectors << $("v[voice=") % format.property(RichDocument::Voice).toString() % $("]");
0067         selectors << $("v[voice=\"") % format.property(RichDocument::Voice).toString() % $("\"]");
0068     }
0069 
0070     QMap<QByteArray, QString> styles = css->match(selectors);
0071     for(auto it = styles.cbegin(); it != styles.cend(); ++it) {
0072         if(it.key() == "font-weight") {
0073             static const QMap<QString, QFont::Weight> wm = {
0074                 { $("normal"), QFont::Normal },
0075                 { $("bold"), QFont::Bold },
0076                 { $("100"), QFont::Thin },
0077                 { $("200"), QFont::ExtraLight },
0078                 { $("300"), QFont::Light },
0079                 { $("400"), QFont::Normal },
0080                 { $("500"), QFont::Medium },
0081                 { $("600"), QFont::DemiBold },
0082                 { $("700"), QFont::Bold },
0083                 { $("800"), QFont::ExtraBold },
0084                 { $("900"), QFont::Black },
0085             };
0086             auto iw = wm.find(it.value());
0087             if(iw != wm.cend())
0088                 fmt.setFontWeight(iw.value());
0089         } else if(it.key() == "font-style") {
0090             fmt.setFontItalic(it.value() != $("normal"));
0091         } else if(it.key() == "text-decoration") {
0092             fmt.setFontUnderline(it.value() == $("underline"));
0093             fmt.setFontStrikeOut(it.value() == $("line-through"));
0094         } else if(it.key() == "color") {
0095             QColor color;
0096             color.setNamedColor(it.value());
0097             fmt.setForeground(QBrush(color));
0098         } else if(it.key() == "background-color") {
0099             QColor color;
0100             color.setNamedColor(it.value());
0101             fmt.setBackground(QBrush(color));
0102         }
0103         // TODO: check what else WebVTT requires
0104     }
0105     return fmt;
0106 }
0107 
0108 QVector<QTextLayout::FormatRange>
0109 RichDocumentLayout::applyCSS(const QVector<QTextLayout::FormatRange> &docFormat) const
0110 {
0111     QVector<QTextLayout::FormatRange> fmts;
0112     for(auto it = docFormat.cbegin(); it != docFormat.cend(); ++it)
0113         fmts.push_back(QTextLayout::FormatRange{it->start, it->length, applyCSS(it->format)});
0114     return fmts;
0115 }
0116 
0117 void
0118 RichDocumentLayout::mergeFormat(QTextCharFormat &fmt, const QTextCharFormat &upper)
0119 {
0120     const QVariant &u = upper.property(RichDocument::Merged);
0121     QTextFormat upp = u.isNull() ? static_cast<QTextFormat>(upper) : u.value<QTextFormat>();
0122 
0123     const QMap<int, QVariant> pa = fmt.properties();
0124     const QMap<int, QVariant> pb = upp.properties();
0125     for(auto it = pb.cbegin(); it != pb.cend(); ++it) {
0126         if(pa.contains(it.key()) && (it.key() == QTextFormat::FontWeight || it.key() == QTextFormat::FontItalic
0127         || it.key() == QTextFormat::FontUnderline || it.key() == QTextFormat::TextUnderlineStyle
0128         || it.key() == QTextFormat::FontStrikeOut)) {
0129             const QVariant &v = pa.find(it.key()).value();
0130             fmt.setProperty(it.key(), v.toInt() > it.value().toInt() ? v : it.value());
0131         } else {
0132             fmt.setProperty(it.key(), it.value());
0133         }
0134     }
0135     if(!fmt.isEmpty())
0136         fmt.setProperty(RichDocument::Merged, upp);
0137 }
0138 
0139 QVector<QTextLayout::FormatRange>
0140 RichDocumentLayout::mergeCSS(const QVector<QTextLayout::FormatRange> &docFormat, const QVector<QTextLayout::FormatRange> &layoutFormat) const
0141 {
0142     QVector<QTextLayout::FormatRange> fmts;
0143     auto di = docFormat.cbegin();
0144     auto li = layoutFormat.cbegin();
0145     int off = 0;
0146     bool docFmtValid = false;
0147     QTextCharFormat docFmt;
0148     for(;;) {
0149         const bool offPastDoc = di == docFormat.cend();
0150         if(!offPastDoc) {
0151             if(!docFmtValid) {
0152                 docFmt = applyCSS(di->format);
0153                 docFmtValid = true;
0154             }
0155             if(off >= di->start + di->length) {
0156                 ++di;
0157                 docFmtValid = false;
0158                 continue;
0159             }
0160         }
0161         const bool offPastLayout = li == layoutFormat.cend();
0162         if(!offPastLayout && off >= li->start + li->length) {
0163             ++li;
0164             continue;
0165         }
0166         if(offPastDoc && offPastLayout)
0167             break;
0168 
0169         bool offNotInDoc = offPastDoc || off < di->start;
0170         bool offNotInLayout = offPastLayout || off < li->start;
0171         if(offNotInDoc && offNotInLayout) {
0172             if(offPastDoc)
0173                 off = li->start;
0174             else if(offPastLayout)
0175                 off = di->start;
0176             else
0177                 off = qMin(di->start, li->start);
0178             continue;
0179         }
0180 
0181         Q_ASSERT(!offNotInDoc || !offNotInLayout);
0182         QTextCharFormat fmt;
0183         if(!offNotInDoc)
0184             fmt = docFmt;
0185         mergeFormat(fmt, offNotInLayout ? QTextCharFormat() : li->format);
0186         int end;
0187         if(!offNotInDoc && !offNotInLayout)
0188             end = qMin(di->start + di->length, li->start + li->length);
0189         else if(!offNotInDoc) // && offNotInLayout
0190             end = offPastLayout ? di->start + di->length : qMin(di->start + di->length, li->start);
0191         else // !offNotInLayout && offNotInDoc
0192             end = offPastDoc ? li->start + li->length : qMin(li->start + li->length, di->start);
0193         if(!fmt.isEmpty())
0194             fmts.push_back(QTextLayout::FormatRange{off, end - off, fmt});
0195         off = end;
0196     }
0197     return fmts;
0198 }
0199 
0200 void
0201 RichDocumentLayout::processLayout(int from, int oldLength, int length)
0202 {
0203     Q_UNUSED(oldLength);
0204 
0205     const QTextFrameFormat &ff = m_doc->rootFrame()->frameFormat();
0206     const qreal lineLeft = ff.border() + ff.padding() + ff.leftMargin();
0207     const qreal lineRight = m_doc->pageSize().width() - ff.border() - ff.padding() - ff.rightMargin();
0208     const qreal lineWidth = ff.width().value(lineRight - lineLeft);
0209 
0210     QTextBlock bi = m_doc->begin();
0211     QTextBlock end = m_doc->findBlock(qMin(m_layoutPosition, from));
0212 
0213     qreal width = 0;
0214     qreal height = ff.border() + ff.padding() + ff.topMargin();
0215 
0216     for(; bi != end; bi = bi.next()) {
0217         QTextLayout *tl = bi.layout();
0218         const int n = tl->lineCount();
0219         for(int i = 0; i < n; i++) {
0220             QTextLine line = tl->lineAt(i);
0221             width = qMax(width, line.naturalTextWidth());
0222             height += line.height();
0223         }
0224     }
0225 
0226     QRectF updateRect;
0227     updateRect.setTopLeft(QPointF(lineLeft, height));
0228     end = m_doc->findBlock(qMax(0, from + length));
0229     if(end.isValid())
0230         end = end.next();
0231     for(; bi != end; bi = bi.next()) {
0232         if(!bi.isVisible())
0233             continue;
0234         QTextLayout *tl = bi.layout();
0235         tl->setFormats(mergeCSS(bi.textFormats(), tl->formats()));
0236         const qreal layoutStart = height;
0237         tl->setPosition(QPointF(lineLeft, layoutStart));
0238 
0239         QTextOption option = m_doc->defaultTextOption();
0240         const QTextBlockFormat &bf = bi.blockFormat();
0241         option.setTextDirection(bf.layoutDirection());
0242         option.setTabs(bf.tabPositions());
0243         Qt::Alignment align = option.alignment();
0244         if(bf.hasProperty(QTextFormat::BlockAlignment))
0245             align = bf.alignment();
0246         option.setAlignment(align);
0247         if(bf.nonBreakableLines() || m_doc->pageSize().width() < 0)
0248             option.setWrapMode(QTextOption::ManualWrap);
0249         tl->setTextOption(option);
0250 
0251         tl->beginLayout();
0252         for(;;) {
0253             QTextLine line = tl->createLine();
0254             if(!line.isValid())
0255                 break;
0256             line.setLeadingIncluded(true);
0257             line.setLineWidth(lineWidth);
0258             line.setPosition(QPointF(0., height - layoutStart));
0259             width = qMax(width, line.naturalTextWidth());
0260             height += line.height();
0261         }
0262         tl->endLayout();
0263 
0264         m_layoutPosition = bi.position() + bi.length();
0265     }
0266     updateRect.setBottomRight(QPointF(lineRight, height));
0267 
0268     end = m_doc->end();
0269     if(bi == end) {
0270         oldLength = 1; // needed to make updateRect full area if document got shorter
0271         m_layoutPosition = INT_MAX;
0272     } else {
0273         for(; bi != end; bi = bi.next()) {
0274             QTextLayout *tl = bi.layout();
0275             const QPointF newPos(lineLeft, height);
0276             tl->setPosition(newPos);
0277             const int n = tl->lineCount();
0278             for(int i = 0; i < n; i++) {
0279                 QTextLine line = tl->lineAt(i);
0280                 width = qMax(width, line.naturalTextWidth());
0281                 height += line.height();
0282             }
0283         }
0284     }
0285 
0286     height += ff.border() + ff.padding() + ff.bottomMargin();
0287     const QSizeF newSize(m_doc->pageSize().width(), height);
0288     if(m_layoutSize != newSize) {
0289         m_layoutSize = newSize;
0290         m_naturalSize = QSizeF(width, height);
0291         emit documentSizeChanged(m_layoutSize);
0292     }
0293 
0294     if(!updateRect.isValid() || oldLength || m_layoutSize != newSize)
0295         updateRect = QRectF(0., 0., qreal(INT_MAX), qreal(INT_MAX));
0296     emit update(updateRect);
0297 }
0298 
0299 void
0300 RichDocumentLayout::draw(QPainter *painter, const PaintContext &context)
0301 {
0302     ensureLayout(INT_MAX);
0303 
0304     for(QTextBlock bi = m_doc->begin(); bi != m_doc->end(); bi = bi.next()) {
0305         QTextLayout *bl = bi.layout();
0306         const int bPos = bi.position();
0307         const int bLen = bi.length();
0308 
0309         const QBrush bg = bi.blockFormat().background();
0310         if(bg != Qt::NoBrush) {
0311             const QRectF rc = bl->boundingRect().translated(bl->position());
0312             painter->save();
0313             if(bg.style() < Qt::LinearGradientPattern || bg.style() > Qt::ConicalGradientPattern)
0314                 painter->setBrushOrigin(rc.topLeft());
0315             painter->fillRect(rc, bg);
0316             painter->restore();
0317         }
0318 
0319         // draw selection
0320         QVector<QTextLayout::FormatRange> selections;
0321         for(const Selection &s: context.selections) {
0322             const int ss = qMin(qMax(0, s.cursor.selectionStart() - bPos), bLen);
0323             const int sl = qMin(qMax(0, s.cursor.selectionEnd() - bPos), bLen) - ss;
0324             if(sl > 0)
0325                 selections.append(QTextLayout::FormatRange{ss, sl, s.format});
0326         }
0327 
0328         // draw text
0329         bl->draw(painter, QPointF(), selections, context.clip);
0330 
0331         // draw cursor
0332         if(context.cursorPosition >= 0) {
0333             const int off = context.cursorPosition - bi.position();
0334             if(off >= 0 && off < bi.length())
0335                 bl->drawCursor(painter, QPointF(), off, 1);
0336         }
0337     }
0338 }
0339 
0340 int
0341 RichDocumentLayout::hitTest(const QPointF &point, Qt::HitTestAccuracy accuracy) const
0342 {
0343     ensureLayout(INT_MAX);
0344 
0345     const QTextBlock end = m_doc->end();
0346     for(QTextBlock bi = m_doc->begin(); bi != end; bi = bi.next()) {
0347          const QTextLayout *tl = bi.layout();
0348          const QRectF brc = tl->boundingRect().translated(tl->position());
0349          if(point.y() < brc.top())
0350              return accuracy == Qt::ExactHit ? -1 : 0;
0351          if(point.y() > brc.bottom())
0352              continue;
0353          // point inside block rect
0354          const QTextLine::CursorPosition cp = accuracy == Qt::ExactHit ? QTextLine::CursorOnCharacter : QTextLine::CursorBetweenCharacters;
0355          const int n = tl->lineCount();
0356          for(int i = 0; i < n; i++) {
0357             const QTextLine &ln = tl->lineAt(i);
0358             const QRectF lrc = ln.naturalTextRect().translated(tl->position());
0359             if(point.y() < lrc.top())
0360                 return accuracy == Qt::ExactHit ? -1 : 0;
0361             if(point.y() > lrc.bottom())
0362                 continue;
0363             // point inside line rect
0364             return bi.position() + ln.xToCursor(point.x() - tl->position().x(), cp);
0365          }
0366          break;
0367     }
0368     if(accuracy == Qt::ExactHit)
0369         return -1;
0370     const QTextBlock bi = m_doc->lastBlock();
0371     return bi.position() + bi.length() - 1;
0372 }
0373 
0374 
0375 int
0376 RichDocumentLayout::pageCount() const
0377 {
0378     const qreal pgHeight = m_doc->pageSize().height();
0379     if(pgHeight < 0)
0380         return 1;
0381     ensureLayout(INT_MAX);
0382     return qCeil(m_layoutSize.height() / pgHeight);
0383 }
0384 
0385 QSizeF
0386 RichDocumentLayout::documentSize() const
0387 {
0388     ensureLayout(INT_MAX);
0389     return m_layoutSize;
0390 }
0391 
0392 QSizeF
0393 RichDocumentLayout::minimumDocumentSize() const
0394 {
0395     ensureLayout(INT_MAX);
0396     return m_naturalSize;
0397 }
0398 
0399 QRectF
0400 RichDocumentLayout::frameBoundingRect(QTextFrame *frame) const
0401 {
0402     Q_ASSERT(frame == m_doc->rootFrame());
0403     return QRectF(QPointF(0., 0.), m_layoutSize);
0404 }
0405 
0406 QRectF
0407 RichDocumentLayout::blockBoundingRect(const QTextBlock &block) const
0408 {
0409     const QTextLayout *tl = block.layout();
0410     QRectF rc = tl->boundingRect();
0411     rc.translate(tl->position());
0412     return rc;
0413 }
0414 
0415 void
0416 RichDocumentLayout::ensureLayout(int position) const
0417 {
0418     if(position <= m_layoutPosition)
0419         return;
0420     const_cast<RichDocumentLayout *>(this)->processLayout(m_layoutPosition, 0, position - m_layoutPosition);
0421 }
0422 
0423 void
0424 RichDocumentLayout::documentChanged(int from, int oldLength, int length)
0425 {
0426     if(m_layoutPosition > from)
0427         m_layoutPosition = from;
0428     processLayout(from, oldLength, length);
0429 }
0430 
0431 void
0432 RichDocumentLayout::separatorResize(const QSizeF &size)
0433 {
0434     if(m_separatorSize == size)
0435         return;
0436 
0437     m_separatorSize = size;
0438     const qreal dy = size.height() * .4 / 5.;
0439     const qreal xc = qFloor(size.width() / 2. - .5);
0440     const qreal xl = xc - size.width() * .15;
0441     const qreal xr = xc + size.width() * .15;
0442     qreal y = size.height() * .3;
0443     m_separatorPoints.clear();
0444     m_separatorPoints.reserve(6);
0445     m_separatorPoints.push_back(QPointF(xl, y));
0446     m_separatorPoints.push_back(QPointF(xr, y += dy));
0447     m_separatorPoints.push_back(QPointF(xl, y += dy));
0448     m_separatorPoints.push_back(QPointF(xr, y += dy));
0449     m_separatorPoints.push_back(QPointF(xl, y += dy));
0450     m_separatorPoints.push_back(QPointF(xr, y + dy));
0451 }
0452 
0453 void
0454 RichDocumentLayout::separatorDraw(QPainter *painter, const QPointF &offset) const
0455 {
0456     Q_ASSERT(!m_separatorSize.isEmpty());
0457     const QPen oldPen = painter->pen();
0458     painter->setPen(QPen(QGuiApplication::palette().color(QPalette::Normal, QPalette::Link), .75));
0459     const QTransform oldTransform = painter->worldTransform();
0460     painter->translate(offset);
0461     painter->drawPolyline(m_separatorPoints);
0462     painter->setWorldTransform(oldTransform);
0463     painter->setPen(oldPen);
0464 }