freetype-commit
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[freetype2-demos] gsoc-2022-chariri-3 4f3a8eb 17/36: [ftinspect] Finish


From: Werner Lemberg
Subject: [freetype2-demos] gsoc-2022-chariri-3 4f3a8eb 17/36: [ftinspect] Finish as well as refactor the "Continuous Mode".
Date: Wed, 27 Jul 2022 06:32:45 -0400 (EDT)

branch: gsoc-2022-chariri-3
commit 4f3a8eba3ab15ac66f11b06b2c1fd98e64095fdd
Author: Charlie Jiang <w@chariri.moe>
Commit: Charlie Jiang <w@chariri.moe>

    [ftinspect] Finish as well as refactor the "Continuous Mode".
    
    This commit finishs the "Continuous Mode", add several options adopted from
    `ftstring` util, and refactors the whole glyph drawing process with logic
    from `FTDemo_String_XXX` functions.
    
    All code related to populating chars to render, loading and positioning
    glyphs is moved to a new class named `StringRenderer`. The old
    `GlyphContinuous` is now only responsible for calling into the renderer,
    doing glyph transformations like emboldening and stroking, and drawing the
    glyphs generated from `StringRenderer`.
    
    * src/ftinspect/engine/stringrenderer.hpp,
      src/ftinspect/engine/stringrenderer.cpp: New files.
    
    * src/ftinspect/rendering/glyphcontinuous.cpp,
      src/ftinspect/rendering/glyphcontinuous.hpp:
      Move out several fields, and refactor using `StringRenderer`. This class
      no longer manages lifecycle of any glyphs, outlines or bitmaps.
    
    * src/ftinspect/engine/engine.cpp, src/ftinspect/engine/engine.hpp:
      Add `lcdSubPixelPositioning_`, some getters/setters.
      Add cache-less glyph loading (via `FT_Load_Glyph` and face slots).
      Add track kerning and per pair kerning fetching.
      Add a function to get the first Unicode charmap index of the active font.
      Add argument `outNode` and `forceRender` to `loadGlyphWithoutUpdate`,
      so the receiver can pin the glyph to a node to avoid accidentally
      flushing. `forceRender` is not used, though.
    
    * src/ftinspect/panels/continuous.cpp, src/ftinspect/panels/continuous.hpp:
      Add position, kerning options; properly sync settings and flush caches of
      the string renderer.
    
    * src/ftinspect/panels/settingpanel.cpp:
      Add support to the "Light (Sub Pixel)" AA mode.
      Make changes to "Embedded bitmap" option trigger font reload request,
      or the cache in the string renderer won't be flushed in time.
    
    * src/ftinspect/models/customcomboboxmodels.hpp,
      src/ftinspect/models/customcomboboxmodels.cpp:
      Add "Light (Sub Pixel)" Anti-Aliasing mode.
    
    * src/ftinspect/CMakeLists.txt, src/ftinspect/meson.build: Updated.
---
 src/ftinspect/CMakeLists.txt                  |   1 +
 src/ftinspect/engine/engine.cpp               |  70 +++-
 src/ftinspect/engine/engine.hpp               |  18 +-
 src/ftinspect/engine/stringrenderer.cpp       | 565 ++++++++++++++++++++++++++
 src/ftinspect/engine/stringrenderer.hpp       | 165 ++++++++
 src/ftinspect/meson.build                     |   1 +
 src/ftinspect/models/customcomboboxmodels.cpp |   4 +
 src/ftinspect/models/customcomboboxmodels.hpp |   1 +
 src/ftinspect/panels/continuous.cpp           |  63 ++-
 src/ftinspect/panels/continuous.hpp           |   7 +-
 src/ftinspect/panels/settingpanel.cpp         |   5 +-
 src/ftinspect/rendering/glyphcontinuous.cpp   | 351 ++++++----------
 src/ftinspect/rendering/glyphcontinuous.hpp   |  58 +--
 13 files changed, 1018 insertions(+), 291 deletions(-)

diff --git a/src/ftinspect/CMakeLists.txt b/src/ftinspect/CMakeLists.txt
index f8f7dea..4fe2a52 100644
--- a/src/ftinspect/CMakeLists.txt
+++ b/src/ftinspect/CMakeLists.txt
@@ -23,6 +23,7 @@ add_executable(ftinspect
   "engine/engine.cpp"
   "engine/fontfilemanager.cpp"
   "engine/charmap.cpp"
+  "engine/stringrenderer.cpp"
 
   "rendering/glyphbitmap.cpp"
   "rendering/glyphoutline.cpp"
diff --git a/src/ftinspect/engine/engine.cpp b/src/ftinspect/engine/engine.cpp
index 6e73ec3..32794de 100644
--- a/src/ftinspect/engine/engine.cpp
+++ b/src/ftinspect/engine/engine.cpp
@@ -246,6 +246,17 @@ Engine::numberOfNamedInstances(int fontIndex,
 }
 
 
+int
+Engine::currentFontFirstUnicodeCharMap()
+{
+  auto& charmaps = currentFontCharMaps();
+  for (auto& cmap : charmaps)
+    if (cmap.encoding == FT_ENCODING_UNICODE)
+      return cmap.index;
+  return -1;
+}
+
+
 int
 Engine::loadFont(int fontIndex,
                  long faceIndex,
@@ -363,7 +374,7 @@ Engine::removeFont(int fontIndex, bool closeFile)
 unsigned
 Engine::glyphIndexFromCharCode(int code, int charMapIndex)
 {
-  if (charMapIndex == -1)
+  if (charMapIndex < 0)
     return code;
   return FTC_CMapCache_Lookup(cmapCache_, scaler_.face_id, charMapIndex, code);
 }
@@ -376,6 +387,42 @@ Engine::currentFontMetrics()
 }
 
 
+FT_GlyphSlot
+Engine::currentFaceSlot()
+{
+  return ftSize_->face->glyph;
+}
+
+
+FT_Pos
+Engine::currentFontTrackingKerning(int degree)
+{
+  FT_Pos result;
+  // this function needs and returns points, not pixels
+  if (!FT_Get_Track_Kerning(ftSize_->face,
+                            static_cast<FT_Fixed>(scaler_.width) << 10, 
+                            -degree,
+                            &result))
+  {
+    result = static_cast<FT_Pos>((result / 1024.0 * scaler_.x_res) / 72.0);
+    return result;
+  }
+  return 0;
+}
+
+
+FT_Vector
+Engine::currentFontKerning(int glyphIndex,
+                           int prevIndex)
+{
+  FT_Vector kern = {0, 0};
+  FT_Get_Kerning(ftSize_->face, 
+                 prevIndex, glyphIndex, 
+                 FT_KERNING_UNFITTED, &kern);
+  return kern;
+}
+
+
 QString
 Engine::glyphName(int index)
 {
@@ -434,20 +481,33 @@ Engine::loadGlyph(int glyphIndex)
 }
 
 
+int
+Engine::loadGlyphIntoSlotWithoutCache(int glyphIndex)
+{
+  return FT_Load_Glyph(ftSize_->face, glyphIndex, loadFlags_);
+}
+
+
 FT_Glyph
-Engine::loadGlyphWithoutUpdate(int glyphIndex)
+Engine::loadGlyphWithoutUpdate(int glyphIndex, 
+                               FTC_Node* outNode,
+                               bool forceRender)
 {
   FT_Glyph glyph;
+  auto oldFlags = imageType_.flags;
+  if (forceRender)
+    imageType_.flags |= FT_LOAD_RENDER;
   if (FTC_ImageCache_Lookup(imageCache_,
                             &imageType_,
                             glyphIndex,
                             &glyph,
-                            NULL))
+                            outNode))
   {
     // XXX error handling?
     return NULL;
   }
 
+  imageType_.flags = oldFlags;
   return glyph;
 }
 
@@ -835,7 +895,7 @@ Engine::convertGlyphToQImage(FT_Glyph src, QRect* outRect)
   if (result && outRect)
   {
     outRect->setLeft(bitmapGlyph->left);
-    outRect->setTop(-bitmapGlyph->top);
+    outRect->setTop(bitmapGlyph->top);
     outRect->setWidth(bitmapGlyph->bitmap.width);
     outRect->setHeight(bitmapGlyph->bitmap.rows);
   }
@@ -860,7 +920,7 @@ Engine::computeGlyphOffset(FT_Glyph glyph)
     cbox.xMax = (cbox.xMax + 63) & ~63;
     cbox.yMax = (cbox.yMax + 63) & ~63;
     return { static_cast<int>(cbox.xMin) / 64,
-               static_cast<int>(-cbox.yMax / 64) };
+               static_cast<int>(cbox.yMax / 64) };
   }
   if (glyph->format == FT_GLYPH_FORMAT_BITMAP)
   {
diff --git a/src/ftinspect/engine/engine.hpp b/src/ftinspect/engine/engine.hpp
index 247e310..47001b7 100644
--- a/src/ftinspect/engine/engine.hpp
+++ b/src/ftinspect/engine/engine.hpp
@@ -77,9 +77,12 @@ public:
                long faceIndex,
                int namedInstanceIndex); // return number of glyphs
   FT_Glyph loadGlyph(int glyphIndex);
+  int loadGlyphIntoSlotWithoutCache(int glyphIndex);
 
   // Sometimes the engine is already updated, and we want to be faster
-  FT_Glyph loadGlyphWithoutUpdate(int glyphIndex);
+  FT_Glyph loadGlyphWithoutUpdate(int glyphIndex,
+                                  FTC_Node* outNode = NULL,
+                                  bool forceRender = false);
 
   // Return `true` if you need to free `out`
   // `out` will be set to NULL in cases of error
@@ -101,6 +104,9 @@ public:
   //////// Getters
 
   FT_Library ftLibrary() const { return library_; }
+  FTC_Manager cacheManager() { return cacheManager_; }
+  int dpi() { return dpi_; }
+  int pointSize() { return pointSize_; }
   int currentFontType() const { return fontType_; }
   const QString& currentFamilyName() { return curFamilyName_; }
   const QString& currentStyleName() { return curStyleName_; }
@@ -110,15 +116,21 @@ public:
   long numberOfFaces(int fontIndex);
   int numberOfNamedInstances(int fontIndex,
                              long faceIndex);
+  int currentFontFirstUnicodeCharMap();
   // Note: the current font face must be properly set
   unsigned glyphIndexFromCharCode(int code, int charMapIndex);
   FT_Size_Metrics const& currentFontMetrics();
-
+  FT_GlyphSlot currentFaceSlot();
+  FT_Pos currentFontTrackingKerning(int degree);
+  FT_Vector currentFontKerning(int glyphIndex, int prevIndex);
+  
   std::vector<CharMapInfo>& currentFontCharMaps() { return curCharMaps_; }
   FontFileManager& fontFileManager() { return fontFileManager_; }
   EngineDefaultValues& engineDefaults() { return engineDefaults_; }
   bool antiAliasingEnabled() { return antiAliasingEnabled_; }
+  bool doHinting() { return doHinting_; }
   bool embeddedBitmapEnabled() { return embeddedBitmap_; }
+  bool lcdUsingSubPixelPositioning() { return lcdSubPixelPositioning_; }
 
   //////// Setters (direct or indirect)
 
@@ -146,6 +158,7 @@ public:
   void setAntiAliasingEnabled(bool enabled) { antiAliasingEnabled_ = enabled; }
   void setEmbeddedBitmap(bool force) { embeddedBitmap_ = force; }
   void setLCDUsesBGR(bool isBGR) { lcdUsesBGR_ = isBGR; }
+  void setLCDSubPixelPositioning(bool sp) { lcdSubPixelPositioning_ = sp; }
 
   // Note: These 3 functions now takes actual mode/version from FreeType,
   // instead of values from enum in MainGUI!
@@ -201,6 +214,7 @@ private:
   bool embeddedBitmap_;
   int antiAliasingTarget_;
   bool lcdUsesBGR_;
+  bool lcdSubPixelPositioning_;
   int renderMode_;
 
   double gamma_;
diff --git a/src/ftinspect/engine/stringrenderer.cpp 
b/src/ftinspect/engine/stringrenderer.cpp
new file mode 100644
index 0000000..ecc5223
--- /dev/null
+++ b/src/ftinspect/engine/stringrenderer.cpp
@@ -0,0 +1,565 @@
+// stringrenderer.cpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#include "stringrenderer.hpp"
+
+#include "engine.hpp"
+
+#include <cmath>
+
+
+StringRenderer::StringRenderer(Engine* engine)
+: engine_(engine)
+{
+}
+
+
+StringRenderer::~StringRenderer()
+{
+  clearActive();
+}
+
+
+void
+StringRenderer::setCharMapIndex(int charMapIndex,
+                                int limitIndex)
+{
+  auto& charmaps = engine_->currentFontCharMaps();
+  if (charMapIndex < 0
+      || static_cast<uint>(charMapIndex) >= charmaps.size())
+    charMapIndex = -1;
+
+  charMapIndex_ = charMapIndex;
+  limitIndex_ = limitIndex;
+}
+
+
+void
+StringRenderer::setRotation(double rotation)
+{
+  rotation_ = rotation;
+
+  if (rotation <= -180)
+    rotation += 360;
+  if (rotation > 180)
+    rotation -= 360;
+
+  if (rotation == 0)
+  {
+    matrixEnabled_ = false;
+    return;
+  }
+
+  matrixEnabled_ = true;
+  double radian = rotation * 3.14159265 / 180.0;
+  auto cosinus = static_cast<FT_Fixed>(cos(radian) * 65536.0);
+  auto sinus = static_cast<FT_Fixed>(sin(radian) * 65536.0);
+
+  matrix_.xx = cosinus;
+  matrix_.yx = sinus;
+  matrix_.xy = -sinus;
+  matrix_.yy = cosinus;
+}
+
+
+void
+StringRenderer::setKerning(bool kerning)
+{
+  if (kerning)
+  {
+    kerningMode_ = KM_Normal;
+    kerningDegree_ = KD_Medium;
+  }
+  else
+  {
+    kerningMode_ = KM_None;
+    kerningDegree_ = KD_None;
+  }
+}
+
+
+void
+StringRenderer::reloadAll()
+{
+  clearActive(usingString_); // if "All Glyphs", then do a complete wipe
+  if (usingString_)
+    reloadGlyphIndices();
+}
+
+void
+StringRenderer::reloadGlyphs()
+{
+  clearActive(true);
+}
+
+
+void
+StringRenderer::setUseString(QString const& string)
+{
+  clearActive(); // clear existing
+  usingString_ = true;
+
+  long long totalCount = 0;
+  for (uint ch : string.toUcs4())
+  {
+    activeGlyphs_.emplace_back();
+    auto& it = activeGlyphs_.back();
+    it.charCode = static_cast<int>(ch);
+    it.glyphIndex = 0;
+    ++totalCount;
+    if (totalCount >= INT_MAX)
+      break;
+  }
+  reloadGlyphIndices();
+}
+
+
+void
+StringRenderer::setUseAllGlyphs()
+{
+  if (usingString_)
+    clearActive();
+  usingString_ = false;
+}
+
+
+void
+StringRenderer::reloadGlyphIndices()
+{
+  if (!usingString_)
+    return;
+  int charMapIndex = charMapIndex_;
+  auto& charmaps = engine_->currentFontCharMaps();
+  if (charMapIndex < 0
+      || static_cast<uint>(charMapIndex) >= charmaps.size()
+      || charmaps[charMapIndex].encoding != FT_ENCODING_UNICODE)
+    charMapIndex = engine_->currentFontFirstUnicodeCharMap();
+
+  if (charMapIndex < 0)
+    return;
+  for (auto& ctx : activeGlyphs_)
+  {
+    auto index = engine_->glyphIndexFromCharCode(ctx.charCode, charMapIndex);
+    ctx.glyphIndex = static_cast<int>(index);
+  }
+}
+
+
+void
+StringRenderer::prepareRendering()
+{
+  engine_->reloadFont();
+  if (kerningDegree_ != KD_None)
+    trackingKerning_ = engine_->currentFontTrackingKerning(kerningDegree_);
+  else
+    trackingKerning_ = 0;
+}
+
+
+void
+StringRenderer::loadSingleContext(GlyphContext* ctx,
+                                  GlyphContext* prev)
+{
+  if (ctx->cacheNode)
+  {
+    FTC_Node_Unref(ctx->cacheNode, engine_->cacheManager());
+    ctx->cacheNode = NULL;
+  }
+  else if (ctx->glyph)
+    FT_Done_Glyph(ctx->glyph); // when caching isn't used
+
+  // TODO use FTC?
+
+  // After `prepareRendering`, current size/face is properly set
+  FT_GlyphSlot slot = engine_->currentFaceSlot();
+  if (engine_->loadGlyphIntoSlotWithoutCache(ctx->glyphIndex) != 0)
+  {
+    ctx->glyph = NULL;
+    return;
+  }
+  if (FT_Get_Glyph(slot, &ctx->glyph) != 0)
+  {
+    ctx->glyph = NULL;
+    return;
+  }
+  auto& metrics = slot->metrics;
+  //ctx->glyph = engine_->loadGlyphWithoutUpdate(ctx->glyphIndex, 
+  //                                            &ctx->cacheNode);
+
+  if (!ctx->glyph)
+    return;
+
+  ctx->vvector.x = metrics.vertBearingX - metrics.horiBearingX;
+  ctx->vvector.y = -metrics.vertBearingY - metrics.horiBearingY;
+
+  ctx->vadvance.x = 0;
+  ctx->vadvance.y = -metrics.vertAdvance;
+
+  ctx->lsbDelta = slot->lsb_delta;
+  ctx->rsbDelta = slot->rsb_delta;
+
+  ctx->hadvance.x = metrics.horiAdvance;
+  ctx->hadvance.y = 0;
+
+  if (engine_->lcdUsingSubPixelPositioning())
+    ctx->hadvance.x += ctx->lsbDelta - ctx->rsbDelta;
+  prev->hadvance.x += trackingKerning_;
+
+  if (kerningMode_ != KM_None)
+  {
+    FT_Vector kern = engine_->currentFontKerning(ctx->glyphIndex, 
+                                        prev->glyphIndex);
+
+    prev->hadvance.x += kern.x;
+    prev->hadvance.y += kern.y;
+
+    if (!engine_->lcdUsingSubPixelPositioning() && kerningMode_ > KM_Normal)
+    {
+      if (prev->rsbDelta - ctx->lsbDelta > 32)
+        prev->hadvance.x -= 64;
+      else if (prev->rsbDelta - ctx->lsbDelta < -31)
+        prev->hadvance.x += 64;
+    }
+  }
+
+  if (!engine_->lcdUsingSubPixelPositioning() && engine_->doHinting())
+  {
+    prev->hadvance.x = (prev->hadvance.x + 32) & -64;
+    prev->hadvance.y = (prev->hadvance.y + 32) & -64;
+  }
+}
+
+
+void
+StringRenderer::loadStringGlyphs()
+{
+  if (!usingString_)
+    return;
+  GlyphContext* prev = &tempGlyphContext_; // = empty
+  tempGlyphContext_ = {};
+
+  for (auto& ctx : activeGlyphs_)
+  {
+    loadSingleContext(&ctx, prev);
+    prev = &ctx;
+  }
+
+  glyphCacheValid_ = true;
+}
+
+
+int
+StringRenderer::prepareLine(int offset,
+                            int lineWidth,
+                            FT_Vector& outActualLineWidth)
+{
+  int totalCount = 0;
+  outActualLineWidth = {0, 0};
+  if (!usingString_)
+  {
+    // The thing gets a little complicated when we're using "All Glyphs" mode
+    // The input sequence is actually infinite
+    // so we have to combine loading glyph into rendering, and can't preload
+    // all glyphs
+
+    // TODO: Low performance when the begin index is large.
+    // TODO: Optimize: use a sparse vector...!
+    // The problem is that when doing a `list::resize`, the ctor is called
+    // for unnecessarily many times.
+    GlyphContext* prev = &tempGlyphContext_;
+    tempGlyphContext_ = {};
+    for (unsigned n = offset; n < static_cast<unsigned>(limitIndex_);)
+    {
+      if (activeGlyphs_.capacity() <= n)
+        activeGlyphs_.reserve(static_cast<size_t>(n) * 2);
+      if (activeGlyphs_.size() <= n)
+        activeGlyphs_.resize(n + 1);
+
+      auto& ctx = activeGlyphs_[n];
+      ctx.charCode = static_cast<int>(n);
+      ctx.glyphIndex = static_cast<int>(
+          engine_->glyphIndexFromCharCode(static_cast<int>(n), charMapIndex_));
+
+      if (!ctx.glyph)
+        loadSingleContext(&ctx, prev);
+
+      if (outActualLineWidth.x + ctx.hadvance.x > lineWidth)
+        break;
+      outActualLineWidth.x += ctx.hadvance.x;
+      outActualLineWidth.y += ctx.hadvance.y;
+      ++n;
+      ++totalCount;
+      prev = &ctx;
+    }
+  }
+  else
+  {
+    if (!glyphCacheValid_)
+    {
+      clearActive(true);
+      loadStringGlyphs();
+    }
+
+    for (int n = offset; n < activeGlyphs_.size();)
+    {
+      auto& ctx = activeGlyphs_[n];
+      if (repeated_) // if repeated, we must stop when we touch the end of line
+      {
+        if (outActualLineWidth.x + ctx.hadvance.x > lineWidth)
+          break;
+        outActualLineWidth.x += ctx.hadvance.x;
+        outActualLineWidth.y += ctx.hadvance.y;
+        ++n;
+        n %= static_cast<int>(activeGlyphs_.size()); // safe
+      }
+      else if (vertical_)
+      {
+        outActualLineWidth.x += ctx.vadvance.x;
+        outActualLineWidth.y += ctx.vadvance.y;
+        ++n;
+      }
+      else
+      {
+        outActualLineWidth.x += ctx.hadvance.x;
+        outActualLineWidth.y += ctx.hadvance.y;
+        ++n;
+      }
+      ++totalCount;
+    }
+  }
+  return totalCount;
+}
+
+
+int
+StringRenderer::render(int width,
+                       int height,
+                       int offset)
+{
+  if (usingString_)
+    offset = 0;
+  if (limitIndex_ <= 0)
+    return 0;
+
+  // Separated into 3 modes:
+  // Waterfall, fill the whole canvas and only single string.
+
+  if (waterfall_)
+  {
+    // Waterfall
+
+    vertical_ = false;
+    auto originalSize = engine_->pointSize() * 64;
+    auto ptSize = originalSize;
+    auto ptHeight = 64 * 72 * height / engine_->dpi();
+    auto step = (ptSize * ptSize / ptHeight + 64) & ~63;
+    ptSize = ptSize - step * (ptSize / step); // modulo
+
+    int y = 0;
+    // no position param in "All Glyphs" mode
+    int x = static_cast<int>(usingString_ ? (width * position_) : 0);
+    int count = 0;
+
+    while (true)
+    {
+      ptSize += step;
+      engine_->setSizeByPoint(ptSize / 64);
+      clearActive(true);
+      prepareRendering(); // set size/face for engine, so metrics are valid
+      auto& metrics = engine_->currentFontMetrics();
+
+      if (ptSize == originalSize)
+      {
+        // TODO draw a blue line
+      }
+
+      engine_->setSizeByPoint(ptSize / 64);
+      y += (metrics.height >> 6) + 1;
+
+      if (y >= height)
+        break;
+
+      if (ptSize == originalSize)
+      {
+        // TODO draw a blue line
+      }
+
+      loadStringGlyphs();
+      auto lcount = renderLine(x, y + (metrics.descender >> 6),
+                                   width, height,
+                                   offset);
+      count = std::max(count, lcount);
+    }
+    engine_->setSizeByPoint(originalSize / 64);
+
+    return count;
+  }
+
+  if (repeated_ || !usingString_)
+  {
+    // Fill the whole canvas
+
+    prepareRendering();
+    auto& metrics = engine_->currentFontMetrics();
+    auto stepY = (metrics.height >> 6) + 1;
+    auto y = 4 + (metrics.ascender >> 6);
+    auto limitY = height + (metrics.descender >> 6);
+
+    for (; y < limitY; y += stepY)
+      offset = renderLine(0, y, width, height, offset);
+    return offset;
+  }
+
+  // Single string
+  prepareRendering();
+  auto& metrics = engine_->currentFontMetrics();
+  auto x = static_cast<int>(width * position_);
+  // Anchor at top-left in vertical mode, at the center in horizontal mode
+  auto y = vertical_ ? 0 : (height / 2);
+  y += 4 + (metrics.ascender >> 6);
+  return renderLine(x, y, width, height, offset);
+}
+
+
+int
+StringRenderer::renderLine(int x,
+                           int y,
+                           int width,
+                           int height,
+                           int offset)
+{
+  if (x < 0 || y < 0 || x > width || y > height)
+    return 0;
+  
+  y = height - y; // change to Cartesian coordinates
+
+  FT_Vector pen = { 0, 0 };
+  FT_Vector advance;
+
+  // When in "All Glyphs"  mode, no vertical support.
+  if (repeated_ || !usingString_)
+    vertical_ = false; // TODO: Support vertical + repeated
+
+  int lineLength = 64 * (vertical_ ? height : width);
+
+  // first prepare the line & determine the line length
+  int totalCount = prepareLine(offset, lineLength, pen);
+
+  // round to control initial pen position and preserve hinting...
+  // pen.x, y is the actual length now, and we multiple it by pos
+  auto centerFixed = static_cast<int>(0x10000 * position_);
+  if (!usingString_ || repeated_)
+    centerFixed = 0;
+  pen.x = FT_MulFix(pen.x, centerFixed) & ~63;
+  pen.y = FT_MulFix(pen.y, centerFixed) & ~63;
+
+  // ... unless rotating; XXX sbits
+  if (matrixEnabled_)
+    FT_Vector_Transform(&pen, &matrix_);
+
+  // get pen position: penPos = center - pos * width
+  pen.x = (x << 6) - pen.x;
+  pen.y = (y << 6) - pen.y;
+
+  for (int i = offset; i < totalCount + offset; i++)
+  {
+    auto& ctx = activeGlyphs_[i % activeGlyphs_.size()];
+    FT_Glyph image = NULL; // Remember to clean up
+    FT_BBox bbox;
+
+    if (!ctx.glyph)
+      continue;
+
+    // copy the glyph because we're doing manipulation
+    auto error = FT_Glyph_Copy(ctx.glyph, &image);
+    if (error)
+      continue;
+
+    glyphPreprocessCallback_(&image);
+
+    if (image->format != FT_GLYPH_FORMAT_BITMAP)
+    {
+      if (vertical_)
+        error = FT_Glyph_Transform(image, NULL, &ctx.vvector);
+
+      if (!error)
+      {
+        if (matrixEnabled_)
+          error = FT_Glyph_Transform(image, &matrix_, &pen);
+        else
+          error = FT_Glyph_Transform(image, NULL, &pen);
+      }
+
+      if (error)
+      {
+        FT_Done_Glyph(image);
+        continue;
+      }
+    }
+    else
+    {
+      auto bitmap = reinterpret_cast<FT_BitmapGlyph>(image);
+
+      if (vertical_)
+      {
+        bitmap->left += (ctx.vvector.x + pen.x) >> 6;
+        bitmap->top += (ctx.vvector.y + pen.y) >> 6;
+      }
+      else
+      {
+        bitmap->left += pen.x >> 6;
+        bitmap->top += pen.y >> 6;
+      }
+    }
+
+    advance = vertical_ ? ctx.vadvance : ctx.hadvance;
+
+    if (matrixEnabled_)
+      FT_Vector_Transform(&advance, &matrix_);
+
+    pen.x += advance.x;
+    pen.y += advance.y;
+
+    FT_Glyph_Get_CBox(image, FT_GLYPH_BBOX_PIXELS, &bbox);
+
+    // check bounding box; if it is completely outside the
+    // display surface, we don't need to render it
+    if (bbox.xMax > 0 
+        && bbox.yMax > 0
+        && bbox.xMin < width
+        && bbox.yMin < height)
+    {
+      renderCallback_(image);
+    }
+
+    FT_Done_Glyph(image);
+  }
+
+  // For repeating
+  if (usingString_ && activeGlyphs_.size())
+    return (offset + totalCount) % activeGlyphs_.size();
+  return offset + totalCount;
+}
+
+
+void
+StringRenderer::clearActive(bool glyphOnly)
+{
+  for (auto& ctx : activeGlyphs_)
+  {
+    if (ctx.cacheNode)
+      FTC_Node_Unref(ctx.cacheNode, engine_->cacheManager());
+    else if (ctx.glyph)
+      FT_Done_Glyph(ctx.glyph); // when caching isn't used
+    ctx.cacheNode = NULL;
+    ctx.glyph = NULL;
+  }
+  if (!glyphOnly)
+    activeGlyphs_.clear();
+
+  glyphCacheValid_ = false;
+}
+
+
+// end of stringrenderer.cpp
diff --git a/src/ftinspect/engine/stringrenderer.hpp 
b/src/ftinspect/engine/stringrenderer.hpp
new file mode 100644
index 0000000..9d61da1
--- /dev/null
+++ b/src/ftinspect/engine/stringrenderer.hpp
@@ -0,0 +1,165 @@
+// stringrenderer.hpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#pragma once
+
+#include <vector>
+#include <functional>
+
+#include <QString>
+
+#include <ft2build.h>
+#include <qslider.h>
+#include <freetype/freetype.h>
+#include <freetype/ftcache.h>
+#include <freetype/ftglyph.h>
+
+// adopted from `ftcommon.h`
+
+class Engine;
+struct GlyphContext
+{
+  int charCode = 0;
+  int glyphIndex = 0;
+  FT_Glyph glyph = NULL;
+  FTC_Node cacheNode = NULL;
+
+  FT_Pos lsbDelta = 0;    // delta caused by hinting
+  FT_Pos rsbDelta = 0;   // delta caused by hinting
+  FT_Vector hadvance = { 0, 0 }; // kerned horizontal advance
+
+  FT_Vector vvector = { 0, 0 };  // vert. origin => hori. origin
+  FT_Vector vadvance = { 0, 0 }; // vertical advance
+};
+
+// Class to populate chars to render, to load and properly position glyphs.
+// Use callbacks to receive characters.
+class StringRenderer
+{
+public:
+  StringRenderer(Engine* engine);
+  ~StringRenderer();
+
+  enum KerningDegree
+  {
+    KD_None = 0,
+    KD_Light,
+    KD_Medium,
+    KD_Tight
+  };
+
+  enum KerningMode
+  {
+    KM_None = 0,
+    KM_Normal,
+    KM_Smart
+  };
+
+  using RenderCallback = std::function<void(FT_Glyph)>;
+  /* The glyph pointer may be replaced. In that case, ownership is transfered
+   * to the renderer, and the new glyph will be eventually freed by
+   * the renderer. The callback is responsible to free the old glyph.
+   * This allows you to do the following:
+   * void callback(FT_Glyph* ptr) {
+   *     ....
+   *     auto oldPtr = *ptr;
+   *     *ptr = ....;
+   *     FT_Done_Glyph(olPtr);
+   * }
+   */
+  using PreprocessCallback = std::function<void(FT_Glyph*)>;
+
+  void setCharMapIndex(int charMapIndex, int limitIndex);
+  void setCallback(RenderCallback cb)
+  {
+    renderCallback_ = std::move(cb);
+  }
+  void setPreprocessCallback(PreprocessCallback cb)
+  {
+    glyphPreprocessCallback_ = std::move(cb);
+  }
+  void setRepeated(bool repeated) { repeated_ = repeated; }
+  void setVertical(bool vertical) { vertical_ = vertical; }
+  void setRotation(double rotation);
+  void setWaterfall(bool waterfall) { waterfall_ = waterfall; }
+  void setPosition(double pos) { position_ = pos; }
+  void setKerning(bool kerning);
+
+  // Need to be called when font or charMap changes
+  void setUseString(QString const& string);
+  void setUseAllGlyphs();
+  
+  int render(int width,
+             int height,
+             int offset);
+  int renderLine(int x, int y,
+                 int width, int height,
+                 int offset);
+
+  void reloadAll();      // text/font/charmap changes, will call
+                         // `reloadGlyphs`
+  void reloadGlyphs();   // any other parameter changes
+
+private:
+  Engine* engine_;
+
+  // Generally, rendering has those steps:
+  // 1. If in string mode, the string is load into `activeGlyphs_`
+  //    (in `updateString`)
+  // 2. The char codes in contexts are converted to glyph indices
+  //    (in `reloadGlyphIndices`)
+  // 3. If in string mode, glyphs are loaded into contexts.
+  //    (in `loadStringGlyphs`)
+  // 4. In `render` function, according to mode, `renderLine` is called line
+  //    by line (as well as `prepareRendering`).
+  // 5. In `renderLine`, if in all glyphs mode, glyphs from the begin index
+  //    are loaded until the line is full (if the glyph already exists, it will
+  //    be reused). If in string mode, it will directly use the prepared 
glyphs.
+  //    Preprocessing is done within this step, such as emboldening or 
stroking.
+  //    Eventually the `FT_Glyph` pointer is passed to the callback.
+  
+  GlyphContext tempGlyphContext_;
+  // This vector stores all active glyphs for rendering. When rendering 
strings,
+  // this is the container for chars, so DO NOT directly clear it to flush
+  // cache, you should clean glyph objects only. However when rendering all
+  // glyphs, it's generally to directly wipe the vector because it's 
dynamically
+  // generated in `render` function (see above).
+  //
+  // Note: Because of kerning, this list must be ordered and allow duplicate
+  //       characters.
+  //
+  // Actually this means 3 parts of storage: string charcode, glyph indices and
+  // glyph (+ all related info). Different parameter changes will trigger
+  // different levels of flushing.
+  std::vector<GlyphContext> activeGlyphs_;
+  bool glyphCacheValid_ = false;
+
+  int charMapIndex_ = 0;
+  int limitIndex_ = 0;
+  bool usingString_ = false;
+  bool waterfall_ = false;
+  bool repeated_ = false;
+  bool vertical_ = false;
+  double position_ = 0;
+  double rotation_ = 0;
+  int kerningDegree_ = KD_None;
+  KerningMode kerningMode_ = KM_None;
+  FT_Pos trackingKerning_ = 0;
+  FT_Matrix matrix_ = {};
+  bool matrixEnabled_ = false;
+
+  RenderCallback renderCallback_;
+  PreprocessCallback glyphPreprocessCallback_;
+
+  void reloadGlyphIndices();
+  void prepareRendering();
+  void loadSingleContext(GlyphContext* ctx, GlyphContext* prev);
+  // Need to be called when font, charMap or size changes;
+  void loadStringGlyphs();
+  // Returns total line count
+  int prepareLine(int offset, 
+                  int lineWidth,
+                  FT_Vector& outActualLineWidth);
+  void clearActive(bool glyphOnly = false);
+};
\ No newline at end of file
diff --git a/src/ftinspect/meson.build b/src/ftinspect/meson.build
index dc3a02f..2b46f62 100644
--- a/src/ftinspect/meson.build
+++ b/src/ftinspect/meson.build
@@ -23,6 +23,7 @@ if qt5_dep.found()
     'engine/engine.cpp',
     'engine/fontfilemanager.cpp',
     'engine/charmap.cpp',
+    'engine/stringrenderer.cpp',
 
     'rendering/glyphbitmap.cpp',
     'rendering/glyphoutline.cpp',
diff --git a/src/ftinspect/models/customcomboboxmodels.cpp 
b/src/ftinspect/models/customcomboboxmodels.cpp
index 5f96274..da82f00 100644
--- a/src/ftinspect/models/customcomboboxmodels.cpp
+++ b/src/ftinspect/models/customcomboboxmodels.cpp
@@ -241,6 +241,10 @@ 
AntiAliasingComboBoxModel::AntiAliasingComboBoxModel(QObject* parent)
     {FT_LOAD_TARGET_LIGHT, FT_RENDER_MODE_LIGHT, false},
     "Light"
   };
+  items_[AntiAliasing_Light_SubPixel] = {
+    {FT_LOAD_TARGET_LIGHT, FT_RENDER_MODE_LIGHT, false},
+    "Light (Sub Pixel)"
+  };
   items_[AntiAliasing_LCD] = {
     {FT_LOAD_TARGET_LCD, FT_RENDER_MODE_LCD, false},
     "LCD (RGB)"
diff --git a/src/ftinspect/models/customcomboboxmodels.hpp 
b/src/ftinspect/models/customcomboboxmodels.hpp
index a445f1a..a79289d 100644
--- a/src/ftinspect/models/customcomboboxmodels.hpp
+++ b/src/ftinspect/models/customcomboboxmodels.hpp
@@ -205,6 +205,7 @@ public:
     AntiAliasing_None,
     AntiAliasing_Normal,
     AntiAliasing_Light,
+    AntiAliasing_Light_SubPixel,
     AntiAliasing_LCD,
     AntiAliasing_LCD_BGR,
     AntiAliasing_LCD_Vertical,
diff --git a/src/ftinspect/panels/continuous.cpp 
b/src/ftinspect/panels/continuous.cpp
index c48edb3..eb82c68 100644
--- a/src/ftinspect/panels/continuous.cpp
+++ b/src/ftinspect/panels/continuous.cpp
@@ -41,6 +41,7 @@ ContinuousTab::reloadFont()
   currentGlyphCount_ = engine_->currentFontNumberOfGlyphs();
   setGlyphCount(qBound(0, currentGlyphCount_, INT_MAX));
   setCharMaps(engine_->currentFontCharMaps());
+  canvas_->stringRenderer().reloadAll();
   repaintGlyph();
 }
 
@@ -54,16 +55,22 @@ ContinuousTab::syncSettings()
   canvas_->setMode(mode);
   canvas_->setSource(src);
   canvas_->setBeginIndex(indexSelector_->currentIndex());
-  canvas_->setLimitIndex(glyphLimitIndex_);
-  canvas_->setCharMapIndex(charMapIndex()); // Not directly from the combo box
+  auto& sr = canvas_->stringRenderer();
+  sr.setWaterfall(waterfallCheckBox_->isChecked());
+  sr.setVertical(verticalCheckBox_->isChecked());
+  sr.setKerning(kerningCheckBox_->isChecked());
+  sr.setRotation(rotationSpinBox_->value());
+  sr.setPosition(positionSlider_->value() / 100.0);
+
+  // Not directly from the combo box
+  sr.setCharMapIndex(charMapIndex(), glyphLimitIndex_);
+
+  //sr.setCentered(centered_->isChecked());
 
   canvas_->setFancyParams(xEmboldeningSpinBox_->value(),
                           yEmboldeningSpinBox_->value(),
                           slantSpinBox_->value());
   canvas_->setStrokeRadius(strokeRadiusSpinBox_->value());
-  canvas_->setRotation(rotationSpinBox_->value());
-  canvas_->setWaterfall(waterfallCheckBox_->isChecked());
-  canvas_->setVertical(verticalCheckBox_->isChecked());
 }
 
 
@@ -185,6 +192,8 @@ ContinuousTab::checkSource()
   indexSelector_->setEnabled(src == GlyphContinuous::SRC_AllGlyphs);
   sourceTextEdit_->setEnabled(isText);
   verticalCheckBox_->setEnabled(isText);
+  positionSlider_->setEnabled(isText);
+  canvas_->setSource(src);
 
   repaintGlyph();
 }
@@ -206,8 +215,8 @@ ContinuousTab::charMapChanged()
   }
   updateLimitIndex();
 
+  canvas_->stringRenderer().reloadAll();
   repaintGlyph();
-
   lastCharMapIndex_ = newIndex;
 }
 
@@ -220,6 +229,14 @@ ContinuousTab::sourceTextChanged()
 }
 
 
+void
+ContinuousTab::reloadGlyphsAndRepaint()
+{
+  canvas_->stringRenderer().reloadGlyphs();
+  repaintGlyph();
+}
+
+
 void
 ContinuousTab::wheelNavigate(int steps)
 {
@@ -266,8 +283,15 @@ ContinuousTab::createLayout()
   sourceSelector_->insertItem(GlyphContinuous::SRC_TextStringRepeated,
                               tr("Text String (Repeated)"));
 
-  verticalCheckBox_ = new QCheckBox(tr("Vertical Layout"), this);
+  verticalCheckBox_ = new QCheckBox(tr("Vertical"), this);
   waterfallCheckBox_ = new QCheckBox(tr("Waterfall"), this);
+  kerningCheckBox_ = new QCheckBox(tr("Kerning"), this);
+
+  positionSlider_ = new QSlider(Qt::Horizontal, this);
+  positionSlider_->setMinimum(0);
+  positionSlider_->setMaximum(100);
+  positionSlider_->setTracking(true);
+  positionSlider_->setSingleStep(5);
 
   modeLabel_ = new QLabel(tr("Mode:"), this);
   sourceLabel_ = new QLabel(tr("Text Source:"), this);
@@ -277,6 +301,9 @@ ContinuousTab::createLayout()
   slantLabel_ = new QLabel(tr("Slanting:"), this);
   strokeRadiusLabel_ = new QLabel(tr("Stroke Radius:"), this);
   rotationLabel_ = new QLabel(tr("Rotation:"), this);
+  positionLabel_ = new QLabel(tr("Pos"), this);
+
+  positionLabel_->setAlignment(Qt::AlignCenter);
 
   xEmboldeningSpinBox_ = new QDoubleSpinBox(this);
   yEmboldeningSpinBox_ = new QDoubleSpinBox(this);
@@ -300,6 +327,10 @@ ContinuousTab::createLayout()
   rotationSpinBox_->setMinimum(-180);
   rotationSpinBox_->setMaximum(180);
 
+  positionLayout_ = new QVBoxLayout;
+  positionLayout_->addWidget(positionLabel_);
+  positionLayout_->addWidget(positionSlider_);
+
   bottomLayout_ = new QGridLayout;
   bottomLayout_->addWidget(sourceLabel_, 0, 0);
   bottomLayout_->addWidget(modeLabel_, 1, 0);
@@ -321,9 +352,11 @@ ContinuousTab::createLayout()
   bottomLayout_->addWidget(rotationSpinBox_, 0, 3);
 
   bottomLayout_->addWidget(indexSelector_, 0, 4, 1, 1);
-  bottomLayout_->addWidget(sourceTextEdit_, 1, 4, 3, 3);
-  bottomLayout_->addWidget(waterfallCheckBox_, 0, 5);
-  bottomLayout_->addWidget(verticalCheckBox_, 0, 6);
+  bottomLayout_->addWidget(sourceTextEdit_, 1, 4, 3, 1);
+  bottomLayout_->addLayout(positionLayout_, 0, 5);
+  bottomLayout_->addWidget(waterfallCheckBox_, 1, 5);
+  bottomLayout_->addWidget(verticalCheckBox_, 2, 5);
+  bottomLayout_->addWidget(kerningCheckBox_, 3, 5);
 
   bottomLayout_->setColumnStretch(4, 1);
 
@@ -340,7 +373,7 @@ void
 ContinuousTab::createConnections()
 {
   connect(sizeSelector_, &FontSizeSelector::valueChanged,
-          this, &ContinuousTab::repaintGlyph);
+          this, &ContinuousTab::reloadGlyphsAndRepaint);
 
   connect(canvas_, &GlyphContinuous::wheelResize, 
           this, &ContinuousTab::wheelResize);
@@ -378,8 +411,13 @@ ContinuousTab::createConnections()
           this, &ContinuousTab::repaintGlyph);
   connect(verticalCheckBox_, &QCheckBox::clicked,
           this, &ContinuousTab::repaintGlyph);
+  connect(kerningCheckBox_, &QCheckBox::clicked,
+          this, &ContinuousTab::reloadGlyphsAndRepaint);
   connect(sourceTextEdit_, &QPlainTextEdit::textChanged,
           this, &ContinuousTab::sourceTextChanged);
+
+  connect(positionSlider_, &QSlider::valueChanged,
+          this, &ContinuousTab::repaintGlyph);
 }
 
 
@@ -391,6 +429,9 @@ ContinuousTab::setDefaults()
   slantSpinBox_->setValue(0.22);
   strokeRadiusSpinBox_->setValue(0.02);
   rotationSpinBox_->setValue(0);
+
+  canvas_->setSourceText(sourceTextEdit_->toPlainText());
+  canvas_->setSource(GlyphContinuous::SRC_AllGlyphs);
 }
 
 
diff --git a/src/ftinspect/panels/continuous.hpp 
b/src/ftinspect/panels/continuous.hpp
index abd1c3f..b76141d 100644
--- a/src/ftinspect/panels/continuous.hpp
+++ b/src/ftinspect/panels/continuous.hpp
@@ -48,6 +48,7 @@ public:
   void checkSource();
   void charMapChanged();
   void sourceTextChanged();
+  void reloadGlyphsAndRepaint();
 
 private slots:
   void wheelNavigate(int steps);
@@ -75,6 +76,7 @@ private:
   QLabel* slantLabel_;
   QLabel* strokeRadiusLabel_;
   QLabel* rotationLabel_;
+  QLabel* positionLabel_;
 
   QDoubleSpinBox* xEmboldeningSpinBox_;
   QDoubleSpinBox* yEmboldeningSpinBox_;
@@ -84,15 +86,18 @@ private:
 
   QCheckBox* verticalCheckBox_;
   QCheckBox* waterfallCheckBox_;
+  QCheckBox* kerningCheckBox_;
+  QSlider* positionSlider_;
 
   GlyphIndexSelector* indexSelector_;
   QPlainTextEdit* sourceTextEdit_;
 
   std::vector<CharMapInfo> charMaps_;
 
+  QVBoxLayout* positionLayout_;
   QGridLayout* bottomLayout_;
   QVBoxLayout* mainLayout_;
-  
+
   void createLayout();
   void createConnections();
 
diff --git a/src/ftinspect/panels/settingpanel.cpp 
b/src/ftinspect/panels/settingpanel.cpp
index c78af33..7825e10 100644
--- a/src/ftinspect/panels/settingpanel.cpp
+++ b/src/ftinspect/panels/settingpanel.cpp
@@ -191,6 +191,9 @@ SettingPanel::syncSettings()
 
   engine_->setEmbeddedBitmap(embeddedBitmapCheckBox_->isChecked());
   engine_->setLCDUsesBGR(aaSettings.isBGR);
+  engine_->setLCDSubPixelPositioning(
+    antiAliasingComboBox_->currentIndex()
+    == AntiAliasingComboBoxModel::AntiAliasing_Light_SubPixel);
 }
 
 
@@ -226,7 +229,7 @@ SettingPanel::createConnections()
   connect(autoHintingCheckBox_, &QCheckBox::clicked,
           this, &SettingPanel::checkAutoHinting);
   connect(embeddedBitmapCheckBox_, &QCheckBox::clicked,
-          this, &SettingPanel::repaintNeeded);
+          this, &SettingPanel::fontReloadNeeded);
 }
 
 
diff --git a/src/ftinspect/rendering/glyphcontinuous.cpp 
b/src/ftinspect/rendering/glyphcontinuous.cpp
index cb2a196..bbee056 100644
--- a/src/ftinspect/rendering/glyphcontinuous.cpp
+++ b/src/ftinspect/rendering/glyphcontinuous.cpp
@@ -5,9 +5,7 @@
 #include "glyphcontinuous.hpp"
 
 #include "../engine/engine.hpp"
-#include "../rendering/renderutils.hpp"
 
-#include <cmath>
 #include <QPainter>
 #include <QWheelEvent>
 
@@ -15,7 +13,9 @@
 
 
 GlyphContinuous::GlyphContinuous(QWidget* parent, Engine* engine)
-: QWidget(parent), engine_(engine)
+: QWidget(parent),
+  engine_(engine),
+  stringRenderer_(engine)
 {
   setAcceptDrops(false);
   setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
@@ -26,11 +26,36 @@ GlyphContinuous::GlyphContinuous(QWidget* parent, Engine* 
engine)
 
 GlyphContinuous::~GlyphContinuous()
 {
-  cleanCloned();
   FT_Stroker_Done(stroker_);
 }
 
 
+void
+GlyphContinuous::setSource(Source source)
+{
+  source_ = source;
+  switch (source)
+  {
+  case SRC_AllGlyphs:
+    stringRenderer_.setUseAllGlyphs();
+    break;
+
+  case SRC_TextStringRepeated:
+  case SRC_TextString:
+    updateRendererText();
+    break;
+  }
+}
+
+
+void
+GlyphContinuous::setSourceText(QString text)
+{
+  text_ = std::move(text);
+  updateRendererText();
+}
+
+
 void
 GlyphContinuous::paintEvent(QPaintEvent* event)
 {
@@ -38,27 +63,10 @@ GlyphContinuous::paintEvent(QPaintEvent* event)
   painter.begin(this);
   painter.fillRect(rect(), Qt::white);
 
-  if (limitIndex_ > 0)
-  {
-    prePaint();
+  prePaint();
 
-    switch (source_)
-    {
-    case SRC_AllGlyphs:
-      switch (mode_)
-      {
-      case M_Normal:
-      case M_Fancy:
-      case M_Stroked:
-        paintAG(&painter);
-        break;
-      }
-      break;
-    case SRC_TextString:
-      break;
-    }
-    emit displayingCountUpdated(displayingCount_);
-  }
+  paintByRenderer(&painter);
+  emit displayingCountUpdated(displayingCount_);
 
   painter.end();
 }
@@ -76,7 +84,7 @@ GlyphContinuous::wheelEvent(QWheelEvent* event)
 
 
 void
-GlyphContinuous::paintAG(QPainter* painter)
+GlyphContinuous::paintByRenderer(QPainter* painter)
 {
   if (mode_ == M_Stroked)
   {
@@ -87,92 +95,49 @@ GlyphContinuous::paintAG(QPainter* painter)
                    0);
   }
 
-  for (int i = beginIndex_; i < limitIndex_; i++)
-  {
-    unsigned index = i;
-    if (charMapIndex_ >= 0)
-      index = engine_->glyphIndexFromCharCode(i, charMapIndex_);
-
-    if (!loadGlyph(index))
-      break;
-
-    // All Glyphs need no tranformation, and Waterfall isn't handled here.
-    switch (mode_)
+  stringRenderer_.setRepeated(source_ == SRC_TextStringRepeated);
+  stringRenderer_.setCallback(
+    [&](FT_Glyph glyph)
     {
-    case M_Fancy:
-      transformGlyphFancy();
-      break;
-    case M_Stroked:
-      transformGlyphStroked();
-      break;
-    default:;
-    }
-
-    if (!paintChar(painter))
-      break;
-    cleanCloned();
-
-    displayingCount_++;
-  }
-  cleanCloned();
+      drawSingleGlyph(painter, glyph);
+    });
+  stringRenderer_.setPreprocessCallback(
+    [&](FT_Glyph* ptr)
+    {
+      preprocessGlyph(ptr);
+    });
+  displayingCount_ = stringRenderer_.render(width(), height(), beginIndex_);
 }
 
 
 void
-GlyphContinuous::transformGlyphFancy()
+GlyphContinuous::transformGlyphFancy(FT_Glyph glyph)
 {
   // adopted from ftview.c:289
-  /***************************************************************/
-  /*                                                             */
-  /*  2*2 affine transformation matrix, 16.16 fixed float format */
-  /*                                                             */
-  /*  Shear matrix:                                              */
-  /*                                                             */
-  /*         | x' |     | 1  k |   | x |          x' = x + ky    */
-  /*         |    |  =  |      | * |   |   <==>                  */
-  /*         | y' |     | 0  1 |   | y |          y' = y         */
-  /*                                                             */
-  /*        outline'     shear    outline                        */
-  /*                                                             */
-  /***************************************************************/
-
-  FT_Matrix shear;
-  FT_Pos xstr, ystr;
-
-  shear.xx = 1 << 16;
-  shear.xy = static_cast<FT_Fixed>(slant_ * (1 << 16));
-  shear.yx = 0;
-  shear.yy = 1 << 16;
-
-  xstr = (FT_Pos)(metrics_.y_ppem * 64 * boldX_);
-  ystr = (FT_Pos)(metrics_.y_ppem * 64 * boldY_);
-
-  if (glyph_->format == FT_GLYPH_FORMAT_OUTLINE)
+  if (glyph->format == FT_GLYPH_FORMAT_OUTLINE)
   {
-    if (!isGlyphCloned_)
-      cloneGlyph();
-    FT_Outline_Transform(&outline_, &shear);
-    if (FT_Outline_EmboldenXY(&outline_, xstr, ystr))
+    auto outline = reinterpret_cast<FT_OutlineGlyph>(glyph)->outline;
+    FT_Glyph_Transform(glyph, &shearMatrix_, NULL);
+    if (FT_Outline_EmboldenXY(&outline, emboldeningX_, emboldeningY_))
     {
       // XXX error handling?
       return;
     }
 
-    if (glyph_->advance.x)
-      glyph_->advance.x += xstr;
+    if (glyph->advance.x)
+      glyph->advance.x += emboldeningX_;
 
-    if (glyph_->advance.y)
-      glyph_->advance.y += ystr;
+    if (glyph->advance.y)
+      glyph->advance.y += emboldeningY_;
   }
-  else if (glyph_->format == FT_GLYPH_FORMAT_BITMAP)
+  else if (glyph->format == FT_GLYPH_FORMAT_BITMAP)
   {
-    if (!isBitmapCloned_)
-      cloneBitmap();
-    xstr &= ~63;
-    ystr &= ~63;
+    auto xstr = emboldeningX_ & ~63;
+    auto ystr = emboldeningY_ & ~63;
 
+    auto bitmap = &reinterpret_cast<FT_BitmapGlyph>(glyph)->bitmap;
     // No shearing support for bitmap
-    FT_Bitmap_Embolden(engine_->ftLibrary(), &bitmap_, 
+    FT_Bitmap_Embolden(engine_->ftLibrary(), bitmap, 
                        xstr, ystr);
   }
   else
@@ -180,21 +145,16 @@ GlyphContinuous::transformGlyphFancy()
 }
 
 
-void
-GlyphContinuous::transformGlyphStroked()
+FT_Glyph
+GlyphContinuous::transformGlyphStroked(FT_Glyph glyph)
 {
   // Well, here only outline glyph is supported.
-  if (glyph_->format != FT_GLYPH_FORMAT_OUTLINE)
-    return;
-  auto oldGlyph = glyph_;
-  auto error = FT_Glyph_Stroke(&glyph_, stroker_, 0);
-  if (!error)
-  {
-    if (isGlyphCloned_)
-      FT_Done_Glyph(oldGlyph);
-    isGlyphCloned_ = true;
-    outline_ = reinterpret_cast<FT_OutlineGlyph>(glyph_)->outline;
-  }
+  if (glyph->format != FT_GLYPH_FORMAT_OUTLINE)
+    return NULL;
+  auto error = FT_Glyph_Stroke(&glyph, stroker_, 0);
+  if (error)
+    return NULL;
+  return glyph;
 }
 
 
@@ -203,162 +163,91 @@ GlyphContinuous::prePaint()
 {
   displayingCount_ = 0;
   engine_->reloadFont();
-  metrics_ = engine_->currentFontMetrics();
+  if (engine_->currentFontNumberOfGlyphs() > 0)
+    metrics_ = engine_->currentFontMetrics();
   x_ = 0;
   // See ftview.c:42
   y_ = ((metrics_.ascender - metrics_.descender + 63) >> 6) + 4;
   stepY_ = ((metrics_.height + 63) >> 6) + 4;
-}
-
-
-bool
-GlyphContinuous::paintChar(QPainter* painter)
-{
-  // ftview.c:557
-  int width = glyph_->advance.x ? glyph_->advance.x >> 16
-                                : metrics_.y_ppem / 2;
-
-  if (!checkFitX(x_ + width))
-  {
-    x_ = 0;
-    y_ += stepY_;
-
-    if (!checkFitY(y_))
-      return false;
-  }
-
-  x_++; // extra space
-  if (glyph_->advance.x == 0)
-  {
-    // Draw a red square to indicate
-      painter->fillRect(x_, y_ - width, width, width,
-                        Qt::red);
-    x_ += width;
-  }
-
-  // The real drawing part
-  // XXX: this is different from what's being done in
-  // `ftcommon.c`:FTDemo_Draw_Slot: is this correct??
 
-  QImage* image;
+  // Used by fancy:
+  // adopted from ftview.c:289
+  /***************************************************************/
+  /*                                                             */
+  /*  2*2 affine transformation matrix, 16.16 fixed float format */
+  /*                                                             */
+  /*  Shear matrix:                                              */
+  /*                                                             */
+  /*         | x' |     | 1  k |   | x |          x' = x + ky    */
+  /*         |    |  =  |      | * |   |   <==>                  */
+  /*         | y' |     | 0  1 |   | y |          y' = y         */
+  /*                                                             */
+  /*        outline'     shear    outline                        */
+  /*                                                             */
+  /***************************************************************/
   
-  if (bitmap_.buffer) // Always prefer `bitmap_` since it can be manipulated
-    image = engine_->convertBitmapToQImage(&bitmap_);
-  else
-    image = engine_->convertGlyphToQImage(glyph_, NULL);
-  auto offset = engine_->computeGlyphOffset(glyph_);
 
-  painter->drawImage(offset + QPoint(x_, y_),
-                     *image);
-  delete image;
+  shearMatrix_.xx = 1 << 16;
+  shearMatrix_.xy = static_cast<FT_Fixed>(slant_ * (1 << 16));
+  shearMatrix_.yx = 0;
+  shearMatrix_.yy = 1 << 16;
 
-  x_ += width;
-  return true;
-}
-
-
-bool
-GlyphContinuous::loadGlyph(int index)
-{
-  if (isGlyphCloned_)
-    FT_Done_Glyph(glyph_);
-  glyph_ = engine_->loadGlyphWithoutUpdate(index);
-  isGlyphCloned_ = false;
-  if (!glyph_)
-    return false;
-  refreshOutlineOrBitmapFromGlyph();
-  return true;
+  emboldeningX_ = (FT_Pos)(metrics_.y_ppem * 64 * boldX_);
+  emboldeningY_ = (FT_Pos)(metrics_.y_ppem * 64 * boldY_);
 }
 
 
 void
-GlyphContinuous::cloneGlyph()
+GlyphContinuous::updateRendererText()
 {
-  if (isGlyphCloned_)
-    return;
-  glyph_ = ::cloneGlyph(glyph_);
-  refreshOutlineOrBitmapFromGlyph();
-  isGlyphCloned_ = true;
+  stringRenderer_.setUseString(text_); // TODO this need to be called when 
font,
+                                       // size or charmap change
 }
 
 
 void
-GlyphContinuous::cloneBitmap()
+GlyphContinuous::preprocessGlyph(FT_Glyph* glyphPtr)
 {
-  if (isBitmapCloned_)
-    return;
-  bitmap_ = ::cloneBitmap(engine_->ftLibrary(), &bitmap_);
-  isBitmapCloned_ = true;
-}
-
-
-void
-GlyphContinuous::refreshOutlineOrBitmapFromGlyph()
-{
-  if (glyph_->format == FT_GLYPH_FORMAT_OUTLINE)
+  auto glyph = *glyphPtr;
+  switch (mode_)
   {
-    outline_ = reinterpret_cast<FT_OutlineGlyph>(glyph_)->outline;
-
-    // Make `bitmap_` invalid
-    if (isBitmapCloned_)
-      FT_Bitmap_Done(engine_->ftLibrary(), &bitmap_);
-    isBitmapCloned_ = false;
-    bitmap_.buffer = NULL;
-  }
-  else if (glyph_->format == FT_GLYPH_FORMAT_BITMAP)
+  case M_Fancy:
+    transformGlyphFancy(glyph);
+    break;
+  case M_Stroked:
   {
-    // Initialize `bitmap_`
-    if (isBitmapCloned_)
-      FT_Bitmap_Done(engine_->ftLibrary(), &bitmap_);
-    isBitmapCloned_ = false;
-    bitmap_ = reinterpret_cast<FT_BitmapGlyph>(glyph_)->bitmap;
-
-    outline_.points = NULL;
+    auto stroked = transformGlyphStroked(glyph);
+    if (stroked)
+    {
+      FT_Done_Glyph(glyph);
+      *glyphPtr = stroked;
+    }
   }
-  else
-  {
-    // Both invalid.
-    outline_.points = NULL;
-
-    if (isBitmapCloned_)
-      FT_Bitmap_Done(engine_->ftLibrary(), &bitmap_);
-    isBitmapCloned_ = false;
-    bitmap_.buffer = NULL;
+  break;
   }
 }
 
 
 void
-GlyphContinuous::cleanCloned()
+GlyphContinuous::drawSingleGlyph(QPainter* painter, FT_Glyph glyph)
 {
-  if (isGlyphCloned_)
-  {
-    if (glyph_->format == FT_GLYPH_FORMAT_BITMAP && !isBitmapCloned_)
-      bitmap_.buffer = NULL;
-
-    FT_Done_Glyph(glyph_);
-    isGlyphCloned_ = false;
-  }
-  if (isBitmapCloned_)
+  // ftview.c:557
+  int width = glyph->advance.x ? glyph->advance.x >> 16
+                                : metrics_.y_ppem / 2;
+  
+  if (glyph->advance.x == 0)
   {
-    FT_Bitmap_Done(engine_->ftLibrary(), &bitmap_);
-    bitmap_.buffer = NULL;
-    isBitmapCloned_ = false;
+    // Draw a red square to indicate
+      painter->fillRect(x_, y_ - width, width, width,
+                        Qt::red);
   }
-}
 
+  QRect rect;
+  QImage* image = engine_->convertGlyphToQImage(glyph, &rect);
+  rect.setTop(height() - rect.top());
 
-bool
-GlyphContinuous::checkFitX(int x)
-{
-  return x < width() - 3;
-}
-
-
-bool
-GlyphContinuous::checkFitY(int y)
-{
-  return y < height() - 3;
+  painter->drawImage(rect.topLeft(), *image);
+  delete image;
 }
 
 
diff --git a/src/ftinspect/rendering/glyphcontinuous.hpp 
b/src/ftinspect/rendering/glyphcontinuous.hpp
index 773d96e..284a6dd 100644
--- a/src/ftinspect/rendering/glyphcontinuous.hpp
+++ b/src/ftinspect/rendering/glyphcontinuous.hpp
@@ -5,6 +5,7 @@
 #pragma once
 
 #include "graphicsdefault.hpp"
+#include "../engine/stringrenderer.hpp"
 
 #include <utility>
 
@@ -40,12 +41,11 @@ public:
   };
 
   int displayingCount() { return displayingCount_; }
+  StringRenderer& stringRenderer() { return stringRenderer_; }
 
   // all those setters don't trigger repaint.
   void setBeginIndex(int index) { beginIndex_ = index; }
-  void setLimitIndex(int index) { limitIndex_ = index; }
-  void setCharMapIndex(int index) { charMapIndex_ = index; }
-  void setSource(Source mode) { source_ = mode; }
+  void setSource(Source source);
   void setMode(Mode mode) { mode_ = mode; }
   void setFancyParams(double boldX, double boldY, double slant)
   {
@@ -54,10 +54,7 @@ public:
     slant_ = slant;
   }
   void setStrokeRadius(double radius) { strokeRadius_ = radius; }
-  void setRotation(double rotation) { rotation_ = rotation; }
-  void setVertical(bool vertical) { vertical_ = vertical; }
-  void setWaterfall(bool waterfall) { waterfall_ = waterfall; }
-  void setSourceText(QString text) { text_ = std::move(text); }
+  void setSourceText(QString text);
 
 signals:
   void wheelNavigate(int steps);
@@ -70,56 +67,37 @@ protected:
 
 private:
   Engine* engine_;
+  StringRenderer stringRenderer_;
 
   Source source_ = SRC_AllGlyphs;
   Mode mode_ = M_Normal;
   int beginIndex_;
-  int limitIndex_;
-  int charMapIndex_;
   double boldX_, boldY_, slant_;
   double strokeRadius_;
-  double rotation_;
-  bool vertical_;
-  bool waterfall_;
   QString text_;
 
   int displayingCount_ = 0;
   FT_Size_Metrics metrics_;
   int x_ = 0, y_ = 0;
   int stepY_ = 0;
-
-  // Pay especially attention to life cycles & ownerships of those objects:
-  // Note that outline and bitmap can be either invalid, owned by glyph or
-  // owned by `this`.
-  // If owned by `this`, then it's safe to do manipulation, and need to cleanup
-  // If owned by glyph, then must clone to do manipulation, and no cleanup
-  // In `loadGraph`, these 3 values will all be updated.
-  // Note that `glyph_` is a pointer, while `outline_` and `bitmap_` are 
structs
-  FT_Glyph glyph_;
-  FT_Outline outline_; // Using outline_->points == NULL to determine validity
-  FT_Bitmap bitmap_; // Using bitmap_->buffer == NULL to determine validity
-  // when glyph is cloned, outline is factually also cloned
-  // never manually clone your outline or you can't easily render it!
-  bool isGlyphCloned_ = false;
-  bool isBitmapCloned_ = false;
+  FT_Pos emboldeningX_, emboldeningY_;
+  FT_Matrix shearMatrix_;
 
   FT_Stroker stroker_;
 
-  void paintAG(QPainter* painter);
-  void transformGlyphFancy();
-  void transformGlyphStroked();
-  void prePaint();
-  // return if there's enough space to paint the current char
-  bool paintChar(QPainter* painter);
-  bool loadGlyph(int index);
+  void paintByRenderer(QPainter* painter);
 
-  void cloneGlyph();
-  void cloneBitmap();
-  void refreshOutlineOrBitmapFromGlyph();
-  void cleanCloned();
+  // These two are used indendpent of current glyph variables
+  // and assumes ownership of glyphs, but don't free them.
+  // However, remember to free the glyph returned from `transformGlyphStroked`
+  void transformGlyphFancy(FT_Glyph glyph);
+  FT_Glyph transformGlyphStroked(FT_Glyph glyph);
 
-  bool checkFitX(int x);
-  bool checkFitY(int y);
+  void prePaint();
+  void updateRendererText();
+  void preprocessGlyph(FT_Glyph* glyphPtr);
+  void drawSingleGlyph(QPainter* painter,
+                       FT_Glyph glyph);
 };
 
 



reply via email to

[Prev in Thread] Current Thread [Next in Thread]