freetype-commit
[Top][All Lists]
Advanced

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

[freetype2-demos] master bf88f4d 26/41: [ftinspect] Add "Font Info" tab.


From: Werner Lemberg
Subject: [freetype2-demos] master bf88f4d 26/41: [ftinspect] Add "Font Info" tab.
Date: Mon, 3 Oct 2022 11:27:03 -0400 (EDT)

branch: master
commit bf88f4d1381c8d20d0cae43ac08449eb0df0ad02
Author: Charlie Jiang <w@chariri.moe>
Commit: Werner Lemberg <wl@gnu.org>

    [ftinspect] Add "Font Info" tab.
    
    * src/ftinspect/panels/info.cpp, src/ftinspect/panels/info.hpp:
      New files, add the `InfoTab` class.
    
    * src/ftinspect/engine/fontinfo.cpp, src/ftinspect/engine/fontinfo.hpp:
      Add `SFNTTableInfo`, `FontBasicInfo`, `FontTypeEntries`, `FontFixedSize`
      and `CompositeGlyphInfo`. The `SFNTTableInfo` and `CompositeGlyphInfo`
      classes retrieve info without the related FreeType API, but directly
      parse the font data.
    
    * src/ftinspect/models/fontinfomodels.cpp,
      src/ftinspect/models/fontinfomodels.hpp:
      New files. Add models for the tables and the tree view in the info tab to
      use.
    
    * src/ftinspect/engine/engine.cpp, src/ftinspect/engine/engine.hpp:
      Add `loadDefaults` function for the composite glyphs view to draw its
      small icon.
      Add `currentFontHasGlyphName`, `currentFontPSInfo`,
      `currentFontPSPrivateInfo` and `currentFontSFNTTableInfo` to obtain info.
      Add getters.
    
    * src/ftinspect/engine/fontinfonamesmapping.cpp: New file for name mapping.
    
    * src/ftinspect/engine/fontfilemanager.cpp,
      src/ftinspect/engine/fontfilemanager.hpp:
      Add `currentReloadDueToPeriodicUpdate` so the composite glyph tree isn't
      refreshed (which is a very expensive process) for the periodic updating of
      symbolic font files.
    
    * src/ftinspect/maingui.cpp, src/ftinspect/maingui.hpp:
      Add the font info tab info the main window and wire events.
    
    * src/ftinspect/CMakeLists.txt, src/ftinspect/meson.build: Updated.
---
 src/ftinspect/CMakeLists.txt                  |    3 +
 src/ftinspect/engine/engine.cpp               |   77 ++
 src/ftinspect/engine/engine.hpp               |   10 +
 src/ftinspect/engine/fontfilemanager.cpp      |    2 +
 src/ftinspect/engine/fontfilemanager.hpp      |    7 +
 src/ftinspect/engine/fontinfo.cpp             |  530 +++++++++++++
 src/ftinspect/engine/fontinfo.hpp             |  276 ++++++-
 src/ftinspect/engine/fontinfonamesmapping.cpp |  567 ++++++++++++++
 src/ftinspect/engine/rendering.cpp            |   18 +
 src/ftinspect/engine/rendering.hpp            |    2 +
 src/ftinspect/maingui.cpp                     |    8 +
 src/ftinspect/maingui.hpp                     |    2 +
 src/ftinspect/meson.build                     |    5 +
 src/ftinspect/models/fontinfomodels.cpp       |  738 ++++++++++++++++++
 src/ftinspect/models/fontinfomodels.hpp       |  294 +++++++
 src/ftinspect/panels/info.cpp                 | 1039 +++++++++++++++++++++++++
 src/ftinspect/panels/info.hpp                 |  327 ++++++++
 src/ftinspect/panels/settingpanel.cpp         |   10 +-
 src/ftinspect/panels/settingpanelmmgx.cpp     |    1 +
 src/ftinspect/widgets/charmapcombobox.cpp     |   21 +-
 20 files changed, 3921 insertions(+), 16 deletions(-)

diff --git a/src/ftinspect/CMakeLists.txt b/src/ftinspect/CMakeLists.txt
index 7f271a7..6a7a025 100644
--- a/src/ftinspect/CMakeLists.txt
+++ b/src/ftinspect/CMakeLists.txt
@@ -26,6 +26,7 @@ add_executable(ftinspect
   "engine/paletteinfo.cpp"
   "engine/mmgx.cpp"
   "engine/fontinfo.cpp"
+  "engine/fontinfonamesmapping.cpp"
   "engine/stringrenderer.cpp"
   "engine/charmap.cpp"
 
@@ -44,12 +45,14 @@ add_executable(ftinspect
   "widgets/charmapcombobox.cpp"
 
   "models/customcomboboxmodels.cpp"
+  "models/fontinfomodels.cpp"
 
   "panels/settingpanel.cpp"
   "panels/settingpanelmmgx.cpp"
   "panels/singular.cpp"
   "panels/continuous.cpp"
   "panels/comparator.cpp"
+  "panels/info.cpp"
   "panels/glyphdetails.cpp"
 )
 target_link_libraries(ftinspect
diff --git a/src/ftinspect/engine/engine.cpp b/src/ftinspect/engine/engine.cpp
index 87ee808..8ebced7 100644
--- a/src/ftinspect/engine/engine.cpp
+++ b/src/ftinspect/engine/engine.cpp
@@ -463,6 +463,15 @@ Engine::currentFontHasColorLayers()
 }
 
 
+bool
+Engine::currentFontHasGlyphName()
+{
+  if (!ftFallbackFace_)
+    return false;
+  return FT_HAS_GLYPH_NAMES(ftFallbackFace_);
+}
+
+
 std::vector<int>
 Engine::currentFontFixedSizes()
 {
@@ -477,6 +486,41 @@ Engine::currentFontFixedSizes()
 }
 
 
+bool
+Engine::currentFontPSInfo(PS_FontInfoRec& outInfo)
+{
+  if (!ftSize_)
+    return false;
+  if (FT_Get_PS_Font_Info(ftSize_->face, &outInfo) == FT_Err_Ok)
+    return true;
+  return false;
+}
+
+
+bool
+Engine::currentFontPSPrivateInfo(PS_PrivateRec& outInfo)
+{
+  if (!ftSize_)
+    return false;
+  if (FT_Get_PS_Font_Private(ftSize_->face, &outInfo) == FT_Err_Ok)
+    return true;
+  return false;
+}
+
+
+std::vector<SFNTTableInfo>&
+Engine::currentFontSFNTTableInfo()
+{
+  if (!curSFNTTablesValid_)
+  {
+    SFNTTableInfo::getForAll(this, curSFNTTables_);
+    curSFNTTablesValid_ = true;
+  }
+
+  return curSFNTTables_;
+}
+
+
 int
 Engine::currentFontFirstUnicodeCharMap()
 {
@@ -816,6 +860,39 @@ Engine::resetCache()
 }
 
 
+void
+Engine::loadDefaults()
+{
+  if (fontType_ == FontType_CFF)
+    setCFFHintingMode(engineDefaults_.cffHintingEngineDefault);
+  else if (fontType_ == FontType_TrueType)
+  {
+    if (currentFontTricky())
+      setTTInterpreterVersion(TT_INTERPRETER_VERSION_35);
+    else
+      setTTInterpreterVersion(engineDefaults_.ttInterpreterVersionDefault);
+  }
+  setStemDarkening(false);
+  applyMMGXDesignCoords(NULL, 0);
+
+  setAntiAliasingEnabled(true);
+  setAntiAliasingTarget(FT_LOAD_TARGET_NORMAL);
+  setHinting(true);
+  setAutoHinting(false);
+  setEmbeddedBitmapEnabled(true);
+  setPaletteIndex(0);
+  setUseColorLayer(true);
+
+  renderingEngine()->setBackground(qRgba(255, 255, 255, 255));
+  renderingEngine()->setForeground(qRgba(0, 0, 0, 255));
+  renderingEngine()->setGamma(1.8);
+
+  resetCache();
+  reloadFont();
+  loadPalette();
+}
+
+
 void
 Engine::queryEngine()
 {
diff --git a/src/ftinspect/engine/engine.hpp b/src/ftinspect/engine/engine.hpp
index d9ea177..e71564e 100644
--- a/src/ftinspect/engine/engine.hpp
+++ b/src/ftinspect/engine/engine.hpp
@@ -95,6 +95,7 @@ public:
   
   void update();
   void resetCache();
+  void loadDefaults();
 
   //////// Getters
 
@@ -108,6 +109,7 @@ public:
   int numberOfOpenedFonts();
 
   // (for current fonts)
+  int currentFontIndex() { return curFontIndex_; }
   FT_Face currentFallbackFtFace() { return ftFallbackFace_; }
   FT_Size currentFtSize() { return ftSize_; }
   FT_Size_Metrics const& currentFontMetrics();
@@ -140,7 +142,12 @@ public:
   bool currentFontBitmapOnly();
   bool currentFontHasEmbeddedBitmap();
   bool currentFontHasColorLayers();
+  bool currentFontHasGlyphName();
+  
   std::vector<int> currentFontFixedSizes();
+  bool currentFontPSInfo(PS_FontInfoRec& outInfo);
+  bool currentFontPSPrivateInfo(PS_PrivateRec& outInfo);
+  std::vector<SFNTTableInfo>& currentFontSFNTTableInfo();
 
   int currentFontFirstUnicodeCharMap();
   // Note: the current font face must be properly set
@@ -226,6 +233,9 @@ private:
   int curNumGlyphs_ = -1;
   std::vector<CharMapInfo> curCharMaps_;
   std::vector<PaletteInfo> curPaletteInfos_;
+
+  bool curSFNTTablesValid_ = false;
+  std::vector<SFNTTableInfo> curSFNTTables_;
   MMGXState curMMGXState_ = MMGXState::NoMMGX;
   std::vector<MMGXAxisInfo> curMMGXAxes_;
   std::vector<SFNTName> curSFNTNames_;
diff --git a/src/ftinspect/engine/fontfilemanager.cpp 
b/src/ftinspect/engine/fontfilemanager.cpp
index 9b44fce..f058dc2 100644
--- a/src/ftinspect/engine/fontfilemanager.cpp
+++ b/src/ftinspect/engine/fontfilemanager.cpp
@@ -183,7 +183,9 @@ FontFileManager::validateFontFile(QString const& fileName)
 void
 FontFileManager::onTimerFire()
 {
+  periodicUpdating_ = true;
   onWatcherFire();
+  periodicUpdating_ = false;
 }
 
 
diff --git a/src/ftinspect/engine/fontfilemanager.hpp 
b/src/ftinspect/engine/fontfilemanager.hpp
index 06a8f52..0874fd0 100644
--- a/src/ftinspect/engine/fontfilemanager.hpp
+++ b/src/ftinspect/engine/fontfilemanager.hpp
@@ -34,6 +34,11 @@ public:
   void timerStart();
   void loadFromCommandLine();
 
+  // If this is true, then the current font reloading is due to a periodic
+  // reloading for symbolic font files. Use this if you want to omit some
+  // updating for periodic reloading.
+  bool currentReloadDueToPeriodicUpdate() { return periodicUpdating_; }
+
 signals:
   void currentFileChanged();
 
@@ -47,6 +52,8 @@ private:
   QFileSystemWatcher* fontWatcher_;
   QTimer* watchTimer_;
 
+  bool periodicUpdating_ = false;
+
   FT_Error validateFontFile(QString const& fileName);
 };
 
diff --git a/src/ftinspect/engine/fontinfo.cpp 
b/src/ftinspect/engine/fontinfo.cpp
index 3eb5b78..5927409 100644
--- a/src/ftinspect/engine/fontinfo.cpp
+++ b/src/ftinspect/engine/fontinfo.cpp
@@ -6,6 +6,8 @@
 
 #include "engine.hpp"
 
+#include <map>
+#include <unordered_map>
 #include <memory>
 #include <utility>
 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
@@ -16,6 +18,14 @@
 #endif
 #include <freetype/ftmodapi.h>
 #include <freetype/ttnameid.h>
+#include <freetype/tttables.h>
+#include <freetype/tttags.h>
+
+#ifdef _MSC_VER // To use intrin
+#define WIN32_LEAN_AND_MEAN
+#include <Windows.h>
+#include <intrin.h>
+#endif
 
 
 void
@@ -163,4 +173,524 @@ SFNTName::utf16BEToQString(char const* str,
 }
 
 
+FontBasicInfo
+FontBasicInfo::get(Engine* engine)
+{
+  auto fontIndex = engine->currentFontIndex();
+  if (fontIndex < 0)
+    return {};
+  FontBasicInfo result;
+  result.numFaces = engine->numberOfFaces(fontIndex);
+
+  engine->reloadFont();
+  auto face = engine->currentFallbackFtFace();
+  if (!face)
+    return result;
+
+  if (face->family_name)
+    result.familyName = QString(face->family_name);
+  if (face->style_name)
+    result.styleName = QString(face->style_name);
+
+  auto psName = FT_Get_Postscript_Name(face);
+  if (psName)
+    result.postscriptName = QString(psName);
+
+  auto head = static_cast<TT_Header*>(FT_Get_Sfnt_Table(face, FT_SFNT_HEAD));
+  if (head)
+  {
+    uint64_t createdTimestamp
+      = head->Created[1] | static_cast<uint64_t>(head->Created[0]) << 32;
+    uint64_t modifiedTimestamp
+      = head->Modified[1] | static_cast<uint64_t>(head->Modified[0]) << 32;
+    
+    result.createdTime
+      = QDateTime::fromSecsSinceEpoch(createdTimestamp, Qt::OffsetFromUTC)
+          .addSecs(-2082844800);
+    result.modifiedTime
+      = QDateTime::fromSecsSinceEpoch(modifiedTimestamp, Qt::OffsetFromUTC)
+          .addSecs(-2082844800);
+
+    auto revDouble = head->Font_Revision / 65536.0;
+    if (head->Font_Revision & 0xFFC0)
+      result.revision = QString::number(revDouble, 'g', 4);
+    else
+      result.revision = QString::number(revDouble, 'g', 2);
+  }
+
+  return result;
+}
+
+
+FontTypeEntries
+FontTypeEntries::get(Engine* engine)
+{
+  engine->reloadFont();
+  auto face = engine->currentFallbackFtFace();
+  if (!face)
+    return {};
+  
+  FontTypeEntries result = {};
+  result.driverName = QString(FT_FACE_DRIVER_NAME(face));
+  result.sfnt = FT_IS_SFNT(face);
+  result.scalable = FT_IS_SCALABLE(face);
+  if (result.scalable)
+    result.mmgx = FT_HAS_MULTIPLE_MASTERS(face);
+  else
+    result.mmgx = false;
+  result.fixedSizes = FT_HAS_FIXED_SIZES(face);
+  result.hasHorizontal = FT_HAS_HORIZONTAL(face);
+  result.hasVertical = FT_HAS_VERTICAL(face);
+  result.fixedWidth = FT_IS_FIXED_WIDTH(face);
+  result.glyphNames = FT_HAS_GLYPH_NAMES(face);
+
+  if (result.scalable)
+  {
+    result.emSize = face->units_per_EM;
+    result.globalBBox = face->bbox;
+    result.ascender = face->ascender;
+    result.descender = face->descender;
+    result.height = face->height;
+    result.maxAdvanceWidth = face->max_advance_width;
+    result.maxAdvanceHeight = face->max_advance_height;
+    result.underlinePos = face->underline_position;
+    result.underlineThickness = face->underline_thickness;
+  }
+
+  return result;
+}
+
+
+bool
+operator==(const PS_FontInfoRec& lhs,
+           const PS_FontInfoRec& rhs)
+{
+  // XXX: possible security flaw with `strcmp`?
+  return strcmp(lhs.version, rhs.version) == 0
+         && strcmp(lhs.notice, rhs.notice) == 0
+         && strcmp(lhs.full_name, rhs.full_name) == 0
+         && strcmp(lhs.family_name, rhs.family_name) == 0
+         && strcmp(lhs.weight, rhs.weight) == 0
+         && lhs.italic_angle == rhs.italic_angle
+         && lhs.is_fixed_pitch == rhs.is_fixed_pitch
+         && lhs.underline_position == rhs.underline_position
+         && lhs.underline_thickness == rhs.underline_thickness;
+}
+
+
+bool
+operator==(const PS_PrivateRec& lhs,
+           const PS_PrivateRec& rhs)
+{
+  return lhs.unique_id == rhs.unique_id
+         && lhs.lenIV == rhs.lenIV
+         && lhs.num_blue_values == rhs.num_blue_values
+         && lhs.num_other_blues == rhs.num_other_blues
+         && lhs.num_family_blues == rhs.num_family_blues
+         && lhs.num_family_other_blues == rhs.num_family_other_blues
+         && std::equal(std::begin(lhs.blue_values), std::end(lhs.blue_values),
+                       std::begin(rhs.blue_values))
+         && std::equal(std::begin(lhs.other_blues), std::end(lhs.other_blues),
+                       std::begin(rhs.other_blues))
+         && std::equal(std::begin(lhs.family_blues), 
std::end(lhs.family_blues),
+                       std::begin(rhs.family_blues))
+         && std::equal(std::begin(lhs.family_other_blues),
+                       std::end(lhs.family_other_blues),
+                       std::begin(rhs.family_other_blues))
+         && lhs.blue_scale == rhs.blue_scale
+         && lhs.blue_shift == rhs.blue_shift
+         && lhs.blue_fuzz == rhs.blue_fuzz
+         && std::equal(std::begin(lhs.standard_width), 
+                       std::end(lhs.standard_width),
+                       std::begin(rhs.standard_width))
+         && std::equal(std::begin(lhs.standard_height),
+                       std::end(lhs.standard_height),
+                       std::begin(rhs.standard_height))
+         && lhs.num_snap_widths == rhs.num_snap_widths
+         && lhs.num_snap_heights == rhs.num_snap_heights
+         && lhs.force_bold == rhs.force_bold
+         && lhs.round_stem_up == rhs.round_stem_up
+         && std::equal(std::begin(lhs.snap_widths), std::end(lhs.snap_widths),
+                       std::begin(rhs.snap_widths))
+         && std::equal(std::begin(lhs.snap_heights), 
std::end(lhs.snap_heights),
+                       std::begin(rhs.snap_heights))
+         && lhs.expansion_factor == rhs.expansion_factor
+         && lhs.language_group == rhs.language_group
+         && lhs.password == rhs.password
+         && std::equal(std::begin(lhs.min_feature), std::end(lhs.min_feature),
+                       std::begin(rhs.min_feature));
+}
+
+
+bool
+FontFixedSize::get(Engine* engine,
+                   std::vector<FontFixedSize>& list,
+                   const std::function<void()>& onUpdateNeeded)
+{
+  engine->reloadFont();
+  auto face = engine->currentFallbackFtFace();
+  if (!face)
+  {
+    if (list.empty())
+      return false;
+
+    onUpdateNeeded();
+    list.clear();
+    return true;
+  }
+  
+  auto changed = false;
+  if (list.size() != static_cast<size_t>(face->num_fixed_sizes))
+  {
+    changed = true;
+    onUpdateNeeded();
+    list.resize(face->num_fixed_sizes);
+  }
+
+  for (int i = 0; i < face->num_fixed_sizes; ++i)
+  {
+    FontFixedSize ffs = {};
+    auto bSize = face->available_sizes + i;
+    ffs.height = bSize->height;
+    ffs.width  = bSize->width;
+    ffs.size   = bSize->size / 64.0;
+    ffs.xPpem  = bSize->x_ppem / 64.0;
+    ffs.yPpem  = bSize->y_ppem / 64.0;
+    if (ffs != list[i])
+    {
+      
+      if (!changed)
+      {
+        onUpdateNeeded();
+        changed = true;
+      }
+      
+      list[i] = ffs;
+    }
+  }
+
+  return changed;
+}
+
+
+struct TTCHeaderRec
+{
+  uint32_t ttcTag;
+  uint16_t majorVersion;
+  uint16_t minorVersion;
+  uint32_t numFonts;
+};
+
+
+struct SFNTHeaderRec
+{
+  uint32_t formatTag;
+  uint16_t numTables;
+  // There'll be some padding, but it doesn't matter.
+};
+
+
+struct TTTableRec
+{
+  uint32_t tag;
+  uint32_t checksum;
+  uint32_t offset;
+  uint32_t length;
+};
+
+
+uint32_t
+bigEndianToNative(uint32_t n)
+{
+#ifdef _MSC_VER
+  #if REG_DWORD == REG_DWORD_LITTLE_ENDIAN
+    return _byteswap_ulong(n);
+  #else
+    return n;
+  #endif
+#else
+  auto np = reinterpret_cast<unsigned char*>(&n);
+
+  return (static_cast<uint32_t>(np[0]) << 24)
+         | (static_cast<uint32_t>(np[1]) << 16)
+         | (static_cast<uint32_t>(np[2]) << 8)
+         | (static_cast<uint32_t>(np[3]));
+#endif
+}
+
+
+uint16_t
+bigEndianToNative(uint16_t n)
+{
+#ifdef _MSC_VER
+#if REG_DWORD == REG_DWORD_LITTLE_ENDIAN
+  return _byteswap_ushort(n);
+#else
+  return n;
+#endif
+#else
+  auto np = reinterpret_cast<unsigned char*>(&n);
+
+  return static_cast<uint16_t>((static_cast<uint16_t>(np[0]) << 8)
+                               | (static_cast<uint16_t>(np[1])));
+#endif
+}
+
+
+void readSingleFace(QFile& file,
+                    uint32_t offset,
+                    unsigned faceIndex,
+                    std::vector<TTTableRec>& tempTables,
+                    std::map<unsigned long, SFNTTableInfo>& result)
+{
+  if (!file.seek(offset))
+    return;
+
+  SFNTHeaderRec sfntHeader = {};
+  if (file.read(reinterpret_cast<char*>(&sfntHeader), 
+                sizeof(SFNTHeaderRec))
+      != sizeof(SFNTHeaderRec))
+    return;
+  sfntHeader.formatTag = bigEndianToNative(sfntHeader.formatTag);
+  sfntHeader.numTables = bigEndianToNative(sfntHeader.numTables);
+
+  unsigned short validEntries = sfntHeader.numTables;
+  
+  if (sfntHeader.formatTag != TTAG_OTTO)
+  {
+    // TODO check SFNT Header
+    //checkSFNTHeader();
+  }
+
+  if (!file.seek(offset + 12))
+    return;
+
+  tempTables.resize(validEntries);
+  auto desiredLen = static_cast<long long>(validEntries * sizeof(TTTableRec));
+  auto readLen = file.read(reinterpret_cast<char*>(tempTables.data()), 
desiredLen);
+  if (readLen != desiredLen)
+    return;
+
+  for (auto& t : tempTables)
+  {
+    t.tag = bigEndianToNative(t.tag);
+    t.offset = bigEndianToNative(t.offset);
+    t.checksum = bigEndianToNative(t.checksum);
+    t.length = bigEndianToNative(t.length);
+
+    auto it = result.find(t.offset);
+    if (it == result.end())
+    {
+      auto emplaced = result.emplace(t.offset, SFNTTableInfo());
+      it = emplaced.first;
+
+      auto& info = it->second;
+      info.tag = t.tag;
+      info.length = t.length;
+      info.offset = t.offset;
+      info.sharedFaces.emplace(faceIndex);
+      info.valid = true;
+    }
+    else
+    {
+      it->second.sharedFaces.emplace(faceIndex);
+      // TODO check
+    }
+  }
+}
+
+
+void
+SFNTTableInfo::getForAll(Engine* engine,
+                         std::vector<SFNTTableInfo>& infos)
+{
+  infos.clear();
+  auto face = engine->currentFallbackFtFace();
+  if (!face || !FT_IS_SFNT(face))
+    return;
+
+  auto index = engine->currentFontIndex();
+  auto& mgr = engine->fontFileManager();
+  if (index < 0 || index >= mgr.size())
+    return;
+
+  auto& fileInfo = mgr[index];
+  QFile file(fileInfo.filePath());
+  if (!file.open(QIODevice::ReadOnly))
+    return;
+
+  auto fileSize = file.size();
+  if (fileSize < 12)
+    return;
+
+  std::vector<TTTableRec> tables;
+  std::map<unsigned long, SFNTTableInfo> result;
+
+  TTCHeaderRec ttcHeader = {};
+  auto readLen = file.read(reinterpret_cast<char*>(&ttcHeader),
+                           sizeof(TTCHeaderRec));
+
+  if (readLen != sizeof(TTCHeaderRec))
+    return;
+
+  ttcHeader.ttcTag = bigEndianToNative(ttcHeader.ttcTag);
+  ttcHeader.majorVersion = bigEndianToNative(ttcHeader.majorVersion);
+  ttcHeader.minorVersion = bigEndianToNative(ttcHeader.minorVersion);
+  ttcHeader.numFonts = bigEndianToNative(ttcHeader.numFonts);
+
+  if (ttcHeader.ttcTag == TTAG_ttcf
+      && (ttcHeader.majorVersion == 2 || ttcHeader.majorVersion == 1))
+  {
+    // Valid TTC file
+    std::unique_ptr<unsigned> offsets(new unsigned[ttcHeader.numFonts]);
+    auto desiredLen = static_cast<long long>(ttcHeader.numFonts
+                                             * sizeof(unsigned));
+    readLen = file.read(reinterpret_cast<char*>(offsets.get()), desiredLen);
+    if (readLen != desiredLen)
+      return;
+    
+    for (unsigned faceIndex = 0; 
+        faceIndex < ttcHeader.numFonts; 
+        faceIndex++)
+    {
+      auto offset = bigEndianToNative(offsets.get()[faceIndex]);
+      readSingleFace(file, offset, faceIndex, tables, result);
+    }
+  }
+  else
+  {
+    // Not TTC file, try single SFNT
+    if (!file.seek(0))
+      return;
+    readSingleFace(file, 0, 0, tables, result);
+  }
+
+  infos.reserve(result.size());
+  for (auto& pr : result)
+    infos.emplace_back(std::move(pr.second));
+}
+
+
+void
+CompositeGlyphInfo::get(Engine* engine, 
+                        std::vector<CompositeGlyphInfo>& list)
+{
+  list.clear();
+  engine->reloadFont();
+  auto face = engine->currentFallbackFtFace();
+  if (!face || !FT_IS_SFNT(face))
+  {
+    if (list.empty())
+      return;
+  }
+
+  // We're not using the FreeType's subglyph APIs, but directly reading from
+  // the `glyf` table since it's faster
+  auto head = static_cast<TT_Header*>(FT_Get_Sfnt_Table(face, FT_SFNT_HEAD));
+  auto maxp
+    = static_cast<TT_MaxProfile*>(FT_Get_Sfnt_Table(face, FT_SFNT_MAXP));
+  if (!head || !maxp)
+    return;
+
+  FT_ULong locaLength = head->Index_To_Loc_Format ? 4 * maxp->numGlyphs + 4
+                                                  : 2 * maxp->numGlyphs + 2;
+  std::unique_ptr<unsigned char[]> locaBufferGuard(
+    new unsigned char[locaLength]);
+  auto offset = locaBufferGuard.get();
+  auto error = FT_Load_Sfnt_Table(face, TTAG_loca, 0, offset, &locaLength);
+  if (error)
+    return;
+
+  FT_ULong glyfLength = 0;
+  error = FT_Load_Sfnt_Table(face, TTAG_glyf, 0, NULL, &glyfLength);
+  if (error || !glyfLength)
+    return;
+
+  std::unique_ptr<unsigned char[]> glyfBufferGuard(
+    new unsigned char[glyfLength]);
+  auto buffer = glyfBufferGuard.get();
+  error = FT_Load_Sfnt_Table(face, TTAG_glyf, 0, buffer, &glyfLength);
+  if (error)
+    return;
+
+  for (size_t i = 0; i < maxp->numGlyphs; i++)
+  {
+    FT_UInt32  loc, end;
+    if (head->Index_To_Loc_Format)
+    {
+      loc = static_cast<FT_UInt32>(offset[4 * i    ]) << 24 |
+            static_cast<FT_UInt32>(offset[4 * i + 1]) << 16 |
+            static_cast<FT_UInt32>(offset[4 * i + 2]) << 8  |
+            static_cast<FT_UInt32>(offset[4 * i + 3])       ;
+      end = static_cast<FT_UInt32>(offset[4 * i + 4]) << 24 |
+            static_cast<FT_UInt32>(offset[4 * i + 5]) << 16 |
+            static_cast<FT_UInt32>(offset[4 * i + 6]) << 8  |
+            static_cast<FT_UInt32>(offset[4 * i + 7])       ;
+    }
+    else
+    {
+      loc = static_cast<FT_UInt32>(offset[2 * i    ]) << 9 |
+            static_cast<FT_UInt32>(offset[2 * i + 1]) << 1 ;
+      end = static_cast<FT_UInt32>(offset[2 * i + 2]) << 9 |
+            static_cast<FT_UInt32>(offset[2 * i + 3]) << 1 ;
+    }
+
+    if (end > glyfLength)
+      end = glyfLength;
+
+    if (loc + 16 > end)
+      continue;
+
+    auto len = static_cast<FT_Int16>(buffer[loc] << 8 | buffer[loc + 1]);
+    loc += 10;  // skip header
+    if (len >= 0) // not a composite one
+      continue;
+
+    std::vector<SubGlyph> subglyphs;
+
+    while (true)
+    {
+      if (loc + 6 > end)
+        break;
+      auto flags = static_cast<FT_UInt16>(buffer[loc] << 8 | buffer[loc + 1]);
+      loc += 2;
+      auto index = static_cast<FT_UInt16>(buffer[loc] << 8 | buffer[loc + 1]);
+      loc += 2;
+      FT_Int16 arg1, arg2;
+
+      // 
https://docs.microsoft.com/en-us/typography/opentype/spec/glyf#composite-glyph-description
+      if (flags & 0x0001)
+      {
+        arg1 = static_cast<FT_Int16>(buffer[loc] << 8 | buffer[loc + 1]);
+        loc += 2;
+        arg2 = static_cast<FT_Int16>(buffer[loc] << 8 | buffer[loc + 1]);
+        loc += 2;
+      }
+      else
+      {
+        arg1 = buffer[loc];
+        arg2 = buffer[loc + 1];
+        loc += 2;
+      }
+      if (flags & 0x0008)
+        loc += 2;
+      else if (flags & 0x0040)
+        loc += 4;
+      else if (flags & 0x0080)
+        loc += 8;
+
+      subglyphs.emplace_back(index, flags,
+                             flags & 0x0002 ? SubGlyph::PT_Offset
+                                            : SubGlyph::PT_Align,
+                             std::pair<short, short>(arg1, arg2));
+
+      if (!(flags & 0x0020))
+        break;
+    }
+
+    list.emplace_back(static_cast<int>(i), std::move(subglyphs));
+  }
+}
+
+
 // end of fontinfo.cpp
diff --git a/src/ftinspect/engine/fontinfo.hpp 
b/src/ftinspect/engine/fontinfo.hpp
index c1b9ff3..f28dc81 100644
--- a/src/ftinspect/engine/fontinfo.hpp
+++ b/src/ftinspect/engine/fontinfo.hpp
@@ -5,13 +5,47 @@
 #pragma once
 
 #include <set>
-#include <vector>
+#include <QDateTime>
 #include <QByteArray>
 #include <QString>
 #include <freetype/freetype.h>
 #include <freetype/ftsnames.h>
+#include <freetype/t1tables.h>
 
 class Engine;
+
+struct SFNTTableInfo
+{
+  unsigned long tag = 0;
+  unsigned long offset = 0;
+  unsigned long length = 0;
+  bool valid = false;
+  std::set<unsigned long> sharedFaces;
+
+  static void getForAll(Engine* engine, std::vector<SFNTTableInfo>& infos);
+
+
+  friend bool
+  operator==(const SFNTTableInfo& lhs,
+             const SFNTTableInfo& rhs)
+  {
+    return lhs.tag == rhs.tag
+      && lhs.offset == rhs.offset
+      && lhs.length == rhs.length
+      && lhs.valid == rhs.valid
+      && lhs.sharedFaces == rhs.sharedFaces;
+  }
+
+
+  friend bool
+  operator!=(const SFNTTableInfo& lhs,
+             const SFNTTableInfo& rhs)
+  {
+    return !(lhs == rhs);
+  }
+};
+
+
 struct SFNTName
 {
   unsigned short nameID;
@@ -58,4 +92,244 @@ struct SFNTName
 };
 
 
+struct FontBasicInfo
+{
+  int numFaces = -1;
+  QString familyName;
+  QString styleName;
+  QString postscriptName;
+  QDateTime createdTime;
+  QDateTime modifiedTime;
+  QString revision;
+  QString copyright;
+  QString trademark;
+  QString manufacturer;
+
+  static FontBasicInfo get(Engine* engine);
+
+
+  // Oh, we have no C++20 :(
+  friend bool
+  operator==(const FontBasicInfo& lhs,
+             const FontBasicInfo& rhs)
+  {
+    return lhs.numFaces == rhs.numFaces
+      && lhs.familyName == rhs.familyName
+      && lhs.styleName == rhs.styleName
+      && lhs.postscriptName == rhs.postscriptName
+      && lhs.createdTime == rhs.createdTime
+      && lhs.modifiedTime == rhs.modifiedTime
+      && lhs.revision == rhs.revision
+      && lhs.copyright == rhs.copyright
+      && lhs.trademark == rhs.trademark
+      && lhs.manufacturer == rhs.manufacturer;
+  }
+
+
+  friend bool
+  operator!=(const FontBasicInfo& lhs,
+             const FontBasicInfo& rhs)
+  {
+    return !(lhs == rhs);
+  }
+};
+
+
+struct FontTypeEntries
+{
+  QString driverName;
+  bool sfnt          : 1;
+  bool scalable      : 1;
+  bool mmgx          : 1;
+  bool fixedSizes    : 1;
+  bool hasHorizontal : 1;
+  bool hasVertical   : 1;
+  bool fixedWidth    : 1;
+  bool glyphNames    : 1;
+
+  int emSize;
+  FT_BBox globalBBox;
+  int ascender;
+  int descender;
+  int height;
+  int maxAdvanceWidth;
+  int maxAdvanceHeight;
+  int underlinePos;
+  int underlineThickness;
+
+  static FontTypeEntries get(Engine* engine);
+
+
+  // Oh, we have no C++20 :(
+  friend bool
+  operator==(const FontTypeEntries& lhs,
+             const FontTypeEntries& rhs)
+  {
+    return lhs.driverName == rhs.driverName
+      && lhs.sfnt == rhs.sfnt
+      && lhs.scalable == rhs.scalable
+      && lhs.mmgx == rhs.mmgx
+      && lhs.fixedSizes == rhs.fixedSizes
+      && lhs.hasHorizontal == rhs.hasHorizontal
+      && lhs.hasVertical == rhs.hasVertical
+      && lhs.fixedWidth == rhs.fixedWidth
+      && lhs.glyphNames == rhs.glyphNames
+      && lhs.emSize == rhs.emSize
+      && lhs.globalBBox.xMax == rhs.globalBBox.xMax
+      && lhs.globalBBox.xMin == rhs.globalBBox.xMin
+      && lhs.globalBBox.yMax == rhs.globalBBox.yMax
+      && lhs.globalBBox.yMin == rhs.globalBBox.yMin
+      && lhs.ascender == rhs.ascender
+      && lhs.descender == rhs.descender
+      && lhs.height == rhs.height
+      && lhs.maxAdvanceWidth == rhs.maxAdvanceWidth
+      && lhs.maxAdvanceHeight == rhs.maxAdvanceHeight
+      && lhs.underlinePos == rhs.underlinePos
+      && lhs.underlineThickness == rhs.underlineThickness;
+  }
+
+
+  friend bool
+  operator!=(const FontTypeEntries& lhs,
+             const FontTypeEntries& rhs)
+  {
+    return !(lhs == rhs);
+  }
+};
+
+
+// For PostScript `PS_FontInfoRec` and `PS_PrivateRec`, we don't create our own
+// structs but direct use the ones provided by FreeType.
+// But we still need to provided `operator==`
+// No operator== for PS_FontInfoRec since there's little point to deep-copy it
+// bool operator==(const PS_FontInfoRec& lhs, const PS_FontInfoRec& rhs);
+bool operator==(const PS_PrivateRec& lhs, const PS_PrivateRec& rhs);
+
+
+struct FontFixedSize
+{
+  short height;
+  short width;
+  double size;
+  double xPpem;
+  double yPpem;
+
+
+  // Returns that if the list is updated
+  // Using a callback because Qt needs `beginResetModel` to be called 
**before**
+  // the internal storage updates.
+  static bool get(Engine* engine,
+                  std::vector<FontFixedSize>& list,
+                  const std::function<void()>& onUpdateNeeded);
+
+
+  friend bool
+  operator==(const FontFixedSize& lhs,
+             const FontFixedSize& rhs)
+  {
+    return lhs.height == rhs.height
+      && lhs.width == rhs.width
+      && lhs.size == rhs.size
+      && lhs.xPpem == rhs.xPpem
+      && lhs.yPpem == rhs.yPpem;
+  }
+
+
+  friend bool
+  operator!=(const FontFixedSize& lhs,
+             const FontFixedSize& rhs)
+  {
+    return !(lhs == rhs);
+  }
+};
+
+
+struct CompositeGlyphInfo
+{
+  struct SubGlyph
+  {
+    enum PositionType
+    {
+      PT_Offset, // Child's points are added with a xy-offset
+      PT_Align // One point of the child is aligned with one point of the 
parent
+    };
+    unsigned short index;
+    unsigned short flag;
+    PositionType positionType;
+    // For PT_Offset: <deltaX, deltaY>
+    // For PT_Align:  <childPoint, parentPoint>
+    std::pair<short, short> position;
+
+    SubGlyph(unsigned short index,
+             unsigned short flag,
+             PositionType positionType,
+             std::pair<short, short> position)
+    : index(index),
+      flag(flag),
+      positionType(positionType),
+      position(std::move(position))
+    { }
+
+
+    friend bool
+    operator==(const SubGlyph& lhs,
+               const SubGlyph& rhs)
+    {
+      return lhs.index == rhs.index
+        && lhs.flag == rhs.flag
+        && lhs.positionType == rhs.positionType
+        && lhs.position == rhs.position;
+    }
+
+
+    friend bool
+    operator!=(const SubGlyph& lhs,
+               const SubGlyph& rhs)
+    {
+      return !(lhs == rhs);
+    }
+  };
+
+
+  int index;
+  std::vector<SubGlyph> subglyphs;
+
+
+  CompositeGlyphInfo(short index,
+                     std::vector<SubGlyph> subglyphs)
+  : index(index),
+    subglyphs(std::move(subglyphs))
+  { }
+
+
+  friend bool
+  operator==(const CompositeGlyphInfo& lhs,
+             const CompositeGlyphInfo& rhs)
+  {
+    return lhs.index == rhs.index
+      && lhs.subglyphs == rhs.subglyphs;
+  }
+
+
+  friend bool
+  operator!=(const CompositeGlyphInfo& lhs,
+             const CompositeGlyphInfo& rhs)
+  {
+    return !(lhs == rhs);
+  }
+
+
+  // expensive
+  static void get(Engine* engine, std::vector<CompositeGlyphInfo>& list);
+};
+
+
+QString* mapSFNTNameIDToName(unsigned short nameID);
+QString* mapTTPlatformIDToName(unsigned short platformID);
+QString* mapTTEncodingIDToName(unsigned short platformID,
+                               unsigned short encodingID);
+QString* mapTTLanguageIDToName(unsigned short platformID,
+                               unsigned short languageID);
+
+
 // end of fontinfo.hpp
diff --git a/src/ftinspect/engine/fontinfonamesmapping.cpp 
b/src/ftinspect/engine/fontinfonamesmapping.cpp
new file mode 100644
index 0000000..e764ff0
--- /dev/null
+++ b/src/ftinspect/engine/fontinfonamesmapping.cpp
@@ -0,0 +1,567 @@
+// fontinfonamesmapping.cpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#include "fontinfo.hpp"
+
+#include <unordered_map>
+#include <freetype/ttnameid.h>
+
+#define FTI_UnknownID 0xFFFE
+
+// No more Qt containers since there's no any apparent advantage.
+using TableType = std::unordered_map<unsigned short, QString>;
+TableType ttSFNTNames;
+
+TableType ttPlatformNames;
+TableType ttEncodingUnicodeNames;
+TableType ttEncodingMacNames;
+TableType ttEncodingWindowsNames;
+TableType ttEncodingISONames;
+TableType ttEncodingAdobeNames;
+
+TableType ttLanguageMacNames;
+TableType ttLanguageWindowsNames;
+
+QString*
+mapSFNTNameIDToName(unsigned short nameID)
+{
+  if (ttSFNTNames.empty())
+  {
+    ttSFNTNames[FTI_UnknownID] = "Unknown";
+    ttSFNTNames[TT_NAME_ID_COPYRIGHT] = "Copyright";
+    ttSFNTNames[TT_NAME_ID_FONT_FAMILY] = "Font Family";
+    ttSFNTNames[TT_NAME_ID_FONT_SUBFAMILY] = "Font Subfamily";
+    ttSFNTNames[TT_NAME_ID_UNIQUE_ID] = "Unique Font ID";
+    ttSFNTNames[TT_NAME_ID_FULL_NAME] = "Full Name";
+    ttSFNTNames[TT_NAME_ID_VERSION_STRING] = "Version String";
+    ttSFNTNames[TT_NAME_ID_PS_NAME] = "PostScript Name";
+    ttSFNTNames[TT_NAME_ID_TRADEMARK] = "Trademark";
+    ttSFNTNames[TT_NAME_ID_MANUFACTURER] = "Manufacturer";
+    ttSFNTNames[TT_NAME_ID_DESIGNER] = "Designer";
+    ttSFNTNames[TT_NAME_ID_DESCRIPTION] = "Description";
+    ttSFNTNames[TT_NAME_ID_VENDOR_URL] = "Vendor URL";
+    ttSFNTNames[TT_NAME_ID_DESIGNER_URL] = "Designer URL";
+    ttSFNTNames[TT_NAME_ID_LICENSE] = "License";
+    ttSFNTNames[TT_NAME_ID_LICENSE_URL] = "License URL";
+    ttSFNTNames[TT_NAME_ID_TYPOGRAPHIC_FAMILY] = "Typographic Family";
+    ttSFNTNames[TT_NAME_ID_TYPOGRAPHIC_SUBFAMILY] = "Typographic Subfamily";
+    ttSFNTNames[TT_NAME_ID_MAC_FULL_NAME] = "Mac Full Name";
+    ttSFNTNames[TT_NAME_ID_SAMPLE_TEXT] = "Sample Text";
+    ttSFNTNames[TT_NAME_ID_WWS_FAMILY] = "WWS Family Name";
+    ttSFNTNames[TT_NAME_ID_WWS_SUBFAMILY] = "WWS Subfamily Name";
+    ttSFNTNames[TT_NAME_ID_LIGHT_BACKGROUND] = "Light Background Palette";
+    ttSFNTNames[TT_NAME_ID_DARK_BACKGROUND] = "Dark Background Palette";
+    ttSFNTNames[TT_NAME_ID_VARIATIONS_PREFIX]
+      = "Variations PostScript Name Prefix";
+  }
+
+  auto it = ttSFNTNames.find(nameID);
+  if (it == ttSFNTNames.end())
+    return &ttSFNTNames[FTI_UnknownID];
+  return &it->second;
+}
+
+QString*
+mapTTPlatformIDToName(unsigned short platformID)
+{
+  if (ttPlatformNames.empty())
+  {
+    ttPlatformNames[FTI_UnknownID] = "Unknown Platform";
+    // Unicode codepoints are encoded as UTF-16BE
+    ttPlatformNames[TT_PLATFORM_APPLE_UNICODE] = "Apple (Unicode)";
+    ttPlatformNames[TT_PLATFORM_MACINTOSH] = "Macintosh";
+    ttPlatformNames[TT_PLATFORM_ISO] = "ISO (deprecated)";
+    ttPlatformNames[TT_PLATFORM_MICROSOFT] = "Microsoft";
+    ttPlatformNames[TT_PLATFORM_CUSTOM] = "Custom";
+    ttPlatformNames[TT_PLATFORM_ADOBE] = "Adobe";
+  }
+  
+  auto it = ttPlatformNames.find(platformID);
+  if (it == ttPlatformNames.end())
+    return &ttPlatformNames[FTI_UnknownID];
+  return &it->second;
+}
+
+
+QString*
+mapTTEncodingIDToName(unsigned short platformID, 
+                      unsigned short encodingID)
+{
+  if (ttEncodingUnicodeNames.empty())
+  {
+    // Note: different from the Apple doc.
+    ttEncodingUnicodeNames[FTI_UnknownID] = "Unknown Encoding";
+    ttEncodingUnicodeNames[TT_APPLE_ID_DEFAULT] = "Unicode 1.0";
+    ttEncodingUnicodeNames[TT_APPLE_ID_UNICODE_1_1] = "Unicode 1.1";
+    ttEncodingUnicodeNames[TT_APPLE_ID_ISO_10646] = "ISO/IEC 10646";
+    ttEncodingUnicodeNames[TT_APPLE_ID_UNICODE_2_0]
+        = "Unicode 2.0 or later (BMP only)";
+    ttEncodingUnicodeNames[TT_APPLE_ID_UNICODE_32]
+        = "Unicode 2.0 or later (non-BMP characters allowed)";
+    //ttEncodingUnicodeNames[TT_APPLE_ID_VARIANT_SELECTOR] = "Variant 
Selector";
+    //ttEncodingUnicodeNames[TT_APPLE_ID_FULL_UNICODE] = ???;
+  }
+
+  if (ttEncodingMacNames.empty())
+  {
+    ttEncodingMacNames[FTI_UnknownID] = "Unknown Encoding";
+    ttEncodingMacNames[0] = "Roman";
+    ttEncodingMacNames[1] = "Japanese";
+    ttEncodingMacNames[2] = "Chinese (Traditional)";
+    ttEncodingMacNames[3] = "Korean";
+    ttEncodingMacNames[4] = "Arabic";
+    ttEncodingMacNames[5] = "Hebrew";
+    ttEncodingMacNames[6] = "Greek";
+    ttEncodingMacNames[7] = "Russian";
+    ttEncodingMacNames[8] = "RSymbol";
+    ttEncodingMacNames[9] = "Devanagari";
+    ttEncodingMacNames[10] = "Gurmukhi";
+    ttEncodingMacNames[11] = "Gujarati";
+    ttEncodingMacNames[12] = "Oriya";
+    ttEncodingMacNames[13] = "Bengali";
+    ttEncodingMacNames[14] = "Tamil";
+    ttEncodingMacNames[15] = "Telugu";
+    ttEncodingMacNames[16] = "Kannada";
+    ttEncodingMacNames[17] = "Malayalam";
+    ttEncodingMacNames[18] = "Sinhalese";
+    ttEncodingMacNames[19] = "Burmese";
+    ttEncodingMacNames[20] = "Khmer";
+    ttEncodingMacNames[21] = "Thai";
+    ttEncodingMacNames[22] = "Laotian";
+    ttEncodingMacNames[23] = "Georgian";
+    ttEncodingMacNames[24] = "Armenian";
+    ttEncodingMacNames[25] = "Chinese (Simplified)";
+    ttEncodingMacNames[26] = "Tibetan";
+    ttEncodingMacNames[27] = "Mongolian";
+    ttEncodingMacNames[28] = "Geez";
+    ttEncodingMacNames[29] = "Slavic";
+    ttEncodingMacNames[30] = "Vietnamese";
+    ttEncodingMacNames[31] = "Sindhi";
+    ttEncodingMacNames[32] = "Uninterpreted";
+  }
+
+  if (ttEncodingWindowsNames.empty())
+  {
+    ttEncodingMacNames[FTI_UnknownID] = "Unknown Encoding";
+    ttEncodingWindowsNames[0] = "Symbol";
+    ttEncodingWindowsNames[1] = "Unicode BMP";
+    ttEncodingWindowsNames[2] = "ShiftJIS";
+    ttEncodingWindowsNames[3] = "GBK";
+    ttEncodingWindowsNames[4] = "Big5";
+    ttEncodingWindowsNames[5] = "Wansung";
+    ttEncodingWindowsNames[6] = "Johab";
+    ttEncodingWindowsNames[7] = "Reserved";
+    ttEncodingWindowsNames[8] = "Reserved";
+    ttEncodingWindowsNames[9] = "Reserved";
+    ttEncodingWindowsNames[10] = "Unicode full repertoire";
+  }
+
+  if (ttEncodingISONames.empty())
+  {
+    ttEncodingISONames[FTI_UnknownID] = "Unknown Encoding";
+    ttEncodingISONames[TT_ISO_ID_7BIT_ASCII] = "ASCII";
+    ttEncodingISONames[TT_ISO_ID_10646] = "ISO/IEC 10646";
+    ttEncodingISONames[TT_ISO_ID_8859_1] = "ISO 8859-1";
+  }
+
+  if (ttEncodingAdobeNames.empty())
+  {
+    ttEncodingAdobeNames[FTI_UnknownID] = "Unknown Encoding";
+    ttEncodingAdobeNames[TT_ADOBE_ID_STANDARD] = "Adobe Standard";
+    ttEncodingAdobeNames[TT_ADOBE_ID_EXPERT] = "Adobe Expert";
+    ttEncodingAdobeNames[TT_ADOBE_ID_CUSTOM] = "Adobe Custom";
+    ttEncodingAdobeNames[TT_ADOBE_ID_LATIN_1] = "Adobe Latin 1";
+  }
+
+  TableType* table = NULL;
+
+  switch (platformID)
+  {
+  case TT_PLATFORM_APPLE_UNICODE:
+    table = &ttEncodingUnicodeNames;
+    break;
+  case TT_PLATFORM_MACINTOSH:
+    table = &ttEncodingMacNames;
+    break;
+  case TT_PLATFORM_MICROSOFT:
+    table = &ttEncodingWindowsNames;
+    break;
+  case TT_PLATFORM_ISO:
+    table = &ttEncodingISONames;
+    break;
+  case TT_PLATFORM_ADOBE:
+    table = &ttEncodingAdobeNames;
+    break;
+
+  default:
+    return &ttEncodingUnicodeNames[FTI_UnknownID];
+  }
+
+  auto it = table->find(encodingID);
+  if (it == table->end())
+    return &(*table)[FTI_UnknownID];
+  return &it->second;
+}
+
+
+QString*
+mapTTLanguageIDToName(unsigned short platformID,
+                      unsigned short languageID)
+{
+  if (ttLanguageMacNames.empty())
+  {
+    ttLanguageMacNames[FTI_UnknownID] = "Unknown Language";
+    ttLanguageMacNames[0] = "English";
+    ttLanguageMacNames[1] = "French";
+    ttLanguageMacNames[2] = "German";
+    ttLanguageMacNames[3] = "Italian";
+    ttLanguageMacNames[4] = "Dutch";
+    ttLanguageMacNames[5] = "Swedish";
+    ttLanguageMacNames[6] = "Spanish";
+    ttLanguageMacNames[7] = "Danish";
+    ttLanguageMacNames[8] = "Portuguese";
+    ttLanguageMacNames[9] = "Norwegian";
+    ttLanguageMacNames[10] = "Hebrew";
+    ttLanguageMacNames[11] = "Japanese";
+    ttLanguageMacNames[12] = "Arabic";
+    ttLanguageMacNames[13] = "Finnish";
+    ttLanguageMacNames[14] = "Greek";
+    ttLanguageMacNames[15] = "Icelandic";
+    ttLanguageMacNames[16] = "Maltese";
+    ttLanguageMacNames[17] = "Turkish";
+    ttLanguageMacNames[18] = "Croatian";
+    ttLanguageMacNames[19] = "Chinese (Traditional)";
+    ttLanguageMacNames[20] = "Urdu";
+    ttLanguageMacNames[21] = "Hindi";
+    ttLanguageMacNames[22] = "Thai";
+    ttLanguageMacNames[23] = "Korean";
+    ttLanguageMacNames[24] = "Lithuanian";
+    ttLanguageMacNames[25] = "Polish";
+    ttLanguageMacNames[26] = "Hungarian";
+    ttLanguageMacNames[27] = "Estonian";
+    ttLanguageMacNames[28] = "Latvian";
+    ttLanguageMacNames[29] = "Sami";
+    ttLanguageMacNames[30] = "Faroese";
+    ttLanguageMacNames[31] = "Farsi/Persian";
+    ttLanguageMacNames[32] = "Russian";
+    ttLanguageMacNames[33] = "Chinese (Simplified)";
+    ttLanguageMacNames[34] = "Flemish";
+    ttLanguageMacNames[35] = "Irish Gaelic";
+    ttLanguageMacNames[36] = "Albanian";
+    ttLanguageMacNames[37] = "Romanian";
+    ttLanguageMacNames[38] = "Czech";
+    ttLanguageMacNames[39] = "Slovak";
+    ttLanguageMacNames[40] = "Slovenian";
+    ttLanguageMacNames[41] = "Yiddish";
+    ttLanguageMacNames[42] = "Serbian";
+    ttLanguageMacNames[43] = "Macedonian";
+    ttLanguageMacNames[44] = "Bulgarian";
+    ttLanguageMacNames[45] = "Ukrainian";
+    ttLanguageMacNames[46] = "Byelorussian";
+    ttLanguageMacNames[47] = "Uzbek";
+    ttLanguageMacNames[48] = "Kazakh";
+    ttLanguageMacNames[49] = "Azerbaijani (Cyrillic script)";
+    ttLanguageMacNames[50] = "Azerbaijani (Arabic script)";
+    ttLanguageMacNames[51] = "Armenian";
+    ttLanguageMacNames[52] = "Georgian";
+    ttLanguageMacNames[53] = "Moldavian";
+    ttLanguageMacNames[54] = "Kirghiz";
+    ttLanguageMacNames[55] = "Tajiki";
+    ttLanguageMacNames[56] = "Turkmen";
+    ttLanguageMacNames[57] = "Mongolian (Mongolian script)";
+    ttLanguageMacNames[58] = "Mongolian (Cyrillic script)";
+    ttLanguageMacNames[59] = "Pashto";
+    ttLanguageMacNames[60] = "Kurdish";
+    ttLanguageMacNames[61] = "Kashmiri";
+    ttLanguageMacNames[62] = "Sindhi";
+    ttLanguageMacNames[63] = "Tibetan";
+    ttLanguageMacNames[64] = "Nepali";
+    ttLanguageMacNames[65] = "Sanskrit";
+    ttLanguageMacNames[66] = "Marathi";
+    ttLanguageMacNames[67] = "Bengali";
+    ttLanguageMacNames[68] = "Assamese";
+    ttLanguageMacNames[69] = "Gujarati";
+    ttLanguageMacNames[70] = "Punjabi";
+    ttLanguageMacNames[71] = "Oriya";
+    ttLanguageMacNames[72] = "Malayalam";
+    ttLanguageMacNames[73] = "Kannada";
+    ttLanguageMacNames[74] = "Tamil";
+    ttLanguageMacNames[75] = "Telugu";
+    ttLanguageMacNames[76] = "Sinhalese";
+    ttLanguageMacNames[77] = "Burmese";
+    ttLanguageMacNames[78] = "Khmer";
+    ttLanguageMacNames[79] = "Lao";
+    ttLanguageMacNames[80] = "Vietnamese";
+    ttLanguageMacNames[81] = "Indonesian";
+    ttLanguageMacNames[82] = "Tagalog";
+    ttLanguageMacNames[83] = "Malay (Roman script)";
+    ttLanguageMacNames[84] = "Malay (Arabic script)";
+    ttLanguageMacNames[85] = "Amharic";
+    ttLanguageMacNames[86] = "Tigrinya";
+    ttLanguageMacNames[87] = "Galla";
+    ttLanguageMacNames[88] = "Somali";
+    ttLanguageMacNames[89] = "Swahili";
+    ttLanguageMacNames[90] = "Kinyarwanda/Ruanda";
+    ttLanguageMacNames[91] = "Rundi";
+    ttLanguageMacNames[92] = "Nyanja/Chewa";
+    ttLanguageMacNames[93] = "Malagasy";
+    ttLanguageMacNames[94] = "Esperanto";
+    ttLanguageMacNames[128] = "Welsh";
+    ttLanguageMacNames[129] = "Basque";
+    ttLanguageMacNames[130] = "Catalan";
+    ttLanguageMacNames[131] = "Latin";
+    ttLanguageMacNames[132] = "Quechua";
+    ttLanguageMacNames[133] = "Guarani";
+    ttLanguageMacNames[134] = "Aymara";
+    ttLanguageMacNames[135] = "Tatar";
+    ttLanguageMacNames[136] = "Uighur";
+    ttLanguageMacNames[137] = "Dzongkha";
+    ttLanguageMacNames[138] = "Javanese (Roman script)";
+    ttLanguageMacNames[139] = "Sundanese (Roman script)";
+    ttLanguageMacNames[140] = "Galician";
+    ttLanguageMacNames[141] = "Afrikaans";
+    ttLanguageMacNames[142] = "Breton";
+    ttLanguageMacNames[143] = "Inuktitut";
+    ttLanguageMacNames[144] = "Scottish Gaelic";
+    ttLanguageMacNames[145] = "Manx Gaelic";
+    ttLanguageMacNames[146] = "Irish Gaelic (with dot above)";
+    ttLanguageMacNames[147] = "Tongan";
+    ttLanguageMacNames[148] = "Greek (polytonic)";
+    ttLanguageMacNames[149] = "Greenlandic";
+    ttLanguageMacNames[150] = "Azerbaijani (Roman script)";
+  }
+
+  if (ttLanguageWindowsNames.empty())
+  {
+    ttLanguageWindowsNames[FTI_UnknownID] = "Unknown Language";
+    ttLanguageWindowsNames[0x0436] = "Afrikaans";
+    ttLanguageWindowsNames[0x041C] = "Albanian";
+    ttLanguageWindowsNames[0x0402] = "Bulgarian";
+    ttLanguageWindowsNames[0x0484] = "Alsatian";
+    ttLanguageWindowsNames[0x045E] = "Amharic";
+    ttLanguageWindowsNames[0x0405] = "Czech";
+    ttLanguageWindowsNames[0x1401] = "Arabic";
+    ttLanguageWindowsNames[0x3C01] = "Arabic";
+    ttLanguageWindowsNames[0x0C01] = "Arabic";
+    ttLanguageWindowsNames[0x0401] = "Arabic";
+    ttLanguageWindowsNames[0x0404] = "Chinese";
+    ttLanguageWindowsNames[0x0406] = "Danish";
+    ttLanguageWindowsNames[0x0423] = "Belarusian";
+    ttLanguageWindowsNames[0x0445] = "Bengali";
+    ttLanguageWindowsNames[0x0465] = "Divehi";
+    ttLanguageWindowsNames[0x0801] = "Arabic";
+    ttLanguageWindowsNames[0x2C01] = "Arabic";
+    ttLanguageWindowsNames[0x0403] = "Catalan";
+    ttLanguageWindowsNames[0x0413] = "Dutch";
+    ttLanguageWindowsNames[0x0483] = "Corsican";
+    ttLanguageWindowsNames[0x0804] = "Chinese";
+    ttLanguageWindowsNames[0x0813] = "Dutch";
+    ttLanguageWindowsNames[0x0845] = "Bengali";
+    ttLanguageWindowsNames[0x1001] = "Arabic";
+    ttLanguageWindowsNames[0x1004] = "Chinese";
+    ttLanguageWindowsNames[0x1009] = "English";
+    ttLanguageWindowsNames[0x1404] = "Chinese";
+    ttLanguageWindowsNames[0x1801] = "Arabic";
+    ttLanguageWindowsNames[0x2001] = "Arabic";
+    ttLanguageWindowsNames[0x2401] = "Arabic";
+    ttLanguageWindowsNames[0x2801] = "Arabic";
+    ttLanguageWindowsNames[0x3001] = "Arabic";
+    ttLanguageWindowsNames[0x3401] = "Arabic";
+    ttLanguageWindowsNames[0x3801] = "Arabic";
+    ttLanguageWindowsNames[0x4001] = "Arabic";
+    ttLanguageWindowsNames[0x1C01] = "Arabic";
+    ttLanguageWindowsNames[0x042B] = "Armenian";
+    ttLanguageWindowsNames[0x044D] = "Assamese";
+    ttLanguageWindowsNames[0x082C] = "Azeri (Cyrillic)";
+    ttLanguageWindowsNames[0x042C] = "Azeri (Latin)";
+    ttLanguageWindowsNames[0x046D] = "Bashkir";
+    ttLanguageWindowsNames[0x042D] = "Basque";
+    ttLanguageWindowsNames[0x201A] = "Bosnian (Cyrillic)";
+    ttLanguageWindowsNames[0x141A] = "Bosnian (Latin)";
+    ttLanguageWindowsNames[0x047E] = "Breton";
+    ttLanguageWindowsNames[0x0C04] = "Chinese";
+    ttLanguageWindowsNames[0x041A] = "Croatian";
+    ttLanguageWindowsNames[0x101A] = "Croatian (Latin)";
+    ttLanguageWindowsNames[0x048C] = "Dari";
+    ttLanguageWindowsNames[0x0C09] = "English";
+    ttLanguageWindowsNames[0x0407] = "German";
+    ttLanguageWindowsNames[0x0408] = "Greek";
+    ttLanguageWindowsNames[0x0409] = "English";
+    ttLanguageWindowsNames[0x0410] = "Italian";
+    ttLanguageWindowsNames[0x0411] = "Japanese";
+    ttLanguageWindowsNames[0x0421] = "Indonesian";
+    ttLanguageWindowsNames[0x0425] = "Estonian";
+    ttLanguageWindowsNames[0x0434] = "isiXhosa";
+    ttLanguageWindowsNames[0x0435] = "isiZulu";
+    ttLanguageWindowsNames[0x0437] = "Georgian";
+    ttLanguageWindowsNames[0x0438] = "Faroese";
+    ttLanguageWindowsNames[0x0439] = "Hindi";
+    ttLanguageWindowsNames[0x0447] = "Gujarati";
+    ttLanguageWindowsNames[0x0453] = "Khmer";
+    ttLanguageWindowsNames[0x0456] = "Galician";
+    ttLanguageWindowsNames[0x0462] = "Frisian";
+    ttLanguageWindowsNames[0x0464] = "Filipino";
+    ttLanguageWindowsNames[0x0468] = "Hausa (Latin)";
+    ttLanguageWindowsNames[0x0470] = "Igbo";
+    ttLanguageWindowsNames[0x0486] = "K’iche";
+    ttLanguageWindowsNames[0x0807] = "German";
+    ttLanguageWindowsNames[0x0809] = "English";
+    ttLanguageWindowsNames[0x0810] = "Italian";
+    ttLanguageWindowsNames[0x1007] = "German";
+    ttLanguageWindowsNames[0x1407] = "German";
+    ttLanguageWindowsNames[0x1409] = "English";
+    ttLanguageWindowsNames[0x1809] = "English";
+    ttLanguageWindowsNames[0x2009] = "English";
+    ttLanguageWindowsNames[0x2409] = "English";
+    ttLanguageWindowsNames[0x2809] = "English";
+    ttLanguageWindowsNames[0x3009] = "English";
+    ttLanguageWindowsNames[0x3409] = "English";
+    ttLanguageWindowsNames[0x4009] = "English";
+    ttLanguageWindowsNames[0x4409] = "English";
+    ttLanguageWindowsNames[0x4809] = "English";
+    ttLanguageWindowsNames[0x1C09] = "English";
+    ttLanguageWindowsNames[0x2C09] = "English";
+    ttLanguageWindowsNames[0x040B] = "Finnish";
+    ttLanguageWindowsNames[0x080C] = "French";
+    ttLanguageWindowsNames[0x0C0C] = "French";
+    ttLanguageWindowsNames[0x040C] = "French";
+    ttLanguageWindowsNames[0x140c] = "French";
+    ttLanguageWindowsNames[0x180C] = "French";
+    ttLanguageWindowsNames[0x100C] = "French";
+    ttLanguageWindowsNames[0x0C07] = "German";
+    ttLanguageWindowsNames[0x046F] = "Greenlandic";
+    ttLanguageWindowsNames[0x040D] = "Hebrew";
+    ttLanguageWindowsNames[0x040E] = "Hungarian";
+    ttLanguageWindowsNames[0x040F] = "Icelandic";
+    ttLanguageWindowsNames[0x045D] = "Inuktitut";
+    ttLanguageWindowsNames[0x085D] = "Inuktitut (Latin)";
+    ttLanguageWindowsNames[0x083C] = "Irish";
+    ttLanguageWindowsNames[0x044B] = "Kannada";
+    ttLanguageWindowsNames[0x043F] = "Kazakh";
+    ttLanguageWindowsNames[0x0412] = "Korean";
+    ttLanguageWindowsNames[0x0426] = "Latvian";
+    ttLanguageWindowsNames[0x0427] = "Lithuanian";
+    ttLanguageWindowsNames[0x0440] = "Kyrgyz";
+    ttLanguageWindowsNames[0x0441] = "Kiswahili";
+    ttLanguageWindowsNames[0x0454] = "Lao";
+    ttLanguageWindowsNames[0x0457] = "Konkani";
+    ttLanguageWindowsNames[0x0481] = "Maori";
+    ttLanguageWindowsNames[0x0487] = "Kinyarwanda";
+    ttLanguageWindowsNames[0x082E] = "Lower Sorbian";
+    ttLanguageWindowsNames[0x046E] = "Luxembourgish";
+    ttLanguageWindowsNames[0x042F] = "Macedonian";
+    ttLanguageWindowsNames[0x083E] = "Malay";
+    ttLanguageWindowsNames[0x043E] = "Malay";
+    ttLanguageWindowsNames[0x044C] = "Malayalam";
+    ttLanguageWindowsNames[0x043A] = "Maltese";
+    ttLanguageWindowsNames[0x047A] = "Mapudungun";
+    ttLanguageWindowsNames[0x044E] = "Marathi";
+    ttLanguageWindowsNames[0x047C] = "Mohawk";
+    ttLanguageWindowsNames[0x0414] = "Norwegian (Bokmal)";
+    ttLanguageWindowsNames[0x0415] = "Polish";
+    ttLanguageWindowsNames[0x0416] = "Portuguese";
+    ttLanguageWindowsNames[0x0417] = "Romansh";
+    ttLanguageWindowsNames[0x0418] = "Romanian";
+    ttLanguageWindowsNames[0x0419] = "Russian";
+    ttLanguageWindowsNames[0x0446] = "Punjabi";
+    ttLanguageWindowsNames[0x0448] = "Odia (formerly Oriya)";
+    ttLanguageWindowsNames[0x0450] = "Mongolian (Cyrillic)";
+    ttLanguageWindowsNames[0x0461] = "Nepali";
+    ttLanguageWindowsNames[0x0463] = "Pashto";
+    ttLanguageWindowsNames[0x0482] = "Occitan";
+    ttLanguageWindowsNames[0x0814] = "Norwegian (Nynorsk)";
+    ttLanguageWindowsNames[0x0816] = "Portuguese";
+    ttLanguageWindowsNames[0x0850] = "Mongolian (Traditional)";
+    ttLanguageWindowsNames[0x046B] = "Quechua";
+    ttLanguageWindowsNames[0x086B] = "Quechua";
+    ttLanguageWindowsNames[0x0C6B] = "Quechua";
+    ttLanguageWindowsNames[0x243B] = "Sami (Inari)";
+    ttLanguageWindowsNames[0x103B] = "Sami (Lule)";
+    ttLanguageWindowsNames[0x143B] = "Sami (Lule)";
+    ttLanguageWindowsNames[0x0C3B] = "Sami (Northern)";
+    ttLanguageWindowsNames[0x043B] = "Sami (Northern)";
+    ttLanguageWindowsNames[0x083B] = "Sami (Northern)";
+    ttLanguageWindowsNames[0x203B] = "Sami (Skolt)";
+    ttLanguageWindowsNames[0x183B] = "Sami (Southern)";
+    ttLanguageWindowsNames[0x1C3B] = "Sami (Southern)";
+    ttLanguageWindowsNames[0x044F] = "Sanskrit";
+    ttLanguageWindowsNames[0x1C1A] = "Serbian (Cyrillic)";
+    ttLanguageWindowsNames[0x0C1A] = "Serbian (Cyrillic)";
+    ttLanguageWindowsNames[0x181A] = "Serbian (Latin)";
+    ttLanguageWindowsNames[0x081A] = "Serbian (Latin)";
+    ttLanguageWindowsNames[0x046C] = "Sesotho sa Leboa";
+    ttLanguageWindowsNames[0x0432] = "Setswana";
+    ttLanguageWindowsNames[0x045B] = "Sinhala";
+    ttLanguageWindowsNames[0x041B] = "Slovak";
+    ttLanguageWindowsNames[0x0424] = "Slovenian";
+    ttLanguageWindowsNames[0x2C0A] = "Spanish";
+    ttLanguageWindowsNames[0x400A] = "Spanish";
+    ttLanguageWindowsNames[0x340A] = "Spanish";
+    ttLanguageWindowsNames[0x240A] = "Spanish";
+    ttLanguageWindowsNames[0x140A] = "Spanish";
+    ttLanguageWindowsNames[0x1C0A] = "Spanish";
+    ttLanguageWindowsNames[0x300A] = "Spanish";
+    ttLanguageWindowsNames[0x440A] = "Spanish";
+    ttLanguageWindowsNames[0x100A] = "Spanish";
+    ttLanguageWindowsNames[0x480A] = "Spanish";
+    ttLanguageWindowsNames[0x080A] = "Spanish";
+    ttLanguageWindowsNames[0x4C0A] = "Spanish";
+    ttLanguageWindowsNames[0x180A] = "Spanish";
+    ttLanguageWindowsNames[0x3C0A] = "Spanish";
+    ttLanguageWindowsNames[0x280A] = "Spanish";
+    ttLanguageWindowsNames[0x500A] = "Spanish";
+    ttLanguageWindowsNames[0x0C0A] = "Spanish (Modern Sort)";
+    ttLanguageWindowsNames[0x040A] = "Spanish (Traditional Sort)";
+    ttLanguageWindowsNames[0x540A] = "Spanish";
+    ttLanguageWindowsNames[0x380A] = "Spanish";
+    ttLanguageWindowsNames[0x200A] = "Spanish";
+    ttLanguageWindowsNames[0x081D] = "Swedish";
+    ttLanguageWindowsNames[0x041D] = "Swedish";
+    ttLanguageWindowsNames[0x045A] = "Syriac";
+    ttLanguageWindowsNames[0x0420] = "Urdu";
+    ttLanguageWindowsNames[0x0428] = "Tajik (Cyrillic)";
+    ttLanguageWindowsNames[0x085F] = "Tamazight (Latin)";
+    ttLanguageWindowsNames[0x0443] = "Uzbek (Latin)";
+    ttLanguageWindowsNames[0x0444] = "Tatar";
+    ttLanguageWindowsNames[0x0449] = "Tamil";
+    ttLanguageWindowsNames[0x044A] = "Telugu";
+    ttLanguageWindowsNames[0x041E] = "Thai";
+    ttLanguageWindowsNames[0x0422] = "Ukrainian";
+    ttLanguageWindowsNames[0x0442] = "Turkmen";
+    ttLanguageWindowsNames[0x0451] = "Tibetan";
+    ttLanguageWindowsNames[0x041F] = "Turkish";
+    ttLanguageWindowsNames[0x0480] = "Uighur";
+    ttLanguageWindowsNames[0x042E] = "Upper Sorbian";
+    ttLanguageWindowsNames[0x0452] = "Welsh";
+    ttLanguageWindowsNames[0x0478] = "Yi";
+    ttLanguageWindowsNames[0x0485] = "Yakut";
+    ttLanguageWindowsNames[0x0488] = "Wolof";
+    ttLanguageWindowsNames[0x0843] = "Uzbek (Cyrillic)";
+    ttLanguageWindowsNames[0x042A] = "Vietnamese";
+    ttLanguageWindowsNames[0x046A] = "Yoruba";
+  }
+
+  TableType* table = NULL;
+
+  switch (platformID)
+  {
+  case TT_PLATFORM_MACINTOSH:
+    table = &ttLanguageMacNames;
+    break;
+  case TT_PLATFORM_MICROSOFT:
+    table = &ttLanguageWindowsNames;
+    break;
+
+  default:
+    return &ttLanguageWindowsNames[FTI_UnknownID];
+  }
+
+  auto it = table->find(languageID);
+  if (it == table->end())
+    return &(*table)[FTI_UnknownID];
+  return &it->second;
+}
+
+
+// end of fontinfonamesmapping.cpp
diff --git a/src/ftinspect/engine/rendering.cpp 
b/src/ftinspect/engine/rendering.cpp
index 4fbb251..4fa28e3 100644
--- a/src/ftinspect/engine/rendering.cpp
+++ b/src/ftinspect/engine/rendering.cpp
@@ -5,6 +5,9 @@
 #include "rendering.hpp"
 
 #include <cmath>
+#include <QPixmap>
+#include <QPainter>
+
 #include <freetype/ftbitmap.h>
 
 #include "engine.hpp"
@@ -446,6 +449,21 @@ RenderingEngine::tryDirectRenderColorLayers(int glyphIndex,
 }
 
 
+QPixmap
+RenderingEngine::padToSize(QImage* image, int ppem)
+{
+  auto width = std::max(image->width(), ppem);
+  auto height = std::max(image->height(), ppem);
+  auto result = QPixmap(width, height);
+  result.fill(backgroundColor_);
+  QPainter painter(&result);
+  auto pos = QPoint { width / 2 - image->width() / 2,
+                      height / 2 - image->height() / 2};
+  painter.drawImage(pos, *image);
+  return result;
+}
+
+
 void
 convertLCDToARGB(FT_Bitmap& bitmap,
                  QImage& image,
diff --git a/src/ftinspect/engine/rendering.hpp 
b/src/ftinspect/engine/rendering.hpp
index c7e44b2..95fe00d 100644
--- a/src/ftinspect/engine/rendering.hpp
+++ b/src/ftinspect/engine/rendering.hpp
@@ -48,6 +48,8 @@ public:
                                      QRect* outRect,
                                      bool inverseRectY = false);
 
+  QPixmap padToSize(QImage* image, int ppem);
+
 private:
   Engine* engine_;
 
diff --git a/src/ftinspect/maingui.cpp b/src/ftinspect/maingui.cpp
index 3aa7281..1b7ca32 100644
--- a/src/ftinspect/maingui.cpp
+++ b/src/ftinspect/maingui.cpp
@@ -276,6 +276,7 @@ MainGUI::createLayout()
   continuousTab_ = new ContinuousTab(this, engine_,
                                      glyphDetailsDockWidget_, glyphDetails_);
   comparatorTab_ = new ComparatorTab(this, engine_);
+  infoTab_ = new InfoTab(this, engine_);
 
   tabWidget_ = new QTabWidget(this);
   tabWidget_->setObjectName("mainTab"); // for stylesheet
@@ -287,6 +288,8 @@ MainGUI::createLayout()
   tabWidget_->addTab(continuousTab_, tr("Continuous View"));
   tabs_.push_back(comparatorTab_);
   tabWidget_->addTab(comparatorTab_, tr("Comparator View"));
+  tabs_.push_back(infoTab_);
+  tabWidget_->addTab(infoTab_, tr("Font Info"));
   lastTab_ = singularTab_;
   
   tabWidget_->setTabToolTip(0, tr("View single glyph in grid view.\n"
@@ -297,6 +300,8 @@ MainGUI::createLayout()
   tabWidget_->setTabToolTip(2, tr("Compare the output of the font "
                                   "in different rendering settings "
                                   "(e.g. hintings)."));
+  tabWidget_->setTabToolTip(3, tr("View font info and metadata."));
+  
   tripletSelector_ = new TripletSelector(this, engine_);
 
   rightLayout_ = new QVBoxLayout;
@@ -349,6 +354,9 @@ MainGUI::createConnections()
 
   connect(continuousTab_, &ContinuousTab::switchToSingular,
           this, &MainGUI::switchToSingular);
+  connect(infoTab_, &InfoTab::switchToSingular,
+          [&](int index) { switchToSingular(index, -1); });
+
   connect(glyphDetails_, &GlyphDetails::closeDockWidget, 
           this, &MainGUI::closeDockWidget);
   connect(glyphDetails_, &GlyphDetails::switchToSingular,
diff --git a/src/ftinspect/maingui.hpp b/src/ftinspect/maingui.hpp
index d401382..add7671 100644
--- a/src/ftinspect/maingui.hpp
+++ b/src/ftinspect/maingui.hpp
@@ -12,6 +12,7 @@
 #include "panels/singular.hpp"
 #include "panels/continuous.hpp"
 #include "panels/comparator.hpp"
+#include "panels/info.hpp"
 #include "panels/glyphdetails.hpp"
 
 #include <vector>
@@ -95,6 +96,7 @@ private:
   SingularTab* singularTab_;
   ContinuousTab* continuousTab_;
   ComparatorTab* comparatorTab_;
+  InfoTab* infoTab_;
   QWidget* lastTab_ = NULL;
 
   QDockWidget* glyphDetailsDockWidget_;
diff --git a/src/ftinspect/meson.build b/src/ftinspect/meson.build
index 546ee5d..13a6bf9 100644
--- a/src/ftinspect/meson.build
+++ b/src/ftinspect/meson.build
@@ -27,6 +27,7 @@ if qt5_dep.found()
     'engine/paletteinfo.cpp',
     'engine/mmgx.cpp',
     'engine/fontinfo.cpp',
+    'engine/fontinfonamesmapping.cpp',
     'engine/stringrenderer.cpp',
     'engine/charmap.cpp',
 
@@ -45,12 +46,14 @@ if qt5_dep.found()
     'widgets/charmapcombobox.cpp',
 
     'models/customcomboboxmodels.cpp',
+    'models/fontinfomodels.cpp',
 
     'panels/settingpanel.cpp',
     'panels/settingpanelmmgx.cpp',
     'panels/singular.cpp',
     'panels/continuous.cpp',
     'panels/comparator.cpp',
+    'panels/info.cpp',
     'panels/glyphdetails.cpp',
 
     'ftinspect.cpp',
@@ -70,11 +73,13 @@ if qt5_dep.found()
       'widgets/charmapcombobox.hpp',
       'maingui.hpp',
       'models/customcomboboxmodels.hpp',
+      'models/fontinfomodels.hpp',
       'panels/settingpanel.hpp',
       'panels/settingpanelmmgx.hpp',
       'panels/singular.hpp',
       'panels/continuous.hpp',
       'panels/comparator.hpp',
+      'panels/info.hpp',
       'panels/glyphdetails.hpp',
     ],
     dependencies: qt5_dep)
diff --git a/src/ftinspect/models/fontinfomodels.cpp 
b/src/ftinspect/models/fontinfomodels.cpp
new file mode 100644
index 0000000..ea63e03
--- /dev/null
+++ b/src/ftinspect/models/fontinfomodels.cpp
@@ -0,0 +1,738 @@
+// fontinfomodels.cpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#include "fontinfomodels.hpp"
+#include "../engine/engine.hpp"
+
+int
+FixedSizeInfoModel::rowCount(const QModelIndex& parent) const
+{
+  if (parent.isValid())
+    return 0;
+  return static_cast<int>(storage_.size());
+}
+
+
+int
+FixedSizeInfoModel::columnCount(const QModelIndex& parent) const
+{
+  if (parent.isValid())
+    return 0;
+  return FSIM_Max;
+}
+
+
+QVariant
+FixedSizeInfoModel::data(const QModelIndex& index,
+                          int role) const
+{
+  if (index.row() < 0 || index.column() < 0)
+    return {};
+  auto r = static_cast<size_t>(index.row());
+  if ((role != Qt::DisplayRole && role != Qt::ToolTipRole)
+      || r > storage_.size())
+    return {};
+
+  auto& obj = storage_[r];
+  switch (static_cast<Columns>(index.column()))
+  {
+  case FSIM_Height:
+    return obj.height;
+  case FSIM_Width:
+    return obj.width;
+  case FSIM_Size:
+    return obj.size;
+  case FSIM_XPpem:
+    return obj.xPpem;
+  case FSIM_YPpem:
+    return obj.yPpem;
+  default:
+    break;
+  }
+
+  return {};
+}
+
+
+QVariant
+FixedSizeInfoModel::headerData(int section,
+                                Qt::Orientation orientation,
+                                int role) const
+{
+  if (role != Qt::DisplayRole)
+    return {};
+  if (orientation == Qt::Vertical)
+    return section;
+  if (orientation != Qt::Horizontal)
+    return {};
+
+  switch (static_cast<Columns>(section))
+  {
+  case FSIM_Height:
+    return tr("Height");
+  case FSIM_Width:
+    return tr("Width");
+  case FSIM_Size:
+    return tr("Size");
+  case FSIM_XPpem:
+    return tr("X ppem");
+  case FSIM_YPpem:
+    return tr("Y ppem");
+  default:
+    break;
+  }
+
+  return {};
+}
+
+
+int
+CharMapInfoModel::rowCount(const QModelIndex& parent) const
+{
+  if (parent.isValid())
+    return 0;
+  return static_cast<int>(storage_.size());
+}
+
+
+int
+CharMapInfoModel::columnCount(const QModelIndex& parent) const
+{
+  if (parent.isValid())
+    return 0;
+  return CMIM_Max;
+}
+
+
+QVariant
+CharMapInfoModel::data(const QModelIndex& index,
+                       int role) const
+{
+  // TODO reduce duplication
+  if (index.row() < 0 || index.column() < 0)
+    return {};
+  auto r = static_cast<size_t>(index.row());
+  if ((role != Qt::DisplayRole && role != Qt::ToolTipRole)
+      || r > storage_.size())
+    return {};
+
+  auto& obj = storage_[r];
+  switch (static_cast<Columns>(index.column()))
+  {
+  case CMIM_Platform: 
+    return QString("%1 {%2}")
+             .arg(obj.platformID)
+             .arg(*mapTTPlatformIDToName(obj.platformID));
+  case CMIM_Encoding:
+    return QString("%1 {%2}")
+             .arg(obj.encodingID)
+             .arg(*obj.encodingName);
+  case CMIM_FormatID: 
+    return static_cast<long long>(obj.formatID);
+  case CMIM_Language:
+    return static_cast<unsigned long long>(obj.languageID);
+  case CMIM_MaxIndex: 
+    return obj.maxIndex;
+  default:
+    break;
+  }
+
+  return {};
+}
+
+
+QVariant
+CharMapInfoModel::headerData(int section,
+                             Qt::Orientation orientation,
+                             int role) const
+{
+  if (role != Qt::DisplayRole)
+    return {};
+  if (orientation == Qt::Vertical)
+    return section;
+  if (orientation != Qt::Horizontal)
+    return {};
+
+  switch (static_cast<Columns>(section))
+  {
+  case CMIM_Platform:
+    return "Platform";
+  case CMIM_Encoding:
+    return "Encoding";
+  case CMIM_FormatID:
+    return "Format ID";
+  case CMIM_Language:
+    return "Language";
+  case CMIM_MaxIndex:
+    return "Max Code Point";
+  default:
+    break;
+  }
+
+  return {};
+}
+
+
+int
+SFNTNameModel::rowCount(const QModelIndex& parent) const
+{
+  if (parent.isValid())
+    return 0;
+  return static_cast<int>(storage_.size());
+}
+
+
+int
+SFNTNameModel::columnCount(const QModelIndex& parent) const
+{
+  if (parent.isValid())
+    return 0;
+  return SNM_Max;
+}
+
+
+QVariant
+SFNTNameModel::data(const QModelIndex& index,
+                    int role) const
+{
+  if (index.row() < 0 || index.column() < 0)
+    return {};
+
+  if (role == Qt::ToolTipRole && index.column() == SNM_Content)
+    return tr("Double click to view the string.");
+
+  auto r = static_cast<size_t>(index.row());
+  if ((role != Qt::DisplayRole && role != Qt::ToolTipRole)
+      || r > storage_.size())
+    return {};
+
+  auto& obj = storage_[r];
+  switch (static_cast<Columns>(index.column()))
+  {
+  case SNM_Name:
+    if (obj.nameID >= 256)
+      return QString::number(obj.nameID);
+    return QString("%1 {%2}").arg(QString::number(obj.nameID),
+                                  *mapSFNTNameIDToName(obj.nameID));
+    
+  case SNM_Platform:
+    return QString("%1 {%2}")
+             .arg(obj.platformID)
+             .arg(*mapTTPlatformIDToName(obj.platformID));
+  case SNM_Encoding:
+    return QString("%1 {%2}")
+             .arg(obj.encodingID)
+             .arg(*mapTTEncodingIDToName(obj.platformID, obj.encodingID));
+  case SNM_Language:
+    if (obj.languageID >= 0x8000)
+      return obj.langTag + "(lang tag)";
+    if (obj.platformID == 3)
+      return QString("0x%1 {%2}")
+               .arg(obj.languageID, 4, 16, QChar('0'))
+               .arg(*mapTTLanguageIDToName(obj.platformID, obj.languageID));
+    return QString("%1 {%2}")
+             .arg(obj.languageID)
+        .arg(*mapTTLanguageIDToName(obj.platformID, obj.languageID));
+  case SNM_Content:
+    return obj.str;
+  default:
+    break;
+  }
+
+  return {};
+}
+
+
+QVariant
+SFNTNameModel::headerData(int section,
+                          Qt::Orientation orientation,
+                          int role) const
+{
+  if (role != Qt::DisplayRole)
+    return {};
+  if (orientation == Qt::Vertical)
+    return section;
+  if (orientation != Qt::Horizontal)
+    return {};
+
+  switch (static_cast<Columns>(section))
+  {
+  case SNM_Name:
+    return "Name";
+  case SNM_Platform:
+    return "Platform";
+  case SNM_Encoding:
+    return "Encoding";
+  case SNM_Language:
+    return "Language";
+  case SNM_Content:
+    return "Content";
+  default:
+    break;
+  }
+
+  return {};
+}
+
+
+QString
+tagToString(unsigned long tag)
+{
+  QString str(4, '0');
+  str[0] = static_cast<char>(tag >> 24);
+  str[1] = static_cast<char>(tag >> 16);
+  str[2] = static_cast<char>(tag >> 8);
+  str[3] = static_cast<char>(tag);
+  return str;
+}
+
+
+int
+SFNTTableInfoModel::rowCount(const QModelIndex& parent) const
+{
+  if (parent.isValid())
+    return 0;
+  return static_cast<int>(storage_.size());
+}
+
+
+int
+SFNTTableInfoModel::columnCount(const QModelIndex& parent) const
+{
+  if (parent.isValid())
+    return 0;
+  return STIM_Max;
+}
+
+
+QVariant
+SFNTTableInfoModel::data(const QModelIndex& index,
+                         int role) const
+{
+  if (index.row() < 0 || index.column() < 0)
+    return {};
+  auto r = static_cast<size_t>(index.row());
+  if ((role != Qt::DisplayRole && role != Qt::ToolTipRole)
+      || r > storage_.size())
+    return {};
+
+  auto& obj = storage_[r];
+  switch (static_cast<Columns>(index.column()))
+  {
+  case STIM_Tag:
+    return tagToString(obj.tag);
+  case STIM_Offset:
+    return static_cast<unsigned long long>(obj.offset);
+  case STIM_Length:
+    return static_cast<unsigned long long>(obj.length);
+  case STIM_Valid:
+    return obj.valid;
+  case STIM_SharedFaces:
+    if (obj.sharedFaces.empty())
+      return "[]";
+  {
+    auto result = QString('[') + QString::number(*obj.sharedFaces.begin());
+    for (auto it = std::next(obj.sharedFaces.begin());
+         it != obj.sharedFaces.end();
+         ++it)
+    {
+      auto xStr = QString::number(*it);
+      result.reserve(result.length() + xStr.length() + 2);
+      result += ", ";
+      result += xStr;
+    }
+    result += ']';
+    return result;
+  }
+  default:
+    break;
+  }
+
+  return {};
+}
+
+
+QVariant
+SFNTTableInfoModel::headerData(int section,
+                               Qt::Orientation orientation,
+                               int role) const
+{
+  if (role != Qt::DisplayRole)
+    return {};
+  if (orientation == Qt::Vertical)
+    return section;
+  if (orientation != Qt::Horizontal)
+    return {};
+
+  switch (static_cast<Columns>(section))
+  {
+  case STIM_Tag:
+    return "Tag";
+  case STIM_Offset:
+    return "Offset";
+  case STIM_Length:
+    return "Length";
+  case STIM_Valid:
+    return "Valid";
+  case STIM_SharedFaces:
+    return "Subfont Indices";
+  default:;
+  }
+
+  return {};
+}
+
+
+int
+MMGXAxisInfoModel::rowCount(const QModelIndex& parent) const
+{
+  if (parent.isValid())
+    return 0;
+  return static_cast<int>(storage_.size());
+}
+
+
+int
+MMGXAxisInfoModel::columnCount(const QModelIndex& parent) const
+{
+  if (parent.isValid())
+    return 0;
+  return MAIM_Max;
+}
+
+
+QVariant
+MMGXAxisInfoModel::data(const QModelIndex& index,
+                        int role) const
+{
+  if (index.row() < 0 || index.column() < 0)
+    return {};
+  auto r = static_cast<size_t>(index.row());
+  if ((role != Qt::DisplayRole && role != Qt::ToolTipRole)
+      || r > storage_.size())
+    return {};
+
+  auto& obj = storage_[r];
+  switch (static_cast<Columns>(index.column()))
+  {
+  case MAIM_Tag:
+    return tagToString(obj.tag);
+  case MAIM_Minimum:
+    return obj.minimum;
+  case MAIM_Default:
+    return obj.def;
+  case MAIM_Maximum:
+    return obj.maximum;
+  case MAIM_Hidden:
+    return obj.hidden;
+  case MAIM_Name:
+    return obj.name;
+  default:
+    break;
+  }
+
+  return {};
+}
+
+
+QVariant
+MMGXAxisInfoModel::headerData(int section,
+                              Qt::Orientation orientation,
+                              int role) const
+{
+  if (role != Qt::DisplayRole)
+    return {};
+  if (orientation == Qt::Vertical)
+    return section;
+  if (orientation != Qt::Horizontal)
+    return {};
+
+  switch (static_cast<Columns>(section))
+  {
+  case MAIM_Tag:
+    return "Tag";
+  case MAIM_Minimum:
+    return "Minimum";
+  case MAIM_Default:
+    return "Default";
+  case MAIM_Maximum:
+    return "Maximum";
+  case MAIM_Hidden:
+    return "Hidden";
+  case MAIM_Name:
+    return "Name";
+  default: ;
+  }
+
+  return {};
+}
+
+
+int
+CompositeGlyphsInfoModel::rowCount(const QModelIndex& parent) const
+{
+  if (!parent.isValid())
+    return static_cast<int>(glyphs_.size());
+  auto id = parent.internalId();
+  if (id < 0 || id >= nodes_.size())
+    return 0;
+  auto gid = nodes_[id].glyphIndex;
+  auto iter = glyphMapper_.find(gid);
+  if (iter == glyphMapper_.end())
+    return 0;
+  if (iter->second > glyphs_.size())
+    return 0;
+  return static_cast<int>(glyphs_[iter->second]. subglyphs.size());
+}
+
+
+int
+CompositeGlyphsInfoModel::columnCount(const QModelIndex& parent) const
+{
+  return CGIM_Max;
+}
+
+
+QModelIndex
+CompositeGlyphsInfoModel::index(int row,
+                                int column,
+                                const QModelIndex& parent) const
+{
+  long long parentIdx = -1; // node index.
+  if (parent.isValid()) // Not top-level
+    parentIdx = static_cast<long long>(parent.internalId());
+  if (parentIdx < 0)
+    parentIdx = -1;
+  // find existing node by row and parent index, -1 for top-level
+  auto lookupPair = std::pair<int, long long>(row, parentIdx);
+
+  auto iter = nodeLookup_.find(lookupPair);
+  if (iter != nodeLookup_.end())
+  {
+    if (iter->second < 0 || static_cast<size_t>(iter->second) >= nodes_.size())
+      return {};
+    return createIndex(row, column, iter->second);
+  }
+
+  int glyphIndex = -1;
+  CompositeGlyphInfo::SubGlyph const* sgInfo = nullptr;
+  if (!parent.isValid()) // top-level nodes
+    glyphIndex = glyphs_[row].index;
+  else if (parent.internalId() < nodes_.size())
+  {
+    auto& parentInfoIndex = nodes_[parent.internalId()].glyphInfoIndex;
+    if (parentInfoIndex < 0
+        || static_cast<size_t>(parentInfoIndex) > glyphs_.size())
+      return {};
+
+    auto& sg = glyphs_[parentInfoIndex].subglyphs;
+    glyphIndex = sg[row].index;
+    sgInfo = &sg[row];
+  }
+
+  if (glyphIndex < 0)
+    return {};
+
+  ptrdiff_t glyphInfoIndex = -1;
+  auto iterGlyphInfoIter = glyphMapper_.find(glyphIndex);
+  if (iterGlyphInfoIter != glyphMapper_.end())
+    glyphInfoIndex = static_cast<ptrdiff_t>(iterGlyphInfoIter->second);
+  
+  InfoNode node = {
+    parentIdx,
+    row, glyphIndex, glyphInfoIndex,
+    sgInfo
+  };
+  nodes_.push_back(node);
+  nodeLookup_.emplace(std::pair<int, long long>(row, parentIdx),
+                      nodes_.size() - 1);
+
+  return createIndex(row, column, nodes_.size() - 1);
+}
+
+
+QModelIndex
+CompositeGlyphsInfoModel::parent(const QModelIndex& child) const
+{
+  if (!child.isValid())
+    return {};
+
+  auto id = static_cast<long long>(child.internalId());
+  if (id < 0 || static_cast<size_t>(id) >= nodes_.size())
+    return {};
+
+  auto pid = nodes_[id].parentNodeIndex;
+  if (pid < 0 || static_cast<size_t>(pid) >= nodes_.size())
+    return {};
+
+  auto& p = nodes_[pid];
+  return createIndex(p.indexInParent, 0, pid);
+}
+
+
+QVariant
+CompositeGlyphsInfoModel::data(const QModelIndex& index,
+                               int role) const
+{
+  if (!index.isValid())
+    return {};
+
+  auto id = index.internalId();
+  if (id >= nodes_.size())
+    return {};
+  auto& n = nodes_[id];
+  auto glyphIdx = n.glyphIndex;
+
+  if (role == Qt::ToolTipRole && index.column() == CGIM_Position)
+  {
+    if (!n.subGlyphInfo)
+      return {};
+    auto pos = n.subGlyphInfo->position;
+    switch (n.subGlyphInfo->positionType)
+    {
+    case CompositeGlyphInfo::SubGlyph::PT_Offset:
+      return QString("Add a offset (%1, %2) to the subglyph's points")
+          .arg(pos.first)
+          .arg(pos.second);
+    case CompositeGlyphInfo::SubGlyph::PT_Align:
+      return QString("Align parent's point %1 to subglyph's point %2")
+          .arg(pos.first)
+          .arg(pos.second);
+    }
+    return {};
+  }
+
+  if (role == Qt::DecorationRole && index.column() == CGIM_Glyph)
+  {
+    auto glyphIndex = n.glyphIndex;
+    auto iter = glyphIcons_.find(glyphIndex);
+    if (iter == glyphIcons_.end())
+      iter = glyphIcons_.emplace(glyphIndex, renderIcon(glyphIndex)).first;
+
+    auto& pixmap = iter->second;
+    if (pixmap.isNull())
+      return {};
+    return pixmap;
+  }
+
+  if (role != Qt::DisplayRole)
+    return {};
+
+  switch (static_cast<Columns>(index.column()))
+  {
+  case CGIM_Glyph:
+    if (engine_->currentFontHasGlyphName())
+      return QString("%1 
{%2}").arg(glyphIdx).arg(engine_->glyphName(glyphIdx));
+    return QString::number(glyphIdx);
+  case CGIM_Flag:
+    if (!n.subGlyphInfo)
+      return {};
+    return QString::number(n.subGlyphInfo->flag, 16).rightJustified(4, '0');
+  case CGIM_Position:
+  {
+    if (!n.subGlyphInfo)
+      return {};
+    auto pos = n.subGlyphInfo->position;
+    switch (n.subGlyphInfo->positionType)
+    {
+    case CompositeGlyphInfo::SubGlyph::PT_Offset:
+      return QString("Offset (%1, %2)").arg(pos.first).arg(pos.second);
+    case CompositeGlyphInfo::SubGlyph::PT_Align:
+      return QString("Align %1 -> %2").arg(pos.first).arg(pos.second);
+    }
+  }
+  default:;
+  }
+
+  return {};
+}
+
+
+QVariant
+CompositeGlyphsInfoModel::headerData(int section,
+                                     Qt::Orientation orientation,
+                                     int role) const
+{
+  if (role != Qt::DisplayRole)
+    return {};
+  if (orientation != Qt::Horizontal)
+    return {};
+
+  switch (static_cast<Columns>(section))
+  {
+  case CGIM_Glyph:
+    return tr("Glyph");
+  case CGIM_Flag:
+    return tr("Flags");
+  case CGIM_Position:
+    return tr("Position");
+  default:;
+  }
+  return {};
+}
+
+
+int
+CompositeGlyphsInfoModel::glyphIndexFromIndex(const QModelIndex& idx)
+{
+  if (!idx.isValid())
+    return -1;
+
+  auto id = idx.internalId();
+  if (id >= nodes_.size())
+    return -1;
+  auto& n = nodes_[id];
+  return n.glyphIndex;
+}
+
+
+void
+CompositeGlyphsInfoModel::beginModelUpdate()
+{
+  beginResetModel();
+  glyphs_.clear();
+  nodeLookup_.clear();
+  nodes_.clear();
+}
+
+
+void
+CompositeGlyphsInfoModel::endModelUpdate()
+{
+  glyphMapper_.clear();
+  for (size_t i = 0; i < glyphs_.size(); i++)
+    glyphMapper_.emplace(glyphs_[i].index, i);
+
+  glyphIcons_.clear();
+  endResetModel();
+}
+
+
+QPixmap
+CompositeGlyphsInfoModel::renderIcon(int glyphIndex) const
+{
+  engine_->setSizeByPixel(20); // This size is arbitrary
+  if (!engine_->currentPalette())
+    engine_->loadPalette();
+  auto image = engine_->renderingEngine()
+                      ->tryDirectRenderColorLayers(glyphIndex, NULL, false);
+  if (!image)
+  {
+    auto glyph = engine_->loadGlyph(glyphIndex);
+    if (!glyph)
+      return {};
+    image = engine_->renderingEngine()
+                   ->convertGlyphToQImage(glyph, NULL, false);
+  }
+
+  if (!image)
+    return {};
+
+  auto result = engine_->renderingEngine()->padToSize(image, 20);
+  delete image;
+  return result;
+}
+
+
+// end of fontinfomodels.cpp
diff --git a/src/ftinspect/models/fontinfomodels.hpp 
b/src/ftinspect/models/fontinfomodels.hpp
new file mode 100644
index 0000000..22d4aab
--- /dev/null
+++ b/src/ftinspect/models/fontinfomodels.hpp
@@ -0,0 +1,294 @@
+// fontinfomodels.hpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#pragma once
+
+#include "../engine/fontinfo.hpp"
+#include "../engine/charmap.hpp"
+#include "../engine/mmgx.hpp"
+
+#include <vector>
+#include <unordered_map>
+#include <QAbstractTableModel>
+#include <QPixmap>
+
+class FixedSizeInfoModel
+: public QAbstractTableModel
+{
+  Q_OBJECT
+public:
+  explicit FixedSizeInfoModel(QObject* parent) : QAbstractTableModel(parent) {}
+  ~FixedSizeInfoModel() override = default;
+
+  int rowCount(const QModelIndex& parent) const override;
+  int columnCount(const QModelIndex& parent) const override;
+  QVariant data(const QModelIndex& index,
+                int role) const override;
+  QVariant headerData(int section,
+                      Qt::Orientation orientation,
+                      int role) const override;
+
+  // Since we need to call `beginResetModel` right before updating, and need to
+  // call `endResetModel` after the internal storage is changed
+  // The model should be updated on-demand, and the internal storage is updated
+  // from `FontFixedSize::get`, we provide a callback for `get` to ensure the
+  // `beginResetModel` is called before the storage is changed,
+  // and the caller is responsible to call `endResetModel` according to `get`'s
+  // return value.
+  void beginModelUpdate() { beginResetModel(); }
+  void endModelUpdate() { endResetModel(); }
+  std::vector<FontFixedSize>& storage() { return storage_; }
+
+  enum Columns : int
+  {
+    FSIM_Height = 0,
+    FSIM_Width,
+    FSIM_Size,
+    FSIM_XPpem,
+    FSIM_YPpem,
+    FSIM_Max
+  };
+
+private:
+  // Don't let the item count exceed INT_MAX!
+  std::vector<FontFixedSize> storage_;
+};
+
+
+class CharMapInfoModel
+: public QAbstractTableModel
+{
+  Q_OBJECT
+public:
+  explicit CharMapInfoModel(QObject* parent) : QAbstractTableModel(parent) {}
+  ~CharMapInfoModel() override = default;
+
+  int rowCount(const QModelIndex& parent) const override;
+  int columnCount(const QModelIndex& parent) const override;
+  QVariant data(const QModelIndex& index,
+                int role) const override;
+  QVariant headerData(int section,
+                      Qt::Orientation orientation,
+                      int role) const override;
+
+  // Same to `FixedSizeInfoModel`
+  void beginModelUpdate() { beginResetModel(); }
+  void endModelUpdate() { endResetModel(); }
+  std::vector<CharMapInfo>& storage() { return storage_; }
+
+  enum Columns : int
+  {
+    CMIM_Platform   = 0,
+    CMIM_Encoding,
+    CMIM_FormatID,
+    CMIM_Language,
+    CMIM_MaxIndex,
+    CMIM_Max
+  };
+
+private:
+  // Don't let the item count exceed INT_MAX!
+  std::vector<CharMapInfo> storage_;
+};
+
+
+class SFNTNameModel
+: public QAbstractTableModel
+{
+  Q_OBJECT
+public:
+  explicit SFNTNameModel(QObject* parent) : QAbstractTableModel(parent) {}
+  ~SFNTNameModel() override = default;
+
+  int rowCount(const QModelIndex& parent) const override;
+  int columnCount(const QModelIndex& parent) const override;
+  QVariant data(const QModelIndex& index,
+                int role) const override;
+  QVariant headerData(int section,
+                      Qt::Orientation orientation,
+                      int role) const override;
+
+  // Same to `FixedSizeInfoModel`
+  void beginModelUpdate() { beginResetModel(); }
+  void endModelUpdate() { endResetModel(); }
+  std::vector<SFNTName>& storage() { return storage_; }
+
+  enum Columns : int
+  {
+    SNM_Name       = 0,
+    SNM_Platform,
+    SNM_Encoding,
+    SNM_Language,
+    SNM_Content,
+    SNM_Max
+  };
+
+private:
+  // Don't let the item count exceed INT_MAX!
+  std::vector<SFNTName> storage_;
+};
+
+
+class SFNTTableInfoModel
+: public QAbstractTableModel
+{
+  Q_OBJECT
+public:
+  explicit SFNTTableInfoModel(QObject* parent) : QAbstractTableModel(parent) {}
+  ~SFNTTableInfoModel() override = default;
+
+  int rowCount(const QModelIndex& parent) const override;
+  int columnCount(const QModelIndex& parent) const override;
+  QVariant data(const QModelIndex& index,
+                int role) const override;
+  QVariant headerData(int section,
+                      Qt::Orientation orientation,
+                      int role) const override;
+
+  // Same to `FixedSizeInfoModel`
+  void beginModelUpdate() { beginResetModel(); }
+  void endModelUpdate() { endResetModel(); }
+  std::vector<SFNTTableInfo>& storage() { return storage_; }
+
+  enum Columns : int
+  {
+    STIM_Tag = 0,
+    STIM_Offset,
+    STIM_Length,
+    STIM_Valid,
+    STIM_SharedFaces,
+    STIM_Max
+  };
+
+private:
+  // Don't let the item count exceed INT_MAX!
+  std::vector<SFNTTableInfo> storage_;
+};
+
+
+class MMGXAxisInfoModel
+: public QAbstractTableModel
+{
+  Q_OBJECT
+public:
+  explicit MMGXAxisInfoModel(QObject* parent) : QAbstractTableModel(parent) {}
+  ~MMGXAxisInfoModel() override = default;
+
+  int rowCount(const QModelIndex& parent) const override;
+  int columnCount(const QModelIndex& parent) const override;
+  QVariant data(const QModelIndex& index,
+                int role) const override;
+  QVariant headerData(int section,
+                      Qt::Orientation orientation,
+                      int role) const override;
+
+  // Same to `FixedSizeInfoModel`
+  void beginModelUpdate() { beginResetModel(); }
+  void endModelUpdate() { endResetModel(); }
+  std::vector<MMGXAxisInfo>& storage() { return storage_; }
+
+  enum Columns : int
+  {
+    MAIM_Tag = 0,
+    MAIM_Minimum,
+    MAIM_Default,
+    MAIM_Maximum,
+    MAIM_Hidden,
+    MAIM_Name,
+    MAIM_Max
+  };
+
+private:
+  // Don't let the item count exceed INT_MAX!
+  std::vector<MMGXAxisInfo> storage_;
+};
+
+
+struct LookupPairHash
+{
+public:
+  std::size_t
+  operator()(const std::pair<int, long long>& p) const
+  {
+    std::size_t seed = 0x291FEEA8;
+    seed ^= (seed << 6) + (seed >> 2) + 0x25F3E86D
+            + static_cast<std::size_t>(p.first);
+    seed ^= (seed << 6) + (seed >> 2) + 0x436E6B92
+            + static_cast<std::size_t>(p.second);
+    return seed;
+  }
+};
+
+
+// A tree model, so much more complicated.
+class CompositeGlyphsInfoModel : public QAbstractItemModel
+{
+  Q_OBJECT
+public:
+  // A lazily created info node.
+  struct InfoNode
+  {
+    long long parentNodeIndex;
+    int indexInParent;
+    int glyphIndex;
+    ptrdiff_t glyphInfoIndex;
+    CompositeGlyphInfo::SubGlyph const* subGlyphInfo;
+  };
+
+  explicit CompositeGlyphsInfoModel(QObject* parent, Engine* engine)
+      : QAbstractItemModel(parent), engine_(engine)
+  {
+  }
+
+  ~CompositeGlyphsInfoModel() override = default;
+
+  int rowCount(const QModelIndex& parent) const override;
+  int columnCount(const QModelIndex& parent) const override;
+  QModelIndex index(int row, int column,
+                    const QModelIndex& parent) const override;
+  QModelIndex parent(const QModelIndex& child) const override;
+  QVariant data(const QModelIndex& index, int role) const override;
+  QVariant headerData(int section, Qt::Orientation orientation,
+                      int role) const override;
+  int glyphIndexFromIndex(const QModelIndex& idx);
+
+  void beginModelUpdate();
+  void endModelUpdate();
+  std::vector<CompositeGlyphInfo>& storage() { return glyphs_; }
+
+  enum Columns : int
+  {
+    CGIM_Glyph = 0, // TODO: transformation, scale? consider more flags?
+    CGIM_Flag = 1,
+    CGIM_Position = 2,
+    CGIM_Max
+  };
+
+private:
+  Engine* engine_;
+  /*
+   * Take care of 3 types of index:
+   * 1. Glyph Index in Font File
+   * 2. Glyph Index in `glyphs_` - often called as "glyph info index"
+   * 3. Node Index
+   */
+  std::vector<CompositeGlyphInfo> glyphs_;
+
+  // glyph index -> glyph info index
+  std::unordered_map<int, size_t> glyphMapper_;
+  // map <row, parentId> to node
+  // the internal id of `QModelIndex` is the node's index
+  mutable std::unordered_map<std::pair<int, long long>,
+                             long long, LookupPairHash>
+          nodeLookup_;
+  mutable std::vector<InfoNode> nodes_;
+
+  mutable std::unordered_map<int, QPixmap> glyphIcons_;
+
+  // has to be const
+  QPixmap renderIcon(int glyphIndex) const;
+};
+
+
+// end of fontinfomodels.hpp
diff --git a/src/ftinspect/panels/info.cpp b/src/ftinspect/panels/info.cpp
new file mode 100644
index 0000000..6c64465
--- /dev/null
+++ b/src/ftinspect/panels/info.cpp
@@ -0,0 +1,1039 @@
+// info.cpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#include "info.hpp"
+
+#include "../uihelper.hpp"
+#include "../engine/engine.hpp"
+
+#include <cstring>
+#include <QStringList>
+#include <QHeaderView>
+
+
+#define GL2CRow(l, w) gridLayout2ColAddWidget(l,                \
+                                              w##PromptLabel_,  \
+                                              w##Label_)
+
+
+InfoTab::InfoTab(QWidget* parent,
+                 Engine* engine)
+: QWidget(parent), engine_(engine)
+{
+  createLayout();
+  createConnections();
+}
+
+
+void
+InfoTab::reloadFont()
+{
+  for (auto tab : tabs_)
+    tab->reloadFont();
+}
+
+
+void
+InfoTab::createLayout()
+{
+  generalTab_ = new GeneralInfoTab(this, engine_);
+  sfntTab_ = new SFNTInfoTab(this, engine_);
+  postScriptTab_ = new PostScriptInfoTab(this, engine_);
+  mmgxTab_ = new MMGXInfoTab(this, engine_);
+  compositeGlyphsTab_ = new CompositeGlyphsTab(this, engine_);
+
+  tab_ = new QTabWidget(this);
+  tab_->addTab(generalTab_, tr("General"));
+  tab_->addTab(sfntTab_, tr("SFNT"));
+  tab_->addTab(postScriptTab_, tr("PostScript"));
+  tab_->addTab(mmgxTab_, tr("MM/GX"));
+  tab_->addTab(compositeGlyphsTab_, tr("Composite Glyphs"));
+
+  tabs_.append(generalTab_);
+  tabs_.append(sfntTab_);
+  tabs_.append(postScriptTab_);
+  tabs_.append(mmgxTab_);
+  tabs_.append(compositeGlyphsTab_);
+
+  layout_ = new QHBoxLayout;
+  layout_->addWidget(tab_);
+
+  setLayout(layout_);
+}
+
+
+void
+InfoTab::createConnections()
+{
+  connect(compositeGlyphsTab_, &CompositeGlyphsTab::switchToSingular,
+          this, &InfoTab::switchToSingular);
+}
+
+
+GeneralInfoTab::GeneralInfoTab(QWidget* parent,
+                               Engine* engine)
+: QWidget(parent), engine_(engine)
+{
+  createLayout();
+}
+
+
+void
+GeneralInfoTab::reloadFont()
+{
+  auto basicInfo = FontBasicInfo::get(engine_);
+  // don't update when unnecessary
+  if (basicInfo != oldFontBasicInfo_) 
+  {
+    oldFontBasicInfo_ = basicInfo;
+    if (basicInfo.numFaces < 0)
+      numFacesLabel_->clear();
+    else
+      numFacesLabel_->setText(QString::number(basicInfo.numFaces));
+    
+          familyLabel_->setText(basicInfo.familyName);
+           styleLabel_->setText(basicInfo.styleName);
+      postscriptLabel_->setText(basicInfo.postscriptName);
+        revisionLabel_->setText(basicInfo.revision);
+       copyrightLabel_->setText(basicInfo.copyright);
+       trademarkLabel_->setText(basicInfo.trademark);
+    manufacturerLabel_->setText(basicInfo.manufacturer);
+    
+    createdLabel_->setText(
+        basicInfo.createdTime.toString("yyyy-MM-dd hh:mm:ss t"));
+    modifiedLabel_->setText(
+        basicInfo.modifiedTime.toString("yyyy-MM-dd hh:mm:ss t"));
+  }
+
+  auto fontTypeEntries = FontTypeEntries::get(engine_);
+
+  if (fontTypeEntries != oldFontTypeEntries_)
+  {
+    oldFontTypeEntries_ = fontTypeEntries;
+    QString directionText;
+    // Don't want to do concat...
+    if (fontTypeEntries.hasHorizontal && fontTypeEntries.hasVertical)
+      directionText = "honizontal, vertical";
+    else if (fontTypeEntries.hasHorizontal)
+      directionText = "horizontal";
+    else if (fontTypeEntries.hasVertical)
+      directionText = "vertical";
+    
+    QStringList types;
+    if (fontTypeEntries.scalable)
+      types += "scalable";
+    if (fontTypeEntries.mmgx)
+      types += "multiple master";
+    if (fontTypeEntries.fixedSizes)
+      types += "fixed sizes";
+    
+    driverNameLabel_->setText(fontTypeEntries.driverName);
+          sfntLabel_->setText(fontTypeEntries.sfnt ? "yes" : "no");
+      fontTypeLabel_->setText(types.join(", "));
+     directionLabel_->setText(directionText);
+    fixedWidthLabel_->setText(fontTypeEntries.fixedWidth ? "yes" : "no");
+    glyphNamesLabel_->setText(fontTypeEntries.glyphNames ? "available"
+                                                           : "unavailable");
+
+    if (fontTypeEntries.scalable)
+    {
+      emSizeLabel_->setText(QString::number(fontTypeEntries.emSize));
+      bboxLabel_->setText(QString("(%1, %2) : (%3, %4)")
+                            .arg(fontTypeEntries.globalBBox.xMin)
+                            .arg(fontTypeEntries.globalBBox.yMin)
+                            .arg(fontTypeEntries.globalBBox.xMax)
+                            .arg(fontTypeEntries.globalBBox.yMax));
+      ascenderLabel_->setText(QString::number(fontTypeEntries.ascender));
+      descenderLabel_->setText(QString::number(fontTypeEntries.descender));
+      maxAdvanceWidthLabel_
+        ->setText(QString::number(fontTypeEntries.maxAdvanceWidth));
+      maxAdvanceHeightLabel_
+        ->setText(QString::number(fontTypeEntries.maxAdvanceHeight));
+      ulPosLabel_
+        ->setText(QString::number(fontTypeEntries.underlinePos));
+      ulThicknessLabel_
+        ->setText(QString::number(fontTypeEntries.underlineThickness));
+
+      for (auto label : scalableOnlyLabels_)
+        label->setEnabled(true);
+    }
+    else
+    {
+      for (auto label : scalableOnlyLabels_)
+        label->setEnabled(false);
+    }
+  }
+
+  fixedSizesTable_->setEnabled(fontTypeEntries.fixedSizes);
+  bool reset
+    = FontFixedSize::get(engine_, 
+                         fixedSizeInfoModel_->storage(),
+                         [&] { fixedSizeInfoModel_->beginModelUpdate(); });
+  if (reset)
+    fixedSizeInfoModel_->endModelUpdate();
+
+  if (engine_->currentFontCharMaps() != charMapInfoModel_->storage())
+  {
+    charMapInfoModel_->beginModelUpdate();
+    charMapInfoModel_->storage() = engine_->currentFontCharMaps();
+    charMapInfoModel_->endModelUpdate();
+  }
+}
+
+
+void
+GeneralInfoTab::createLayout()
+{
+      numFacesPromptLabel_  = new QLabel(tr("Num of Faces:"), this);
+        familyPromptLabel_  = new QLabel(tr("Family Name:"), this);
+         stylePromptLabel_  = new QLabel(tr("Style Name:"), this);
+    postscriptPromptLabel_  = new QLabel(tr("PostScript Name:"), this);
+       createdPromptLabel_  = new QLabel(tr("Created at:"), this);
+      modifiedPromptLabel_  = new QLabel(tr("Modified at:"), this);
+      revisionPromptLabel_  = new QLabel(tr("Font Revision:"), this);
+     copyrightPromptLabel_  = new QLabel(tr("Copyright:"), this);
+     trademarkPromptLabel_  = new QLabel(tr("Trademark:"), this);
+  manufacturerPromptLabel_  = new QLabel(tr("Manufacturer:"), this);
+
+      numFacesLabel_ = new QLabel(this);
+        familyLabel_ = new QLabel(this);
+         styleLabel_ = new QLabel(this);
+    postscriptLabel_ = new QLabel(this);
+       createdLabel_ = new QLabel(this);
+      modifiedLabel_ = new QLabel(this);
+      revisionLabel_ = new QLabel(this);
+     copyrightLabel_ = new QLabel(this);
+     trademarkLabel_ = new QLabel(this);
+  manufacturerLabel_ = new QLabel(this);
+
+  setLabelSelectable(    numFacesLabel_);
+  setLabelSelectable(      familyLabel_);
+  setLabelSelectable(       styleLabel_);
+  setLabelSelectable(  postscriptLabel_);
+  setLabelSelectable(     createdLabel_);
+  setLabelSelectable(    modifiedLabel_);
+  setLabelSelectable(    revisionLabel_);
+  setLabelSelectable(   copyrightLabel_);
+  setLabelSelectable(   trademarkLabel_);
+  setLabelSelectable(manufacturerLabel_);
+
+     copyrightLabel_->setWordWrap(true);
+     trademarkLabel_->setWordWrap(true);
+  manufacturerLabel_->setWordWrap(true);
+
+  driverNamePromptLabel_ = new QLabel(tr("Driver:"), this);
+        sfntPromptLabel_ = new QLabel(tr("SFNT Wrapped:"), this);
+    fontTypePromptLabel_ = new QLabel(tr("Type:"), this);
+   directionPromptLabel_ = new QLabel(tr("Direction:"), this);
+  fixedWidthPromptLabel_ = new QLabel(tr("Fixed Width:"), this);
+  glyphNamesPromptLabel_ = new QLabel(tr("Glyph Names:"), this);
+
+  driverNameLabel_ = new QLabel(this);
+        sfntLabel_ = new QLabel(this);
+    fontTypeLabel_ = new QLabel(this);
+   directionLabel_ = new QLabel(this);
+  fixedWidthLabel_ = new QLabel(this);
+  glyphNamesLabel_ = new QLabel(this);
+
+  setLabelSelectable(driverNameLabel_);
+  setLabelSelectable(      sfntLabel_);
+  setLabelSelectable(  fontTypeLabel_);
+  setLabelSelectable( directionLabel_);
+  setLabelSelectable(fixedWidthLabel_);
+  setLabelSelectable(glyphNamesLabel_);
+
+            emSizePromptLabel_ = new QLabel(tr("EM Size:"), this);
+              bboxPromptLabel_ = new QLabel(tr("Global BBox:"), this);
+          ascenderPromptLabel_ = new QLabel(tr("Ascender:"), this);
+         descenderPromptLabel_ = new QLabel(tr("Descender:"), this);
+   maxAdvanceWidthPromptLabel_ = new QLabel(tr("Max Advance Width:"), this);
+  maxAdvanceHeightPromptLabel_ = new QLabel(tr("Max Advance Height:"), this);
+             ulPosPromptLabel_ = new QLabel(tr("Underline Position:"), this);
+       ulThicknessPromptLabel_ = new QLabel(tr("Underline Thickness:"), this);
+
+            emSizeLabel_ = new QLabel(this);
+              bboxLabel_ = new QLabel(this);
+          ascenderLabel_ = new QLabel(this);
+         descenderLabel_ = new QLabel(this);
+   maxAdvanceWidthLabel_ = new QLabel(this);
+  maxAdvanceHeightLabel_ = new QLabel(this);
+             ulPosLabel_ = new QLabel(this);
+       ulThicknessLabel_ = new QLabel(this);
+
+  setLabelSelectable(          emSizeLabel_);
+  setLabelSelectable(            bboxLabel_);
+  setLabelSelectable(        ascenderLabel_);
+  setLabelSelectable(       descenderLabel_);
+  setLabelSelectable( maxAdvanceWidthLabel_);
+  setLabelSelectable(maxAdvanceHeightLabel_);
+  setLabelSelectable(           ulPosLabel_);
+  setLabelSelectable(     ulThicknessLabel_);
+
+  scalableOnlyLabels_.push_back(          emSizePromptLabel_);
+  scalableOnlyLabels_.push_back(            bboxPromptLabel_);
+  scalableOnlyLabels_.push_back(        ascenderPromptLabel_);
+  scalableOnlyLabels_.push_back(       descenderPromptLabel_);
+  scalableOnlyLabels_.push_back( maxAdvanceWidthPromptLabel_);
+  scalableOnlyLabels_.push_back(maxAdvanceHeightPromptLabel_);
+  scalableOnlyLabels_.push_back(           ulPosPromptLabel_);
+  scalableOnlyLabels_.push_back(     ulThicknessPromptLabel_);
+  scalableOnlyLabels_.push_back(          emSizeLabel_);
+  scalableOnlyLabels_.push_back(            bboxLabel_);
+  scalableOnlyLabels_.push_back(        ascenderLabel_);
+  scalableOnlyLabels_.push_back(       descenderLabel_);
+  scalableOnlyLabels_.push_back( maxAdvanceWidthLabel_);
+  scalableOnlyLabels_.push_back(maxAdvanceHeightLabel_);
+  scalableOnlyLabels_.push_back(           ulPosLabel_);
+  scalableOnlyLabels_.push_back(     ulThicknessLabel_);
+
+        basicGroupBox_ = new QGroupBox(tr("Basic"), this);
+  typeEntriesGroupBox_ = new QGroupBox(tr("Type Entries"), this);
+      charMapGroupBox_ = new QGroupBox(tr("CharMaps"), this);
+  fixedSizesGroupBox_  = new QGroupBox(tr("Fixed Sizes"), this);
+
+  charMapsTable_ = new QTableView(this);
+  fixedSizesTable_ = new QTableView(this);
+
+  charMapInfoModel_ = new CharMapInfoModel(this);
+  charMapsTable_->setModel(charMapInfoModel_);
+  auto header = charMapsTable_->verticalHeader();
+  // This will force the minimal size to be used
+  header->setDefaultSectionSize(0);
+  header->setSectionResizeMode(QHeaderView::Fixed);
+
+  fixedSizeInfoModel_ = new FixedSizeInfoModel(this);
+  fixedSizesTable_->setModel(fixedSizeInfoModel_);
+  header = fixedSizesTable_->verticalHeader();
+  header->setDefaultSectionSize(0);
+  header->setSectionResizeMode(QHeaderView::Fixed);
+
+  leftWidget_ = new QWidget(this);
+  leftScrollArea_ = new UnboundScrollArea(this);
+  leftScrollArea_->setWidgetResizable(true);
+  leftScrollArea_->setWidget(leftWidget_);
+  leftScrollArea_->setStyleSheet("QScrollArea 
{background-color:transparent;}");
+  leftWidget_->setStyleSheet("background-color:transparent;");
+
+  basicLayout_       = new QGridLayout;
+  typeEntriesLayout_ = new QGridLayout;
+  charMapLayout_     = new QHBoxLayout;
+  fixedSizesLayout_  = new QHBoxLayout;
+
+#define BasicRow(w) GL2CRow(basicLayout_, w)
+#define FTERow(w) GL2CRow(typeEntriesLayout_, w)
+
+  BasicRow(    numFaces);
+  BasicRow(      family);
+  BasicRow(       style);
+  BasicRow(  postscript);
+  BasicRow(     created);
+  BasicRow(    modified);
+  BasicRow(    revision);
+  BasicRow(   copyright);
+  BasicRow(   trademark);
+  BasicRow(manufacturer);
+
+  FTERow(      driverName);
+  FTERow(            sfnt);
+  FTERow(        fontType);
+  FTERow(       direction);
+  FTERow(      fixedWidth);
+  FTERow(      glyphNames);
+  FTERow(          emSize);
+  FTERow(            bbox);
+  FTERow(        ascender);
+  FTERow(       descender);
+  FTERow( maxAdvanceWidth);
+  FTERow(maxAdvanceHeight);
+  FTERow(           ulPos);
+  FTERow(     ulThickness);
+
+        basicLayout_->setColumnStretch(1, 1);      
+  typeEntriesLayout_->setColumnStretch(1, 1);
+
+  charMapLayout_->addWidget(charMapsTable_);
+  fixedSizesLayout_->addWidget(fixedSizesTable_);
+
+        basicGroupBox_ ->setLayout(basicLayout_      );
+  typeEntriesGroupBox_ ->setLayout(typeEntriesLayout_);
+      charMapGroupBox_ ->setLayout(charMapLayout_    );
+  fixedSizesGroupBox_  ->setLayout(fixedSizesLayout_ );
+
+  leftLayout_ = new QVBoxLayout;
+  rightLayout_ = new QVBoxLayout;
+  mainLayout_ = new QHBoxLayout;
+
+  leftLayout_->addWidget(basicGroupBox_);
+  leftLayout_->addWidget(typeEntriesGroupBox_);
+  leftLayout_->addSpacerItem(new QSpacerItem(0, 0, 
+                                             QSizePolicy::Preferred, 
+                                             QSizePolicy::Expanding));
+
+  leftWidget_->setLayout(leftLayout_);
+
+  rightLayout_->addWidget(charMapGroupBox_);
+  rightLayout_->addWidget(fixedSizesGroupBox_);
+
+  mainLayout_->addWidget(leftScrollArea_);
+  mainLayout_->addLayout(rightLayout_);
+  setLayout(mainLayout_);
+}
+
+
+StringViewDialog::StringViewDialog(QWidget* parent)
+: QDialog(parent)
+{
+  createLayout();
+}
+
+
+void
+StringViewDialog::updateString(QByteArray const& rawArray,
+                               QString const& str)
+{
+  textEdit_->setText(str);
+  hexTextEdit_->setText(rawArray.toHex());
+}
+
+
+void
+StringViewDialog::createLayout()
+{
+  textEdit_ = new QTextEdit(this);
+  hexTextEdit_ = new QTextEdit(this);
+
+  textEdit_->setLineWrapMode(QTextEdit::WidgetWidth);
+  hexTextEdit_->setLineWrapMode(QTextEdit::WidgetWidth);
+
+  textLabel_ = new QLabel(tr("Text"), this);
+  hexTextLabel_ = new QLabel(tr("Raw Bytes"), this);
+
+  layout_ = new QVBoxLayout;
+
+  layout_->addWidget(textLabel_);
+  layout_->addWidget(textEdit_);
+  layout_->addWidget(hexTextLabel_);
+  layout_->addWidget(hexTextEdit_);
+
+  resize(600, 400);
+
+  setLayout(layout_);
+}
+
+
+SFNTInfoTab::SFNTInfoTab(QWidget* parent,
+                         Engine* engine)
+: QWidget(parent), engine_(engine)
+{
+  createLayout();
+  createConnections();
+}
+
+
+void
+SFNTInfoTab::reloadFont()
+{
+  engine_->reloadFont();
+  auto face = engine_->currentFallbackFtFace();
+  setEnabled(face && FT_IS_SFNT(face));
+
+  if (engine_->currentFontSFNTNames() != sfntNamesModel_->storage())
+  {
+    sfntNamesModel_->beginModelUpdate();
+    sfntNamesModel_->storage() = engine_->currentFontSFNTNames();
+    sfntNamesModel_->endModelUpdate();
+  }
+
+  if (engine_->currentFontSFNTTableInfo() != sfntTablesModel_->storage())
+  {
+    sfntTablesModel_->beginModelUpdate();
+    sfntTablesModel_->storage() = engine_->currentFontSFNTTableInfo();
+    sfntTablesModel_->endModelUpdate();
+  }
+}
+
+
+void
+SFNTInfoTab::createLayout()
+{
+  sfntNamesGroupBox_ = new QGroupBox(tr("SFNT Name Table"), this);
+  sfntTablesGroupBox_ = new QGroupBox(tr("SFNT Tables"), this);
+
+  sfntNamesTable_ = new QTableView(this);
+  sfntTablesTable_ = new QTableView(this);
+
+  sfntNamesModel_ = new SFNTNameModel(this);
+  sfntNamesTable_->setModel(sfntNamesModel_);
+  auto header = sfntNamesTable_->verticalHeader();
+  // This will force the minimal size to be used
+  header->setDefaultSectionSize(0);
+  header->setSectionResizeMode(QHeaderView::Fixed);
+  sfntNamesTable_->horizontalHeader()->setStretchLastSection(true);
+
+  sfntTablesModel_ = new SFNTTableInfoModel(this);
+  sfntTablesTable_->setModel(sfntTablesModel_);
+  header = sfntTablesTable_->verticalHeader();
+  // This will force the minimal size to be used
+  header->setDefaultSectionSize(0);
+  header->setSectionResizeMode(QHeaderView::Fixed);
+  sfntTablesTable_->horizontalHeader()->setStretchLastSection(true);
+
+  sfntNamesLayout_ = new QHBoxLayout;
+  sfntTablesLayout_ = new QHBoxLayout;
+
+  sfntNamesLayout_->addWidget(sfntNamesTable_);
+  sfntTablesLayout_->addWidget(sfntTablesTable_);
+
+  sfntNamesGroupBox_->setLayout(sfntNamesLayout_);
+  sfntTablesGroupBox_->setLayout(sfntTablesLayout_);
+
+  mainLayout_ = new QHBoxLayout;
+
+  mainLayout_->addWidget(sfntNamesGroupBox_);
+  mainLayout_->addWidget(sfntTablesGroupBox_);
+
+  setLayout(mainLayout_);
+
+  stringViewDialog_ = new StringViewDialog(this);
+}
+
+
+void
+SFNTInfoTab::createConnections()
+{
+  connect(sfntNamesTable_, &QTableView::doubleClicked,
+          this, &SFNTInfoTab::nameTableDoubleClicked);
+}
+
+
+void
+SFNTInfoTab::nameTableDoubleClicked(QModelIndex const& index)
+{
+  if (index.column() != SFNTNameModel::SNM_Content)
+    return;
+  auto& storage = sfntNamesModel_->storage();
+  if (index.row() < 0 || static_cast<size_t>(index.row()) > storage.size())
+    return;
+
+  auto& obj = storage[index.row()];
+  stringViewDialog_->updateString(obj.strBuf, obj.str);
+  stringViewDialog_->exec();
+}
+
+
+PostScriptInfoTab::PostScriptInfoTab(QWidget* parent,
+                                     Engine* engine)
+: QWidget(parent), engine_(engine)
+{
+  std::memset(&oldFontPrivate_, 0, sizeof(PS_PrivateRec));
+  createLayout();
+}
+
+
+template<class T>
+QString genArrayString(T* arr, size_t size)
+{
+  // TODO: optimize
+  QString result = "[";
+  for (size_t i = 0; i < size; i++)
+  {
+    result += QString::number(arr[i]);
+    if (i < size - 1)
+      result += ", ";
+  }
+  return result + "]";
+}
+
+
+// We don't have C++20, so...
+template <class T, std::ptrdiff_t N>
+constexpr std::ptrdiff_t
+arraySize(const T (&)[N]) noexcept
+{
+  return N;
+}
+
+
+void
+PostScriptInfoTab::reloadFont()
+{
+  PS_FontInfoRec fontInfo;
+  auto hasInfo = engine_->currentFontPSInfo(fontInfo);
+  infoGroupBox_->setEnabled(hasInfo);
+  if (hasInfo)
+  {
+        versionLabel_->setText(QString::fromUtf8(fontInfo.version));
+         noticeLabel_->setText(QString::fromUtf8(fontInfo.notice));
+       fullNameLabel_->setText(QString::fromUtf8(fontInfo.full_name));
+     familyNameLabel_->setText(QString::fromUtf8(fontInfo.family_name));
+         weightLabel_->setText(QString::fromUtf8(fontInfo.weight));
+    italicAngleLabel_->setText(QString::number(fontInfo.italic_angle));
+     fixedPitchLabel_->setText(fontInfo.is_fixed_pitch ? "yes" : "no");
+          ulPosLabel_->setText(QString::number(fontInfo.underline_position));
+    ulThicknessLabel_->setText(QString::number(fontInfo.underline_thickness));
+  }
+  else
+  {
+        versionLabel_->clear();
+         noticeLabel_->clear();
+       fullNameLabel_->clear();
+     familyNameLabel_->clear();
+         weightLabel_->clear();
+    italicAngleLabel_->clear();
+     fixedPitchLabel_->clear();
+          ulPosLabel_->clear();
+    ulThicknessLabel_->clear();
+  }
+
+  PS_PrivateRec fontPrivate;
+  // Don't do zero-initialization since we need to zero out paddings
+  std::memset(&fontPrivate, 0, sizeof(PS_PrivateRec));
+  hasInfo = engine_->currentFontPSPrivateInfo(fontPrivate);
+  privateGroupBox_->setEnabled(hasInfo);
+  if (hasInfo)
+  {
+    if (std::memcmp(&fontPrivate, &oldFontPrivate_, sizeof(PS_PrivateRec)))
+    {
+      std::memcpy(&oldFontPrivate_, &fontPrivate, sizeof(PS_PrivateRec));
+      
+      uniqueIDLabel_->setText(QString::number(fontPrivate.unique_id));
+      blueShiftLabel_->setText(QString::number(fontPrivate.blue_shift));
+      blueFuzzLabel_->setText(QString::number(fontPrivate.blue_fuzz));
+      forceBoldLabel_->setText(fontPrivate.force_bold ? "true" : "false");
+      
languageGroupLabel_->setText(QString::number(fontPrivate.language_group));
+      passwordLabel_->setText(QString::number(fontPrivate.password));
+      lenIVLabel_->setText(QString::number(fontPrivate.lenIV));
+      roundStemUpLabel_->setText(fontPrivate.round_stem_up ? "true" : "false");
+
+      familyBluesLabel_->setText(
+        genArrayString(fontPrivate.family_blues, 
fontPrivate.num_family_blues));
+      blueValuesLabel_->setText(
+        genArrayString(fontPrivate.blue_values, fontPrivate.num_blue_values));
+      otherBluesLabel_->setText(
+        genArrayString(fontPrivate.other_blues, fontPrivate.num_other_blues));
+      familyOtherBluesLabel_->setText(
+        genArrayString(fontPrivate.family_other_blues,
+        fontPrivate.num_family_other_blues));
+      stdWidthsLabel_->setText(
+        genArrayString(fontPrivate.standard_width,
+                       arraySize(fontPrivate.standard_width)));
+      stdHeightsLabel_->setText(
+        genArrayString(fontPrivate.standard_height, 
+                       arraySize(fontPrivate.standard_height)));
+      snapWidthsLabel_->setText(
+        genArrayString(fontPrivate.snap_widths, fontPrivate.num_snap_widths));
+      snapHeightsLabel_->setText(
+        genArrayString(fontPrivate.snap_heights, 
fontPrivate.num_snap_heights));
+      minFeatureLabel_->setText(
+        genArrayString(fontPrivate.min_feature,
+                       arraySize(fontPrivate.min_feature)));
+
+      blueScaleLabel_->setText(
+        QString::number(fontPrivate.blue_scale / 65536.0 / 1000.0, 'f', 6));
+      expansionFactorLabel_->setText(
+        QString::number(fontPrivate.expansion_factor / 65536.0, 'f', 4));
+    }
+  }
+  else
+  {
+    std::memset(&oldFontPrivate_, 0, sizeof(PS_PrivateRec));
+            uniqueIDLabel_->clear();
+          blueValuesLabel_->clear();
+          otherBluesLabel_->clear();
+         familyBluesLabel_->clear();
+    familyOtherBluesLabel_->clear();
+           blueScaleLabel_->clear();
+           blueShiftLabel_->clear();
+            blueFuzzLabel_->clear();
+           stdWidthsLabel_->clear();
+          stdHeightsLabel_->clear();
+          snapWidthsLabel_->clear();
+         snapHeightsLabel_->clear();
+           forceBoldLabel_->clear();
+       languageGroupLabel_->clear();
+            passwordLabel_->clear();
+               lenIVLabel_->clear();
+          minFeatureLabel_->clear();
+         roundStemUpLabel_->clear();
+     expansionFactorLabel_->clear();
+  }
+}
+
+
+void
+PostScriptInfoTab::createLayout()
+{
+      versionPromptLabel_  = new QLabel(tr("/version:"), this);
+       noticePromptLabel_  = new QLabel(tr("/Notice:"), this);
+     fullNamePromptLabel_  = new QLabel(tr("/FullName:"), this);
+   familyNamePromptLabel_  = new QLabel(tr("/FamilyName:"), this);
+       weightPromptLabel_  = new QLabel(tr("/Weight:"), this);
+  italicAnglePromptLabel_  = new QLabel(tr("/ItaticAngle:"), this);
+   fixedPitchPromptLabel_  = new QLabel(tr("/isFixedPitch:"), this);
+        ulPosPromptLabel_  = new QLabel(tr("/UnderlinePosition:"), this);
+  ulThicknessPromptLabel_  = new QLabel(tr("/UnderlineThickness:"), this);
+
+      versionLabel_ = new QLabel(this);
+       noticeLabel_ = new QLabel(this);
+     fullNameLabel_ = new QLabel(this);
+   familyNameLabel_ = new QLabel(this);
+       weightLabel_ = new QLabel(this);
+  italicAngleLabel_ = new QLabel(this);
+   fixedPitchLabel_ = new QLabel(this);
+        ulPosLabel_ = new QLabel(this);
+  ulThicknessLabel_ = new QLabel(this);
+
+  setLabelSelectable(    versionLabel_);
+  setLabelSelectable(     noticeLabel_);
+  setLabelSelectable(   fullNameLabel_);
+  setLabelSelectable( familyNameLabel_);
+  setLabelSelectable(     weightLabel_);
+  setLabelSelectable(italicAngleLabel_);
+  setLabelSelectable( fixedPitchLabel_);
+  setLabelSelectable(      ulPosLabel_);
+  setLabelSelectable(ulThicknessLabel_);
+
+          uniqueIDPromptLabel_  = new QLabel(tr("/UniqueID:"), this);
+        blueValuesPromptLabel_  = new QLabel(tr("/BlueValues:"), this);
+        otherBluesPromptLabel_  = new QLabel(tr("/OtherBlues:"), this);
+       familyBluesPromptLabel_  = new QLabel(tr("/FamilyBlues:"), this);
+  familyOtherBluesPromptLabel_  = new QLabel(tr("/FamilyOtherBlues:"), this);
+         blueScalePromptLabel_  = new QLabel(tr("/BlueScale:"), this);
+         blueShiftPromptLabel_  = new QLabel(tr("/BlueShift:"), this);
+          blueFuzzPromptLabel_  = new QLabel(tr("/BlueFuzz:"), this);
+         stdWidthsPromptLabel_  = new QLabel(tr("/StdHW:"), this);
+        stdHeightsPromptLabel_  = new QLabel(tr("/StdVW:"), this);
+        snapWidthsPromptLabel_  = new QLabel(tr("/StemSnapH:"), this);
+       snapHeightsPromptLabel_  = new QLabel(tr("/StemSnapV:"), this);
+         forceBoldPromptLabel_  = new QLabel(tr("/ForceBold:"), this);
+     languageGroupPromptLabel_  = new QLabel(tr("/LanguageGroup:"), this);
+          passwordPromptLabel_  = new QLabel(tr("/password:"), this);
+             lenIVPromptLabel_  = new QLabel(tr("/lenIV:"), this);
+        minFeaturePromptLabel_  = new QLabel(tr("/MinFeature:"), this);
+       roundStemUpPromptLabel_  = new QLabel(tr("/RndStemUp:"), this);
+   expansionFactorPromptLabel_  = new QLabel(tr("/ExpansionFactor:"), this);
+
+          uniqueIDLabel_ = new QLabel(this);
+        blueValuesLabel_ = new QLabel(this);
+        otherBluesLabel_ = new QLabel(this);
+       familyBluesLabel_ = new QLabel(this);
+  familyOtherBluesLabel_ = new QLabel(this);
+         blueScaleLabel_ = new QLabel(this);
+         blueShiftLabel_ = new QLabel(this);
+          blueFuzzLabel_ = new QLabel(this);
+         stdWidthsLabel_ = new QLabel(this);
+        stdHeightsLabel_ = new QLabel(this);
+        snapWidthsLabel_ = new QLabel(this);
+       snapHeightsLabel_ = new QLabel(this);
+         forceBoldLabel_ = new QLabel(this);
+     languageGroupLabel_ = new QLabel(this);
+          passwordLabel_ = new QLabel(this);
+             lenIVLabel_ = new QLabel(this);
+        minFeatureLabel_ = new QLabel(this);
+       roundStemUpLabel_ = new QLabel(this);
+   expansionFactorLabel_ = new QLabel(this);
+
+  setLabelSelectable(        uniqueIDLabel_);
+  setLabelSelectable(      blueValuesLabel_);
+  setLabelSelectable(      otherBluesLabel_);
+  setLabelSelectable(     familyBluesLabel_);
+  setLabelSelectable(familyOtherBluesLabel_);
+  setLabelSelectable(       blueScaleLabel_);
+  setLabelSelectable(       blueShiftLabel_);
+  setLabelSelectable(        blueFuzzLabel_);
+  setLabelSelectable(       stdWidthsLabel_);
+  setLabelSelectable(      stdHeightsLabel_);
+  setLabelSelectable(      snapWidthsLabel_);
+  setLabelSelectable(     snapHeightsLabel_);
+  setLabelSelectable(       forceBoldLabel_);
+  setLabelSelectable(   languageGroupLabel_);
+  setLabelSelectable(        passwordLabel_);
+  setLabelSelectable(           lenIVLabel_);
+  setLabelSelectable(      minFeatureLabel_);
+  setLabelSelectable(     roundStemUpLabel_);
+  setLabelSelectable( expansionFactorLabel_);
+
+  noticeLabel_->setWordWrap(true);
+  familyBluesLabel_->setWordWrap(true);
+  blueValuesLabel_->setWordWrap(true);
+  otherBluesLabel_->setWordWrap(true);
+  familyOtherBluesLabel_->setWordWrap(true);
+  stdWidthsLabel_->setWordWrap(true);
+  stdHeightsLabel_->setWordWrap(true);
+  snapWidthsLabel_->setWordWrap(true);
+  snapHeightsLabel_->setWordWrap(true);
+  minFeatureLabel_->setWordWrap(true);
+
+  infoGroupBox_ = new QGroupBox(tr("PostScript /FontInfo dictionary"), this);
+  privateGroupBox_ = new QGroupBox(tr("PostScript /Private dictionary"), this);
+
+  infoWidget_ = new QWidget(this);
+  privateWidget_ = new QWidget(this);
+
+  infoScrollArea_ = new UnboundScrollArea(this);
+  infoScrollArea_->setWidget(infoWidget_);
+  infoScrollArea_->setWidgetResizable(true);
+  infoScrollArea_->setStyleSheet("QScrollArea 
{background-color:transparent;}");
+  infoWidget_->setStyleSheet("background-color:transparent;");
+  infoWidget_->setContentsMargins(0, 0, 0, 0);
+
+  privateScrollArea_ = new UnboundScrollArea(this);
+  privateScrollArea_->setWidget(privateWidget_);
+  privateScrollArea_->setWidgetResizable(true);
+  privateScrollArea_->setStyleSheet("QScrollArea 
{background-color:transparent;}");
+  privateWidget_->setStyleSheet("background-color:transparent;");
+  privateWidget_->setContentsMargins(0, 0, 0, 0);
+
+  infoLayout_ = new QGridLayout;
+  privateLayout_ = new QGridLayout;
+  infoGroupBoxLayout_ = new QHBoxLayout;
+  privateGroupBoxLayout_ = new QHBoxLayout;
+
+#define PSI2Row(w) GL2CRow(infoLayout_, w)
+#define PSP2Row(w) GL2CRow(privateLayout_, w)
+
+  PSI2Row(    version);
+  PSI2Row(     notice);
+  PSI2Row(   fullName);
+  PSI2Row( familyName);
+  PSI2Row(     weight);
+  PSI2Row(italicAngle);
+  PSI2Row( fixedPitch);
+  PSI2Row(      ulPos);
+  PSI2Row(ulThickness);
+
+  PSP2Row(        uniqueID);
+  PSP2Row(      blueValues);
+  PSP2Row(      otherBlues);
+  PSP2Row(     familyBlues);
+  PSP2Row(familyOtherBlues);
+  PSP2Row(       blueScale);
+  PSP2Row(       blueShift);
+  PSP2Row(        blueFuzz);
+  PSP2Row(       stdWidths);
+  PSP2Row(      stdHeights);
+  PSP2Row(      snapWidths);
+  PSP2Row(     snapHeights);
+  PSP2Row(       forceBold);
+  PSP2Row(   languageGroup);
+  PSP2Row(        password);
+  PSP2Row(           lenIV);
+  PSP2Row(      minFeature);
+  PSP2Row(     roundStemUp);
+  PSP2Row( expansionFactor);
+
+  infoLayout_->addItem(new QSpacerItem(0, 0, 
+                                       QSizePolicy::Preferred, 
+                                       QSizePolicy::Expanding),
+                       infoLayout_->rowCount(), 0, 1, 2);
+  privateLayout_->addItem(new QSpacerItem(0, 0, 
+                                          QSizePolicy::Preferred, 
+                                          QSizePolicy::Expanding),
+                          privateLayout_->rowCount(), 0, 1, 2);
+
+     infoLayout_->setColumnStretch(1, 1);
+  privateLayout_->setColumnStretch(1, 1);
+
+  infoWidget_->setLayout(infoLayout_);
+  privateWidget_->setLayout(privateLayout_);
+  infoGroupBox_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Expanding);
+  privateGroupBox_->setSizePolicy(QSizePolicy::Ignored, 
QSizePolicy::Expanding);
+
+  infoGroupBoxLayout_->addWidget(infoScrollArea_);
+  privateGroupBoxLayout_->addWidget(privateScrollArea_);
+  infoGroupBox_->setLayout(infoGroupBoxLayout_);
+  privateGroupBox_->setLayout(privateGroupBoxLayout_);
+
+  mainLayout_ = new QHBoxLayout;
+  mainLayout_->addWidget(infoGroupBox_);
+  mainLayout_->addWidget(privateGroupBox_);
+  setLayout(mainLayout_);
+}
+
+
+MMGXInfoTab::MMGXInfoTab(QWidget* parent,
+                         Engine* engine)
+: QWidget(parent), engine_(engine)
+{
+  createLayout();
+}
+
+
+void
+MMGXInfoTab::reloadFont()
+{
+  auto state = engine_->currentFontMMGXState();
+  axesGroupBox_->setEnabled(state != MMGXState::NoMMGX);
+  switch (state)
+  {
+  case MMGXState::NoMMGX: 
+    mmgxTypeLabel_->setText("No MM/GX");
+    break;
+  case MMGXState::MM:
+    mmgxTypeLabel_->setText("Adobe Multiple Master");
+    break;
+  case MMGXState::GX_OVF:
+    mmgxTypeLabel_->setText("TrueType GX or OpenType Variable Font");
+    break;
+  default:
+    mmgxTypeLabel_->setText("Unknown");
+  }
+
+  if (engine_->currentFontMMGXAxes() != axesModel_->storage())
+  {
+    axesModel_->beginModelUpdate();
+    axesModel_->storage() = engine_->currentFontMMGXAxes();
+    axesModel_->endModelUpdate();
+  }
+
+  setEnabled(state != MMGXState::NoMMGX);
+}
+
+
+void
+MMGXInfoTab::createLayout()
+{
+  mmgxTypePromptLabel_ = new QLabel(tr("MM/GX Type:"));
+  mmgxTypeLabel_ = new QLabel(this);
+  setLabelSelectable(mmgxTypeLabel_);
+
+  axesTable_ = new QTableView(this);
+
+  axesModel_ = new MMGXAxisInfoModel(this);
+  axesTable_->setModel(axesModel_);
+  auto header = axesTable_->verticalHeader();
+  // This will force the minimal size to be used
+  header->setDefaultSectionSize(0);
+  header->setSectionResizeMode(QHeaderView::Fixed);
+  axesTable_->horizontalHeader()->setStretchLastSection(true);
+
+  axesGroupBox_ = new QGroupBox("MM/GX Axes");
+
+  axesLayout_ = new QHBoxLayout;
+  axesLayout_->addWidget(axesTable_);
+
+  axesGroupBox_->setLayout(axesLayout_);
+
+  infoLayout_ = new QGridLayout;
+#define MMGXI2Row(w) GL2CRow(infoLayout_, w)
+  auto r = MMGXI2Row(mmgxType);
+
+  infoLayout_->addItem(new QSpacerItem(0, 0, 
+                                       QSizePolicy::Expanding, 
+                                       QSizePolicy::Preferred),
+                       r, 2);
+
+  mainLayout_ = new QVBoxLayout;
+  mainLayout_->addLayout(infoLayout_);
+  mainLayout_->addWidget(axesGroupBox_, 1);
+
+  setLayout(mainLayout_);
+}
+
+
+CompositeGlyphsTab::CompositeGlyphsTab(QWidget* parent,
+                                       Engine* engine)
+: QWidget(parent), engine_(engine)
+{
+  createLayout();
+  createConnections();
+}
+
+
+void
+CompositeGlyphsTab::reloadFont()
+{
+  if (engine_->fontFileManager().currentReloadDueToPeriodicUpdate())
+    return;
+  forceReloadFont();
+}
+
+
+void
+CompositeGlyphsTab::createLayout()
+{
+  compositeGlyphCountPromptLabel_ = new QLabel(tr("Composite Glyphs Count:"));
+  compositeGlyphCountLabel_ = new QLabel(this);
+  forceRefreshButton_ = new QPushButton(tr("Force Refresh"), this);
+  compositeTreeView_ = new QTreeView(this);
+
+  compositeModel_ = new CompositeGlyphsInfoModel(this, engine_);
+  compositeTreeView_->setModel(compositeModel_);
+
+  forceRefreshButton_->setToolTip(tr(
+    "Force refresh the tree view.\n"
+    "Note that periodic reloading of fonts loaded from symbolic links won't\n"
+    "trigger automatically refreshing, so you need to manually reload."));
+
+  // Layouting
+  countLayout_ = new QHBoxLayout;
+  countLayout_->addWidget(compositeGlyphCountPromptLabel_);
+  countLayout_->addWidget(compositeGlyphCountLabel_);
+  countLayout_->addWidget(forceRefreshButton_);
+  countLayout_->addStretch(1);
+
+  mainLayout_ = new QVBoxLayout;
+  mainLayout_->addLayout(countLayout_);
+  mainLayout_->addWidget(compositeTreeView_);
+
+  setLayout(mainLayout_);
+}
+
+
+void
+CompositeGlyphsTab::createConnections()
+{
+  connect(forceRefreshButton_, &QPushButton::clicked,
+          this, &CompositeGlyphsTab::forceReloadFont);
+  connect(compositeTreeView_, &QTreeView::doubleClicked,
+          this, &CompositeGlyphsTab::treeRowDoubleClicked);
+}
+
+
+void
+CompositeGlyphsTab::forceReloadFont()
+{
+  engine_->loadDefaults(); // this would reload the font
+  auto face = engine_->currentFallbackFtFace();
+
+  std::vector<CompositeGlyphInfo> list;
+  CompositeGlyphInfo::get(engine_, list);
+  if (list != compositeModel_->storage())
+  {
+    compositeModel_->beginModelUpdate();
+    compositeModel_->storage() = std::move(list);
+    compositeModel_->endModelUpdate();
+  }
+
+  if (!face || !FT_IS_SFNT(face))
+  {
+    compositeGlyphCountPromptLabel_->setVisible(false);
+    compositeGlyphCountLabel_->setText(tr("Not a SFNT font."));
+  }
+  else if (compositeModel_->storage().empty())
+  {
+    compositeGlyphCountPromptLabel_->setVisible(false);
+    compositeGlyphCountLabel_->setText(
+      tr("No composite glyphs in the 'glyf' table."));
+  }
+  else
+  {
+    compositeGlyphCountPromptLabel_->setVisible(true);
+    compositeGlyphCountLabel_->setText(
+      QString::number(compositeModel_->storage().size()));
+  }
+}
+
+
+void
+CompositeGlyphsTab::treeRowDoubleClicked(const QModelIndex& idx)
+{
+  auto gidx = compositeModel_->glyphIndexFromIndex(idx);
+  if (gidx < 0)
+    return;
+  emit switchToSingular(gidx);
+}
+
+
+// end of info.cpp
diff --git a/src/ftinspect/panels/info.hpp b/src/ftinspect/panels/info.hpp
new file mode 100644
index 0000000..6890d23
--- /dev/null
+++ b/src/ftinspect/panels/info.hpp
@@ -0,0 +1,327 @@
+// info.hpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#pragma once
+
+#include "abstracttab.hpp"
+#include "../engine/fontinfo.hpp"
+#include "../models/fontinfomodels.hpp"
+#include "../widgets/customwidgets.hpp"
+
+#include <vector>
+#include <QWidget>
+#include <QTabWidget>
+#include <QBoxLayout>
+#include <QTextEdit>
+#include <QDialog>
+#include <QGridLayout>
+#include <QVector>
+#include <QLabel>
+#include <QGroupBox>
+#include <QTableView>
+#include <QTreeView>
+
+class Engine;
+class GeneralInfoTab;
+class SFNTInfoTab;
+class PostScriptInfoTab;
+class MMGXInfoTab;
+class CompositeGlyphsTab;
+
+class InfoTab
+: public QWidget, public AbstractTab
+{
+  Q_OBJECT
+public:
+  InfoTab(QWidget* parent, Engine* engine);
+  ~InfoTab() override = default;
+
+  void repaintGlyph() override {}
+  void reloadFont() override;
+
+signals:
+  void switchToSingular(int glyphIndex);
+
+private:
+  Engine* engine_;
+
+  QVector<AbstractTab*> tabs_;
+  GeneralInfoTab*     generalTab_;
+  SFNTInfoTab*        sfntTab_;
+  PostScriptInfoTab*  postScriptTab_;
+  MMGXInfoTab*        mmgxTab_;
+  CompositeGlyphsTab* compositeGlyphsTab_;
+
+  QTabWidget* tab_;
+  QHBoxLayout* layout_;
+
+  void createLayout();
+  void createConnections();
+};
+
+
+#define LabelPair(name) \
+  QLabel* name##Label_; \
+  QLabel* name##PromptLabel_;
+
+
+class GeneralInfoTab
+: public QWidget, public AbstractTab
+{
+  Q_OBJECT
+public:
+  GeneralInfoTab(QWidget* parent, Engine* engine);
+  ~GeneralInfoTab() override = default;
+
+  void repaintGlyph() override {}
+  void reloadFont() override;
+
+private:
+  Engine* engine_;
+
+  LabelPair(    numFaces)
+  LabelPair(      family)
+  LabelPair(       style)
+  LabelPair(  postscript)
+  LabelPair(     created)
+  LabelPair(    modified)
+  LabelPair(    revision)
+  LabelPair(   copyright)
+  LabelPair(   trademark)
+  LabelPair(manufacturer)
+
+  LabelPair(driverName)
+  LabelPair(      sfnt)
+  LabelPair(  fontType)
+  LabelPair( direction)
+  LabelPair(fixedWidth)
+  LabelPair(glyphNames)
+
+  LabelPair(          emSize)
+  LabelPair(            bbox)
+  LabelPair(        ascender)
+  LabelPair(       descender)
+  LabelPair( maxAdvanceWidth)
+  LabelPair(maxAdvanceHeight)
+  LabelPair(           ulPos)
+  LabelPair(     ulThickness)
+
+  QGroupBox*       basicGroupBox_;
+  QGroupBox* typeEntriesGroupBox_;
+  QGroupBox*     charMapGroupBox_;
+  QGroupBox*  fixedSizesGroupBox_;
+
+  QTableView*   charMapsTable_;
+  QTableView* fixedSizesTable_;
+
+  FixedSizeInfoModel* fixedSizeInfoModel_;
+  CharMapInfoModel* charMapInfoModel_;
+
+  UnboundScrollArea* leftScrollArea_;
+
+  QWidget* leftWidget_;
+  QHBoxLayout* mainLayout_;
+  QVBoxLayout* leftLayout_;
+  QVBoxLayout* rightLayout_;
+  QGridLayout* basicLayout_;
+  QGridLayout* typeEntriesLayout_;
+  QHBoxLayout* charMapLayout_;
+  QHBoxLayout* fixedSizesLayout_;
+
+  std::vector<QLabel*> scalableOnlyLabels_;
+
+  FontBasicInfo oldFontBasicInfo_ = {};
+  FontTypeEntries oldFontTypeEntries_ = {};
+
+  void createLayout();
+};
+
+
+class StringViewDialog
+: public QDialog
+{
+  Q_OBJECT
+public:
+  StringViewDialog(QWidget* parent);
+  ~StringViewDialog() override = default;
+
+  void updateString(QByteArray const& rawArray, QString const& str);
+
+private:
+  QLabel* textLabel_;
+  QLabel* hexTextLabel_;
+
+  QTextEdit* textEdit_;
+  QTextEdit* hexTextEdit_;
+
+  QVBoxLayout* layout_;
+
+  void createLayout();
+};
+
+
+class SFNTInfoTab
+: public QWidget, public AbstractTab
+{
+  Q_OBJECT
+public:
+  SFNTInfoTab(QWidget* parent, Engine* engine);
+  ~SFNTInfoTab() override = default;
+
+  void repaintGlyph() override {}
+  void reloadFont() override;
+
+private:
+  Engine* engine_;
+
+  QGroupBox* sfntNamesGroupBox_;
+  QGroupBox* sfntTablesGroupBox_;
+
+  QTableView* sfntNamesTable_;
+  QTableView* sfntTablesTable_;
+
+  SFNTNameModel* sfntNamesModel_;
+  SFNTTableInfoModel* sfntTablesModel_;
+
+  QHBoxLayout* sfntNamesLayout_;
+  QHBoxLayout* sfntTablesLayout_;
+  QHBoxLayout* mainLayout_;
+
+  StringViewDialog* stringViewDialog_;
+
+  void createLayout();
+  void createConnections();
+
+  void nameTableDoubleClicked(QModelIndex const& index);
+};
+
+
+class PostScriptInfoTab
+: public QWidget, public AbstractTab
+{
+  Q_OBJECT
+public:
+  PostScriptInfoTab(QWidget* parent, Engine* engine);
+  ~PostScriptInfoTab() override = default;
+
+  void repaintGlyph() override {}
+  void reloadFont() override;
+
+private:
+  Engine* engine_;
+
+  LabelPair(    version)
+  LabelPair(     notice)
+  LabelPair(   fullName)
+  LabelPair( familyName)
+  LabelPair(     weight)
+  LabelPair(italicAngle)
+  LabelPair( fixedPitch)
+  LabelPair(      ulPos)
+  LabelPair(ulThickness)
+
+  LabelPair(        uniqueID)
+  LabelPair(      blueValues)
+  LabelPair(      otherBlues)
+  LabelPair(     familyBlues)
+  LabelPair(familyOtherBlues)
+  LabelPair(       blueScale)
+  LabelPair(       blueShift)
+  LabelPair(        blueFuzz)
+  LabelPair(       stdWidths)
+  LabelPair(      stdHeights)
+  LabelPair(      snapWidths)
+  LabelPair(     snapHeights)
+  LabelPair(       forceBold)
+  LabelPair(   languageGroup)
+  LabelPair(        password)
+  LabelPair(           lenIV)
+  LabelPair(      minFeature)
+  LabelPair(     roundStemUp)
+  LabelPair( expansionFactor)
+
+  QGroupBox* infoGroupBox_;
+  QGroupBox* privateGroupBox_;
+
+  QWidget* infoWidget_;
+  QWidget* privateWidget_;
+
+  UnboundScrollArea* infoScrollArea_;
+  UnboundScrollArea* privateScrollArea_;
+
+  QGridLayout* infoLayout_;
+  QGridLayout* privateLayout_;
+  QHBoxLayout* infoGroupBoxLayout_;
+  QHBoxLayout* privateGroupBoxLayout_;
+  QHBoxLayout* mainLayout_;
+
+  PS_PrivateRec oldFontPrivate_;
+
+  void createLayout();
+};
+
+
+class MMGXInfoTab
+: public QWidget, public AbstractTab
+{
+  Q_OBJECT
+public:
+  MMGXInfoTab(QWidget* parent, Engine* engine);
+  ~MMGXInfoTab() override = default;
+
+  void repaintGlyph() override {}
+  void reloadFont() override;
+
+private:
+  Engine* engine_;
+
+  LabelPair(mmgxType)
+
+  QGroupBox* axesGroupBox_;
+  QTableView* axesTable_;
+
+  QGridLayout* infoLayout_;
+  QHBoxLayout* axesLayout_;
+  QVBoxLayout* mainLayout_;
+
+  MMGXAxisInfoModel* axesModel_;
+
+  void createLayout();
+};
+
+
+class CompositeGlyphsTab
+: public QWidget, public AbstractTab
+{
+  Q_OBJECT
+public:
+  CompositeGlyphsTab(QWidget* parent, Engine* engine);
+  ~CompositeGlyphsTab() override = default;
+
+  void repaintGlyph() override {}
+  void reloadFont() override;
+
+signals:
+  void switchToSingular(int glyphIndex);
+
+private:
+  Engine* engine_;
+
+  LabelPair(compositeGlyphCount)
+  QPushButton* forceRefreshButton_;
+  QTreeView* compositeTreeView_;
+  CompositeGlyphsInfoModel* compositeModel_;
+
+  QHBoxLayout* countLayout_;
+  QVBoxLayout* mainLayout_;
+
+  void createLayout();
+  void createConnections();
+
+  void forceReloadFont();
+  void treeRowDoubleClicked(const QModelIndex& idx);
+};
+
+
+// end of info.hpp
diff --git a/src/ftinspect/panels/settingpanel.cpp 
b/src/ftinspect/panels/settingpanel.cpp
index 3db0bde..dda3873 100644
--- a/src/ftinspect/panels/settingpanel.cpp
+++ b/src/ftinspect/panels/settingpanel.cpp
@@ -298,11 +298,11 @@ SettingPanel::populatePalettes()
     QSignalBlocker blocker(paletteComboBox_);
     paletteComboBox_->clear();
     for (int i = 0; i < newSize; ++i)
-      paletteComboBox_->addItem(
-        QString("%1: %2")
-          .arg(i)
-          .arg(newPalettes[i].name),
-        newPalettes[i].name);
+    {
+      auto str = QString("%1: %2").arg(i).arg(newPalettes[i].name);
+      paletteComboBox_->addItem(str, newPalettes[i].name);
+      paletteComboBox_->setItemData(i, str, Qt::ToolTipRole);
+    }
   }
 
   emit fontReloadNeeded();
diff --git a/src/ftinspect/panels/settingpanelmmgx.cpp 
b/src/ftinspect/panels/settingpanelmmgx.cpp
index f80c318..a1a81dc 100644
--- a/src/ftinspect/panels/settingpanelmmgx.cpp
+++ b/src/ftinspect/panels/settingpanelmmgx.cpp
@@ -95,6 +95,7 @@ SettingPanelMMGX::createLayout()
   itemsListWidget_ = new QWidget(this);
   scrollArea_ = new UnboundScrollArea(this);
 
+  scrollArea_->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Ignored);
   scrollArea_->setWidget(itemsListWidget_);
   scrollArea_->setWidgetResizable(true);
   itemsListWidget_->setAutoFillBackground(false);
diff --git a/src/ftinspect/widgets/charmapcombobox.cpp 
b/src/ftinspect/widgets/charmapcombobox.cpp
index b9caa91..dadb483 100644
--- a/src/ftinspect/widgets/charmapcombobox.cpp
+++ b/src/ftinspect/widgets/charmapcombobox.cpp
@@ -77,23 +77,24 @@ CharMapComboBox::repopulate(std::vector<CharMapInfo>& 
charMaps)
       addItem(tr("Glyph Order"));
       setItemData(0, 0u, EncodingRole);
     }
-
-    int i = 0;
+    
     int newIndex = 0;
     for (auto& map : charMaps)
     {
-      addItem(tr("%1: %2 (platform %3, encoding %4)")
-                .arg(i)
-                .arg(*map.encodingName)
-                .arg(map.platformID)
-                .arg(map.encodingID));
+      auto i = count();
+      auto str = tr("%1: %2 (platform %3, encoding %4)")
+                   .arg(i)
+                   .arg(*map.encodingName)
+                   .arg(map.platformID)
+                   .arg(map.encodingID);
+      addItem(str);
+      setItemData(i, str, Qt::ToolTipRole);
+
       auto encoding = static_cast<unsigned>(map.encoding);
-      setItemData(haveGlyphOrder_ ? i + 1 : i, encoding, EncodingRole);
+      setItemData(i, encoding, EncodingRole);
 
       if (encoding == oldEncoding && i == oldIndex)
         newIndex = i;
-    
-      i++;
     }
 
     // this shouldn't emit any event either, because force repainting



reply via email to

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