qt5base-lts/tests/auto/other/qcomplextext/tst_qcomplextext.cpp
Lars Knoll 035d80407b Fix two smaller bugs in the BiDi engine
Remove wrong code changing the Bido level of line separators. This
lead to wrong ordering of the string in case the line separator was
meant to be ignored and the string should be rendered in one line. Line
breaks are anyways already reset to the paragraph level by the algorithm
and reordering is done on a line by line basis, so this will work
correctly when doing proper line breaking.

Secondly fix a small bug found while testing the above change, where
we wouldn't set the correct levels for boundary neutrals and explicit
embedding chars because we did that processing before we were fully
done with the BiDi algorithm.

Change-Id: Id88f91cd58d2ab29be864aef34ca1727c1586611
Reviewed-by: Konstantin Ritt <ritt.ks@gmail.com>
Reviewed-by: Eskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io>
2019-02-09 08:01:26 +00:00

540 lines
16 KiB
C++

/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include <QtTest/QtTest>
#include <QtGui/QtGui>
#include <private/qtextengine_p.h>
#include "bidireorderstring.h"
class tst_QComplexText : public QObject
{
Q_OBJECT
private slots:
void bidiReorderString_data();
void bidiReorderString();
void bidiCursor_qtbug2795();
void bidiCursor_PDF();
void bidiCursorMovement_data();
void bidiCursorMovement();
void bidiCursorLogicalMovement_data();
void bidiCursorLogicalMovement();
void bidiInvalidCursorNoMovement_data();
void bidiInvalidCursorNoMovement();
void bidiCharacterTest();
void bidiTest();
};
void tst_QComplexText::bidiReorderString_data()
{
QTest::addColumn<QString>("logical");
QTest::addColumn<QString>("VISUAL");
QTest::addColumn<int>("basicDir");
const LV *data = logical_visual;
while ( data->name ) {
//next we fill it with data
QTest::newRow( data->name )
<< QString::fromUtf8( data->logical )
<< QString::fromUtf8( data->visual )
<< (int) data->basicDir;
QTest::newRow( QByteArray(data->name) + " (doubled)" )
<< (QString::fromUtf8( data->logical ) + QChar(QChar::ParagraphSeparator) + QString::fromUtf8( data->logical ))
<< (QString::fromUtf8( data->visual ) + QChar(QChar::ParagraphSeparator) + QString::fromUtf8( data->visual ))
<< (int) data->basicDir;
data++;
}
QString isolateAndBoundary = QString(QChar(0x2068 /* DirFSI */)) + QChar(0x1c /* DirB */) + QChar(0x2069 /* DirPDI */);
QTest::newRow( "isolateAndBoundary" )
<< QString::fromUtf8( data->logical )
<< QString::fromUtf8( data->visual )
<< (int) QChar::DirL;
}
void tst_QComplexText::bidiReorderString()
{
QFETCH( QString, logical );
QFETCH( int, basicDir );
// replace \n with Unicode newline. The new algorithm ignores \n
logical.replace(QChar('\n'), QChar(0x2028));
QTextEngine e(logical, QFont());
e.option.setTextDirection((QChar::Direction)basicDir == QChar::DirL ? Qt::LeftToRight : Qt::RightToLeft);
e.itemize();
quint8 levels[256];
int visualOrder[256];
int nitems = e.layoutData->items.size();
int i;
for (i = 0; i < nitems; ++i) {
//qDebug("item %d bidiLevel=%d", i, e.items[i].analysis.bidiLevel);
levels[i] = e.layoutData->items[i].analysis.bidiLevel;
}
e.bidiReorder(nitems, levels, visualOrder);
QString visual;
for (i = 0; i < nitems; ++i) {
QScriptItem &si = e.layoutData->items[visualOrder[i]];
QString sub = logical.mid(si.position, e.length(visualOrder[i]));
if (si.analysis.bidiLevel % 2) {
// reverse sub
QChar *a = sub.data();
QChar *b = a + sub.length() - 1;
while (a < b) {
QChar tmp = *a;
*a = *b;
*b = tmp;
++a;
--b;
}
a = (QChar *)sub.unicode();
b = a + sub.length();
while (a<b) {
*a = a->mirroredChar();
++a;
}
}
visual += sub;
}
// replace Unicode newline back with \n to compare.
visual.replace(QChar(0x2028), QChar('\n'));
QTEST(visual, "VISUAL");
}
void tst_QComplexText::bidiCursor_qtbug2795()
{
QString str = QString::fromUtf8("الجزيرة نت");
QTextLayout l1(str);
l1.beginLayout();
l1.setCacheEnabled(true);
QTextLine line1 = l1.createLine();
l1.endLayout();
qreal x1 = line1.cursorToX(0) - line1.cursorToX(str.size());
str.append("1");
QTextLayout l2(str);
l2.setCacheEnabled(true);
l2.beginLayout();
QTextLine line2 = l2.createLine();
l2.endLayout();
qreal x2 = line2.cursorToX(0) - line2.cursorToX(str.size());
// The cursor should remain at the same position after a digit is appended
QCOMPARE(x1, x2);
}
void tst_QComplexText::bidiCursorMovement_data()
{
QTest::addColumn<QString>("logical");
QTest::addColumn<int>("basicDir");
const LV *data = logical_visual;
while ( data->name ) {
//next we fill it with data
QTest::newRow( data->name )
<< QString::fromUtf8( data->logical )
<< (int) data->basicDir;
data++;
}
}
void tst_QComplexText::bidiCursorMovement()
{
QFETCH(QString, logical);
QFETCH(int, basicDir);
QTextLayout layout(logical);
layout.setCacheEnabled(true);
QTextOption option = layout.textOption();
option.setTextDirection(basicDir == QChar::DirL ? Qt::LeftToRight : Qt::RightToLeft);
layout.setTextOption(option);
layout.setCursorMoveStyle(Qt::VisualMoveStyle);
bool moved;
int oldPos, newPos = 0;
qreal x, newX;
layout.beginLayout();
QTextLine line = layout.createLine();
layout.endLayout();
newX = line.cursorToX(0);
do {
oldPos = newPos;
x = newX;
newX = line.cursorToX(oldPos);
if (basicDir == QChar::DirL) {
QVERIFY(newX >= x);
newPos = layout.rightCursorPosition(oldPos);
} else
{
QVERIFY(newX <= x);
newPos = layout.leftCursorPosition(oldPos);
}
moved = (oldPos != newPos);
} while (moved);
}
void tst_QComplexText::bidiCursorLogicalMovement_data()
{
bidiCursorMovement_data();
}
void tst_QComplexText::bidiCursorLogicalMovement()
{
QFETCH(QString, logical);
QFETCH(int, basicDir);
QTextLayout layout(logical);
QTextOption option = layout.textOption();
option.setTextDirection(basicDir == QChar::DirL ? Qt::LeftToRight : Qt::RightToLeft);
layout.setTextOption(option);
bool moved;
int oldPos, newPos = 0;
do {
oldPos = newPos;
newPos = layout.nextCursorPosition(oldPos);
QVERIFY(newPos >= oldPos);
moved = (oldPos != newPos);
} while (moved);
do {
oldPos = newPos;
newPos = layout.previousCursorPosition(oldPos);
QVERIFY(newPos <= oldPos);
moved = (oldPos != newPos);
} while (moved);
}
void tst_QComplexText::bidiInvalidCursorNoMovement_data()
{
bidiCursorMovement_data();
}
void tst_QComplexText::bidiInvalidCursorNoMovement()
{
QFETCH(QString, logical);
QFETCH(int, basicDir);
QTextLayout layout(logical);
QTextOption option = layout.textOption();
option.setTextDirection(basicDir == QChar::DirL ? Qt::LeftToRight : Qt::RightToLeft);
layout.setTextOption(option);
// visual
QCOMPARE(layout.rightCursorPosition(-1000), -1000);
QCOMPARE(layout.rightCursorPosition(1000), 1000);
QCOMPARE(layout.leftCursorPosition(-1000), -1000);
QCOMPARE(layout.leftCursorPosition(1000), 1000);
// logical
QCOMPARE(layout.nextCursorPosition(-1000), -1000);
QCOMPARE(layout.nextCursorPosition(1000), 1000);
QCOMPARE(layout.previousCursorPosition(-1000), -1000);
QCOMPARE(layout.previousCursorPosition(1000), 1000);
}
void tst_QComplexText::bidiCursor_PDF()
{
QString str = QString::fromUtf8("\342\200\252hello\342\200\254");
QTextLayout layout(str);
layout.setCacheEnabled(true);
layout.beginLayout();
QTextLine line = layout.createLine();
layout.endLayout();
int size = str.size();
QVERIFY(line.cursorToX(size) == line.cursorToX(size - 1));
}
static void testBidiString(const QString &data, int paragraphDirection, const QVector<int> &resolvedLevels, const QVector<int> &visualOrder)
{
Q_UNUSED(resolvedLevels);
QTextEngine e(data, QFont());
Qt::LayoutDirection pDir = Qt::LeftToRight;
if (paragraphDirection == 1)
pDir = Qt::RightToLeft;
else if (paragraphDirection == 2)
pDir = Qt::LayoutDirectionAuto;
e.option.setTextDirection(pDir);
e.itemize();
quint8 levels[1024];
int visual[1024];
int nitems = e.layoutData->items.size();
int i;
for (i = 0; i < nitems; ++i) {
//qDebug("item %d bidiLevel=%d", i, e.items[i].analysis.bidiLevel);
levels[i] = e.layoutData->items[i].analysis.bidiLevel;
}
e.bidiReorder(nitems, levels, visual);
QString visualString;
for (i = 0; i < nitems; ++i) {
QScriptItem &si = e.layoutData->items[visual[i]];
QString sub;
for (int j = si.position; j < si.position + e.length(visual[i]); ++j) {
switch (data.at(j).direction()) {
case QChar::DirLRE:
case QChar::DirRLE:
case QChar::DirLRO:
case QChar::DirRLO:
case QChar::DirPDF:
case QChar::DirBN:
continue;
default:
break;
}
sub += data.at(j);
}
// remove explicit embedding characters, as the test data has them removed as well
sub.remove(QChar(0x202a));
sub.remove(QChar(0x202b));
sub.remove(QChar(0x202c));
sub.remove(QChar(0x202d));
sub.remove(QChar(0x202e));
if (si.analysis.bidiLevel % 2) {
// reverse sub
QChar *a = sub.data();
QChar *b = a + sub.length() - 1;
while (a < b) {
QChar tmp = *a;
*a = *b;
*b = tmp;
++a;
--b;
}
a = (QChar *)sub.unicode();
b = a + sub.length();
// while (a<b) {
// *a = a->mirroredChar();
// ++a;
// }
}
visualString += sub;
}
QString expected;
// qDebug() << "expected visual order";
for (int i : visualOrder) {
// qDebug() << " " << i << hex << data[i].unicode();
expected.append(data[i]);
}
QCOMPARE(visualString, expected);
}
void tst_QComplexText::bidiCharacterTest()
{
QString testFile = QFINDTESTDATA("data/BidiCharacterTest.txt");
QFile f(testFile);
QVERIFY(f.exists());
f.open(QIODevice::ReadOnly);
int linenum = 0;
while (!f.atEnd()) {
linenum++;
QByteArray line = f.readLine().simplified();
if (line.startsWith('#') || line.isEmpty())
continue;
QVERIFY(!line.contains('#'));
QList<QByteArray> parts = line.split(';');
QVERIFY(parts.size() == 5);
QString data;
QList<QByteArray> dataParts = parts.at(0).split(' ');
for (const auto &p : dataParts) {
bool ok;
data += QChar((ushort)p.toInt(&ok, 16));
QVERIFY(ok);
}
int paragraphDirection = parts.at(1).toInt();
// int resolvedParagraphLevel = parts.at(2).toInt();
QVector<int> resolvedLevels;
QList<QByteArray> levelParts = parts.at(3).split(' ');
for (const auto &p : levelParts) {
if (p == "x") {
resolvedLevels += -1;
} else {
bool ok;
resolvedLevels += p.toInt(&ok);
QVERIFY(ok);
}
}
QVector<int> visualOrder;
QList<QByteArray> orderParts = parts.at(4).split(' ');
for (const auto &p : orderParts) {
bool ok;
visualOrder += p.toInt(&ok);
QVERIFY(ok);
}
const QByteArray nm = "line #" + QByteArray::number(linenum);
testBidiString(data, paragraphDirection, resolvedLevels, visualOrder);
}
}
ushort unicodeForDirection(const QByteArray &direction)
{
struct {
const char *string;
ushort unicode;
} dirToUnicode[] = {
{ "L", 0x41 },
{ "R", 0x5d0 },
{ "EN", 0x30 },
{ "ES", 0x2b },
{ "ET", 0x24 },
{ "AN", 0x660 },
{ "CS", 0x2c },
{ "B", '\n' },
{ "S", 0x9 },
{ "WS", 0x20 },
{ "ON", 0x2a },
{ "LRE", 0x202a },
{ "LRO", 0x202d },
{ "AL", 0x627 },
{ "RLE", 0x202b },
{ "RLO", 0x202e },
{ "PDF", 0x202c },
{ "NSM", 0x300 },
{ "BN", 0xad },
{ "LRI", 0x2066 },
{ "RLI", 0x2067 },
{ "FSI", 0x2068 },
{ "PDI", 0x2069 }
};
for (const auto &e : dirToUnicode) {
if (e.string == direction)
return e.unicode;
}
Q_UNREACHABLE();
}
void tst_QComplexText::bidiTest()
{
QString testFile = QFINDTESTDATA("data/BidiTest.txt");
QFile f(testFile);
QVERIFY(f.exists());
f.open(QIODevice::ReadOnly);
int linenum = 0;
QVector<int> resolvedLevels;
QVector<int> visualOrder;
while (!f.atEnd()) {
linenum++;
QByteArray line = f.readLine().simplified();
if (line.startsWith('#') || line.isEmpty())
continue;
QVERIFY(!line.contains('#'));
if (line.startsWith("@Levels:")) {
line = line.mid(strlen("@Levels:")).simplified();
resolvedLevels.clear();
QList<QByteArray> levelParts = line.split(' ');
for (const auto &p : levelParts) {
if (p == "x") {
resolvedLevels += -1;
} else {
bool ok;
resolvedLevels += p.toInt(&ok);
QVERIFY(ok);
}
}
continue;
} else if (line.startsWith("@Reorder:")) {
line = line.mid(strlen("@Reorder:")).simplified();
visualOrder.clear();
QList<QByteArray> orderParts = line.split(' ');
for (const auto &p : orderParts) {
if (p.isEmpty())
continue;
bool ok;
visualOrder += p.toInt(&ok);
QVERIFY(ok);
}
continue;
}
QList<QByteArray> parts = line.split(';');
Q_ASSERT(parts.size() == 2);
QString data;
QList<QByteArray> dataParts = parts.at(0).split(' ');
for (const auto &p : dataParts) {
ushort uc = unicodeForDirection(p);
data += QChar(uc);
}
int paragraphDirections = parts.at(1).toInt();
const QByteArray nm = "line #" + QByteArray::number(linenum);
if (paragraphDirections & 1)
testBidiString(data, 2, resolvedLevels, visualOrder);
if (paragraphDirections & 2)
testBidiString(data, 0, resolvedLevels, visualOrder);
if (paragraphDirections & 4)
testBidiString(data, 1, resolvedLevels, visualOrder);
}
}
QTEST_MAIN(tst_QComplexText)
#include "tst_qcomplextext.moc"