From ab2f4ef8fad82fd07dac8768b8dcb94b11b271b6 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Wed, 10 Apr 2024 16:30:42 +0200 Subject: [PATCH] QTextDocument: Add support for responsive images Add support for the max-width css attribute in image. This allows images to be responsive: it adapts their size to the size of the QTextDocument so that they never grow bigger than the QTextDocument pageSize. This is implemented for the image handler used in QTextEdit and other QtWidget text related classes. [ChangeLog][QtGui][CSS] The max-width style can now be applied to to set the maximum width in pixels or percentage. Task-number: QTBUG-12283 Change-Id: Ic94e16279a1240ab4a509823de59dc0bfc920bb9 Reviewed-by: Shawn Rutledge --- src/gui/text/qcssparser.cpp | 12 ++++ src/gui/text/qcssparser_p.h | 3 +- src/gui/text/qtextdocument.cpp | 17 ++++- src/gui/text/qtextformat.cpp | 21 +++++- src/gui/text/qtextformat.h | 8 +++ src/gui/text/qtexthtmlparser.cpp | 7 ++ src/gui/text/qtextimagehandler.cpp | 26 +++++++- .../tst_qtextimagehandler.cpp | 66 ++++++++++++++++++- 8 files changed, 152 insertions(+), 8 deletions(-) diff --git a/src/gui/text/qcssparser.cpp b/src/gui/text/qcssparser.cpp index e5815ffce2..916e96ea63 100644 --- a/src/gui/text/qcssparser.cpp +++ b/src/gui/text/qcssparser.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include @@ -396,6 +397,8 @@ LengthData ValueExtractor::lengthValue(const Value& v) if (data.unit != LengthData::None) s.chop(2); + else if (v.type == Value::Percentage) + data.unit = LengthData::Percent; data.number = s.toDouble(); return data; @@ -409,6 +412,15 @@ static int lengthValueFromData(const LengthData& data, const QFont& f) return qRound(qBound(double(INT_MIN) + 0.1, scale * data.number, double(INT_MAX))); } +QTextLength ValueExtractor::textLength(const Declaration &decl) +{ + const LengthData data = lengthValue(decl.d->values.at(0)); + if (data.unit == LengthData::Percent) + return QTextLength(QTextLength::PercentageLength, data.number); + + return QTextLength(QTextLength::FixedLength, lengthValueFromData(data, f)); +} + int ValueExtractor::lengthValue(const Declaration &decl) { if (decl.d->parsed.isValid()) diff --git a/src/gui/text/qcssparser_p.h b/src/gui/text/qcssparser_p.h index c1cfb1ac9b..7742271e41 100644 --- a/src/gui/text/qcssparser_p.h +++ b/src/gui/text/qcssparser_p.h @@ -392,7 +392,7 @@ QT_CSS_DECLARE_TYPEINFO(BackgroundData, Q_RELOCATABLE_TYPE) struct LengthData { qreal number; - enum { None, Px, Ex, Em } unit; + enum { None, Px, Ex, Em, Percent } unit; }; QT_CSS_DECLARE_TYPEINFO(LengthData, Q_PRIMITIVE_TYPE) @@ -835,6 +835,7 @@ struct Q_GUI_EXPORT ValueExtractor bool extractIcon(QIcon *icon, QSize *size); void lengthValues(const Declaration &decl, int *m); + QTextLength textLength(const Declaration &decl); private: void extractFont(); diff --git a/src/gui/text/qtextdocument.cpp b/src/gui/text/qtextdocument.cpp index 15a313e13d..ab788a5f9b 100644 --- a/src/gui/text/qtextdocument.cpp +++ b/src/gui/text/qtextdocument.cpp @@ -2944,6 +2944,17 @@ void QTextHtmlExporter::emitFragment(const QTextFragment &fragment) html += "(doc->objectForFormat(imgFmt))) emitFloatStyle(imageFrame->frameFormat().position()); diff --git a/src/gui/text/qtextformat.cpp b/src/gui/text/qtextformat.cpp index daa79a55d2..dacef70812 100644 --- a/src/gui/text/qtextformat.cpp +++ b/src/gui/text/qtextformat.cpp @@ -745,6 +745,7 @@ Q_GUI_EXPORT QDataStream &operator>>(QDataStream &stream, QTextTableCellFormat & \value ImageWidth \value ImageHeight \value ImageQuality + \value ImageMaxWidth This enum value has been added in Qt 6.8. Selection properties @@ -3426,7 +3427,7 @@ QTextImageFormat::QTextImageFormat(const QTextFormat &fmt) Sets the \a width of the rectangle occupied by the image. - \sa width(), setHeight() + \sa width(), setHeight(), maximumWidth() */ @@ -3438,6 +3439,24 @@ QTextImageFormat::QTextImageFormat(const QTextFormat &fmt) \sa height(), setWidth() */ +/*! + \fn void QTextImageFormat::setMaximumWidth(QTextLength maximumWidth) + + Sets the \a maximumWidth of the rectangle occupied by the image. This + can be an absolute number or a percentage of the available document size. + + \sa width(), setHeight() +*/ + + +/*! + \fn QTextLength QTextImageFormat::maximumWidth() const + + Returns the maximum width of the rectangle occupied by the image. + + \sa width(), setMaximumWidth() +*/ + /*! \fn void QTextImageFormat::setHeight(qreal height) diff --git a/src/gui/text/qtextformat.h b/src/gui/text/qtextformat.h index c009d328cb..2fa86ed0d1 100644 --- a/src/gui/text/qtextformat.h +++ b/src/gui/text/qtextformat.h @@ -241,6 +241,7 @@ public: ImageWidth = 0x5010, ImageHeight = 0x5011, ImageQuality = 0x5014, + ImageMaxWidth = 0x5015, // internal /* @@ -796,6 +797,10 @@ public: inline qreal width() const { return doubleProperty(ImageWidth); } + inline void setMaximumWidth(QTextLength maxWidth); + inline QTextLength maximumWidth() const + { return lengthProperty(ImageMaxWidth); } + inline void setHeight(qreal height); inline qreal height() const { return doubleProperty(ImageHeight); } @@ -823,6 +828,9 @@ inline void QTextImageFormat::setName(const QString &aname) inline void QTextImageFormat::setWidth(qreal awidth) { setProperty(ImageWidth, awidth); } +inline void QTextImageFormat::setMaximumWidth(QTextLength maxWidth) +{ setProperty(ImageMaxWidth, maxWidth); } + inline void QTextImageFormat::setHeight(qreal aheight) { setProperty(ImageHeight, aheight); } diff --git a/src/gui/text/qtexthtmlparser.cpp b/src/gui/text/qtexthtmlparser.cpp index ee92cece78..bc2200697d 100644 --- a/src/gui/text/qtexthtmlparser.cpp +++ b/src/gui/text/qtexthtmlparser.cpp @@ -1428,6 +1428,13 @@ void QTextHtmlParserNode::applyCssDeclarations(const QList &d charFormat.setForeground(brush); break; } + case QCss::MaximumWidth: + if (id == Html_img) { + auto imageFormat = charFormat.toImageFormat(); + imageFormat.setMaximumWidth(extractor.textLength(decl)); + charFormat = imageFormat; + } + break; default: break; } } diff --git a/src/gui/text/qtextimagehandler.cpp b/src/gui/text/qtextimagehandler.cpp index 5c56c30711..920e6c689c 100644 --- a/src/gui/text/qtextimagehandler.cpp +++ b/src/gui/text/qtextimagehandler.cpp @@ -12,6 +12,7 @@ #include #include #include +#include QT_BEGIN_NAMESPACE @@ -72,21 +73,40 @@ template static QSize getSize(QTextDocument *doc, const QTextImageFormat &format) { const bool hasWidth = format.hasProperty(QTextFormat::ImageWidth); - const int width = qRound(format.width()); + int width = qRound(format.width()); const bool hasHeight = format.hasProperty(QTextFormat::ImageHeight); const int height = qRound(format.height()); + const bool hasMaxWidth = format.hasProperty(QTextFormat::ImageMaxWidth); + const auto maxWidth = format.maximumWidth(); + + int effectiveMaxWidth = std::numeric_limits::max(); + if (hasMaxWidth) { + if (maxWidth.type() == QTextLength::PercentageLength) + effectiveMaxWidth = (doc->pageSize().width() - 2 * doc->documentMargin()) * maxWidth.value(100) / 100; + else + effectiveMaxWidth = maxWidth.rawValue(); + + width = qMin(effectiveMaxWidth, width); + } + T source; QSize size(width, height); if (!hasWidth || !hasHeight) { source = getAs(doc, format); - const QSizeF sourceSize = source.deviceIndependentSize(); + QSizeF sourceSize = source.deviceIndependentSize(); + + if (sourceSize.width() > effectiveMaxWidth) { + // image is bigger than effectiveMaxWidth, scale it down + sourceSize.setHeight(effectiveMaxWidth * (sourceSize.height() / qreal(sourceSize.width()))); + sourceSize.setWidth(effectiveMaxWidth); + } if (!hasWidth) { if (!hasHeight) size.setWidth(sourceSize.width()); else - size.setWidth(qRound(height * (sourceSize.width() / qreal(sourceSize.height())))); + size.setWidth(qMin(effectiveMaxWidth, qRound(height * (sourceSize.width() / qreal(sourceSize.height()))))); } if (!hasHeight) { if (!hasWidth) diff --git a/tests/auto/gui/text/qtextimagehandler/tst_qtextimagehandler.cpp b/tests/auto/gui/text/qtextimagehandler/tst_qtextimagehandler.cpp index 8327a5d187..0048623d0e 100644 --- a/tests/auto/gui/text/qtextimagehandler/tst_qtextimagehandler.cpp +++ b/tests/auto/gui/text/qtextimagehandler/tst_qtextimagehandler.cpp @@ -6,6 +6,10 @@ #include #include +using namespace Qt::StringLiterals; + +// #define DEBUG_WRITE_HTML + class tst_QTextImageHandler : public QObject { Q_OBJECT @@ -20,6 +24,8 @@ private slots: void loadAtNImages_data(); #ifndef QT_NO_TEXTHTMLPARSER void loadAtNImages(); + void maxWidth_data(); + void maxWidth(); #endif }; @@ -61,7 +67,7 @@ void tst_QTextImageHandler::loadAtNImages() const auto it = std::find_if(formats.begin(), formats.end(), [](const auto &format){ return format.objectType() == QTextFormat::ImageObject; }); - QVERIFY(it != formats.end()); + QCOMPARE_NE(it, formats.end()); const QTextImageFormat format = (*it).toImageFormat(); QTextImageHandler handler; @@ -77,6 +83,64 @@ void tst_QTextImageHandler::loadAtNImages() QCOMPARE(img.pixelColor(0, 0), expectedColor); } } + +void tst_QTextImageHandler::maxWidth_data() +{ + QTest::addColumn("imageFile"); + QTest::addColumn("pageSize"); + QTest::addColumn("maxWidth"); + QTest::addColumn("expectedSize"); + + QTest::addRow("constrained-percentage") << QFINDTESTDATA("data/image.png") << QSizeF(16, 16) << QTextLength(QTextLength::PercentageLength, 100) << QSizeF(12, 12); + QTest::addRow("not-constrained-percentage") << QFINDTESTDATA("data/image.png") << QSizeF(200, 200) << QTextLength(QTextLength::PercentageLength, 100) << QSizeF(16, 16); + QTest::addRow("constrained-fixed") << QFINDTESTDATA("data/image.png") << QSizeF(16, 16) << QTextLength(QTextLength::FixedLength, 5) << QSizeF(5, 5); + QTest::addRow("not-constrained-fixed") << QFINDTESTDATA("data/image.png") << QSizeF(200, 200) << QTextLength(QTextLength::FixedLength, 5) << QSizeF(5, 5); + QTest::addRow("not-constrained-default") << QFINDTESTDATA("data/image.png") << QSizeF(200, 200) << QTextLength(QTextLength::VariableLength, 5) << QSizeF(16, 16); +} + +void tst_QTextImageHandler::maxWidth() +{ + QFETCH(QString, imageFile); + QFETCH(QSizeF, pageSize); + QFETCH(QTextLength, maxWidth); + QFETCH(QSizeF, expectedSize); + + QTextDocument doc; + doc.setPageSize(pageSize); + doc.setDocumentMargin(2); + QTextCursor c(&doc); + QString style; + if (maxWidth.type() == QTextLength::PercentageLength) + style = " style=\"max-width:"_L1 + QString::number(maxWidth.rawValue()) + "%;\""_L1; + else if (maxWidth.type() == QTextLength::FixedLength) + style = " style=\"max-width:"_L1 + QString::number(maxWidth.rawValue()) + "px;\""_L1; + const QString html = ""; + c.insertHtml(html); + +#ifdef DEBUG_WRITE_HTML + { + QFile out("/tmp/" + QLatin1String(QTest::currentDataTag()) + ".html"); + out.open(QFile::WriteOnly); + out.write(html.toLatin1()); + out.close(); + } + { + QFile out("/tmp/" + QLatin1String(QTest::currentDataTag()) + "_rewrite.html"); + out.open(QFile::WriteOnly); + out.write(doc.toHtml().toLatin1()); + out.close(); + } +#endif + const auto formats = doc.allFormats(); + const auto it = std::find_if(formats.begin(), formats.end(), [](const auto &format){ + return format.objectType() == QTextFormat::ImageObject; + }); + QCOMPARE_NE(it, formats.end()); + const QTextImageFormat format = (*it).toImageFormat(); + QTextImageHandler handler; + + QCOMPARE(handler.intrinsicSize(&doc, 0, format), expectedSize); +} #endif QTEST_MAIN(tst_QTextImageHandler)