7f504283ef
The UBA in Qt was out of date, implementing the spec from pre Unicode 6.3 days. It missed handling of directional isolates and paired brackets. This adds a completely new implementation of the UBA, that is compliant with Unicode 10. Added the test data from Unicode 10 to the qcomplextext auto test and ensure that we pass the test suite. Task-number: QTBUG-57743 Change-Id: Ie2d957bc9775d82f0a51d1c78dc6bd154f22847c Reviewed-by: Lars Knoll <lars.knoll@qt.io>
561 lines
16 KiB
C++
561 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_data();
|
|
void bidiCharacterTest();
|
|
void bidiTest_data();
|
|
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;
|
|
data++;
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
void tst_QComplexText::bidiCharacterTest_data()
|
|
{
|
|
QTest::addColumn<QString>("data");
|
|
QTest::addColumn<int>("paragraphDirection");
|
|
QTest::addColumn<QVector<int>>("resolvedLevels");
|
|
QTest::addColumn<QVector<int>>("visualOrder");
|
|
|
|
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);
|
|
QTest::newRow(nm.constData()) << data << paragraphDirection << resolvedLevels << visualOrder;
|
|
}
|
|
}
|
|
|
|
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()
|
|
{
|
|
QFETCH(QString, data);
|
|
QFETCH(int, paragraphDirection);
|
|
QFETCH(QVector<int>, resolvedLevels);
|
|
QFETCH(QVector<int>, visualOrder);
|
|
|
|
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", QChar::ParagraphSeparator },
|
|
{ "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_data()
|
|
{
|
|
QTest::addColumn<QString>("data");
|
|
QTest::addColumn<int>("paragraphDirection");
|
|
QTest::addColumn<QVector<int>>("resolvedLevels");
|
|
QTest::addColumn<QVector<int>>("visualOrder");
|
|
|
|
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)
|
|
QTest::newRow((nm + " (Auto)").constData()) << data << 2 << resolvedLevels << visualOrder;
|
|
if (paragraphDirections & 2)
|
|
QTest::newRow((nm + " (LTR)").constData()) << data << 0 << resolvedLevels << visualOrder;
|
|
if (paragraphDirections & 4)
|
|
QTest::newRow((nm + " (RTL)").constData()) << data << 1 << resolvedLevels << visualOrder;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
void tst_QComplexText::bidiTest()
|
|
{
|
|
QFETCH(QString, data);
|
|
QFETCH(int, paragraphDirection);
|
|
QFETCH(QVector<int>, resolvedLevels);
|
|
QFETCH(QVector<int>, visualOrder);
|
|
|
|
testBidiString(data, paragraphDirection, resolvedLevels, visualOrder);
|
|
}
|
|
|
|
|
|
|
|
QTEST_MAIN(tst_QComplexText)
|
|
#include "tst_qcomplextext.moc"
|