UTF-8文字列のバイト配列(TBytes型)からUTF-16LE文字列のバイト配列に変換(DelphiのString型に変換可能)する関数を実装する ~Delphiでお手軽プログラミング

UTF-8文字列のバイト配列(TBytes型)からUTF-16LE文字列のバイト配列に変換(DelphiのString型に変換可能)する関数を実装する ~Delphiでお手軽プログラミング

UTF-8文字列のバイト配列からDelphiのString型(UTF-16LE)に変換するには、 System.SysUtilsユニットのTEncoding.UTF8.GetString(バイト配列)関数を使えば可能ですが、 バイト配列に不正な値が1ビットでも存在すると、一切変換してくれません。
変換可能な個所だけでも変換した値がほしいので自作します。

Unicodeについて
ユニコードは「U+10FFFF」のように表現される(21ビット)
最初の5ビットが「面」(0~16)、次の8ビットが「句」(0~255)、次の8ビットが「点」(0~255)と言われているそうです。

UMamUnicode.pasファイル

unit UMamUnicode;

interface

uses System.SysUtils;

function MamU8ToU16(b:TBytes):TBytes;  //UTF8→UTF16LEへの変換関数
function MamU32ToU16(b:TBytes):TBytes; //Unicode→UTF16LEへの変換関数
function MamU8ToU32(b:TBytes):TBytes;  //UTF8→Unicodeへの変換関数

implementation

function MamU8ToU16(b:TBytes):TBytes;
var b1:TBytes;
begin
  b1:=MamU8ToU32(b);
  result:=MamU32ToU16(b1);
end;

function MamU32ToU16(b:TBytes):TBytes;
var i,bl,rl:integer;
begin
  //「𠮷」UTF32(UNICODE)でU+20BB7 の場合
  //b[0]=$02,b[1]=$0B,b[2]=$B7
  //b[0]が1以上だとサロゲートペアになる
  //サロゲートペアの場合、
  //  ①b[0]:=b[0]-1;を行う
  //  ②
  //            b[0]   b[1]      b[2]
  //            0001 0000 1011 1011 0111
  //            ~~~~ ~~~~ ~~                      LEなので
  //w1= 1101 10 0001 0000 10    ↓   ↓  = D842 → 42 D8
  //w2= 1101 11             11 1011 0111 = DFB7 → B7 DF

  rl:=0;
  SetLength(result,rl);
  bl:=Length(b);
  i:=0;
  while (i+2)<bl do
  begin
    if b[i]<=0 then
    begin
      //サロゲートペアではない
      inc(rl,2);
      setLength(result,rl);
      result[rl-1]:=b[i+1];
      result[rl-2]:=b[i+2];
    end
    else
    begin
      //サロゲートペアの場合
      b[i]:=b[i]-1;
      inc(rl,2);
      setLength(result,rl);
      result[rl-1]:=$D8 or ((b[i] and $0C) shr 2);
      result[rl-2]:=((b[i] and $03) shl 6) or
                    ((b[i+1] and $FC) shr 2);
      inc(rl,2);
      setLength(result,rl);
      result[rl-1]:=$DC or (b[i+1] and $03);
      result[rl-2]:=b[i+2];
    end;
    inc(i,3);
  end;

end;

function MamU8ToU32(b:TBytes):TBytes;
var i:integer;
    bl,rl:Integer;
begin
  //例えば「𠮷」はUTF8でF0A0AEB7 でバイト型配列で届く
  //b[0]=$F0,b[1]=$A0,b[2]=$AE,b[3]=$B7
  //この関数は「𠮷」をUNICODEでU+20BB7 として3バイトで値を返すものとする
  //result[0]=$02,result[1]=$0B,result[2]=$B7

  //ユニコードの各21ビットを
  //wwwww xxxxxyyy yzzzzzzz
  //とする場合

  //ユニコード     UTF-8
  //               1Byte目  2Byte目  3Byte目  4Byte目
  //    0~    7F  0zzzzzzz                            1バイト型UTF8
  //   80~   7FF  110yyyyz 10zzzzzz                   2バイト型UTF8
  //  800~  FFFF  1110xxxx 10xyyyyz 10zzzzzz          3バイト型UTF8
  //10000~10FFFF  11110www 10wwxxxx 10xyyyyz 10zzzzzz 4バイト型UTF8
  //               赤字の個所の値 がすべて 0 は不正な文字として扱う

  bl:=Length(b);
  rl:=0;
  SetLength(result,rl);

  i:=0;
  while i<bl do
  begin
    if b[i]<$80 then
    begin
      //1Byte
      inc(rl,3);
      SetLength(result,rl);
      result[rl-1]:=b[i];
      result[rl-2]:=0;
      result[rl-3]:=0;
      inc(i);
    end
    else if (b[i]>=$C2) and (b[i]<$E0) then
    begin
      //2Byte
      if ((i+1)<bl) and (b[i+1]>=$80) and (b[i+1]<$C0) and ((b[0] and $1E)<>0) then
      begin
        inc(rl,3);
        SetLength(result,rl);
        result[rl-1]:=(b[i+1] and $3F) OR ((b[i] and $03) shl 6);
        result[rl-2]:=(b[i] and $1C) shr 2;
        result[rl-3]:=0;
        inc(i,2);
      end
      else
      begin
        //不正な値なのでスキップする
        inc(i);
      end;
    end
    else if (b[i]>=$E0) and (b[i]<$F0) then
    begin
      //3Byte
      if ((i+2)<bl) and
         (b[i+1]>=$80) and (b[i+1]<$C0) and
         (b[i+2]>=$80) and (b[i+2]<$C0) and
         (((b[i] and $0F)<>0) or ((b[i+1] and $20)<>0)) then
      begin
        inc(rl,3);
        SetLength(result,rl);
        result[rl-1]:=(b[i+2] and $3F) OR ((b[i+1] and $03) shl 6);
        result[rl-2]:=((b[i+1] and $3C) shr 2) + ((b[i] and $0F) shl 4);
        result[rl-3]:=0;
        inc(i,3);
      end
      else
      begin
        //不正な値なのでスキップする
        inc(i);
      end;
    end
    else if (b[i]>=$F0) and (b[i]<$F8) then
    begin
      //4Byte
      if ((i+3)<bl) and
         (b[i+1]>=$80) and (b[i+1]<$C0) and
         (b[i+2]>=$80) and (b[i+2]<$C0) and
         (b[i+3]>=$80) and (b[i+3]<$C0) and
         (((b[i] and $07)<>0) or ((b[i+1] and $30)<>0)) then
      begin
        inc(rl,3);
        SetLength(result,rl);
        result[rl-1]:=(b[i+3] and $3F) OR ((b[i+2] and $03) shl 6);
        result[rl-2]:=((b[i+2] and $3C) shr 2) + ((b[i+1] and $0F) shl 4);
        result[rl-3]:=((b[i+1] and $30) shr 4) + ((b[i] and $07) shl 4);
        inc(i,4);
      end
      else
      begin
        //不正な値なのでスキップする
        inc(i);
      end;
    end
    else
    begin
      //不正な値なのでスキップする
      inc(i);
    end;
  end;
end;

end.

使用例

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
  Vcl.StdCtrls, System.Net.URLClient, System.Net.HttpClient,
  System.Net.HttpClientComponent,
  UMamUnicode;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    NetHTTPClient1: TNetHTTPClient;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var b:TBytes;
    http:TNetHTTPClient;
    res:IHTTPResponse;
begin
  http:=TNetHTTPClient.Create(nil);
  try
    res:=http.Get('https://mam-mam.net/');
    SetLength(b,res.ContentStream.Size);
    res.ContentStream.Position:=0;
    res.ContentStream.Read(b,Length(b));
    b:=MamU8ToU16(b); //UTF8のバイト型配列 ⇒ UTF16LEバイト型配列
    Memo1.Lines.Add(
      TEncoding.Unicode.GetString(b) //UTF16LEバイト型配列からString(文字列)型に変換
    );
  finally
    http.Free;
  end;
end;

end.