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
<img/> to set the maximum width in pixels or percentage.

Task-number: QTBUG-12283
Change-Id: Ic94e16279a1240ab4a509823de59dc0bfc920bb9
Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
bb10
Carl Schwan 2024-04-10 16:30:42 +02:00 committed by Shawn Rutledge
parent fb2ea1e52a
commit ab2f4ef8fa
8 changed files with 152 additions and 8 deletions

View File

@ -12,6 +12,7 @@
#include <qfontmetrics.h>
#include <qbrush.h>
#include <qimagereader.h>
#include <qtextformat.h>
#include <algorithm>
@ -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())

View File

@ -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();

View File

@ -2944,6 +2944,17 @@ void QTextHtmlExporter::emitFragment(const QTextFragment &fragment)
html += "<img"_L1;
QString maxWidthCss;
if (imgFmt.hasProperty(QTextFormat::ImageMaxWidth)) {
auto length = imgFmt.lengthProperty(QTextFormat::ImageMaxWidth);
maxWidthCss += "max-width:"_L1;
if (length.type() == QTextLength::PercentageLength)
maxWidthCss += QString::number(length.rawValue()) + "%;"_L1;
else if (length.type() == QTextLength::FixedLength)
maxWidthCss += QString::number(length.rawValue()) + "px;"_L1;
}
if (imgFmt.hasProperty(QTextFormat::ImageName))
emitAttribute("src", imgFmt.name());
@ -2960,9 +2971,11 @@ void QTextHtmlExporter::emitFragment(const QTextFragment &fragment)
emitAttribute("height", QString::number(imgFmt.height()));
if (imgFmt.verticalAlignment() == QTextCharFormat::AlignMiddle)
html += " style=\"vertical-align: middle;\""_L1;
html += " style=\"vertical-align: middle;"_L1 + maxWidthCss + u'\"';
else if (imgFmt.verticalAlignment() == QTextCharFormat::AlignTop)
html += " style=\"vertical-align: top;\""_L1;
html += " style=\"vertical-align: top;"_L1 + maxWidthCss + u'\"';
else if (!maxWidthCss.isEmpty())
html += " style=\""_L1 + maxWidthCss + u'\"';
if (QTextFrame *imageFrame = qobject_cast<QTextFrame *>(doc->objectForFormat(imgFmt)))
emitFloatStyle(imageFrame->frameFormat().position());

View File

@ -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)

View File

@ -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); }

View File

@ -1428,6 +1428,13 @@ void QTextHtmlParserNode::applyCssDeclarations(const QList<QCss::Declaration> &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;
}
}

View File

@ -12,6 +12,7 @@
#include <private/qtextengine_p.h>
#include <qpalette.h>
#include <qthread.h>
#include <limits>
QT_BEGIN_NAMESPACE
@ -72,21 +73,40 @@ template<typename T>
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<int>::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<T>(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)

View File

@ -6,6 +6,10 @@
#include <QPainter>
#include <private/qtextimagehandler_p.h>
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<QString>("imageFile");
QTest::addColumn<QSizeF>("pageSize");
QTest::addColumn<QTextLength>("maxWidth");
QTest::addColumn<QSizeF>("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 = "<img src=\"" + imageFile + u'\"' + style + "\">";
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)