52. Russian 글꼴 추가 - Part 1.

EFI_NARROW_GLYPH/EFI_WIDE_GLYPH 형식 조사 및 *.woff 글꼴 파일에서 GLYPH 배열 구성

UNI 파일은 UEFI의 로컬화를 위한 것이다. 지금까지 우리는, UNI 파일에서 영어와 프랑스어만 출력해 보았다. 이 두 언어의 공통점은, 라틴어 문자 집합의 기호를 사용한다는 점에서 같다.

다른 문자 집합에서 문자열을 출력하려면 올바른 문장이 출력되지 않는다. 예를 들어, 간단하게 Hello를 러시아어로 출력하려면,

Print(L"Привет!\n");

다음과 같이 출력된다.

nographic 모드에서는 다음과 같이 출력될 것이다.

FS0:\> HIIFont.efi
?@825B!

이 출력을 구현하는 것에는 신경쓰지 말자. 이는 다른 번역 레벨에서 발생하는 것이고, 이번 장에서는 UEFI Graphic(Native 또는 VNC)을 살펴보도록 하자.

어쨌건 출력으로 보이듯이, ! 하나만 출력된 것을 볼 수 있다. UEFI 시스템 자체에는 러시아어 글꼴이 없기 때문에 위와 같은 일이 발생한 것이다.

컴퓨터의 언어로 해석하자면, 러시아어 유니코드 기호 코드를 기호 이미지로 변환하는 방법을 UEFI 시스템은 모르기 때문이다.

글꼴 정보는 HII DB에 저장되므로, 문제를 해결하려면 러시아 코드가 있는 유니코드 기호에 대한 "그림"이 있는 글꼴 유형 패키지가 포함된 패키지 목록을 제공하기만 하면 된다.

UEFI는 작게는 8x19에 넓게는 16x19 글꼴은 사용한다. 코드에서 기호 데이터는 EFI_NARROW_GLYPHEFI_WIDE_GLYPH(UefiInternalFormRepresentation.h) 구조로 인코딩된다.

$ cat MdePkg/Include/Uefi/UefiInternalFormRepresentation.h
---
#define EFI_GLYPH_HEIGHT                     19
#define EFI_GLYPH_WIDTH                      8
///@}

///
/// The EFI_NARROW_GLYPH has a preferred dimension (w x h) of 8 x 19 pixels.
///
typedef struct {
  ///
  /// The Unicode representation of the glyph. The term weight is the
  /// technical term for a character code.
  ///
  CHAR16                 UnicodeWeight;
  ///
  /// The data element containing the glyph definitions.
  ///
  UINT8                  Attributes;
  ///
  /// The column major glyph representation of the character. Bits
  /// with values of one indicate that the corresponding pixel is to be
  /// on when normally displayed; those with zero are off.
  ///
  UINT8                  GlyphCol1[EFI_GLYPH_HEIGHT];
} EFI_NARROW_GLYPH;

///
/// The EFI_WIDE_GLYPH has a preferred dimension (w x h) of 16 x 19 pixels, which is large enough
/// to accommodate logographic characters.
///
typedef struct {
  ///
  /// The Unicode representation of the glyph. The term weight is the
  /// technical term for a character code.
  ///
  CHAR16                 UnicodeWeight;
  ///
  /// The data element containing the glyph definitions.
  ///
  UINT8                  Attributes;
  ///
  /// The column major glyph representation of the character. Bits
  /// with values of one indicate that the corresponding pixel is to be
  /// on when normally displayed; those with zero are off.
  ///
  UINT8                  GlyphCol1[EFI_GLYPH_HEIGHT];
  ///
  /// The column major glyph representation of the character. Bits
  /// with values of one indicate that the corresponding pixel is to be
  /// on when normally displayed; those with zero are off.
  ///
  UINT8                  GlyphCol2[EFI_GLYPH_HEIGHT];
  ///
  /// Ensures that sizeof (EFI_WIDE_GLYPH) is twice the
  /// sizeof (EFI_NARROW_GLYPH). The contents of Pad must
  /// be zero.
  ///
  UINT8                  Pad[3];
} EFI_WIDE_GLYPH;

이유는 모르겠지만, mFontBin 구조의 String.c에서 일부 히브리어 문자에 대한 예를 찾아볼 수 있었다. 이 구조에서 유니코드 0x05d2가 있는 기호 하나를 살펴보도록 하겠다.

GlyphCol1 배열을 보고, 이진 시스템에서 배열 데이터를 인쇄해보자. X/-로도 표현해 보았다.

$ cat MdeModulePkg/Application/UiApp/String.c
---
        {
      0x05d2,
      0x00,
      {
        0x00,      // 00000000 // --------
        0x00,      // 00000000 // --------
        0x00,      // 00000000 // --------
        0x78,      // 01111000 // -XXXX---
        0x7C,      // 01111100 // -XXXXX--
        0x0C,      // 00001100 // ----XX--
        0x0C,      // 00001100 // ----XX--
        0x0C,      // 00001100 // ----XX--
        0x0C,      // 00001100 // ----XX--
        0x0C,      // 00001100 // ----XX--
        0x0C,      // 00001100 // ----XX--
        0x1C,      // 00011100 // ---XXX--
        0x3E,      // 00111110 // --XXXXX-
        0x66,      // 01100110 // -XX--XX-
        0x66,      // 01100110 // -XX--XX-
        0x00,      // 00000000 // --------
        0x00,      // 00000000 // --------
        0x00,      // 00000000 // --------
        0x00       // 00000000 // --------
      }
    },

이는 실제 히브리어 문자인 gimel &#0x05D2(U+05D2)로 보인다. https://unicodemap.org/details/0x05D2/index.html

EDKII 코드베이스에 기호에 대한 에는 없지만, 간단하다. GlyphCol1은 기호 이미지의 왼쪽 절반을 인코딩하고, GlyphCol2는 남은 오른쪽 절반을 인코딩한다.

예를 들어, A는 다음과 같을 수있다.

    {
      0x05d2,
      0x00,
      {
        0x00,      // 00000000 // --------
        0x00,      // 00000000 // --------
        0x00,      // 00000000 // --------
        0x01,      // 00000001 // -------X
        0x02,      // 00000010 // ------X-
        0x02,      // 00000010 // ------X-
        0x04,      // 00000100 // -----X--
        0x04,      // 00000100 // -----X--
        0x08,      // 00001000 // ----X---
        0x0F,      // 00001111 // ----XXXX
        0x10,      // 00010000 // ---X----
        0x10,      // 00010000 // ---X----
        0x20,      // 00100000 // --X-----
        0x20,      // 00100000 // --X-----
        0x70,      // 01110000 // -XXX----
        0x00,      // 00000000 // --------
        0x00,      // 00000000 // --------
        0x00,      // 00000000 // --------
        0x00       // 00000000 // --------
      },
      {
        0x00,      // 00000000 // --------
        0x00,      // 00000000 // --------
        0x00,      // 00000000 // --------
        0x00,      // 00000000 // --------
        0x80,      // 10000000 // X-------
        0x80,      // 10000000 // X-------
        0x40,      // 01000000 // -X------
        0x40,      // 01000000 // -X------
        0x20,      // 00100000 // --X-----
        0xE0,      // 11100000 // XXX-----
        0x10,      // 00010000 // ---X----
        0x10,      // 00010000 // ---X----
        0x08,      // 00001000 // ----X---
        0x08,      // 00001000 // ----X---
        0x1C,      // 00011100 // ---XXX--
        0x00,      // 00000000 // --------
        0x00,      // 00000000 // --------
        0x00,      // 00000000 // --------
        0x00       // 00000000 // --------
      },
      {
        0x00,
        0x00,
        0x00
      }
    },

Glyph 기본 글꼴

기본 글꼴의 정의 위치가 궁금할 경우, LaffStd.c를 참조하는 것을 추천한다.

$ cat MdeModulePkg/Universal/Console/GraphicsConsoleDxe/LaffStd.c
---
EFI_NARROW_GLYPH  gUsStdNarrowGlyphData[] = {
  { 0x0020, 0x00, {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}},
  { 0x0021, 0x00, {0x00,0x00,0x00,0x18,0x3C,0x3C,0x3C,0x18,0x18,0x18,0x18,0x18,0x00,0x18,0x18,0x00,0x00,0x00,0x00}},
  { 0x0022, 0x00, {0x00,0x00,0x00,0x6C,0x6C,0x6C,0x28,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}},
  ...
  { 0x0000, 0x00, {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}} //EOL
};

// Get available Unicode glyphs narrow fonts(8*19 pixels) size.
UINT32 mNarrowFontSize =  sizeof (gUsStdNarrowGlyphData);

gUsStdNarrowGlyphData 배열은 글꼴 패키지를 생성하고, HII 데이터베이스에 등록하는 데 사용된다. 실제 코드는 MdeModulePkg/Universal/Console/GraphicsConsoleDxe/GraphicsConsole.c 파일의 RegisterFontPackage 함수를 참조하도록 하자.

HII 데이터베이스에 패키지 목록을 조사했을 때, 이것이 이 글꼴을 담당했다.

PackageList[10]: GUID=F5F219D3-7006-4648-AC8D-D61DFB7BC6AD; size=0x14EC  // mFontPackageListGuid
        Package[0]: type=SIMPLE_FONTS; size=0x14D4
        Package[1]: type=END; size=0x4

러시아어 글꼴에 대한 Glyph 데이터 가져오기

Glyph 데이터를 얻어오는 것은 글꼴을 추가하는 작업에 있어서 가장 어려운 것이다.

우선 크기가 8x19/16x19인 픽셀 글꼴을 찾아야 하는데, 이는 이미 어려운 작업일 수 있다. https://int10h.org/oldschool-pc-fonts/fontlist/font?ast_premiumexec 에서 키릴 문자가 포함된 AST PremiumExec 글꼴을 찾았다. 이 글꼴에는 대부분의 키릴 유니코드 블록(U+0400 - U+04FF)의 심볼들을 포함하고 있다.

하지만 이 글꼴은 *.woff 형식으로 인코딩되어 있으므로, Glyph 데이터로 변환해야 한다. 수동으로 변환해주기에는 어려움이 있기에, https://github.com/zhenghuadai/uefi-programming/tree/master/book/GUIbasics/font서 이 작업에 HTML canvas를 사용해보자는 아이디어를 차용했다.

HTML canvas의 글꼴에서 각 기호를 하나씩 인쇄한다. 각 기호에 대해 이미지 비트맵을 가져오고 해당 데이터를 사용하여 UEFI 환경에서 사용할 수 있는 Glyph 배열을 구성한다. 결국 Glyph 배열을 화면에 출력한다.

이는 자바스크립트와 관련된 장장이 아니지만, 스크립트에 대한 설명이 필요하다고 생각한다. 먼저 글꼴을 로드하고 canvas와 스크립트 코드를 선언하는 HTML 부분이다.

<html>
  <head>
    <style>
      @font-face {
        font-family: "AST";
        src: url(web_ast_premiumexec.woff) format('woff');
      }
    </style>
  </head>
  <body>
    <canvas id="canvas" width="32" height="32"></canvas>
    <script type="text/javascript">
      ...
    </script>
  </body>
</html>

우리의 코드는 핵심, 다음과 같다.

const unicode_start_code = 0x0400;
const unicode_end_code = 0x045F;
var f = new FontFace('AST', 'url(web_ast_premiumexec.woff)');
f.load().then(function() {
  document.write(UnicodeToGlyphs(unicode_start_code, unicode_end_code));
})

글꼴이 로드되면, canvas에 U+0400에서 U+045F까지의 유니코드 기호를 인쇄하고, 데이터를 조사하고 화면의 UEFI에 대한 최종 C 배열을 출력하는 사용자 지정 함수 UnicodeToGlyphs를 실행한다.

그리고 여기에 나머지 자바스크립트코드가 있다. 이는 매우 간단하므로, 이해하기 어렵지 않을 것이라 생각한다.

function decimalToHex(d, padding) {
  var hex = Number(d).toString(16);
  padding = typeof (padding) === "undefined" || padding === null ? padding = 2 : padding;

  while (hex.length < padding) {
    hex = "0" + hex;
  }

  return hex;
}

function UnicodeToGlyphs(unicode_start_code, unicode_end_code) {
  const threshold = 100;                // `A` threshold to count data as a black pixel
  const FW = 16;
  const FH = 19;
  const left_glyph_start_column = 0;
  const left_glyph_end_column = FW/2 - 1;
  const right_glyph_start_column = FW/2;
  const right_glyph_end_column = FW - 1;

  const canvas = document.getElementById('canvas');
  canvas.width *= window.devicePixelRatio
  canvas.height *= window.devicePixelRatio
  canvas.style.width = 32
  canvas.style.height = 32
  const ctx = canvas.getContext('2d');
  ctx.strokeRect(0, 0, FW, FH);
  ctx.font = "19px AST"
  ctx.fillstyle='#00f';

  var wide_glyphs_str="EFI_WIDE_GLYPH  gSimpleFontWideGlyphData[] = {<BR>";
  var narrow_glyphs_str="EFI_NARROW_GLYPH  gSimpleFontNarrowGlyphData[] = {<BR>";
  for(i=unicode_start_code; i<=unicode_end_code; i++){
    wide_glyph = false;
    ctx.clearRect(0, 0, FW, FH);
    ctx.fillText(String.fromCharCode(i), 0, 0 + FW-1);
    var bitmapimg = ctx.getImageData(0, 0, FW, FH);
    var bitmap = bitmapimg.data;
    var left_glyph = " ";
    var right_glyph = " ";
    for (row=0; row<FH; row++){
      left_row = 0;
      for (col=left_glyph_start_column; col<=left_glyph_end_column; col++){
        left_row <<= 1
        if (bitmap[row*FW*4 + col*4 + 3] > threshold)
          left_row |= 1;
      }
      left_glyph += "0x" + decimalToHex(left_row);

      right_row = 0;
      for (col=right_glyph_start_column; col<=right_glyph_end_column; col++){
        right_row <<= 1
        if(bitmap[row*FW*4 + col*4 + 3] > threshold) {
          wide_glyph = true;
          right_row |= 1;
        }
      }
      right_glyph += "0x" + decimalToHex(right_row);

      if (row < (FH-1)) {
        left_glyph += ",";
        right_glyph += ",";
      }
    }
    if (wide_glyph)
      wide_glyphs_str += "{ 0x" + decimalToHex(i) + ", 0x00, " + "{" +  left_glyph + "}, {" + right_glyph + "}, {0x00,0x00,0x00}}," + "<BR>";
    else
      narrow_glyphs_str += "{ 0x" + decimalToHex(i) + ", 0x00, " + "{" +  left_glyph + "}},"+ "<BR>";
  }
  narrow_glyphs_str += "};<BR>"
  narrow_glyphs_str += "UINT32 gSimpleFontNarrowBytes = sizeof(gSimpleFontNarrowGlyphData);<BR>"
  wide_glyphs_str += "{ 0x00, 0x00, { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}}" + "<BR>"
  wide_glyphs_str += "};<BR>"
  wide_glyphs_str += "UINT32 gSimpleFontWideBytes = sizeof(gSimpleFontWideGlyphData);<BR>"
  return wide_glyphs_str + "<BR>" + narrow_glyphs_str;
}

create_font_data.htmlweb_ast_premiumexec.woff를 두고 create_font_data.html을 연다. 이 작업을 위해 Chrome(version 95.0.4638.69)을 사용했다. 페이지는 다음과 같이 출력되어야 한다.

EFI_WIDE_GLYPH gSimpleFontWideGlyphData[] = {
{ 0x00, 0x00, { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}}
};
UINT32 gSimpleFontWideBytes = sizeof(gSimpleFontWideGlyphData);

EFI_NARROW_GLYPH gSimpleFontNarrowGlyphData[] = {
{ 0x400, 0x00, { 0x60,0x30,0x00,0xfe,0x66,0x62,0x60,0x68,0x78,0x68,0x60,0x60,0x62,0x66,0xfe,0x00,0x00,0x00,0x00}},
{ 0x401, 0x00, { 0x66,0x66,0x00,0xfe,0x66,0x62,0x60,0x68,0x78,0x68,0x60,0x60,0x62,0x66,0xfe,0x00,0x00,0x00,0x00}},
{ 0x402, 0x00, { 0x00,0x00,0x00,0xfc,0x64,0x60,0x60,0x6c,0x76,0x66,0x66,0x66,0x66,0x66,0xe6,0x0c,0x00,0x00,0x00}},
{ 0x403, 0x00, { 0x0c,0x18,0x00,0xfe,0x66,0x62,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0xf0,0x00,0x00,0x00,0x00}},
 ...
{ 0x45c, 0x00, { 0x00,0x00,0x00,0x0c,0x18,0x30,0x00,0xe6,0x66,0x6c,0x78,0x78,0x6c,0x66,0xe6,0x00,0x00,0x00,0x00}},
{ 0x45d, 0x00, { 0x00,0x00,0x00,0x60,0x30,0x18,0x00,0xc6,0xc6,0xce,0xde,0xf6,0xe6,0xc6,0xc6,0x00,0x00,0x00,0x00}},
{ 0x45e, 0x00, { 0x00,0x00,0x00,0x00,0x6c,0x38,0x00,0xc6,0xc6,0xc6,0xc6,0xc6,0xc6,0x7e,0x06,0x06,0x0c,0xf8,0x00}},
{ 0x45f, 0x00, { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xee,0x6c,0x6c,0x6c,0x6c,0x6c,0x6c,0xfe,0x10,0x10,0x00,0x00}},
};
UINT32 gSimpleFontNarrowBytes = sizeof(gSimpleFontNarrowGlyphData);

우리가 가져온 글꼴은 고정된 크기인 8x19 문자만 포함하므로, gSimpleFontNarrowGlyphData 배열만 데이터로 채워진다. 이제 UEFI 애플리케이션을 구성하고, 글꼴을 HII 데이터베이스에 추가할 때이다.

Last updated