1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the test suite of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28
29#include <QtTest/QtTest>
30#include <QtGui/QtGui>
31#include <private/qtextengine_p.h>
32
33#include "bidireorderstring.h"
34
35class tst_QComplexText : public QObject
36{
37Q_OBJECT
38
39private slots:
40 void bidiReorderString_data();
41 void bidiReorderString();
42 void bidiCursor_qtbug2795();
43 void bidiCursor_PDF();
44 void bidiCursorMovement_data();
45 void bidiCursorMovement();
46 void bidiCursorLogicalMovement_data();
47 void bidiCursorLogicalMovement();
48 void bidiInvalidCursorNoMovement_data();
49 void bidiInvalidCursorNoMovement();
50
51 void bidiCharacterTest();
52 void bidiTest();
53
54};
55
56void tst_QComplexText::bidiReorderString_data()
57{
58 QTest::addColumn<QString>(name: "logical");
59 QTest::addColumn<QString>(name: "VISUAL");
60 QTest::addColumn<int>(name: "basicDir");
61
62 const LV *data = logical_visual;
63 while ( data->name ) {
64 //next we fill it with data
65 QTest::newRow( dataTag: data->name )
66 << QString::fromUtf8( str: data->logical )
67 << QString::fromUtf8( str: data->visual )
68 << (int) data->basicDir;
69
70 QTest::newRow( dataTag: QByteArray(data->name) + " (doubled)" )
71 << (QString::fromUtf8( str: data->logical ) + QChar(QChar::ParagraphSeparator) + QString::fromUtf8( str: data->logical ))
72 << (QString::fromUtf8( str: data->visual ) + QChar(QChar::ParagraphSeparator) + QString::fromUtf8( str: data->visual ))
73 << (int) data->basicDir;
74 data++;
75 }
76
77 QString isolateAndBoundary = QString(QChar(0x2068 /* DirFSI */)) + QChar(0x1c /* DirB */) + QChar(0x2069 /* DirPDI */);
78 QTest::newRow( dataTag: "isolateAndBoundary" )
79 << QString::fromUtf8( str: data->logical )
80 << QString::fromUtf8( str: data->visual )
81 << (int) QChar::DirL;
82}
83
84void tst_QComplexText::bidiReorderString()
85{
86 QFETCH( QString, logical );
87 QFETCH( int, basicDir );
88
89 // replace \n with Unicode newline. The new algorithm ignores \n
90 logical.replace(before: QChar('\n'), after: QChar(0x2028));
91
92 QTextEngine e(logical, QFont());
93 e.option.setTextDirection((QChar::Direction)basicDir == QChar::DirL ? Qt::LeftToRight : Qt::RightToLeft);
94 e.itemize();
95 quint8 levels[256];
96 int visualOrder[256];
97 int nitems = e.layoutData->items.size();
98 int i;
99 for (i = 0; i < nitems; ++i) {
100 //qDebug("item %d bidiLevel=%d", i, e.items[i].analysis.bidiLevel);
101 levels[i] = e.layoutData->items[i].analysis.bidiLevel;
102 }
103 e.bidiReorder(numRuns: nitems, levels, visualOrder);
104
105 QString visual;
106 for (i = 0; i < nitems; ++i) {
107 QScriptItem &si = e.layoutData->items[visualOrder[i]];
108 QString sub = logical.mid(position: si.position, n: e.length(item: visualOrder[i]));
109 if (si.analysis.bidiLevel % 2) {
110 // reverse sub
111 QChar *a = sub.data();
112 QChar *b = a + sub.length() - 1;
113 while (a < b) {
114 QChar tmp = *a;
115 *a = *b;
116 *b = tmp;
117 ++a;
118 --b;
119 }
120 a = (QChar *)sub.unicode();
121 b = a + sub.length();
122 while (a<b) {
123 *a = a->mirroredChar();
124 ++a;
125 }
126 }
127 visual += sub;
128 }
129 // replace Unicode newline back with \n to compare.
130 visual.replace(before: QChar(0x2028), after: QChar('\n'));
131
132 QTEST(visual, "VISUAL");
133}
134
135void tst_QComplexText::bidiCursor_qtbug2795()
136{
137 QString str = QString::fromUtf8(str: "الجزيرة نت");
138 QTextLayout l1(str);
139
140 l1.beginLayout();
141 l1.setCacheEnabled(true);
142 QTextLine line1 = l1.createLine();
143 l1.endLayout();
144
145 qreal x1 = line1.cursorToX(cursorPos: 0) - line1.cursorToX(cursorPos: str.size());
146
147 str.append(s: "1");
148 QTextLayout l2(str);
149 l2.setCacheEnabled(true);
150 l2.beginLayout();
151 QTextLine line2 = l2.createLine();
152 l2.endLayout();
153
154 qreal x2 = line2.cursorToX(cursorPos: 0) - line2.cursorToX(cursorPos: str.size());
155
156 // The cursor should remain at the same position after a digit is appended
157 QCOMPARE(x1, x2);
158}
159
160void tst_QComplexText::bidiCursorMovement_data()
161{
162 QTest::addColumn<QString>(name: "logical");
163 QTest::addColumn<int>(name: "basicDir");
164
165 const LV *data = logical_visual;
166 while ( data->name ) {
167 //next we fill it with data
168 QTest::newRow( dataTag: data->name )
169 << QString::fromUtf8( str: data->logical )
170 << (int) data->basicDir;
171 data++;
172 }
173}
174
175void tst_QComplexText::bidiCursorMovement()
176{
177 QFETCH(QString, logical);
178 QFETCH(int, basicDir);
179
180 QTextLayout layout(logical);
181 layout.setCacheEnabled(true);
182
183 QTextOption option = layout.textOption();
184 option.setTextDirection(basicDir == QChar::DirL ? Qt::LeftToRight : Qt::RightToLeft);
185 layout.setTextOption(option);
186 layout.setCursorMoveStyle(Qt::VisualMoveStyle);
187 bool moved;
188 int oldPos, newPos = 0;
189 qreal x, newX;
190
191 layout.beginLayout();
192 QTextLine line = layout.createLine();
193 layout.endLayout();
194
195 newX = line.cursorToX(cursorPos: 0);
196 do {
197 oldPos = newPos;
198 x = newX;
199 newX = line.cursorToX(cursorPos: oldPos);
200 if (basicDir == QChar::DirL) {
201 QVERIFY(newX >= x);
202 newPos = layout.rightCursorPosition(oldPos);
203 } else
204 {
205 QVERIFY(newX <= x);
206 newPos = layout.leftCursorPosition(oldPos);
207 }
208 moved = (oldPos != newPos);
209 } while (moved);
210}
211
212void tst_QComplexText::bidiCursorLogicalMovement_data()
213{
214 bidiCursorMovement_data();
215}
216
217void tst_QComplexText::bidiCursorLogicalMovement()
218{
219 QFETCH(QString, logical);
220 QFETCH(int, basicDir);
221
222 QTextLayout layout(logical);
223
224 QTextOption option = layout.textOption();
225 option.setTextDirection(basicDir == QChar::DirL ? Qt::LeftToRight : Qt::RightToLeft);
226 layout.setTextOption(option);
227 bool moved;
228 int oldPos, newPos = 0;
229
230 do {
231 oldPos = newPos;
232 newPos = layout.nextCursorPosition(oldPos);
233 QVERIFY(newPos >= oldPos);
234 moved = (oldPos != newPos);
235 } while (moved);
236
237 do {
238 oldPos = newPos;
239 newPos = layout.previousCursorPosition(oldPos);
240 QVERIFY(newPos <= oldPos);
241 moved = (oldPos != newPos);
242 } while (moved);
243}
244
245void tst_QComplexText::bidiInvalidCursorNoMovement_data()
246{
247 bidiCursorMovement_data();
248}
249
250void tst_QComplexText::bidiInvalidCursorNoMovement()
251{
252 QFETCH(QString, logical);
253 QFETCH(int, basicDir);
254
255 QTextLayout layout(logical);
256
257 QTextOption option = layout.textOption();
258 option.setTextDirection(basicDir == QChar::DirL ? Qt::LeftToRight : Qt::RightToLeft);
259 layout.setTextOption(option);
260
261 // visual
262 QCOMPARE(layout.rightCursorPosition(-1000), -1000);
263 QCOMPARE(layout.rightCursorPosition(1000), 1000);
264
265 QCOMPARE(layout.leftCursorPosition(-1000), -1000);
266 QCOMPARE(layout.leftCursorPosition(1000), 1000);
267
268 // logical
269 QCOMPARE(layout.nextCursorPosition(-1000), -1000);
270 QCOMPARE(layout.nextCursorPosition(1000), 1000);
271
272 QCOMPARE(layout.previousCursorPosition(-1000), -1000);
273 QCOMPARE(layout.previousCursorPosition(1000), 1000);
274}
275
276void tst_QComplexText::bidiCursor_PDF()
277{
278 QString str = QString::fromUtf8(str: "\342\200\252hello\342\200\254");
279 QTextLayout layout(str);
280 layout.setCacheEnabled(true);
281
282 layout.beginLayout();
283 QTextLine line = layout.createLine();
284 layout.endLayout();
285
286 int size = str.size();
287
288 QVERIFY(line.cursorToX(size) == line.cursorToX(size - 1));
289}
290
291static void testBidiString(const QString &data, int paragraphDirection, const QVector<int> &resolvedLevels, const QVector<int> &visualOrder)
292{
293 Q_UNUSED(resolvedLevels);
294
295 QTextEngine e(data, QFont());
296 Qt::LayoutDirection pDir = Qt::LeftToRight;
297 if (paragraphDirection == 1)
298 pDir = Qt::RightToLeft;
299 else if (paragraphDirection == 2)
300 pDir = Qt::LayoutDirectionAuto;
301
302 e.option.setTextDirection(pDir);
303 e.itemize();
304 quint8 levels[1024];
305 int visual[1024];
306 int nitems = e.layoutData->items.size();
307 int i;
308 for (i = 0; i < nitems; ++i) {
309 //qDebug("item %d bidiLevel=%d", i, e.items[i].analysis.bidiLevel);
310 levels[i] = e.layoutData->items[i].analysis.bidiLevel;
311 }
312 e.bidiReorder(numRuns: nitems, levels, visualOrder: visual);
313
314 QString visualString;
315 for (i = 0; i < nitems; ++i) {
316 QScriptItem &si = e.layoutData->items[visual[i]];
317 QString sub;
318 for (int j = si.position; j < si.position + e.length(item: visual[i]); ++j) {
319 switch (data.at(i: j).direction()) {
320 case QChar::DirLRE:
321 case QChar::DirRLE:
322 case QChar::DirLRO:
323 case QChar::DirRLO:
324 case QChar::DirPDF:
325 case QChar::DirBN:
326 continue;
327 default:
328 break;
329 }
330 sub += data.at(i: j);
331 }
332
333 // remove explicit embedding characters, as the test data has them removed as well
334 sub.remove(c: QChar(0x202a));
335 sub.remove(c: QChar(0x202b));
336 sub.remove(c: QChar(0x202c));
337 sub.remove(c: QChar(0x202d));
338 sub.remove(c: QChar(0x202e));
339 if (si.analysis.bidiLevel % 2) {
340 // reverse sub
341 QChar *a = sub.data();
342 QChar *b = a + sub.length() - 1;
343 while (a < b) {
344 QChar tmp = *a;
345 *a = *b;
346 *b = tmp;
347 ++a;
348 --b;
349 }
350 a = (QChar *)sub.unicode();
351 b = a + sub.length();
352// while (a<b) {
353// *a = a->mirroredChar();
354// ++a;
355// }
356 }
357 visualString += sub;
358 }
359 QString expected;
360// qDebug() << "expected visual order";
361 for (int i : visualOrder) {
362// qDebug() << " " << i << hex << data[i].unicode();
363 expected.append(c: data[i]);
364 }
365
366 QCOMPARE(visualString, expected);
367
368}
369
370void tst_QComplexText::bidiCharacterTest()
371{
372 QString testFile = QFINDTESTDATA("data/BidiCharacterTest.txt");
373 QFile f(testFile);
374 QVERIFY(f.exists());
375
376 f.open(flags: QIODevice::ReadOnly);
377
378 int linenum = 0;
379 while (!f.atEnd()) {
380 linenum++;
381
382 QByteArray line = f.readLine().simplified();
383 if (line.startsWith(c: '#') || line.isEmpty())
384 continue;
385 QVERIFY(!line.contains('#'));
386
387 QList<QByteArray> parts = line.split(sep: ';');
388 QVERIFY(parts.size() == 5);
389
390 QString data;
391 QList<QByteArray> dataParts = parts.at(i: 0).split(sep: ' ');
392 for (const auto &p : dataParts) {
393 bool ok;
394 data += QChar((ushort)p.toInt(ok: &ok, base: 16));
395 QVERIFY(ok);
396 }
397
398 int paragraphDirection = parts.at(i: 1).toInt();
399// int resolvedParagraphLevel = parts.at(2).toInt();
400
401 QVector<int> resolvedLevels;
402 QList<QByteArray> levelParts = parts.at(i: 3).split(sep: ' ');
403 for (const auto &p : levelParts) {
404 if (p == "x") {
405 resolvedLevels += -1;
406 } else {
407 bool ok;
408 resolvedLevels += p.toInt(ok: &ok);
409 QVERIFY(ok);
410 }
411 }
412
413 QVector<int> visualOrder;
414 QList<QByteArray> orderParts = parts.at(i: 4).split(sep: ' ');
415 for (const auto &p : orderParts) {
416 bool ok;
417 visualOrder += p.toInt(ok: &ok);
418 QVERIFY(ok);
419 }
420
421 const QByteArray nm = "line #" + QByteArray::number(linenum);
422
423 testBidiString(data, paragraphDirection, resolvedLevels, visualOrder);
424 }
425}
426
427ushort unicodeForDirection(const QByteArray &direction)
428{
429 struct {
430 const char *string;
431 ushort unicode;
432 } dirToUnicode[] = {
433 { .string: "L", .unicode: 0x41 },
434 { .string: "R", .unicode: 0x5d0 },
435 { .string: "EN", .unicode: 0x30 },
436 { .string: "ES", .unicode: 0x2b },
437 { .string: "ET", .unicode: 0x24 },
438 { .string: "AN", .unicode: 0x660 },
439 { .string: "CS", .unicode: 0x2c },
440 { .string: "B", .unicode: '\n' },
441 { .string: "S", .unicode: 0x9 },
442 { .string: "WS", .unicode: 0x20 },
443 { .string: "ON", .unicode: 0x2a },
444 { .string: "LRE", .unicode: 0x202a },
445 { .string: "LRO", .unicode: 0x202d },
446 { .string: "AL", .unicode: 0x627 },
447 { .string: "RLE", .unicode: 0x202b },
448 { .string: "RLO", .unicode: 0x202e },
449 { .string: "PDF", .unicode: 0x202c },
450 { .string: "NSM", .unicode: 0x300 },
451 { .string: "BN", .unicode: 0xad },
452 { .string: "LRI", .unicode: 0x2066 },
453 { .string: "RLI", .unicode: 0x2067 },
454 { .string: "FSI", .unicode: 0x2068 },
455 { .string: "PDI", .unicode: 0x2069 }
456 };
457 for (const auto &e : dirToUnicode) {
458 if (e.string == direction)
459 return e.unicode;
460 }
461 Q_UNREACHABLE();
462}
463
464void tst_QComplexText::bidiTest()
465{
466 QString testFile = QFINDTESTDATA("data/BidiTest.txt");
467 QFile f(testFile);
468 QVERIFY(f.exists());
469
470 f.open(flags: QIODevice::ReadOnly);
471
472 int linenum = 0;
473 QVector<int> resolvedLevels;
474 QVector<int> visualOrder;
475 while (!f.atEnd()) {
476 linenum++;
477
478 QByteArray line = f.readLine().simplified();
479 if (line.startsWith(c: '#') || line.isEmpty())
480 continue;
481 QVERIFY(!line.contains('#'));
482
483 if (line.startsWith(c: "@Levels:")) {
484 line = line.mid(index: strlen(s: "@Levels:")).simplified();
485
486 resolvedLevels.clear();
487 QList<QByteArray> levelParts = line.split(sep: ' ');
488 for (const auto &p : levelParts) {
489 if (p == "x") {
490 resolvedLevels += -1;
491 } else {
492 bool ok;
493 resolvedLevels += p.toInt(ok: &ok);
494 QVERIFY(ok);
495 }
496 }
497 continue;
498 } else if (line.startsWith(c: "@Reorder:")) {
499 line = line.mid(index: strlen(s: "@Reorder:")).simplified();
500
501 visualOrder.clear();
502 QList<QByteArray> orderParts = line.split(sep: ' ');
503 for (const auto &p : orderParts) {
504 if (p.isEmpty())
505 continue;
506 bool ok;
507 visualOrder += p.toInt(ok: &ok);
508 QVERIFY(ok);
509 }
510 continue;
511 }
512
513 QList<QByteArray> parts = line.split(sep: ';');
514 Q_ASSERT(parts.size() == 2);
515
516 QString data;
517 QList<QByteArray> dataParts = parts.at(i: 0).split(sep: ' ');
518 for (const auto &p : dataParts) {
519 ushort uc = unicodeForDirection(direction: p);
520 data += QChar(uc);
521 }
522
523 int paragraphDirections = parts.at(i: 1).toInt();
524
525 const QByteArray nm = "line #" + QByteArray::number(linenum);
526 if (paragraphDirections & 1)
527 testBidiString(data, paragraphDirection: 2, resolvedLevels, visualOrder);
528 if (paragraphDirections & 2)
529 testBidiString(data, paragraphDirection: 0, resolvedLevels, visualOrder);
530 if (paragraphDirections & 4)
531 testBidiString(data, paragraphDirection: 1, resolvedLevels, visualOrder);
532
533 }
534}
535
536
537
538QTEST_MAIN(tst_QComplexText)
539#include "tst_qcomplextext.moc"
540

source code of qtbase/tests/auto/other/qcomplextext/tst_qcomplextext.cpp