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
parent
fb2ea1e52a
commit
ab2f4ef8fa
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue