Fix clipped text when combining multiple writing systems

When a QTextLine consists of multiple different scripts and the
fonts had negative bearing, the background for a script item could
overdraw the previous item's text, causing it to look clipped.

This was because the background and text was drawn in a single pass,
and moving the background drawing into its own pre-pass fixes the
issue.

[ChangeLog][QtGui] Fixed an issue where drawing text from different
writing systems in the same line and including a background could
cause parts of the text to be clipped.

Pick-to: 6.6 6.5
Fixes: QTBUG-121040
Change-Id: I3f79e6d33c09a2a92853bc8752dbe11a0bea2dd0
Reviewed-by: Lars Knoll <lars@knoll.priv.no>
(cherry picked from commit 8be3c9f4867ce7982387b075739b8f55c5c45753)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
Eskil Abrahamsen Blomfeldt 2024-01-23 10:50:43 +01:00 committed by Qt Cherry-pick Bot
parent 5b2f352042
commit 55b2011bcd
2 changed files with 216 additions and 173 deletions

View File

@ -2228,20 +2228,20 @@ int QTextLine::textLength() const
return eng->lines.at(index).length + eng->lines.at(index).trailingSpaces; return eng->lines.at(index).length + eng->lines.at(index).trailingSpaces;
} }
static void setPenAndDrawBackground(QPainter *p, const QPen &defaultPen, const QTextCharFormat &chf, const QRectF &r) static void drawBackground(QPainter *p, const QTextCharFormat &chf, const QRectF &r)
{ {
QBrush c = chf.foreground();
if (c.style() == Qt::NoBrush) {
p->setPen(defaultPen);
}
QBrush bg = chf.background(); QBrush bg = chf.background();
if (bg.style() != Qt::NoBrush && !chf.property(SuppressBackground).toBool()) if (bg.style() != Qt::NoBrush && !chf.property(SuppressBackground).toBool())
p->fillRect(r.toAlignedRect(), bg); p->fillRect(r.toAlignedRect(), bg);
if (c.style() != Qt::NoBrush) { }
p->setPen(QPen(c, 0));
}
static void setPen(QPainter *p, const QPen &defaultPen, const QTextCharFormat &chf)
{
QBrush c = chf.foreground();
if (c.style() == Qt::NoBrush)
p->setPen(defaultPen);
else
p->setPen(QPen(c, 0));
} }
#if !defined(QT_NO_RAWFONT) #if !defined(QT_NO_RAWFONT)
@ -2636,7 +2636,6 @@ void QTextLine::draw_internal(QPainter *p, const QPointF &origPos,
Q_ASSERT(!eng->useRawFont); Q_ASSERT(!eng->useRawFont);
#endif #endif
const QScriptLine &line = eng->lines[index]; const QScriptLine &line = eng->lines[index];
QPen pen = p->pen();
bool noText = (selection && selection->format.property(SuppressText).toBool()); bool noText = (selection && selection->format.property(SuppressText).toBool());
@ -2648,8 +2647,7 @@ void QTextLine::draw_internal(QPainter *p, const QPointF &origPos,
const qreal lineHeight = line.height().toReal(); const qreal lineHeight = line.height().toReal();
QRectF r(origPos.x() + line.x.toReal(), origPos.y() + line.y.toReal(), QRectF r(origPos.x() + line.x.toReal(), origPos.y() + line.y.toReal(),
lineHeight / 2, QFontMetrics(eng->font()).horizontalAdvance(u' ')); lineHeight / 2, QFontMetrics(eng->font()).horizontalAdvance(u' '));
setPenAndDrawBackground(p, QPen(), selection->format, r); drawBackground(p, selection->format, r);
p->setPen(pen);
} }
return; return;
} }
@ -2662,7 +2660,7 @@ void QTextLine::draw_internal(QPainter *p, const QPointF &origPos,
else else
p->translate(origPos); p->translate(origPos);
QTextLineItemIterator iterator(eng, index, pos, selection);
QFixed lineBase = line.base(); QFixed lineBase = line.base();
eng->clearDecorations(); eng->clearDecorations();
eng->enableDelayDecorations(); eng->enableDelayDecorations();
@ -2672,183 +2670,207 @@ void QTextLine::draw_internal(QPainter *p, const QPointF &origPos,
const QTextFormatCollection *formatCollection = eng->formatCollection(); const QTextFormatCollection *formatCollection = eng->formatCollection();
bool suppressColors = (eng->option.flags() & QTextOption::SuppressColors); bool suppressColors = (eng->option.flags() & QTextOption::SuppressColors);
while (!iterator.atEnd()) {
QScriptItem &si = iterator.next();
if (selection && selection->start >= 0 && iterator.isOutsideSelection()) auto prepareFormat = [suppressColors, selection, this](QTextCharFormat &format,
continue; QScriptItem *si) {
format.merge(eng->format(si));
if (si.analysis.flags == QScriptAnalysis::LineOrParagraphSeparator if (suppressColors) {
&& !(eng->option.flags() & QTextOption::ShowLineAndParagraphSeparators)) format.clearForeground();
continue; format.clearBackground();
format.clearProperty(QTextFormat::TextUnderlineColor);
}
if (selection)
format.merge(selection->format);
};
QFixed itemBaseLine = y; {
QFont f = eng->font(si); QTextLineItemIterator iterator(eng, index, pos, selection);
QTextCharFormat format; while (!iterator.atEnd()) {
if (formatCollection != nullptr) QScriptItem &si = iterator.next();
format = formatCollection->defaultTextFormat();
if (eng->hasFormats() || selection || formatCollection) { if (eng->hasFormats() || selection || formatCollection) {
format.merge(eng->format(&si)); QTextCharFormat format;
if (formatCollection != nullptr)
if (suppressColors) { format = formatCollection->defaultTextFormat();
format.clearForeground(); prepareFormat(format, &si);
format.clearBackground(); drawBackground(p, format, QRectF(iterator.x.toReal(), (y - lineBase).toReal(),
format.clearProperty(QTextFormat::TextUnderlineColor); iterator.itemWidth.toReal(), line.height().toReal()));
}
if (selection)
format.merge(selection->format);
setPenAndDrawBackground(p, pen, format, QRectF(iterator.x.toReal(), (y - lineBase).toReal(),
iterator.itemWidth.toReal(), line.height().toReal()));
const qreal baseLineOffset = format.baselineOffset() / 100.0;
QTextCharFormat::VerticalAlignment valign = format.verticalAlignment();
if (valign == QTextCharFormat::AlignSuperScript
|| valign == QTextCharFormat::AlignSubScript
|| !qFuzzyIsNull(baseLineOffset))
{
QFontEngine *fe = f.d->engineForScript(si.analysis.script);
QFixed height = fe->ascent() + fe->descent();
itemBaseLine -= height * QFixed::fromReal(baseLineOffset);
if (valign == QTextCharFormat::AlignSubScript)
itemBaseLine += height * QFixed::fromReal(format.subScriptBaseline() / 100.0);
else if (valign == QTextCharFormat::AlignSuperScript)
itemBaseLine -= height * QFixed::fromReal(format.superScriptBaseline() / 100.0);
} }
} }
}
if (si.analysis.flags >= QScriptAnalysis::TabOrObject) { QPen pen = p->pen();
{
QTextLineItemIterator iterator(eng, index, pos, selection);
while (!iterator.atEnd()) {
QScriptItem &si = iterator.next();
if (eng->hasFormats()) { if (selection && selection->start >= 0 && iterator.isOutsideSelection())
p->save(); continue;
if (si.analysis.flags == QScriptAnalysis::Object && QTextDocumentPrivate::get(eng->block)) {
QFixed itemY = y - si.ascent;
switch (format.verticalAlignment()) {
case QTextCharFormat::AlignTop:
itemY = y - lineBase;
break;
case QTextCharFormat::AlignMiddle:
itemY = y - lineBase + (line.height() - si.height()) / 2;
break;
case QTextCharFormat::AlignBottom:
itemY = y - lineBase + line.height() - si.height();
break;
default:
break;
}
QRectF itemRect(iterator.x.toReal(), itemY.toReal(), iterator.itemWidth.toReal(), si.height().toReal()); if (si.analysis.flags == QScriptAnalysis::LineOrParagraphSeparator
&& !(eng->option.flags() & QTextOption::ShowLineAndParagraphSeparators))
continue;
eng->docLayout()->drawInlineObject(p, itemRect, QFixed itemBaseLine = y;
QTextInlineObject(iterator.item, eng), QFont f = eng->font(si);
si.position + eng->block.position(), QTextCharFormat format;
format); if (formatCollection != nullptr)
if (selection) { format = formatCollection->defaultTextFormat();
QBrush bg = format.brushProperty(ObjectSelectionBrush);
if (bg.style() != Qt::NoBrush) { if (eng->hasFormats() || selection || formatCollection) {
QColor c = bg.color(); prepareFormat(format, &si);
c.setAlpha(128); setPen(p, pen, format);
p->fillRect(itemRect, c);
const qreal baseLineOffset = format.baselineOffset() / 100.0;
QTextCharFormat::VerticalAlignment valign = format.verticalAlignment();
if (valign == QTextCharFormat::AlignSuperScript
|| valign == QTextCharFormat::AlignSubScript
|| !qFuzzyIsNull(baseLineOffset))
{
QFontEngine *fe = f.d->engineForScript(si.analysis.script);
QFixed height = fe->ascent() + fe->descent();
itemBaseLine -= height * QFixed::fromReal(baseLineOffset);
if (valign == QTextCharFormat::AlignSubScript)
itemBaseLine += height * QFixed::fromReal(format.subScriptBaseline() / 100.0);
else if (valign == QTextCharFormat::AlignSuperScript)
itemBaseLine -= height * QFixed::fromReal(format.superScriptBaseline() / 100.0);
}
}
if (si.analysis.flags >= QScriptAnalysis::TabOrObject) {
if (eng->hasFormats()) {
p->save();
if (si.analysis.flags == QScriptAnalysis::Object && QTextDocumentPrivate::get(eng->block)) {
QFixed itemY = y - si.ascent;
switch (format.verticalAlignment()) {
case QTextCharFormat::AlignTop:
itemY = y - lineBase;
break;
case QTextCharFormat::AlignMiddle:
itemY = y - lineBase + (line.height() - si.height()) / 2;
break;
case QTextCharFormat::AlignBottom:
itemY = y - lineBase + line.height() - si.height();
break;
default:
break;
} }
}
} else { // si.isTab
QFont f = eng->font(si);
QTextItemInt gf(si, &f, format);
gf.chars = nullptr;
gf.num_chars = 0;
gf.width = iterator.itemWidth;
QPainterPrivate::get(p)->drawTextItem(QPointF(iterator.x.toReal(), y.toReal()), gf, eng);
if (eng->option.flags() & QTextOption::ShowTabsAndSpaces) {
const QChar visualTab = QChar(QChar::VisualTabCharacter);
int w = QFontMetrics(f).horizontalAdvance(visualTab);
qreal x = iterator.itemWidth.toReal() - w; // Right-aligned
if (x < 0)
p->setClipRect(QRectF(iterator.x.toReal(), line.y.toReal(),
iterator.itemWidth.toReal(), line.height().toReal()),
Qt::IntersectClip);
else
x /= 2; // Centered
p->setFont(f);
p->drawText(QPointF(iterator.x.toReal() + x,
y.toReal()), visualTab);
}
QRectF itemRect(iterator.x.toReal(), itemY.toReal(), iterator.itemWidth.toReal(), si.height().toReal());
eng->docLayout()->drawInlineObject(p, itemRect,
QTextInlineObject(iterator.item, eng),
si.position + eng->block.position(),
format);
if (selection) {
QBrush bg = format.brushProperty(ObjectSelectionBrush);
if (bg.style() != Qt::NoBrush) {
QColor c = bg.color();
c.setAlpha(128);
p->fillRect(itemRect, c);
}
}
} else { // si.isTab
QFont f = eng->font(si);
QTextItemInt gf(si, &f, format);
gf.chars = nullptr;
gf.num_chars = 0;
gf.width = iterator.itemWidth;
QPainterPrivate::get(p)->drawTextItem(QPointF(iterator.x.toReal(), y.toReal()), gf, eng);
if (eng->option.flags() & QTextOption::ShowTabsAndSpaces) {
const QChar visualTab = QChar(QChar::VisualTabCharacter);
int w = QFontMetrics(f).horizontalAdvance(visualTab);
qreal x = iterator.itemWidth.toReal() - w; // Right-aligned
if (x < 0)
p->setClipRect(QRectF(iterator.x.toReal(), line.y.toReal(),
iterator.itemWidth.toReal(), line.height().toReal()),
Qt::IntersectClip);
else
x /= 2; // Centered
p->setFont(f);
p->drawText(QPointF(iterator.x.toReal() + x,
y.toReal()), visualTab);
}
}
p->restore();
} }
continue;
}
unsigned short *logClusters = eng->logClusters(&si);
QGlyphLayout glyphs = eng->shapedGlyphs(&si);
QTextItemInt gf(glyphs.mid(iterator.glyphsStart, iterator.glyphsEnd - iterator.glyphsStart),
&f, eng->layoutData->string.unicode() + iterator.itemStart,
iterator.itemEnd - iterator.itemStart, eng->fontEngine(si), format);
gf.logClusters = logClusters + iterator.itemStart - si.position;
gf.width = iterator.itemWidth;
gf.justified = line.justified;
gf.initWithScriptItem(si);
Q_ASSERT(gf.fontEngine);
QPointF pos(iterator.x.toReal(), itemBaseLine.toReal());
if (format.penProperty(QTextFormat::TextOutline).style() != Qt::NoPen) {
QPainterPath path;
path.setFillRule(Qt::WindingFill);
if (gf.glyphs.numGlyphs)
gf.fontEngine->addOutlineToPath(pos.x(), pos.y(), gf.glyphs, &path, gf.flags);
if (gf.flags) {
const QFontEngine *fe = gf.fontEngine;
const qreal lw = fe->lineThickness().toReal();
if (gf.flags & QTextItem::Underline) {
qreal offs = fe->underlinePosition().toReal();
path.addRect(pos.x(), pos.y() + offs, gf.width.toReal(), lw);
}
if (gf.flags & QTextItem::Overline) {
qreal offs = fe->ascent().toReal() + 1;
path.addRect(pos.x(), pos.y() - offs, gf.width.toReal(), lw);
}
if (gf.flags & QTextItem::StrikeOut) {
qreal offs = fe->ascent().toReal() / 3;
path.addRect(pos.x(), pos.y() - offs, gf.width.toReal(), lw);
}
}
p->save();
p->setRenderHint(QPainter::Antialiasing);
//Currently QPen with a Qt::NoPen style still returns a default
//QBrush which != Qt::NoBrush so we need this specialcase to reset it
if (p->pen().style() == Qt::NoPen)
p->setBrush(Qt::NoBrush);
else
p->setBrush(p->pen().brush());
p->setPen(format.textOutline());
p->drawPath(path);
p->restore(); p->restore();
} else {
if (noText)
gf.glyphs.numGlyphs = 0; // slightly less elegant than it should be
QPainterPrivate::get(p)->drawTextItem(pos, gf, eng);
} }
continue; if ((si.analysis.flags == QScriptAnalysis::Space
} || si.analysis.flags == QScriptAnalysis::Nbsp)
&& (eng->option.flags() & QTextOption::ShowTabsAndSpaces)) {
unsigned short *logClusters = eng->logClusters(&si); QBrush c = format.foreground();
QGlyphLayout glyphs = eng->shapedGlyphs(&si); if (c.style() != Qt::NoBrush)
p->setPen(c.color());
QTextItemInt gf(glyphs.mid(iterator.glyphsStart, iterator.glyphsEnd - iterator.glyphsStart), const QChar visualSpace = si.analysis.flags == QScriptAnalysis::Space ? u'\xb7' : u'\xb0';
&f, eng->layoutData->string.unicode() + iterator.itemStart, QFont oldFont = p->font();
iterator.itemEnd - iterator.itemStart, eng->fontEngine(si), format); p->setFont(eng->font(si));
gf.logClusters = logClusters + iterator.itemStart - si.position; p->drawText(QPointF(iterator.x.toReal(), itemBaseLine.toReal()), visualSpace);
gf.width = iterator.itemWidth; p->setPen(pen);
gf.justified = line.justified; p->setFont(oldFont);
gf.initWithScriptItem(si);
Q_ASSERT(gf.fontEngine);
QPointF pos(iterator.x.toReal(), itemBaseLine.toReal());
if (format.penProperty(QTextFormat::TextOutline).style() != Qt::NoPen) {
QPainterPath path;
path.setFillRule(Qt::WindingFill);
if (gf.glyphs.numGlyphs)
gf.fontEngine->addOutlineToPath(pos.x(), pos.y(), gf.glyphs, &path, gf.flags);
if (gf.flags) {
const QFontEngine *fe = gf.fontEngine;
const qreal lw = fe->lineThickness().toReal();
if (gf.flags & QTextItem::Underline) {
qreal offs = fe->underlinePosition().toReal();
path.addRect(pos.x(), pos.y() + offs, gf.width.toReal(), lw);
}
if (gf.flags & QTextItem::Overline) {
qreal offs = fe->ascent().toReal() + 1;
path.addRect(pos.x(), pos.y() - offs, gf.width.toReal(), lw);
}
if (gf.flags & QTextItem::StrikeOut) {
qreal offs = fe->ascent().toReal() / 3;
path.addRect(pos.x(), pos.y() - offs, gf.width.toReal(), lw);
}
} }
p->save();
p->setRenderHint(QPainter::Antialiasing);
//Currently QPen with a Qt::NoPen style still returns a default
//QBrush which != Qt::NoBrush so we need this specialcase to reset it
if (p->pen().style() == Qt::NoPen)
p->setBrush(Qt::NoBrush);
else
p->setBrush(p->pen().brush());
p->setPen(format.textOutline());
p->drawPath(path);
p->restore();
} else {
if (noText)
gf.glyphs.numGlyphs = 0; // slightly less elegant than it should be
QPainterPrivate::get(p)->drawTextItem(pos, gf, eng);
}
if ((si.analysis.flags == QScriptAnalysis::Space
|| si.analysis.flags == QScriptAnalysis::Nbsp)
&& (eng->option.flags() & QTextOption::ShowTabsAndSpaces)) {
QBrush c = format.foreground();
if (c.style() != Qt::NoBrush)
p->setPen(c.color());
const QChar visualSpace = si.analysis.flags == QScriptAnalysis::Space ? u'\xb7' : u'\xb0';
QFont oldFont = p->font();
p->setFont(eng->font(si));
p->drawText(QPointF(iterator.x.toReal(), itemBaseLine.toReal()), visualSpace);
p->setPen(pen);
p->setFont(oldFont);
} }
} }
eng->drawDecorations(p); eng->drawDecorations(p);

View File

@ -17,6 +17,7 @@ public:
private slots: private slots:
void tst_render_data(); void tst_render_data();
void tst_render(); void tst_render();
void tst_differentScriptsBackgrounds();
private: private:
QDir htmlDir; QDir htmlDir;
@ -81,6 +82,26 @@ void tst_Text::tst_render()
QBASELINE_TEST(image); QBASELINE_TEST(image);
} }
void tst_Text::tst_differentScriptsBackgrounds()
{
QTextDocument textDocument;
textDocument.setPageSize(QSizeF(800, 600));
textDocument.setHtml(QString::fromUtf8("<i><font style=\"font-size:72px\"><font style=\"background:#FFFF00\">イ雨エ</font></font></i>"));
QImage image(800, 600, QImage::Format_ARGB32);
image.fill(Qt::white);
{
QPainter painter(&image);
QAbstractTextDocumentLayout::PaintContext context;
context.palette.setColor(QPalette::Text, Qt::black);
textDocument.documentLayout()->draw(&painter, context);
}
QBASELINE_CHECK(image, "tst_differentScriptsBackgrounds");
}
#define main _realmain #define main _realmain
QTEST_MAIN(tst_Text) QTEST_MAIN(tst_Text)