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 | |
35 | class tst_QComplexText : public QObject |
36 | { |
37 | Q_OBJECT |
38 | |
39 | private 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 | |
56 | void 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 | |
84 | void 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 | |
135 | void 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 | |
160 | void 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 | |
175 | void 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 | |
212 | void tst_QComplexText::bidiCursorLogicalMovement_data() |
213 | { |
214 | bidiCursorMovement_data(); |
215 | } |
216 | |
217 | void 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 | |
245 | void tst_QComplexText::bidiInvalidCursorNoMovement_data() |
246 | { |
247 | bidiCursorMovement_data(); |
248 | } |
249 | |
250 | void 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 | |
276 | void 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 | |
291 | static 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 | |
370 | void 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 | |
427 | ushort 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 | |
464 | void 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 | |
538 | QTEST_MAIN(tst_QComplexText) |
539 | #include "tst_qcomplextext.moc" |
540 | |