sunLoadingImage
whowedImag
decoration left 1
decoration left 2
transhome
transprojects
transgallery
transarticles
decoration rigth
English
Українська
Show/Hide search bar
black cat logo variable logo
[23 Січ 2013]

Контури символа True Type шрифта

В цьому уроці показано, як завантажити True Type шрифт з допомогою WinAPI. Потім показано як для символа дістати і розпарсити інформацію про контури символа (symbol outlines), і перетворити контури у скінченну кількість точок. Точки можна використати для створення ліній-сегментів (для рендерінгу контурів), або для тріангуляції (для рендерінгу заповненого тексту). Тріангуляція контурів показана в наступному уроці. Формування стрічки тексту з контурів розглянута в іншому уроці.
Відрендерені контури для символів виглядять так:
Для завантаження шрифта потрібно викликати функцію initializeFont(). Після цього ми маємо вказівник на створений шрифт.
HFONT fontPtr;
const int sizeOfFont = 30;

// завантажити шрифт по назві, наприклад, initializeFont("Arial", false, false);
void initializeFont(const std::string fontName, bool bold, bool italic)
{
   bool weight = FW_REGULAR;
   if(bold)
   {
      weight = FW_BOLD;
   }

   // створити WinAPI шрифт
   fontPtr = CreateFont(
         sizeOfFont, 0, 0, 0,
         weight, italic, 0, 0, DEFAULT_CHARSET,
         OUT_DEVICE_PRECIS, CLIP_DEFAULT_PRECIS, NONANTIALIASED_QUALITY,
         FF_DONTCARE, fontName.c_str()
      );

   if(fontPtr == nullptr)
   {
      // неможливо створити шрифт
   }
}
Отримати контури для символа можна з допомогою функції getSymbolOutlines(). Спочатку оголошується масив для збереження контурів. Кожен контур буде представлено масивом точок. Якщо точки з'єднати лініями, то отримаємо замкнутий контур. В символі може бути декілька контурів. Для багатьох символів є один зовнішній контур, наприклад, для символа T. Для символів типу O є зовнішній і внутрішній контур. Для символів з багатьох частин, як у символі i є окремі контури для кожної частини (у цьому випадку два зовнішніх контура).
Потрібно дізнатися скільки пам'яті необхідно для збереження інформації про контури символа, потім виділити необхідну кількість пам'яті і дістати контури від WinAPI. Контури повертаються у досить незручному виді, тож дані необхідно розпарсити і провести інші перетворення.
// дістати контур для символа, getSymbolOutlines('a');
std::vector<std::vector<glm::vec2>> getSymbolOutlines(char symbol)
{
   std::vector<std::vector<glm::vec2>> out;

   HDC hdc = CreateCompatibleDC(NULL);
   if (hdc != NULL)
   {
      SelectObject(hdc, fontPtr);

      // дістати розмір даних, які містять інформацію про контур символа
      std::unique_ptr<GLYPHMETRICS> glyphMetrics(new GLYPHMETRICS);
      static const MAT2 rotation = { 0, 1, 0, 0, 0, 0, 0, 1 };
      DWORD bufferSize = GetGlyphOutline(hdc, symbol, GGO_NATIVE,
                        glyphMetrics.get(), 0, 0, &rotation);

      // перевірити чи розмір даних з контурами не завеликий
      if(bufferSize > 200000)
      {
         // щось не так
      }

      // дістати дані, які містіть інформацію про контур символа
      std::vector<char> outBuffer(bufferSize);
      DWORD error = GetGlyphOutline(hdc, symbol, splineBuildType,
                     glyphMetrics.get(), bufferSize, outBuffer.data(), &identity);
      TTPOLYGONHEADER * outlinesData = reinterpret_cast(outBuffer.data());

      // дані про контури символа містяться в структурі TTPOLYGONHEADER
      out = parseSymbolOutlinesData(hdc, outlinesData, bufferSize);

      DeleteDC(hdc);
   }

   return out;
}
Наступна функція парсить буфер з контурами, який був наданий WinAPI. В буфері збережені полігони. Кожен полігон має свій тип. Типом може бути проста лінія чи сплайн безьє. Сплайни безье можуть бути квадратичними чи кубічними. Якщо запит до GetGlyphOutline відбувся з прапорцем GGO_NATIVE, то буфер не буде містити кубічних сплайнів безьє. А якщо використано GGO_BEZIER, то в буфері не буде квадратичних сплайнів безьє. В наступному прикладі є підтримка тільки ліній і квадратичних сплайнів безьє. Лінії додаються до контура як дві точки (початкова і кінцева), а сплайни розбирваються на певну кількість сегментів. Координати в буфері збережені у формі числа з фіксованою точкою. Число з фіксованою точкою перетворюється на число з плаваючою точкою функцією fromFixed().
// розпарсити дані контура
std::vector<std::vector<glm::vec2>> parseSymbolOutlinesData(
   HDC hDC,
   LPTTPOLYGONHEADER outlinesData,
   DWORD size
)
{
   // вказівник на початок буфера
   LPTTPOLYGONHEADER dataStart = outlinesData;
   // поточна лінія / крива контура
   LPTTPOLYCURVE curCuve;
   // помилка, якщо true
   bool error = false;

   // обробити усі криві, що містяться в контурі
   std::vector<std::vector<glm::vec2>> contours;
   while ((DWORD)outlinesData < (DWORD)(((LPSTR)dataStart) + size))
   {
      std::vector<glm::vec2> contour;

      if (outlinesData->dwType == TT_POLYGON_TYPE)
      {
         // дадати першу точку до контура
         contour.push_back(glm::vec2(fromFixed(outlinesData->pfxStart.x),
                  fromFixed(outlinesData->pfxStart.y)));

         // наступна контрольна точка
         curCuve = (LPTTPOLYCURVE) (outlinesData + 1);

         // зібрати усі контрольні точки кривої
         while ((DWORD)curCuve < (DWORD)(((LPSTR)outlinesData) + outlinesData->cb))
         {
            POINTFX ptStart = *(LPPOINTFX)((LPSTR)curCuve - sizeof(POINTFX));
            WORD offset = 0;
            if (curCuve->wType == TT_PRIM_LINE)
            {
               appendLine(ptStart, curCuve, contour);
            }
            else if(curCuve->wType == TT_PRIM_QSPLINE)
            {
            // квадратична крива безьє (при параметрі GGO_NATIVE для getGlyphOutline)
               appendQuadraticBezier(ptStart, curCuve, contour,
                     angleEpsilon, distanceEpsilon);
            }
            else if(curCuve->wType == TT_PRIM_QSPLINE)
            {
               // кубічна крива безье (при параметрі GGO_BEZIER для getGlyphOutline)
               // тут повинна бути функція аланалочна appendQuadraticBezier();
               // для цього просто читається на одну контрольну точку більше
               error = true;
               break;
            }
            else
            {
               // невідомий примітив
               error = true;
               break;
            }

            // до наступної кривої в контурі
            curCuve = (LPTTPOLYCURVE)&(curCuve->apfx[curCuve->cpfx]);
         }

         if(error)
         {
            break;
         }

         // на даний момент закінчено формування одного з контрурів символа
      }
      else
      {
         // невідома структура
         error = true;
         break;
      }

      // до наступного контура
      outlinesData = (LPTTPOLYGONHEADER)(((LPSTR)outlinesData) + outlinesData->cb);

      contours.push_back(std::move(contour));
   }

   return contours;
}
Функція для перетворення fixed point числа в floating point число:
float fromFixed(FIXED fixed)
{
   // цілочисельна частина + дробова частина
   // дробова частина закодована в unsigned short
   return float(fixed.value) + float(fixed.fract) / USHRT_MAX;
}
Функція для додавання нової точки до контура:
void appendToResult(float x, float y, std::vector<glm::vec2&gr; & out)
{
   // додати нове значеня до контура
   uint s = out.size();
   if(s>1)
   {
      // не така ж точка як попередня
      if(x != out[s-1].x || y != out[s-1].y)
      {
         out.push_back(glm::vec2(x, y));
      }
   }
   else
   {
      out.push_back(glm::vec2(x, y));
   }
}
Функція парсить і додає лінію до контура:
void appendLine(POINTFX start, LPTTPOLYCURVE curCuve, std::vector<glm::vec2> & out)
{
   // додати лінію
   for (WORD i = 0; i < curCuve->cpfx; i++)
   {
      float x = fromFixed(curCuve->apfx[i].x);
      float y = fromFixed(curCuve->apfx[i].y);
      appendToResult(x, y, out);
   }
}
Функція парсить і додає квадратичну криву безьє до контура:
void appendQuadraticBezier(POINTFX start, LPTTPOLYCURVE curCuve, std::vector<glm::vec2> & out)
{
   // квадратичні криві безьє генеруються тільки при
   // параметрі GGO_NATIVE для GetGlyphOutline()

   // контрольні точки квадратичного сплайну
   std::vector<glm::vec3> controlPoints(3);

   // Перша котрольна точка
   controlPoints[0] = gp_Pnt2d(fromFixed(start.x), fromFixed(start.y));

   for (WORD i = 0; i < curCuve->cpfx; )
   {
      // Друга контрольна точка
      controlPoints[1] = gp_Pnt2d(fromFixed(curCuve->apfx[i].x),
                  fromFixed(curCuve->apfx[i].y));
      i++;

      // Третя контрольна точка
      if (i == (curCuve->cpfx - 1))
      {
         controlPoints[2] = glm::vec2(fromFixed(curCuve->apfx[i].x),
               fromFixed(curCuve->apfx[i].y));
         i++;
      }
      else
      {
         // третя контрольна точка між попередньою і поточною
         controlPoints[2].x((fromFixed(curCuve->apfx[i-1].x) +
                  fromFixed(curCuve->apfx[i].x))/2);
         controlPoints[2].y((fromFixed(curCuve->apfx[i-1].y) +
                  fromFixed(curCuve->apfx[i].y))/2);
      }

      // розбити криву безьє на скінченну кількість ліній
      {
         PS. Тут можна провести теселяцію, яка залежить від кривизни кривої

         int detail = 5;
         float step = 1.0f / detail;

         for(int j=0; j<=detail; j++)
         {
            float t = step * j;
            float ti = 1.0f - t;

            float m0 = pow(t, 0) * pow(ti, 2);
            float m1 = pow(t, 1) * pow(ti, 1) * 2.0f;
            float m2 = pow(t, 2) * pow(ti, 0);

            p.x += controlPoints[0].x * m0 +
                  controlPoints[1].x * m1 + controlPoints[2].x * m2;
            p.y += controlPoints[0].y * m0 +
                  controlPoints[1].y * m1 + controlPoints[2].y * m2;

            appendToResult(p.x, p.y, out);
         }
      }

      // Кінець попередньої кривої є початком нової кривої
      controlPoints[0] = controlPoints[2];
   }
}
Не забудьте видалити шрифт з пам'яті після закінчення роботи з ним
if(m_impl->fontPtr)
{
   DeleteObject(m_impl->fontPtr);
}



Sun and Black Cat- Ігор Дихта (igor dykhta email) © 2007-2014