UTF8String으로 비정규 화 된 문자 변환

Aug 25 2020

UTF-8로 인코딩 된 이모지를 문자열로 변환 할 때 UTF8ToString을 사용하여 올바른 문자를 얻지 못했습니다. 이러한 UTF8 문자는 외부 인터페이스에서 수신됩니다. 온라인 UTF8 디코더로 UTF 문자를 테스트 한 결과 올바른 문자가 포함되어 있는지 확인했습니다. 나는 이것이 합성 문자라고 생각합니다.

procedure TestUTF8Convertion;
const
  utf8Denormalized: RawByteString = #$ED#$A0#$BD#$ED#$B8#$85#$20 + #$ED#$A0#$BD#$ED#$B8#$86#$20 + #$ED#$A0#$BD#$ED#$B8#$8A;
  utf8Normalized: RawByteString = #$F0#$9F#$98#$85 + #$F0#$9F#$98#$86 + #$F0#$9F#$98#$8A;
begin
  Memo1.Lines.Add(UTF8ToString(utf8Denormalized));
  Memo1.Lines.Add(UTF8ToString(utf8Normalized));
end;

메모 1의 출력 :

비정규 화 :

정규화 : 😅😆😊

WinApi 함수를 기반으로 자체 변환 함수를 작성해도이 MultiByteToWideChar문제가 해결되지 않았습니다.

function UTF8DenormalizedToString(s: PAnsiChar): string;
var
  pwc: PWideChar;
  len: cardinal;
begin
  GetMem(pwc, (Length(s) + 1) * SizeOf(WideChar));
  len := MultiByteToWideChar(CP_UTF8, MB_PRECOMPOSED, @s[0], -1, pwc, length(s));
  SetString(result, pwc, len);
  FreeMem(pwc);
end;

답변

2 SalvadorDíazFau Aug 26 2020 at 23:09

버퍼에 CESU-8 데이터가 있고이를 UTF-8로 변환해야하는 경우 대리 쌍을 단일 UTF-8 인코딩 문자로 바꿀 수 있습니다. 나머지 데이터는 변경하지 않고 그대로 둘 수 있습니다.

이 경우 이모티콘은 다음과 같습니다.

  • 코드 포인트 : 01 F6 05
  • UTF-8 : F0 9F 98 85
  • UTF-16 : D8 3D DE 05
  • CESU-8 : ED A0 BD ED B8 85

CESU-8의 상위 대리자는이 데이터를 가지고 있습니다 : $ 003D

CESU-8의 하위 대리 데이터는 다음과 같습니다. $ 0205

Remy와 AmigoJack이 지적했듯이 UTF-16 버전의 이모티콘을 디코딩 할 때 이러한 값을 찾을 수 있습니다.

UTF-16의 경우에도 곱해야합니다. $003D value by $400 (shl 10), 결과를 다음에 추가하십시오. $0205 and then add $코드 포인트를 얻기 위해 최종 결과에 10000.

코드 포인트가 있으면이를 4 바이트 UTF-8 값 세트로 변환 할 수 있습니다.

function ValidHighSurrogate(const aBuffer: array of AnsiChar; i: integer): boolean;
var
  n: byte;
begin
  Result := False;
  if (ord(aBuffer[i]) <> $ED) then exit; n := ord(aBuffer[i + 1]) shr 4; if ((n and $A) <> $A) then exit; n := ord(aBuffer[i + 2]) shr 6; if ((n and $2) = $2) then Result := True; end; function ValidLowSurrogate(const aBuffer: array of AnsiChar; i: integer): boolean; var n: byte; begin Result := False; if (ord(aBuffer[i]) <> $ED) then
    exit;

  n := ord(aBuffer[i + 1]) shr 4;
  if ((n and $B) <> $B) then
    exit;

  n := ord(aBuffer[i + 2]) shr 6;
  if ((n and $2) = $2) then
    Result := True;
end;

function GetRawSurrogateValue(const aBuffer: array of AnsiChar; i: integer): integer;
var
  a, b: integer;
begin
  a := ord(aBuffer[i + 1]) and $0F; b := ord(aBuffer[i + 2]) and $3F;

  Result := (a shl 6) or b;
end;

function CESU8ToUTF8(const aBuffer: array of AnsiChar): boolean;
var
  TempBuffer: array of AnsiChar;
  i, j, TempLen: integer;
  TempHigh, TempLow, TempCodePoint: integer;
begin
  TempLen := length(aBuffer);
  SetLength(TempBuffer, TempLen);

  i := 0;
  j := 0;
  while (i < TempLen) do
    if (i + 5 < TempLen) and ValidHighSurrogate(aBuffer, i) and
      ValidLowSurrogate(aBuffer, i + 3) then
    begin
      TempHigh := GetRawSurrogateValue(aBuffer, i);
      TempLow := GetRawSurrogateValue(aBuffer, i + 3);
      TempCodePoint := (TempHigh shl 10) + TempLow + $10000; TempBuffer[j] := AnsiChar($F0 + ((TempCodePoint and $1C0000) shr 18)); TempBuffer[j + 1] := AnsiChar($80 + ((TempCodePoint and $3F000) shr 12)); TempBuffer[j + 2] := AnsiChar($80 + ((TempCodePoint and $FC0) shr 6)); TempBuffer[j + 3] := AnsiChar($80 + (TempCodePoint and $3F));
      inc(j, 4);
      inc(i, 6);
    end
    else
    begin
      TempBuffer[j] := aBuffer[i];
      inc(i);
      inc(j);
    end;

  Result := < save the buffer here >;
end;
2 AmigoJack Aug 25 2020 at 23:27
  • UTF-8 은 문자 당 1, 2, 3 또는 4 바이트로 구성됩니다. 코드 포인트 U + 1F605는 .#$F0#$9F#$98#$85
  • UTF-16 은 문자 당 2 바이트 또는 4 바이트로 구성됩니다. 4 바이트 시퀀스는 U + FFFF (대부분의 이모 지 등)를 넘어서는 코드 포인트를 인코딩하는 데 필요합니다. UCS-2 만 U + 0000에서 U + FFFF까지의 코드 포인트로 제한됩니다 (2000 이전의 Windows NT 버전에 적용됨).
  • (UTF-8 high surrogate, low surrogate) 와 같은 시퀀스 는 유효한 UTF-8이 아니지만 CESU-8- 순진한 결과이므로 UTF-16에서 UTF-8 로의 부적절한 변환 : 대신 (인식 및 ) 4 바이트 UTF-16 시퀀스 (한 코드 포인트 인코딩)를 4 바이트 UTF-8 시퀀스로만 변환하고 항상 2 바이트가 변환되어 2x2 바이트가 잘못된 6 바이트 UTF-8 시퀀스로 변환됩니다.#$ED#$A0#$BD#$ED#$B8#$85

유효한 UTF-8 시퀀스 를 유효한 UTF-16 시퀀스로 변환하는 것이 저에게 효과적입니다. 물론, 실제로 이모티콘을 렌더링 할 수있는 적절한 글꼴을 사용하고 있는지 확인하십시오.#$F0#$9F#$98#$85#$3d#$d8#$05#$de

// const CP_UTF8= 65001;

function Utf8ToUtf16( const sIn: AnsiString; iSrcCodePage: DWord= CP_UTF8 ): WideString;
var
  iLenDest, iLenSrc: Integer;
begin
  // First calculate how much space is needed
  iLenSrc:= Length( sIn );
  iLenDest:= MultiByteToWideChar( iSrcCodePage, 0, PAnsiChar(sIn), iLenSrc, nil, 0 );

  // Now provide the accurate space
  SetLength( result, iLenDest );
  if iLenDest> 0 then begin  // Otherwise ERROR_INVALID_PARAMETER might occur
    if MultiByteToWideChar( iSrcCodePage, 0, PAnsiChar(sIn), iLenSrc, PWideChar(result), iLenDest )= 0 then begin
      // GetLastError();
      result:= '';
    end;
  end;
end;

...
  Edit1.Font.Name:= 'Segoe UI Symbol';  // Already available in Win7
  Edit1.Text:= Utf8ToUtf16( AnsiString(#$F0#$9F#$98#$85' vs. '#$ED#$A0#$BD#$ED#$B8#$85) );
  // Should display: 😅 vs. ����

내가 아는 한 Windows에는 CESU-8이나 WTF-8에 대한 코드 페이지 가 없으므로 잘못된 UTF-8을 처리하지 않습니다. 또한의 사용 MB_PRECOMPOSED은 권장되지 않으며 어쨌든이 경우에 적용되지 않습니다.

유효하지 않은 UTF-8을 제공하는 사람에게 말하고 그의 작업을 올바르게 작성하도록 요구하십시오 (또는 즉시 UTF-16을 제공하십시오). 그렇지 않으면 일치하는 서로 게이트 쌍을 검색하여 들어오는 UTF-8을 사전 처리 한 다음 해당 바이트를 적절한 시퀀스로 교체해야합니다. 불가능하지도 않고 어렵지도 않지만 인내심의 지루한 작업입니다.

2 RemyLebeau Aug 25 2020 at 23:25

#$ED#$A0#$BD유니 코드 코드 포인트들의 UTF-8 인코딩 된 형태 U+D83DA는, 높은 대리 .

#$ED#$B8#$85유니 코드 코드 포인트들의 UTF-8 인코딩 된 형태 U+DE05A는, 로우 대리 .

#$F0#$9F#$98#$85UTF-8로 인코딩 된 Unicode codepoint 형식입니다 U+1F605.

서로 게이트 범위의 유니 코드 코드 포인트는 UTF-16 용 으로 예약되어 있으며 자체적으로 사용하는 것은 불법이므로� 인쇄 할 때 표시 됩니다.

이러한 서로 게이트는 유니 코드 코드 포인트 U + 1F605 ( 😅)에 대한 적절한 UTF-16 서로 게이트입니다 .

따라서 UTF-8 데이터가 생성되는 소스에서 수정해야하는 이중 인코딩 문제가 있습니다. U+1F605먼저 UTF-8이 아닌 UTF-16으로 인코딩 된 다음 해당 대리자가 유니 코드 코드 포인트 로 잘못 취급되고 개별적으로 UTF-8로 인코딩됩니다. 대신 원하는 것은 코드 포인트 U+1F605가 그대로 UTF-8로 직접 인코딩되는 것입니다.

UTF-8 데이터의 소스를 수정할 수없는 경우이 잘못된 인코딩을 수동으로 감지하고 대신 UTF-16으로 데이터를 처리해야합니다. UTF-8 데이터를 UTF-32로 디코딩하고 결과에 서로 게이트 코드 포인트가 포함 된 경우 동일한 길이의 별도 UTF-16 문자열을 만들고 코드 포인트를 그대로 해당 문자열에 복사하여 값을 16 비트로 자릅니다. 그런 다음 필요에 따라 해당 UTF-16 문자열을 사용할 수 있습니다. 그렇지 않으면 서로 게이트가 없으면 UTF-8을 UTF-16 문자열로 직접 디코딩하고 대신 해당 결과를 사용할 수 있습니다.

업데이트 : @AmigoJack의 답변에서 언급 했듯이이 데이터는 CESU-8 인코딩을 사용하고 있습니다 (소스 인터페이스에 문서화되어 있습니까?). 이제 이것을 알면 수동 감지를 포기하고이 소스의 모든 UTF-8 데이터가 CESU-8이라고 가정하고 위에서 설명한대로 수동으로 디코딩 할 수 있습니다 ( MultiByteToWideChar()Delphi RTL 도이를 자동으로 처리 할 수 ​​없습니다). 당신), 적어도 인터페이스가 수정 될 때까지, 예 :

function UTF8DenormalizedToString(s: PAnsiChar): UnicodeString;
var
  utf32: UCS4String;
  len, i: Integer;
begin
  utf32 := ... decode utf8 to utf32 ...; // I leave this as an exercise for you!
  len := Length(utf32) - 1; // UCS4String includes a null terminator
  SetLength(Result, len);
  for i := 1 to len do
    Result[i] := WideChar(utf32[i-1] and $FFFF); // UCS4String is 0-indexed
end;