jak odczytac plik UTF8 ?

0

Chciałbym do pliku txt zapisać tekst np. "ABC가나다ĄąĆć"
Potem odczytać i dla każdej litery wyświetlić numer znaku unicode

Plik zapisuje się poprawnie, w edytorze HEX wygląda zgodnie z oczekiwaniem
Nie wiem jednak jak odczytać plik utf8, chatgpt sugeruje odczytywanie do zmiennej typu char (1 BAJT) wiec nie możemy sie dogadać ;(

#include <fstream>
#include <iostream>
#include <string>
#include <iterator>

const char* hex(char c) {
    const char REF[] = "0123456789ABCDEF";
    static char output[3] = "XX";
    output[0] = REF[0x0f & c>>4];
    output[1] = REF[0x0f & c];
    return output;
}

int main() {

    {
        std::string str1 = "ABC가나다ĄąĆć";
        std::ofstream ofs("plik.txt",std::ios::trunc);
        ofs << str1;
    }

    {

        std::ifstream file("plik.txt");
        std::string buffer((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
        for (auto c : buffer) {
            std::cout << hex(c)<< " ";
        }
        std::cout << "\n";

        std::locale utf8_locale("zh_CN.utf8");
        std::locale::global(utf8_locale);

        file.seekg(0, std::ios::beg);
        char znak;
        while (file.get(znak)) {
            std::cout << znak;
        }
    }
}
2

jak odczytac plik UTF8 ?

Przerzucanie utf-8 z miejsca na miejsce jest proste, po prostu przerzucasz tablicę bajtów, co jest najszybsze i niezawodne.

Co do interpretacji: nie robiłem tego nigdy w C++, ale to wygląda legitnie https://stackoverflow.com/a/26083718/4638604 . Tak czy owak: musisz mieć gdzieś krok, który iteruje się po stringu/tablicy bajtów w taki sposób, że zwraca ci iterator, który ma 2 czy tam 4 bajty

Nie wiem jednak jak odczytać plik utf8, chatgpt sugeruje odczytywanie do zmiennej typu char (1 BAJT) wiec nie możemy sie dogadać ;(

Bo tak się robi. Praca z UTF-8 zazwyczaj wygląda tak, że olewasz format kodowania (tj. nie liczysz długości, nie odnosisz się do konkretnego indeksu) tylko iterujesz się po tablicy interpretując kolejne znaki. Jak chcesz mieć pełną swobodę to musisz przejść z UTF-8 do jakiegoś kodowania fixed-width. ChatGPT podpowiada coś takiego, nie wiem czy jest dobrze:

std::wstring utf8_to_wstring(const std::string& utf8)
{
    std::wstring_convert<std::codecvt_utf8<wchar_t>> converter;
    return converter.from_bytes(utf8);
}
1
 if(rozmiar_linii==-91){
  outfile2<<(char)196;outfile2<<(char)132; };

  if(rozmiar_linii==-71){
  // ShowMessage("IntToStr(rozmiar_calkowity)");

       outfile2<<(char)196;outfile2<<(char)133; };

   if(rozmiar_linii==-58){
  outfile2<<(char)196;outfile2<<(char)134; };

  if(rozmiar_linii==-26){
  outfile2<<(char)196;outfile2<<(char)135; };

  if(rozmiar_linii==-54){
  outfile2<<(char)196;outfile2<<(char)136; };

  if(rozmiar_linii==-22){
  outfile2<<(char)196;outfile2<<(char)153; };

   if(rozmiar_linii==-45){
  outfile2<<(char)195;outfile2<<(char)147; };

   if(rozmiar_linii==-13){
  outfile2<<(char)195;outfile2<<(char)179; };

   if(rozmiar_linii==-113){
  outfile2<<(char)197;outfile2<<(char)185; };

   if(rozmiar_linii==-97){
  outfile2<<(char)197;outfile2<<(char)186; };

   if(rozmiar_linii==-81){
  outfile2<<(char)197;outfile2<<(char)187; };

   if(rozmiar_linii==-65){
  outfile2<<(char)197;outfile2<<(char)188; };

     if(rozmiar_linii==-93){
  outfile2<<(char)197;outfile2<<(char)129; };

   if(rozmiar_linii==-77){
  outfile2<<(char)197;outfile2<<(char)130; };

   if(rozmiar_linii==-116){
  outfile2<<(char)197;outfile2<<(char)154; };

   if(rozmiar_linii==-100){
  outfile2<<(char)197;outfile2<<(char)155; };

to są chyba wszystkie polskie czcionki -- masz minus znaczy, że to jest pierwszy bajt UTF-8 (o ile czcionka) i potem masz 2 zapisane, sprawdź.

1

Można podejść tak: Każdy plik jest ciągiem bajtowym, więc można odczytać całą zawartość do listy lub tablicy char, a potem zamienić na tekst już jakimś algorytmem. Oczywiście, analogiczną zmianę można wykonać w drugą stronę.

Ja sam miałem dość podobny problem, czyli odczytywanie i zapisywanie tekstu w różnych kodowaniach i podam takiego gotowca:
https://github.com/andrzejlisek/TextPaintWeb/blob/main/prog/textcodec.h
https://github.com/andrzejlisek/TextPaintWeb/blob/main/prog/textcodec.cpp

Przykład użycia obiektu typu TextCodec:

// Zapis pliku:
Codec.get()->Reset();
Codec.get()->AddBOM();
Codec.get()->EnqueueStr(Text);
Codec.get()->DequeueRaw(BinaryData);

// Odczyt pliku:
Codec.get()->Reset();
Codec.get()->EnqueueRaw(BinaryData);
Codec.get()->RemoveBOM();
Codec.get()->DequeueStr(Text);

Jest to moja implementacja różnych kodeków, między innymi UTF-8, która może się przydać.

Idea jest taka, że obiekt konstruuje się numerem kodeka, numery są takie same, jak TextEncoding w C#. Sam kodek działa tak, że w każdej chwili można dopisać dane jako string EnqueueStr, wewnątrz zamienią się na dane surowe i można wyciągnąć jako dane surowe EnqueueRaw. Implementację zamiana danych surowych na tekstowe jest w funkcji DequeueStr. Przy tej implementacji można konwertować tekst fragmentami, nie potrzeba wczytywać od razu całego pliku do kodeka i z niego wyciągać. Kodek też pamięta stan konwersji, bo np. w UTF-8 jeden znak może zajmować 2 lub 3 bajty.

Inaczej mówiąc, można powiedzieć, że kodek działa na zasadzie strumieni, że wchodzi strumień bajtowy i wychodzi strumień tekstowy lub wchodzi strumień tekstowy i wychodzi strumień bajtowy. W przypadku konwersji z UTF-8/UTF-16 na tekst, kodek uwzględnia przypadek końca danych "w połowie znaku", czyli jest pierwszy bajt wielobajtowego znaku i na nim strumień się kończy. W takim przypadku, nie można z kodeka wyciągnąć znaku tekstowego (nic nie zostanie dopisane do strumienia tekstowego), dopóki do ciągu binarnego nie będą dopisane wszystkie bajty składowe znaku.

Możesz przejrzeć cały projekt, nie widze sensu wstawiać implementacji typów Raw i Str, są to odpowiednio tablica danych surowych i odpowiednik zwykłego String. Własne struktury w tym przypadku wynikają z tego, że ja ten program przerabiałem z C# i chciałem mieć interfejs standardowych struktur podobny do tego w C#, a potem dopisywałem dodatkowe funkcje w miare potrzeb.

Myślę, że bez większego trudu można przerobić funkcje kodeka tak, żeby wchodziły do nich standardowe struktury, jak np std::vector do danych surowych i std::string do tekstu.

Jest jeszcze inny problem: Typ std::string przeznacza jeden bajt na tekst i w zależności od kompilatora może domyślnie interpretować jako UTF-8 przy cin i cout, jestdnak trzeba to mieć na uwadze. Jest to jeszcze jeden powód, dla którego utworzyłem customowy typ Str, który tak naprawdę jest wektorem liczb i nie ma problemu z dziwnymi znakami. Pierwotnie, w C# też posługiwałem się listą liczb, bo w tym przypadku były jakieś kłopoty przy "egzotycznych" znakach.

1

Obsługa UTF8 to dość skomplikowana sprawa, bo znak może być zakodowany na różnych długościach. Nie tylko trzeba sprawdzać starsze bity bajtu, żeby się dowiedzieć czy zaczyna się wielobajtowa sekwencja i ile ma znaków. Do tego zdaje się dochodzą znaki diakrytyczne i inne bajery jako odrębne znaki. Żeby pisać kod produkcyjny, pisanie tego samemu to szukanie problemów. W celach edukacyjnych, to tu jest to dość szeroko opisane: https://en.wikipedia.org/wiki/UTF-8 (rozdział encoding).

1
elwis napisał(a):

Obsługa UTF8 to dość skomplikowana sprawa, bo znak może być zakodowany na różnych długościach. Nie tylko trzeba sprawdzać starsze bity bajtu, żeby się dowiedzieć czy zaczyna się wielobajtowa sekwencja i ile ma znaków. Do tego zdaje się dochodzą znaki diakrytyczne i inne bajery jako odrębne znaki. Żeby pisać kod produkcyjny, pisanie tego samemu to szukanie problemów. W celach edukacyjnych, to tu jest to dość szeroko opisane: https://en.wikipedia.org/wiki/UTF-8 (rozdział encoding).

Mój kodek UTF-8 to wszystko uwzględnia, przy czym nigdy nie był testowany na nietypowym zapisie, bo ten sam znak można zapisać na kilka sposobów, ale za poprawny uznaje się najkrótszy z możliwych. Tak naprawdę, całe dekodowanie UTF-8 jest we funkcji void TextCodec::DequeueStr(Str &Text), w bloku case UTF8. Pozostałe znaki, do 6 bajtów włącznie, dekoduje bez problemu, w sensie zamiany ciągu bajtów na liczbę prezentującą numer znaku. Zmienna DequeueTextState jest globalna w kodeku i jest to ważne w przypadku urwanego strumienia bajtowego na wejściu, żeby móc kontynuować po uzupełnieniu wejściowego strumienia bajtów.

1
andrzejlisek napisał(a):

Mój kodek UTF-8 to wszystko uwzględnia, przy czym nigdy nie był testowany na nietypowym zapisie, bo ten sam znak można zapisać na kilka sposobów, ale za poprawny uznaje się najkrótszy z możliwych. Tak naprawdę, całe dekodowanie UTF-8 jest we funkcji void TextCodec::DequeueStr(Str &Text), w bloku case UTF8. Pozostałe znaki, do 6 bajtów włącznie, dekoduje bez problemu, w sensie zamiany ciągu bajtów na liczbę prezentującą numer znaku. Zmienna DequeueTextState jest globalna w kodeku i jest to ważne w przypadku urwanego strumienia bajtowego na wejściu, żeby móc kontynuować po uzupełnieniu wejściowego strumienia bajtów.

Może i tak. W każdym razie jest tam całkiem sporo kodu. Poza tym, przetestuj sobie jak radzi sobie z devanagari (np: गते गते पारगते पारसंगते बोधि स्वाहा) i emoji. Jeśli sobie z tym radzi (dobrze dzieli na pojedyncze znaki), nie sypie się i nie rzuciłeś zbyt wiele mięsa pisząc ten kod, to przyznam ci, że przesadziłem z tym skomplikowaniem. :)

Choć wydaje mi się, że trochę zabawy może z tym być (wydaje mi się, że niczego bardziej pokręconego w UTF-8 nie ma):

~$ hexdump -C 
गते गते पारगते पारसंगते बोधि स्वाहा
00000000  e0 a4 97 e0 a4 a4 e0 a5  87 20 e0 a4 97 e0 a4 a4  |......... ......|
00000010  e0 a5 87 20 e0 a4 aa e0  a4 be e0 a4 b0 e0 a4 97  |... ............|
00000020  e0 a4 a4 e0 a5 87 20 e0  a4 aa e0 a4 be e0 a4 b0  |...... .........|
00000030  e0 a4 b8 e0 a4 82 e0 a4  97 e0 a4 a4 e0 a5 87 20  |............... |
00000040  e0 a4 ac e0 a5 8b e0 a4  a7 e0 a4 bf 20 e0 a4 b8  |............ ...|
00000050  e0 a5 8d e0 a4 b5 e0 a4  be e0 a4 b9 e0 a4 be 0a  |................|
1

Może i tak. W każdym razie jest tam całkiem sporo kodu. Poza tym, przetestuj sobie jak radzi sobie z devanagari (np: गते गते पारगते पारसंगते बोधि स्वाहा) i emoji. Jeśli sobie z tym radzi, nie sypie się i nie rzuciłeś zbyt wiele mięsa pisząc ten kod, to przyznam ci, że przesadziłem z tym skomplikowaniem. :)

Przetestowałem poprzez próbę wczytania tego pliku jako UTF-8 do programu TextPaint, którego częścią jest ten właśnie kod.

Program odczytał taką sekwencję znaków (numery w systemie szesnatkowym):

0917, 0924, 0947, 20
0917, 0924, 0947, 20
092A, 093E, 0930, 0917, 0924, 0947, 20
092A, 093E, 0930, 0938, 0902, 0917, 0924, 0947, 20
092C, 094B, 0927, 093F, 20
0938, 094D, 0935, 093E, 0939, 093E

A w czcionce Unifont, wczytany tekst wygląda tak:

UTF-8 TextPaint.png

Program TextPaint wyświetla każdy znak jako osobny byt, również te znaki, które w zamierzeniu twórców Unicode mają dodawać dodatkowy element do poprzedniego znaku. Ale to już zupełnie inny temat, niezwiązany z odczytem i zapisem plików.

Mam na koncie jeszcze inny projekt, dużo starszy:
https://github.com/andrzejlisek/Unicode
https://andrzejlisek.github.io/Unicode/unicode.htm
Tam też jest dekodowanie UTF-8. Należy wpisać kod szesnastkowy w polu "Find" i kliknąć "UTF-8" poniżej i pokazała się ta sama lista znaków:

UTF-8 Unicode.png

Algorytm dekodowania UTF-8 w Unicode jest w pliku https://github.com/andrzejlisek/Unicode/blob/master/codec.js funkcja function CodeToChars(RawData, Code), algorytm jest ten sam, bo sam algorytm wymyśliłem na użytek właśnie tego projektu Unicode, a później zaadaptowałem go do Textpaint.

Jak widać, obie implementacje poradziły sobie z Twoim przykładem bez żadnego problemu.

1
andrzejlisek napisał(a):

Przetestowałem poprzez próbę wczytania tego pliku jako UTF-8 do programu TextPaint, którego częścią jest ten właśnie kod.

To nie wygląda dobrze. Na pewno wyrenderowany tekst jest błędny, bo znaki diakrytyczne pojawiają się osobno. Przyjrzyj się uważnie, jak masz गते to ta kreska nad drugą literką u ciebie jest obok, jako osobny znak. Także ostatnie słowo: स्वाहा wygląda źle, ale to chyba nie miejsce na wykład o piśmie devanagari…

Poza tym już na pierwszych wyrazach गते widać że rozkład na kody też jest źle. 20 to jest spacja. Do pierwszej spacji wykrywa ci 3 znaki, a są 2. Znak diakrytyczny został policzony osobno.

Innymi słowy wystąpił przypadek, którego nie przewidziałeś. Do zabawy dobre, ale na produkcję lepiej użyć biblioteki. ;)

1
elwis napisał(a):

To nie wygląda dobrze. Na pewno wyrenderowany tekst jest błędny, bo znaki diakrytyczne pojawiają się osobno. Także ostatnie słowo: स्वाहा wygląda źle, ale to chyba nie miejsce na wykład o piśmie devanagari… Przyjrzyj się uważnie, jak masz गते to ta kreska nad drugą literką u ciebie jest obok, jako osobny znak.

Poza tym już na pierwszych wyrazach गते widać że jest źle. 20 to jest spacja. Do pierwszej spacji wykrywa ci 3 znaki, a są 2. Znak diakrytyczny został policzony osobno.

Innymi słowy wystąpił przypadek, którego nie przewidziałeś. Do zabawy dobre, ale na produkcję lepiej użyć biblioteki. ;)

Tak, jak napisałem wyżej, ta nieprawidłowość nie wynika z wadliwości algorytmu dekodującego UTF-8, tylko wynika z uproszczeń implementacji renderowania i przeznaczenia programu. Jedynie uwzględnione jest to, że spacja zajmuje jedno miejsce, a te znaki dwa miejsca, bo Unifont jest czcionką typu "duospace'.

Program TextPaintWeb wyświetla kod znaku stojącego pod kursorem i numery znaków się zgadzają, co dowodzi poprawności zdekodowania, ale na jednym screenie nie da się przedstawić wszystkich kodów. A że źle renderuje znaki diakrytyczne w tym tekście to już zupełnie odrębny temat. Sekwencja znaków w pamięci jest dokładnie ta sama.

Inaczej mówiąc, w tym programie, przy wczytywaniu pliku jest taka sekwencja działań:

  1. Ciąg binarny z pliku trafia do kodeka.
  2. Z kodeka wychodzi ciąg znaków i trafia do pamięci.
  3. Na ekranie wyświetlony jest obraz pamięci.

Problem, na który słusznie zwracasz mi uwagę jest między 2, a 3, a nie między 1 a 2.

1
andrzejlisek napisał(a):

Program TextPaintWeb wyświetla kod znaku stojącego pod kursorem i numery znaków się zgadzają, co dowodzi poprawności zdekodowania, ale na jednym screenie nie da się przedstawić wszystkich kodów. A że źle renderuje znaki diakrytyczne w tym tekście to już zupełnie odrębny temat. Sekwencja znaków w pamięci jest dokładnie ta sama.

No tabelka wygląda spoko, przyznaję. Zasugerowałem się tym co było w plaintexcie. Myślałem, że to już gotowy podział.

I co, dużo miałeś frajdy z pisania tego?

1

I co, dużo miałeś frajdy z pisania tego?

Miałem dużo frajdy, a największa miałem, jak program potrafił, oprócz pisania tekstu, robić dwie rzeczy:

  1. Wyświetlać prawie wszystkie pliki *.ANS (również animowane) z https://16colo.rs/ i przy tym musiałem rozróżnić dwa tryby działania ze względu na kompatybiiność. Tu króluje strona kodowa 437 ale kodek też to ma zaimplementowane.
  2. Być klientem Telnet, między innymi do tego niezbędny był UTF-8, a do tego bił na głowę inne programy pod względem udawania prawdziwych terminali VT100, VT220. Przede wszystkim jest to prawdziwy "fullscreen", wyświetlanie animacji zrealizowanych z wykorzystaniem powolnego wczytywania znaków, płynne przewijanie i mruganie poprzez przyciemnianie tekstu lub zanikanie tekstu do wyboru.

Prawdziwy fullscreen znaczy tyle, że jeżeli okno ma rozdzielczość 80x24 znaki, to po włączeniu pełnego ekranu będzie również mieć rozdzielczość 80x24 i większe litery, a nie większą rozdzielczośćć, jak to bywa w większości emulatorów terminala.

W Linux jest program VTTEST, który służy do testowania terminali. Tutaj filmik, jak ten program wygląda w akcji
Ten sam program występuje również od miejsca 5:30.

Poproszę nazwę chociaż jednego istniejącego "terminala-konsoli" do Windowsa lub Linuxa, który uwzględni te niuanse w programie VTTEST. A animacje "bożonarodzeniowe" w tym filmiku, to po prostu gość wydał cat *, system wypuścił na ekran tekst z plików jak leci, a całą resztę wykonał sam terminal. Sprawdziłem konsole, gnome-terminal, xterm, PuTTY, a nawet komercyjny ZOC. Wszystkie wymienione poległy przy tych oldschoolowych animacjach i renderowaniu VTTEST, a mój TextPaint daje radę, bo na tym testowałem.

Frajdy było co nie miara i jest to zgodne z powiedzeniem "apetyt rośnie w miarę jedzenia". Miał być tylko edytor tekstu, a jest 3-in-1, bo edytor, odtwarzacz animacji i emulator terminala.

1

Tak, można odczytywać bajt po bajcie. Dekodowanie pojedynczych znaków jest tutaj. Potrzeba do tego dodać jeszcze łączenie znaków z atrybutem ‘combining’, ale do tego jest potrzebna tabela znaków.

0

finale rozwiązanie poniżej:

  1. Odczytuje plik binarnie do std::string
  2. używam funkcji utf8_to_wstring zaproponowanej przez @slsy

Co jest ciekawe:

  1. Na Windows sizeof(wchar_t) ==2 a na godbolt (pewnie OS to Linux) sizeof(wchar_t) ==4
  2. codecvt_utf8<wchar_t> jest zaznaczony jako deprecated , nie mam pomysłu jak to poprawić
[build] D:/a/char_reader/main.cpp:27:31: warning: 'codecvt_utf8<wchar_t>' is deprecated [-Wdeprecated-declarations]
[build]    27 |     std::wstring_convert<std::codecvt_utf8<wchar_t>> converter;
[build]       |                               ^
[build] C:/msys64/clang64/include/c++/v1/codecvt:194:28: note: 'codecvt_utf8<wchar_t>' has been explicitly marked deprecated here
[build]   194 | class _LIBCPP_TEMPLATE_VIS _LIBCPP_DEPRECATED_IN_CXX17 codecvt_utf8 : public __codecvt_utf8<_Elem> {
[build]       |                            ^
[build] C:/msys64/clang64/include/c++/v1/__config:1001:41: note: expanded from macro '_LIBCPP_DEPRECATED_IN_CXX17'
[build]  1001 | #    define _LIBCPP_DEPRECATED_IN_CXX17 _LIBCPP_DEPRECATED
[build]       |                                         ^
[build] C:/msys64/clang64/include/c++/v1/__config:974:49: note: expanded from macro '_LIBCPP_DEPRECATED'
[build]   974 | #      define _LIBCPP_DEPRECATED __attribute__((__deprecated__))
#include <fstream>
#include <iostream>
#include <string>
#include <iterator>


#include <codecvt> //for wstring_convert and codecvt_utf8 if using C++17 or earlier
#include <iostream>
#include <locale>
#include <string>

const char* hex(char c) {
    const char REF[] = "0123456789ABCDEF";
    static char output[3] = "XX";
    output[0] = REF[0x0f & c>>4];
    output[1] = REF[0x0f & c];
    return output;
}

std::wstring utf8_to_wstring(const std::string& utf8)
{
    std::wstring_convert<std::codecvt_utf8<wchar_t>> converter;
    return converter.from_bytes(utf8);
}

inline void read_file(const std::string &path, std::string &out) {
  std::ifstream fs(path, std::ios_base::binary);
  fs.seekg(0, std::ios_base::end);
  auto size = fs.tellg();
  fs.seekg(0);
  out.resize(static_cast<size_t>(size));
  fs.read(&out[0], static_cast<std::streamsize>(size));
}

int main() {

    {
        std::cout << "[1]\n";
        std::string str1 = "ABC가나다ĄąĆć";
        std::ofstream ofs("plik.txt",std::ios::trunc);
        ofs << str1;
        std::cout << "str1.size():"<< str1.size() << " str1.length():" << str1.length() <<"\n";
    }

    {

        std::ifstream file("plik.txt");
        std::string buffer((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
        for (auto c : buffer) {
            std::cout << hex(c)<< " ";
        }
        std::cout << "\n";
    }

    {
        std::string str2;
        read_file("plik.txt", str2);
        std::cout << "\n\n" << str2;
        std::cout << "\nstr2.size():" << str2.length() << " str2.length():" << str2.size() << "\n";
        auto str3 = utf8_to_wstring(str2);
        std::cout << "\nstr3.size():" << str3.length() << " str3.length():" << str3.size() << "\n";

        for(size_t i = 0 ; i <str3.length(); ++i )
        {
          auto c = str3[i];
          std::cout << (int) c << "\n";
        }
        std::cout << "sizeof(wchar_t):"<< sizeof(wchar_t);

    }
}

https://godbolt.org/z/fxhe9zEe5

1

Twoje finalne rozwiązanie nie rozwiązuje problemu, który postawiłeś:

Potem odczytać i dla każdej litery wyświetlić numer znaku unicode

Aby rozwiązać problem, który postawiłeś, nie możesz konwertować stringa UTF-8 do std::wstring. Jak sam zauważyłeś, na Windowsie wchar_t ma 2 bajty a to oznacza, że nie wszystkie znaki UTF-8 są zakodowane.

Aby poprawnie wykonać zadanie, musisz zrobić co następuje.

Przy wczytywaniu znaku, powinieneś wiedzieć ile dany znak zajmuje miejsca. Możesz do tego napisać funkcję:

size_t utf8_character_length(char first_byte)
{
    if ((first_byte & 0x80) == 0) return 1;
    else if ((first_byte & 0xE0) == 0xC0) return 2;
    else if ((first_byte & 0xF0) == 0xE0) return 3;
    else if ((first_byte & 0xF8) == 0xF0) return 4;
    else throw std::runtime_error("utf8_character_length: invalid UTF-8");
}

(https://stackoverflow.com/a/4063258)

Dzięki temu, możesz wczytać wszystkie bajty dla znaku:

        file.seekg(0, std::ios::beg);
        char first_byte;
        while (file.get(first_byte)) {

            const size_t character_length = utf8_character_length(first_byte);
            
            char32_t character = (unsigned char)first_byte;
            for (size_t i = 1; i < character_length; ++i) {
                char current_byte;
                file.get(current_byte);
                character |= char32_t((unsigned char)(current_byte)) << (8 * i);
            }

            // w tym miejscu character ma wszystkie bajty danego znaku

Następnie, potrzebujesz funkcji, która dla bajtów znaku potrafi zwrócić numer znaku Unicode:

int utf8_character_number(char32_t character)
{
    const size_t len = utf8_character_length((char)character);
    if (len == 1) {
        return (int)character;
    }
    else if (len == 2) {
        int a = ((character >> 0) & 0b00011111) << 6;
        int b = ((character >> 8) & 0b00111111);
        return a | b;
    }
    else if (len == 3) {
        int a = ((character >> 0)  & 0b00001111) << 12;
        int b = ((character >> 8)  & 0b00111111) << 6;
        int c = ((character >> 16) & 0b00111111);
        return a | b | c;
    }
    else if (len == 4) {
        int a = ((character >> 0)  & 0b00000111) << 18;
        int b = ((character >> 8)  & 0b00111111) << 12;
        int c = ((character >> 16) & 0b00111111) << 6;
        int d = ((character >> 24) & 0b00111111);
        return a | b | c | d;
    }
    else {
        throw std::runtime_error("utf8_to_number: invalid UTF-8");
    }
}

(https://stackoverflow.com/a/68835029)

I trzeba to wszystko skleić w całość:

    {

        std::ifstream file("plik.txt");
        std::string buffer((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
        for (auto c : buffer) {
            std::cout << hex(c)<< " ";
        }
        std::cout << "\n";

        file.seekg(0, std::ios::beg);
        char first_byte;
        while (file.get(first_byte)) {

            const size_t character_length = utf8_character_length(first_byte);
            
            char32_t character = (unsigned char)first_byte;
            for (size_t i = 1; i < character_length; ++i) {
                char current_byte;
                file.get(current_byte);
                character |= char32_t((unsigned char)(current_byte)) << (8 * i);
            }

            std::cout << "&#" << utf8_character_number(character) << ";";
        }
    }

https://godbolt.org/z/5x5K4qjcf

Wynik działania programu to:

41 42 43 EA B0 80 EB 82 98 EB 8B A4 C4 84 C4 85 C4 86 C4 87 
&#65;&#66;&#67;&#44032;&#45208;&#45796;&#260;&#261;&#262;&#263;

Przeklejasz wynik do HTML i wyświetlasz w przeglądarce, np tutaj: https://codepen.io/pen/

Powinno to wyglądąć tak:
ABC가나다ĄąĆć

0

@mwl4: dziękuje za finalne rozwiązanie,
rzeczywiście moja wersja nie dawała radę ze znakami które nie mieszczą się w 16bitach np.

    U+1F4A9 PILE OF POO: 💩
        Nº: 128169
        UTF-8: F0 9F 92 A9
        UTF-16: D8 3D DC A9
    U+1F680 ROCKET: 🚀
        Nº: 128640
        UTF-8: F0 9F 9A 80
        UTF-16: D8 3D DE 80

dodanie takiego znaku dają wyjątek libc++abi: terminating due to uncaught exception of type std::range_error: wstring_convert: from_bytes error

1

Jak Wy wiecie gdzie bit się kończy? Pomyślałem, że może wszystkie są ujemne, ale u mnie ujemny jest pierwszy. Ostatni jest nieujemny?

0

@johnny_Be_good: ja nie rozumiem o co dokładnie pytasz, @mwl4 dokładnie wytłumaczył wszystko

1
Marius.Maximus napisał(a):

@johnny_Be_good: ja nie rozumiem o co dokładnie pytasz, @mwl4 dokładnie wytłumaczył wszystko

Patrz

int utf8_character_number(char32_t character)
{
    const size_t len = utf8_character_length((char)character);

Piszecie coś takiego, a dla mnie te dane to 0101010101001010101010101010101 - ja nie operuję na "char32_t" - dla mnie to 4 bajty, więc mnie zaciekawiło jaki jest znak(bit) końca

Weszliście w dziwną specyfikację i wcześniej czy później Was to pokara.

0

ja tam ekspertem od c++ nie jestem za wielkim ale mi to wygląda że z tych 4 bajtów używasz najmłodszego do wyznaczenia długości znaku :D
Wiec nie wiem dlaczego ma pokarać , chyba ze dane będądurne

1

Jeżeli w_string ma 2 bajty, to jest taka możliwość, że znaki są kodowane w UTF-16. Dla każdego znaku są takie trzy możliwości:

  1. Numer z zakresu od D800 do DBFF -> Pierwsza połowa znaku 4-bajtowego, znak ma numer powyżej FFFF.
  2. Numer z zakresu od DC00 do DFFF -> Druga połowa znaku 4-bajtowego, znak ma numer powyżej FFFF.
  3. Każdy inny numer znaku -> Jest to znak 2-bajtowy o numerze od 0000 do FFFF z wyłączeniem dwóch w/w przedziałów.

Dawniej, w JavaScript (przy opracowywaniu wyżej przytoczonego projektu Unicode) stwierdziłem, że "string" również zawiera znaki dwubajtowe, bo były problemy dekodowaniem znaków o numerach powyżej FFFF we funkcji wyświetlającej poszczególne znaki wklejonego tekstu. Po dorobieniu analizy znaków pod kątem par złożonych ze znaków od D800 do DFFF i traktowaniu takich par jako jeden znak o numerze uzyskanym zgodnie ze specyfikacją UTF-16, problem się skończył.

W C# dla Windows i Linux też miałem taki sam problem, jakim jest odczyt i zapis "egzotycznych" znaków, dlatego na potrzeby TextPaint pisanego dawniej jako apka desktopowa w C#, wymyśliłem, że zamiast standardowego string będzie lista liczb int i to rozwiązało problem.

Większym problemem od samego wyświetlenia jest błędna długość i problemy z wyciąganiem fragmentów napisu zawierającego takie znaki.

Na polskiej i angielskiej Wikipedii jest wyczerpująco opisana budowa UTF-16 z przykładami, więc nie ma po co opisywać tego standardu.

2

https://godbolt.org/z/GYK7zeYr1

#include <fstream>
#include <iostream>
#include <locale>
#include <iomanip>

int main()
try
{
    std::locale utf8{ "en_US.UTF-8" };
    std::locale::global(utf8);

    std::wifstream f { "resources/hello.txt" };
    f.imbue(std::locale { utf8 });
    std::wcout.imbue(std::locale { "" });

    if (f) {
        std::wstring s;
        while (getline(f, s)) {
            std::wcout << L":---------:\n" << s << '\n';
            for (auto ch : s) {
                std::wcout << ch << " - " << std::showbase << std::hex << std::setw(10) << +ch << '\n';
            }
        }
    } else {
        perror("hello.txt");
    }
}
catch(const std::exception& e)
{
    std::cerr << e.what() << '\n';
}

Jako, że na Windows wchar_t ma 16 bitów (UTF-16), to tam nie do końca zadziała.

Przykładowy wynik działania:

:---------:
ÓŁÓĄŚŻ
Ó -       0xd3
Ł -      0x141
Ó -       0xd3
Ą -      0x104
Ś -      0x15a
Ż -      0x17b
:---------:
ABC가나다ĄąĆć
A -       0x41
B -       0x42
C -       0x43
가 -     0xac00
나 -     0xb098
다 -     0xb2e4
Ą -      0x104
ą -      0x105
Ć -      0x106
ć -      0x107
:---------:
🎌🥕🎏
🎌 -    0x1f38c
🥕 -    0x1f955
🎏 -    0x1f38f

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.