Об использовании кодировок в Qt5/6 :: Cетевой уголок Majestio

Об использовании кодировок в Qt5/6


Важно
В Qt5/Qt6 строки QString представляются в кодировке UTF-16. Строковые константы, которые в редакторе кода записываются, как правило в кодировке UTF-8 - всё равно преобразуются компилятором во внутреннее представление в UTF-16.

Основные определения

Немного о нормализации в Unicode

Самое важное - нормализация работает не везде. Иными словами, не все графемные кластеры можно нормализовать — в смысле «свести к одной и той же последовательности кодов» универсально для всех случаев. Почему не все можно нормализовать:

  1. Есть кластеры без канонической формы, пример: 👨‍👩‍👧‍👦
  2. Нормализация не разбирает кластер
  3. Есть кластеры, зависящие от языка или шрифта

Стандарты Unicode Normalization

Форма Назначение Пример
NFC Каноническая композиция U+0065 + U+0301 → U+00E9
NFD Каноническая декомпозиция U+00E9 → U+0065 + U+0301
NFKC Совместимая композиция ¹ → 1
NFKD Совместимая декомпозиция ① → 1 + ◯

Возможные проблемы

Пока мы в Qt5/Qt6 работаем с числами, латиницей, кириллицей - проблем возникать не должно. Но как только приходится использовать символы за диапазоном BMP, или графемные кластеры - "привычные" функции/методы из библиотеки начинают работать не так, как ожидалось.

Функции/методы определения длины строки (а нам важно именно количество графем, включая кластеры графем) начинают возвращать не те цифры, функции/методы получения части строк могут свободно разрезать строки не в тех местах - разрезать суррогатные пары или графемные кластеры. Ниже небольшой пример это демонстрирующий:

#include <QCoreApplication>
#include <QByteArray>
#include <QTimer>
#include <QDebug>
#include <QList>
#include <unicode/uversion.h>
#include <unicode/brkiter.h>
#include <unicode/unistr.h>

int countGraphemes(const QString& str) {
    UErrorCode status = U_ZERO_ERROR;
    // Конвертируем QString (UTF-16) в icu::UnicodeString
    icu::UnicodeString uniStr(str.utf16(), str.length());
    // Создаём BreakIterator для графем
    icu::BreakIterator* graphemeIter = icu::BreakIterator::createCharacterInstance(icu::Locale(), status);
    if (U_FAILURE(status)) {
        qDebug() << "Failed to create BreakIterator:" << u_errorName(status);
        return -1;
    }
    graphemeIter->setText(uniStr);
    int32_t count = 0;
    while (graphemeIter->next() != UBRK_DONE) {
        count++;
    }
    delete graphemeIter;
    return count;
}

int main(int argc, char* argv[]) {
    QCoreApplication app(argc, argv);
    QString utf16_1 = "Hello 😊";
    QByteArray utf8_1 = utf16_1.toUtf8();
    QList<uint> utf32_1 = utf16_1.toUcs4().toList(); //.toList() для совместимости с Qt5
    qDebug() << "String: " << utf16_1;
    qDebug() << "Size UTF-8 (bytes): " << utf8_1.size();
    qDebug() << "Length (UTF-16 code units): " << utf16_1.length();
    qDebug() << "Length (UTF-32 code points): " << utf32_1.size();
    qDebug() << "Graphemes:" << countGraphemes(utf16_1);
    qDebug() << "- - - - - - - - - - breakaway line - - - - - - - - - - - - ";

    QString utf16_2 = "Хэлоу ворлд!";
    QByteArray utf8_2 = utf16_2.toUtf8();
    QList<uint> utf32_2 = utf16_2.toUcs4().toList(); //.toList() для совместимости с Qt5
    qDebug() << "String: " << utf16_2;
    qDebug() << "Size UTF-8 (bytes): " << utf8_2.size();
    qDebug() << "Length (UTF-16 code units): " << utf16_2.length();
    qDebug() << "Length (UTF-32 code points): " << utf32_2.size();
    qDebug() << "Graphemes:" << countGraphemes(utf16_2);
    qDebug() << "- - - - - - - - - - breakaway line - - - - - - - - - - - - ";

    // в нормализованной форме (NFC) U+00E9
    QString utf16_3 = "é";
    QByteArray utf8_3 = utf16_3.toUtf8();
    QList<uint> utf32_3 = utf16_3.toUcs4().toList(); //.toList() для совместимости с Qt5
    qDebug() << "String: " << utf16_3;
    qDebug() << "Size UTF-8 (bytes): " << utf8_3.size();
    qDebug() << "Length (UTF-16 code units): " << utf16_3.length();
    qDebug() << "Length (UTF-32 code points): " << utf32_3.size();
    qDebug() << "Graphemes:" << countGraphemes(utf16_3);
    qDebug() << "- - - - - - - - - - breakaway line - - - - - - - - - - - - ";

    //в декомпозированной форме (NFD) — 2 кодовые точки (U+0065 + U+0301)
    QString utf16_4 = QString::fromUtf8("\x65\xCC\x81");
    QByteArray utf8_4 = utf16_4.toUtf8();
    QList<uint> utf32_4 = utf16_4.toUcs4().toList(); //.toList() для совместимости с Qt5
    qDebug() << "String: " << utf16_4;
    qDebug() << "Size UTF-8 (bytes): " << utf8_4.size();
    qDebug() << "Length (UTF-16 code units): " << utf16_4.length();
    qDebug() << "Length (UTF-32 code points): " << utf32_4.size();
    qDebug() << "Graphemes:" << countGraphemes(utf16_4);

    QTimer::singleShot(1000, &app, &QCoreApplication::quit);
    return app.exec();
}

А вывод будет следующий:

String:  "Hello 😊"
Size UTF-8 (bytes):  10
Length (UTF-16 code units):  8
Length (UTF-32 code points):  7
Graphemes: 7
- - - - - - - - - - breakaway line - - - - - - - - - - - - 
String:  "Хэлоу ворлд!"
Size UTF-8 (bytes):  22
Length (UTF-16 code units):  12
Length (UTF-32 code points):  12
Graphemes: 12
- - - - - - - - - - breakaway line - - - - - - - - - - - - 
String:  "é"
Size UTF-8 (bytes):  2
Length (UTF-16 code units):  1
Length (UTF-32 code points):  1
Graphemes: 1
- - - - - - - - - - breakaway line - - - - - - - - - - - - 
String:  "é"
Size UTF-8 (bytes):  3
Length (UTF-16 code units):  2
Length (UTF-32 code points):  2
Graphemes: 1

Возможные способы устранения

Способ Ожидаемый результат Описание
Использование UTF-32 удовлетворительный защищает только от порезки code points, но не от порезки графемных кластеров
Использование QTextBoundaryFinder::Grapheme хороший работает с графемными кластерами, функционал может быть слегка ограничен
Использование ICU лучший работает с графемными кластерами, функционал может быть выше чем у QTextBoundaryFinder, доступны расширенные настройки
Рейтинг: 0/5 - 0 голосов