[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[freetype2-demos] master e9aa456 22/41: # This is a combination of 2 com
From: |
Werner Lemberg |
Subject: |
[freetype2-demos] master e9aa456 22/41: # This is a combination of 2 commits. |
Date: |
Mon, 3 Oct 2022 11:27:02 -0400 (EDT) |
branch: master
commit e9aa456e6e40f6c7b85cbcb44d4a8a6a9a0aa278
Author: Charlie Jiang <w@chariri.moe>
Commit: Werner Lemberg <wl@gnu.org>
# This is a combination of 2 commits.
# This is the 1st commit message:
[ftinspect] Add "Continuous View".
Most new features in the continuous view are included in the commit, except
the mouse left (details pane)/right (go to singular) click behaviour.
* src/ftinspect/panels/continuous.cpp, src/ftinspect/panels/continuous.hpp:
New files, the main continuous tab.
* src/ftinspect/glyphcomponents/glyphcontinuous.cpp,
src/ftinspect/glyphcomponents/glyphcontinuous.hpp:
New files, adding the `GlyphContinuous` as the actual canvas for
continuous rendering.
* src/ftinspect/engine/stringrenderer.cpp,
src/ftinspect/engine/stringrenderer.hpp:
New files, adding `StringRenderer` to layout the strings and produce
glyphs for the canvas to draw.
* src/ftinspect/widgets/charmapcombobox.cpp,
src/ftinspect/widgets/charmapcombobox.hpp:
New files, add the `CharMapComboBox` widget.
* src/ftinspect/engine/charmap.cpp,src/ftinspect/engine/charmap.hpp:
New files, adding `CharMapInfo` class.
* src/ftinspect/engine/engine.cpp, src/ftinspect/engine/engine.hpp:
Add necessary fields and getters for string rendering.
Retrieve charmap when loading fonts.
* src/ftinspect/maingui.cpp, src/ftinspect/maingui.hpp:
Add the continuous view to the main window.
Call `ContinuousTab::highlightGlyph` when switching from singular to
continuous view.
* src/ftinspect/panels/settingpanel.cpp: Uncomment functional code.
* src/ftinspect/CMakeLists.txt, src/ftinspect/meson.build: Updated.
# This is the commit message #2:
* src/ftinspect/engine/stringrenderer.cpp: Fix infinite loop.
Don't limit the glyph position and exit too early, or the loop in `render`
function won't stop forever. Also, since the string can be moved by the
mouse, there's no point to limit the line position in the canvas viewport.
---
src/ftinspect/CMakeLists.txt | 5 +
src/ftinspect/engine/charmap.cpp | 158 +++++
src/ftinspect/engine/charmap.hpp | 61 ++
src/ftinspect/engine/engine.cpp | 77 +++
src/ftinspect/engine/engine.hpp | 21 +-
src/ftinspect/engine/stringrenderer.cpp | 721 ++++++++++++++++++++++
src/ftinspect/engine/stringrenderer.hpp | 241 ++++++++
src/ftinspect/glyphcomponents/glyphcontinuous.cpp | 602 ++++++++++++++++++
src/ftinspect/glyphcomponents/glyphcontinuous.hpp | 204 ++++++
src/ftinspect/maingui.cpp | 14 +-
src/ftinspect/maingui.hpp | 2 +
src/ftinspect/meson.build | 8 +
src/ftinspect/panels/continuous.cpp | 699 +++++++++++++++++++++
src/ftinspect/panels/continuous.hpp | 153 +++++
src/ftinspect/panels/settingpanel.cpp | 6 +-
src/ftinspect/widgets/charmapcombobox.cpp | 118 ++++
src/ftinspect/widgets/charmapcombobox.hpp | 40 ++
17 files changed, 3124 insertions(+), 6 deletions(-)
diff --git a/src/ftinspect/CMakeLists.txt b/src/ftinspect/CMakeLists.txt
index 19c9591..098f331 100644
--- a/src/ftinspect/CMakeLists.txt
+++ b/src/ftinspect/CMakeLists.txt
@@ -26,8 +26,11 @@ add_executable(ftinspect
"engine/paletteinfo.cpp"
"engine/mmgx.cpp"
"engine/fontinfo.cpp"
+ "engine/stringrenderer.cpp"
+ "engine/charmap.cpp"
"glyphcomponents/glyphbitmap.cpp"
+ "glyphcomponents/glyphcontinuous.cpp"
"glyphcomponents/glyphoutline.cpp"
"glyphcomponents/glyphpointnumbers.cpp"
"glyphcomponents/glyphpoints.cpp"
@@ -38,12 +41,14 @@ add_executable(ftinspect
"widgets/tripletselector.cpp"
"widgets/glyphindexselector.cpp"
"widgets/fontsizeselector.cpp"
+ "widgets/charmapcombobox.cpp"
"models/customcomboboxmodels.cpp"
"panels/settingpanel.cpp"
"panels/settingpanelmmgx.cpp"
"panels/singular.cpp"
+ "panels/continuous.cpp"
)
target_link_libraries(ftinspect
Qt5::Core Qt5::Widgets
diff --git a/src/ftinspect/engine/charmap.cpp b/src/ftinspect/engine/charmap.cpp
new file mode 100644
index 0000000..680b6cd
--- /dev/null
+++ b/src/ftinspect/engine/charmap.cpp
@@ -0,0 +1,158 @@
+// charmap.cpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#include "charmap.hpp"
+
+#include <QHash>
+#include <freetype/freetype.h>
+#include <freetype/tttables.h>
+
+namespace
+{
+QHash<FT_Encoding, QString>& encodingNames();
+}
+
+CharMapInfo::CharMapInfo(int index, FT_CharMap cmap)
+: index(index), ptr(cmap),
+ encoding(cmap->encoding),
+ platformID(cmap->platform_id),
+ encodingID(cmap->encoding_id),
+ formatID(FT_Get_CMap_Format(cmap)),
+ languageID(FT_Get_CMap_Language_ID(cmap)),
+ maxIndex(-1)
+{
+ auto& names = encodingNames();
+ auto it = names.find(encoding);
+ if (it == names.end())
+ encodingName = &names[static_cast<FT_Encoding>(FT_ENCODING_OTHER)];
+ else
+ encodingName = &it.value();
+
+ if (encoding != FT_ENCODING_OTHER)
+ maxIndex = computeMaxIndex();
+}
+
+
+QString
+CharMapInfo::stringifyIndex(int code, int idx)
+{
+ return QString("CharCode: %1 (glyph idx %2)")
+ .arg(stringifyIndexShort(code))
+ .arg(idx);
+}
+
+
+QString
+CharMapInfo::stringifyIndexShort(int code)
+{
+ return (encoding == FT_ENCODING_UNICODE ? "U+" : "0x")
+ + QString::number(code, 16).rightJustified(4, '0').toUpper();
+}
+
+
+int
+CharMapInfo::computeMaxIndex()
+{
+ int result;
+ switch (encoding)
+ {
+ case FT_ENCODING_UNICODE:
+ result = maxIndexForFaceAndCharMap(ptr, 0x110000) + 1;
+ break;
+
+ case FT_ENCODING_ADOBE_LATIN_1:
+ case FT_ENCODING_ADOBE_STANDARD:
+ case FT_ENCODING_ADOBE_EXPERT:
+ case FT_ENCODING_ADOBE_CUSTOM:
+ case FT_ENCODING_APPLE_ROMAN:
+ result = 0x100;
+ break;
+
+ /* some fonts use range 0x00-0x100, others have 0xF000-0xF0FF */
+ case FT_ENCODING_MS_SYMBOL:
+ result = maxIndexForFaceAndCharMap(ptr, 0x10000) + 1;
+ break;
+
+ default:
+ // Some encodings can reach > 0x10000, e.g. GB 18030.
+ result = maxIndexForFaceAndCharMap(ptr, 0x110000) + 1;
+ }
+ return result;
+}
+
+
+int
+CharMapInfo::maxIndexForFaceAndCharMap(FT_CharMap charMap,
+ unsigned maxIn)
+{
+ // code adopted from `ftcommon.c`
+ // This never overflows since no format here exceeds INT_MAX...
+ FT_ULong min = 0, max = maxIn;
+ FT_UInt glyphIndex;
+ FT_Face face = charMap->face;
+
+ if (FT_Set_Charmap(face, charMap))
+ return -1;
+
+ do
+ {
+ FT_ULong mid = (min + max) >> 1;
+ FT_ULong res = FT_Get_Next_Char(face, mid, &glyphIndex);
+
+ if (glyphIndex)
+ min = res;
+ else
+ {
+ max = mid;
+
+ // once moved, it helps to advance min through sparse regions
+ if (min)
+ {
+ res = FT_Get_Next_Char(face, min, &glyphIndex);
+
+ if (glyphIndex)
+ min = res;
+ else
+ max = min; // found it
+ }
+ }
+ } while (max > min);
+
+ return static_cast<int>(max);
+}
+
+
+namespace
+{
+// Mapping for `FT_Encoding` is placed here since it's only for the charmap.
+QHash<FT_Encoding, QString> encodingNamesCache;
+QHash<FT_Encoding, QString>&
+encodingNames()
+{
+ if (encodingNamesCache.empty())
+ {
+ encodingNamesCache[static_cast<FT_Encoding>(FT_ENCODING_OTHER)]
+ = "Unknown Encoding";
+ encodingNamesCache[FT_ENCODING_NONE] = "No Encoding";
+ encodingNamesCache[FT_ENCODING_MS_SYMBOL] = "MS Symbol (symb)";
+ encodingNamesCache[FT_ENCODING_UNICODE] = "Unicode (unic)";
+ encodingNamesCache[FT_ENCODING_SJIS] = "Shift JIS (sjis)";
+ encodingNamesCache[FT_ENCODING_PRC] = "PRC/GB 18030 (gb)";
+ encodingNamesCache[FT_ENCODING_BIG5] = "Big5 (big5)";
+ encodingNamesCache[FT_ENCODING_WANSUNG] = "Wansung (wans)";
+ encodingNamesCache[FT_ENCODING_JOHAB] = "Johab (joha)";
+ encodingNamesCache[FT_ENCODING_ADOBE_STANDARD] = "Adobe Standard (ADOB)";
+ encodingNamesCache[FT_ENCODING_ADOBE_EXPERT] = "Adobe Expert (ADBE)";
+ encodingNamesCache[FT_ENCODING_ADOBE_CUSTOM] = "Adobe Custom (ADBC)";
+ encodingNamesCache[FT_ENCODING_ADOBE_LATIN_1] = "Latin 1 (lat1)";
+ encodingNamesCache[FT_ENCODING_OLD_LATIN_2] = "Latin 2 (lat2)";
+ encodingNamesCache[FT_ENCODING_APPLE_ROMAN] = "Apple Roman (armn)";
+ }
+
+ return encodingNamesCache;
+}
+}
+
+
+// end of charmap.cpp
diff --git a/src/ftinspect/engine/charmap.hpp b/src/ftinspect/engine/charmap.hpp
new file mode 100644
index 0000000..8b73470
--- /dev/null
+++ b/src/ftinspect/engine/charmap.hpp
@@ -0,0 +1,61 @@
+// charmap.hpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#pragma once
+
+#include <QString>
+
+#include <ft2build.h>
+#include <freetype/freetype.h>
+
+class Engine;
+
+#define FT_ENCODING_OTHER 0xFFFE
+struct CharMapInfo
+{
+ int index;
+ FT_CharMap ptr;
+ FT_Encoding encoding;
+ unsigned short platformID;
+ unsigned short encodingID;
+ long formatID;
+ unsigned long languageID;
+ QString* encodingName;
+
+ // Actually this shouldn't go here, but for convenience...
+ int maxIndex;
+
+ CharMapInfo(int index, FT_CharMap cmap);
+
+ QString stringifyIndex(int code, int idx);
+ QString stringifyIndexShort(int code);
+
+
+ friend bool
+ operator==(const CharMapInfo& lhs, const CharMapInfo& rhs)
+ {
+ // omitting `ptr` by design!
+ return lhs.index == rhs.index
+ && lhs.encoding == rhs.encoding
+ && lhs.platformID == rhs.platformID
+ && lhs.encodingID == rhs.encodingID
+ && lhs.formatID == rhs.formatID
+ && lhs.languageID == rhs.languageID;
+ }
+
+
+ friend bool
+ operator!=(const CharMapInfo& lhs, const CharMapInfo& rhs)
+ {
+ return !(lhs == rhs);
+ }
+
+
+private:
+ int computeMaxIndex();
+ static int maxIndexForFaceAndCharMap(FT_CharMap charMap, unsigned max);
+};
+
+
+// end of charmap.hpp
diff --git a/src/ftinspect/engine/engine.cpp b/src/ftinspect/engine/engine.cpp
index 459f815..87ee808 100644
--- a/src/ftinspect/engine/engine.cpp
+++ b/src/ftinspect/engine/engine.cpp
@@ -152,6 +152,12 @@ Engine::Engine()
// XXX error handling
}
+ error = FTC_CMapCache_New(cacheManager_, &cmapCache_);
+ if (error)
+ {
+ // XXX error handling
+ }
+
queryEngine();
renderingEngine_
= std::unique_ptr<RenderingEngine>(new RenderingEngine(this));
@@ -329,6 +335,7 @@ Engine::loadFont(int fontIndex,
curFamilyName_ = QString();
curStyleName_ = QString();
+ curCharMaps_.clear();
curPaletteInfos_.clear();
curSFNTNames_.clear();
}
@@ -347,6 +354,11 @@ Engine::loadFont(int fontIndex,
else
fontType_ = FontType_Other;
+ curCharMaps_.clear();
+ curCharMaps_.reserve(ftFallbackFace_->num_charmaps);
+ for (int i = 0; i < ftFallbackFace_->num_charmaps; i++)
+ curCharMaps_.emplace_back(i, ftFallbackFace_->charmaps[i]);
+
SFNTName::get(this, curSFNTNames_);
loadPaletteInfos();
curMMGXState_ = MMGXAxisInfo::get(this, curMMGXAxes_);
@@ -465,6 +477,58 @@ Engine::currentFontFixedSizes()
}
+int
+Engine::currentFontFirstUnicodeCharMap()
+{
+ auto& charmaps = currentFontCharMaps();
+ for (auto& cmap : charmaps)
+ if (cmap.encoding == FT_ENCODING_UNICODE)
+ return cmap.index;
+ return -1;
+}
+
+
+unsigned
+Engine::glyphIndexFromCharCode(int code, int charMapIndex)
+{
+ if (charMapIndex < 0)
+ return code;
+ return FTC_CMapCache_Lookup(cmapCache_, scaler_.face_id, charMapIndex, code);
+}
+
+
+FT_Pos
+Engine::currentFontTrackingKerning(int degree)
+{
+ if (!ftSize_)
+ return 0;
+
+ FT_Pos result;
+ // this function needs and returns points, not pixels
+ if (!FT_Get_Track_Kerning(ftSize_->face,
+ static_cast<FT_Fixed>(scaler_.width) << 10,
+ -degree,
+ &result))
+ {
+ result = static_cast<FT_Pos>((result / 1024.0 * scaler_.x_res) / 72.0);
+ return result;
+ }
+ return 0;
+}
+
+
+FT_Vector
+Engine::currentFontKerning(int glyphIndex,
+ int prevIndex)
+{
+ FT_Vector kern = {0, 0};
+ FT_Get_Kerning(ftSize_->face,
+ prevIndex, glyphIndex,
+ FT_KERNING_UNFITTED, &kern);
+ return kern;
+}
+
+
std::pair<int, int>
Engine::currentSizeAscDescPx()
{
@@ -571,6 +635,19 @@ Engine::loadGlyphWithoutUpdate(int glyphIndex,
}
+FT_Size_Metrics const&
+Engine::currentFontMetrics()
+{
+ return ftSize_->metrics;
+}
+
+FT_GlyphSlot
+Engine::currentFaceSlot()
+{
+ return ftSize_->face->glyph;
+}
+
+
bool
Engine::renderReady()
{
diff --git a/src/ftinspect/engine/engine.hpp b/src/ftinspect/engine/engine.hpp
index cba152e..d9ea177 100644
--- a/src/ftinspect/engine/engine.hpp
+++ b/src/ftinspect/engine/engine.hpp
@@ -6,12 +6,13 @@
#pragma once
#include "fontfilemanager.hpp"
-
#include "paletteinfo.hpp"
#include "fontinfo.hpp"
#include "mmgx.hpp"
#include "rendering.hpp"
+#include "charmap.hpp"
+#include <vector>
#include <memory>
#include <utility>
#include <QString>
@@ -109,6 +110,9 @@ public:
// (for current fonts)
FT_Face currentFallbackFtFace() { return ftFallbackFace_; }
FT_Size currentFtSize() { return ftSize_; }
+ FT_Size_Metrics const& currentFontMetrics();
+ FT_GlyphSlot currentFaceSlot();
+
bool renderReady(); // Can we render bitmaps? (implys `fontValid`)
bool fontValid(); // Is the current font valid (valid font may be unavailable
// to render, such as non-scalable font with invalid sizes)
@@ -124,6 +128,7 @@ public:
MMGXState currentFontMMGXState() { return curMMGXState_; }
std::vector<MMGXAxisInfo>& currentFontMMGXAxes() { return curMMGXAxes_; }
std::vector<SFNTName>& currentFontSFNTNames() { return curSFNTNames_; }
+ std::vector<CharMapInfo>& currentFontCharMaps() { return curCharMaps_; }
QString glyphName(int glyphIndex);
long numberOfFaces(int fontIndex);
@@ -137,13 +142,21 @@ public:
bool currentFontHasColorLayers();
std::vector<int> currentFontFixedSizes();
+ int currentFontFirstUnicodeCharMap();
+ // Note: the current font face must be properly set
+ unsigned glyphIndexFromCharCode(int code, int charMapIndex);
+ FT_Pos currentFontTrackingKerning(int degree);
+ FT_Vector currentFontKerning(int glyphIndex, int prevIndex);
std::pair<int, int> currentSizeAscDescPx();
// (settings)
int dpi() { return dpi_; }
+ double pointSize() { return pointSize_; }
FTC_ImageType imageType() { return &imageType_; }
bool antiAliasingEnabled() { return antiAliasingEnabled_; }
+ bool doHinting() { return doHinting_; }
bool embeddedBitmapEnabled() { return embeddedBitmap_; }
+ bool lcdUsingSubPixelPositioning() { return lcdSubPixelPositioning_; }
bool useColorLayer() { return useColorLayer_; }
int paletteIndex() { return paletteIndex_; }
FT_Render_Mode
@@ -179,8 +192,9 @@ public:
void setEmbeddedBitmapEnabled(bool enabled) { embeddedBitmap_ = enabled; }
void setUseColorLayer(bool colorLayer) { useColorLayer_ = colorLayer; }
void setPaletteIndex(int index) { paletteIndex_ = index; }
+ void setLCDSubPixelPositioning(bool sp) { lcdSubPixelPositioning_ = sp; }
+
// (settings without backing fields)
-
// Note: These 3 functions now takes actual mode/version from FreeType,
// instead of values from enum in MainGUI!
void setLcdFilter(FT_LcdFilter filter);
@@ -210,6 +224,7 @@ private:
QString curFamilyName_;
QString curStyleName_;
int curNumGlyphs_ = -1;
+ std::vector<CharMapInfo> curCharMaps_;
std::vector<PaletteInfo> curPaletteInfos_;
MMGXState curMMGXState_ = MMGXState::NoMMGX;
std::vector<MMGXAxisInfo> curMMGXAxes_;
@@ -220,6 +235,7 @@ private:
FTC_Manager cacheManager_;
FTC_ImageCache imageCache_;
FTC_SBitCache sbitsCache_;
+ FTC_CMapCache cmapCache_;
EngineDefaultValues engineDefaults_;
// settings
@@ -249,6 +265,7 @@ private:
bool useColorLayer_;
int paletteIndex_ = -1;
int antiAliasingTarget_;
+ bool lcdSubPixelPositioning_;
int renderMode_;
unsigned long loadFlags_;
diff --git a/src/ftinspect/engine/stringrenderer.cpp
b/src/ftinspect/engine/stringrenderer.cpp
new file mode 100644
index 0000000..bd9eaf1
--- /dev/null
+++ b/src/ftinspect/engine/stringrenderer.cpp
@@ -0,0 +1,721 @@
+// stringrenderer.cpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#include "stringrenderer.hpp"
+
+#include "engine.hpp"
+
+#include <cmath>
+#include <QTextCodec>
+
+
+StringRenderer::StringRenderer(Engine* engine)
+: engine_(engine)
+{
+}
+
+
+StringRenderer::~StringRenderer()
+{
+ clearActive();
+}
+
+
+void
+StringRenderer::setCharMapIndex(int charMapIndex,
+ int limitIndex)
+{
+ auto& charMaps = engine_->currentFontCharMaps();
+ if (charMapIndex < 0
+ || static_cast<unsigned>(charMapIndex) >= charMaps.size())
+ charMapIndex = -1;
+
+ charMapIndex_ = charMapIndex;
+ limitIndex_ = limitIndex;
+}
+
+
+void
+StringRenderer::setRotation(double rotation)
+{
+ rotation_ = rotation;
+
+ if (rotation <= -180)
+ rotation += 360;
+ if (rotation > 180)
+ rotation -= 360;
+
+ if (rotation == 0)
+ {
+ matrixEnabled_ = false;
+ return;
+ }
+
+ matrixEnabled_ = true;
+ double radian = rotation * 3.14159265 / 180.0;
+ auto cosinus = static_cast<FT_Fixed>(cos(radian) * 65536.0);
+ auto sinus = static_cast<FT_Fixed>(sin(radian) * 65536.0);
+
+ matrix_.xx = cosinus;
+ matrix_.yx = sinus;
+ matrix_.xy = -sinus;
+ matrix_.yy = cosinus;
+}
+
+
+void
+StringRenderer::setKerning(bool kerning)
+{
+ if (kerning)
+ {
+ kerningMode_ = KM_Smart;
+ kerningDegree_ = KD_Medium;
+ }
+ else
+ {
+ kerningMode_ = KM_None;
+ kerningDegree_ = KD_None;
+ }
+}
+
+
+void
+StringRenderer::reloadAll()
+{
+ clearActive(usingString_); // if "All Glyphs", then do a complete wipe
+ if (usingString_)
+ reloadGlyphIndices();
+}
+
+
+void
+StringRenderer::reloadGlyphs()
+{
+ clearActive(true);
+}
+
+
+void
+StringRenderer::setUseString(QString const& string)
+{
+ clearActive(); // clear existing
+ usingString_ = true;
+
+ long long totalCount = 0;
+ for (uint ch : string.toUcs4())
+ {
+ activeGlyphs_.emplace_back();
+ auto& it = activeGlyphs_.back();
+ it.charCodeUcs4 = it.charCode = static_cast<int>(ch);
+ it.glyphIndex = 0;
+ ++totalCount;
+ if (totalCount >= INT_MAX) // Prevent overflow
+ break;
+ }
+ reloadGlyphIndices();
+}
+
+
+void
+StringRenderer::setUseAllGlyphs()
+{
+ if (usingString_)
+ clearActive();
+ usingString_ = false;
+}
+
+
+void
+StringRenderer::reloadGlyphIndices()
+{
+ if (!usingString_)
+ return;
+ int charMapIndex = charMapIndex_;
+ auto& charMaps = engine_->currentFontCharMaps();
+ if (charMaps.empty())
+ return;
+ if (charMapIndex < 0
+ || static_cast<unsigned>(charMapIndex) >= charMaps.size())
+ charMapIndex = engine_->currentFontFirstUnicodeCharMap();
+ if (charMapIndex < 0
+ || static_cast<unsigned>(charMapIndex) >= charMaps.size())
+ charMapIndex = 0;
+ auto encoding = charMaps[charMapIndex].encoding;
+
+ if (charMapIndex < 0)
+ return;
+ for (auto& ctx : activeGlyphs_)
+ {
+ if (encoding != FT_ENCODING_UNICODE)
+ ctx.charCode = convertCharEncoding(ctx.charCodeUcs4, encoding);
+
+ auto index = engine_->glyphIndexFromCharCode(ctx.charCode, charMapIndex);
+ ctx.glyphIndex = static_cast<int>(index);
+ }
+}
+
+
+void
+StringRenderer::prepareRendering()
+{
+ engine_->reloadFont();
+ if (!engine_->renderReady())
+ return;
+ engine_->loadPalette();
+ if (kerningDegree_ != KD_None)
+ trackingKerning_ = engine_->currentFontTrackingKerning(kerningDegree_);
+ else
+ trackingKerning_ = 0;
+}
+
+
+void
+StringRenderer::loadSingleContext(GlyphContext* ctx,
+ GlyphContext* prev)
+{
+ if (ctx->cacheNode)
+ {
+ FTC_Node_Unref(ctx->cacheNode, engine_->cacheManager());
+ ctx->cacheNode = NULL;
+ }
+ else if (ctx->glyph)
+ FT_Done_Glyph(ctx->glyph); // when caching isn't used
+
+ // TODO use FTC?
+
+ // After `prepareRendering`, current size/face is properly set
+ FT_GlyphSlot slot = engine_->currentFaceSlot();
+ if (engine_->loadGlyphIntoSlotWithoutCache(ctx->glyphIndex) != 0)
+ {
+ ctx->glyph = NULL;
+ return;
+ }
+ if (FT_Get_Glyph(slot, &ctx->glyph) != 0)
+ {
+ ctx->glyph = NULL;
+ return;
+ }
+ auto& metrics = slot->metrics;
+ //ctx->glyph = engine_->loadGlyphWithoutUpdate(ctx->glyphIndex,
+ // &ctx->cacheNode);
+
+ if (!ctx->glyph)
+ return;
+
+ ctx->vvector.x = metrics.vertBearingX - metrics.horiBearingX;
+ ctx->vvector.y = -metrics.vertBearingY - metrics.horiBearingY;
+
+ ctx->vadvance.x = 0;
+ ctx->vadvance.y = -metrics.vertAdvance;
+
+ ctx->lsbDelta = slot->lsb_delta;
+ ctx->rsbDelta = slot->rsb_delta;
+
+ ctx->hadvance.x = metrics.horiAdvance;
+ ctx->hadvance.y = 0;
+
+ if (lsbRsbDeltaEnabled_ && engine_->lcdUsingSubPixelPositioning())
+ ctx->hadvance.x += ctx->lsbDelta - ctx->rsbDelta;
+ prev->hadvance.x += trackingKerning_;
+
+ if (kerningMode_ != KM_None)
+ {
+ FT_Vector kern = engine_->currentFontKerning(ctx->glyphIndex,
+ prev->glyphIndex);
+
+ prev->hadvance.x += kern.x;
+ prev->hadvance.y += kern.y;
+ }
+
+ if (!engine_->lcdUsingSubPixelPositioning()
+ && lsbRsbDeltaEnabled_)
+ {
+ if (prev->rsbDelta - ctx->lsbDelta > 32)
+ prev->hadvance.x -= 64;
+ else if (prev->rsbDelta - ctx->lsbDelta < -31)
+ prev->hadvance.x += 64;
+ }
+
+ if (!engine_->lcdUsingSubPixelPositioning() && engine_->doHinting())
+ {
+ prev->hadvance.x = (prev->hadvance.x + 32) & -64;
+ prev->hadvance.y = (prev->hadvance.y + 32) & -64;
+ }
+}
+
+
+void
+StringRenderer::loadStringGlyphs()
+{
+ if (!usingString_)
+ return;
+ GlyphContext* prev = &tempGlyphContext_; // = empty
+ tempGlyphContext_ = {};
+
+ for (auto& ctx : activeGlyphs_)
+ {
+ loadSingleContext(&ctx, prev);
+ prev = &ctx;
+ }
+
+ glyphCacheValid_ = true;
+}
+
+
+int
+StringRenderer::prepareLine(int offset,
+ int lineWidth,
+ FT_Vector& outActualLineWidth,
+ int nonSpacingPlaceholder,
+ bool handleMultiLine)
+{
+ int totalCount = 0;
+ outActualLineWidth = {0, 0};
+ if (!usingString_) // All glyphs
+ {
+ // The thing gets a little complicated when we're using "All Glyphs" mode
+ // The input sequence is actually infinite
+ // so we have to combine loading glyph into rendering, and can't preload
+ // all glyphs
+
+ // TODO: Low performance when the begin index is large.
+ // TODO: Optimize: use a sparse vector...!
+ // The problem is that when doing a `list::resize`, the ctor is called
+ // for unnecessarily many times.
+ tempGlyphContext_ = {};
+ for (unsigned n = offset; n < static_cast<unsigned>(limitIndex_);)
+ {
+ if (activeGlyphs_.capacity() <= n)
+ activeGlyphs_.reserve(static_cast<size_t>(n) * 2);
+ if (activeGlyphs_.size() <= n)
+ activeGlyphs_.resize(n + 1);
+
+ auto& ctx = activeGlyphs_[n];
+ ctx.charCode = static_cast<int>(n);
+ ctx.glyphIndex = static_cast<int>(
+ engine_->glyphIndexFromCharCode(static_cast<int>(n), charMapIndex_));
+
+ auto prev = n == 0 ? &tempGlyphContext_ : &activeGlyphs_[n - 1];
+ if (!ctx.glyph)
+ loadSingleContext(&ctx, prev);
+
+ // In All Glyphs mode, a red placeholder should be drawn for non-spacing
+ // glyphs (e.g. the stress mark)
+ auto actualAdvanceX = ctx.hadvance.x ? ctx.hadvance.x
+ : nonSpacingPlaceholder << 6;
+ if (outActualLineWidth.x + actualAdvanceX > lineWidth)
+ break;
+ outActualLineWidth.x += actualAdvanceX;
+ outActualLineWidth.y += ctx.hadvance.y;
+ ++n;
+ ++totalCount;
+ }
+ }
+ else // strings
+ {
+ if (!glyphCacheValid_)
+ {
+ clearActive(true);
+ loadStringGlyphs();
+ }
+
+ for (unsigned n = offset; n < activeGlyphs_.size();)
+ {
+ auto& ctx = activeGlyphs_[n];
+
+ if (handleMultiLine && ctx.charCode == '\n')
+ {
+ totalCount += 1; // Break here.
+ break;
+ }
+
+ if (repeated_) // if repeated, we must stop when we touch the end of line
+ {
+ if (outActualLineWidth.x + ctx.hadvance.x > lineWidth)
+ break;
+ outActualLineWidth.x += ctx.hadvance.x;
+ outActualLineWidth.y += ctx.hadvance.y;
+ ++n;
+ n %= static_cast<int>(activeGlyphs_.size()); // safe
+ }
+ else if (vertical_)
+ {
+ outActualLineWidth.x += ctx.vadvance.x;
+ outActualLineWidth.y += ctx.vadvance.y;
+ ++n;
+ }
+ else
+ {
+ outActualLineWidth.x += ctx.hadvance.x;
+ outActualLineWidth.y += ctx.hadvance.y;
+ ++n;
+ }
+ ++totalCount;
+ }
+ }
+ return totalCount;
+}
+
+
+int
+StringRenderer::render(int width,
+ int height,
+ int offset)
+{
+ engine_->reloadFont();
+
+ if (usingString_)
+ offset = 0;
+ if (!usingString_ && limitIndex_ <= 0)
+ return 0;
+ if (!engine_->fontValid())
+ return 0;
+
+ auto initialOffset = offset;
+
+ // Separated into 3 modes:
+ // Waterfall, fill the whole canvas and only single string.
+ if (waterfall_)
+ {
+ // Waterfall
+
+ vertical_ = false;
+ // They're only effective for non-bitmap-only (scalable) fonts!
+ auto originalSize = static_cast<int>(engine_->pointSize() * 64);
+ auto ptSize = originalSize;
+ auto ptHeight = 64 * 72 * height / engine_->dpi();
+ int step = 0;
+
+ auto bitmapOnly = engine_->currentFontBitmapOnly();
+ auto fixedSizes = engine_->currentFontFixedSizes();
+ std::sort(fixedSizes.begin(), fixedSizes.end());
+ auto fixedSizesIter = fixedSizes.begin();
+
+ if (waterfallStart_ <= 0)
+ {
+ // auto
+ step = (originalSize * originalSize / ptHeight + 64) & ~63;
+ ptSize = ptSize - step * (ptSize / step); // modulo
+ ptSize += step;
+ }
+ else if (!bitmapOnly)
+ {
+ ptSize = static_cast<int>(waterfallStart_ * 64.0) & ~31;
+ // we first get a ratio since height & ppem are near proportional...
+ // 64.0 is somewhat a magic reference number
+ engine_->setSizeByPoint(64.0);
+ engine_->reloadFont();
+ if (!engine_->renderReady())
+ return -1;
+ auto pixelActual = engine_->currentFontMetrics().height >> 6;
+
+ auto heightPt = height * 64.0 / pixelActual;
+
+ if (waterfallEnd_ < waterfallStart_)
+ waterfallEnd_ = waterfallStart_ + 1;
+
+ auto n = heightPt * 2 / (waterfallStart_ + waterfallEnd_);
+ auto stepTemp = (waterfallEnd_ - waterfallStart_) / (n + 1);
+ // rounding to 0.25
+ step = static_cast<int>(std::round(stepTemp * 4)) * 16 & ~15;
+ if (step == 0)
+ step = 16; // 0.25 pt
+ }
+
+ int y = 0;
+ // no position param in "All Glyphs" or repeated mode
+ int x = static_cast<int>((usingString_ && !repeated_) ? (width * position_)
+ : 0);
+ int count = 0;
+
+ while (true)
+ {
+ if (!bitmapOnly)
+ engine_->setSizeByPoint(ptSize / 64.0);
+ else
+ {
+ if (fixedSizesIter == fixedSizes.end())
+ break;
+ engine_->setSizeByPixel(*fixedSizesIter);
+ }
+ clearActive(true);
+ prepareRendering(); // set size/face for engine, so metrics are valid
+ auto& metrics = engine_->currentFontMetrics();
+
+ y += static_cast<int>(metrics.height >> 6) + 1;
+ if (y >= height && !bitmapOnly)
+ break;
+
+ loadStringGlyphs();
+ auto lcount = renderLine(x, y + static_cast<int>(metrics.descender >> 6),
+ width, height,
+ offset);
+ count = std::max(count, lcount);
+
+ if (!bitmapOnly)
+ {
+ if (step == 0)
+ break;
+ ptSize += step;
+ }
+ else
+ ++fixedSizesIter;
+ }
+ engine_->setSizeByPoint(originalSize / 64.0);
+
+ return count;
+ }
+ // end of waterfall
+
+ if (repeated_ || !usingString_)
+ {
+ // Fill the whole canvas (string repeated or all glyphs)
+
+ prepareRendering();
+ if (!engine_->renderReady())
+ return 0;
+ auto& metrics = engine_->currentFontMetrics();
+ auto y = 4 + static_cast<int>(metrics.ascender >> 6);
+ auto stepY = static_cast<int>(metrics.height >> 6) + 1;
+ auto limitY = height + static_cast<int>(metrics.descender >> 6);
+
+ // Only care about multiline when in string mode
+ for (; y < limitY; y += stepY)
+ {
+ offset = renderLine(0, y, width, height, offset, usingString_);
+ // For repeating
+ if (usingString_ && repeated_ && !activeGlyphs_.empty())
+ offset %= static_cast<int>(activeGlyphs_.size());
+ }
+ if (!usingString_) // only return count for All Glyphs mode.
+ return offset - initialOffset;
+ return 0;
+ }
+
+ // Single string
+ prepareRendering();
+ if (!engine_->renderReady())
+ return 0;
+
+ auto& metrics = engine_->currentFontMetrics();
+ auto x = static_cast<int>(width * position_);
+ // Anchor at top-left in vertical mode, at the center in horizontal mode
+ auto y = vertical_ ? 0 : (height / 2);
+ auto stepY = static_cast<int>(metrics.height >> 6) + 1;
+ y += 4 + static_cast<int>(metrics.ascender >> 6);
+
+ while (offset < static_cast<int>(activeGlyphs_.size()))
+ {
+ offset = renderLine(x, y, width, height, offset, true);
+ y += stepY;
+ }
+ return offset - initialOffset;
+}
+
+
+int
+StringRenderer::renderLine(int x,
+ int y,
+ int width,
+ int height,
+ int offset,
+ bool handleMultiLine)
+{
+ y = height - y; // change to Cartesian coordinates
+
+ FT_Vector pen = { 0, 0 };
+ FT_Vector advance;
+ auto nonSpacingPlaceholder = engine_->currentFontMetrics().y_ppem / 2 + 2;
+
+ // When in "All Glyphs" mode, no vertical support.
+ if (repeated_ || !usingString_)
+ vertical_ = false; // TODO: Support vertical + repeated
+
+ int lineLength = 64 * (vertical_ ? height : width);
+
+ // first prepare the line & determine the line length
+ int totalCount = prepareLine(offset, lineLength, pen,
+ nonSpacingPlaceholder, handleMultiLine);
+
+ // round to control initial pen position and preserve hinting...
+ // pen.x, y is the actual length now, and we multiple it by pos
+ auto centerFixed = static_cast<int>(0x10000 * position_);
+ if (!usingString_ || repeated_)
+ centerFixed = 0;
+ pen.x = FT_MulFix(pen.x, centerFixed) & ~63;
+ pen.y = FT_MulFix(pen.y, centerFixed) & ~63;
+
+ // ... unless rotating; XXX sbits
+ if (matrixEnabled_)
+ FT_Vector_Transform(&pen, &matrix_);
+
+ // get pen position: penPos = center - pos * width
+ pen.x = (x << 6) - pen.x;
+ pen.y = (y << 6) - pen.y;
+
+ // Need to transform the coord back to normal coord system
+ lineBeginCallback_({ (pen.x >> 6),
+ height - (pen.y >> 6) },
+ engine_->pointSize());
+
+ for (int i = offset; i < totalCount + offset; i++)
+ {
+ auto& ctx = activeGlyphs_[i % activeGlyphs_.size()];
+ if (handleMultiLine && ctx.charCode == '\n')
+ continue; // skip \n
+ FT_Glyph image = NULL; // Remember to clean up
+ FT_BBox bbox;
+
+ if (!ctx.glyph)
+ continue;
+
+ advance = vertical_ ? ctx.vadvance : ctx.hadvance;
+
+ QRect rect;
+ QImage* colorLayerImage
+ = engine_->renderingEngine()->tryDirectRenderColorLayers(ctx.glyphIndex,
+ &rect, true);
+
+ if (colorLayerImage)
+ {
+ FT_Vector penPos = { (pen.x >> 6), height - (pen.y >> 6) };
+ renderImageCallback_(colorLayerImage, rect, penPos, advance, ctx);
+ }
+ else
+ {
+ // copy the glyph because we're doing manipulation
+ auto error = FT_Glyph_Copy(ctx.glyph, &image);
+ if (error)
+ continue;
+
+ glyphPreprocessCallback_(&image);
+
+ if (image->format != FT_GLYPH_FORMAT_BITMAP)
+ {
+ if (vertical_)
+ error = FT_Glyph_Transform(image, NULL, &ctx.vvector);
+
+ if (!error)
+ {
+ if (matrixEnabled_)
+ error = FT_Glyph_Transform(image, &matrix_, NULL);
+ }
+
+ if (error)
+ {
+ FT_Done_Glyph(image);
+ continue;
+ }
+ }
+ else
+ {
+ auto bitmap = reinterpret_cast<FT_BitmapGlyph>(image);
+
+ if (vertical_)
+ {
+ bitmap->left += static_cast<int>(ctx.vvector.x) >> 6;
+ bitmap->top += static_cast<int>(ctx.vvector.y) >> 6;
+ }
+ }
+
+ if (matrixEnabled_)
+ FT_Vector_Transform(&advance, &matrix_);
+
+ FT_Glyph_Get_CBox(image, FT_GLYPH_BBOX_PIXELS, &bbox);
+
+ // Don't check for bounding box here.
+ FT_Vector penPos = { (pen.x >> 6), height - (pen.y >> 6) };
+ renderCallback_(image, penPos, ctx);
+
+ FT_Done_Glyph(image);
+ }
+
+ pen.x += advance.x;
+ pen.y += advance.y;
+
+ if (!advance.x && !usingString_) // add placeholder
+ pen.x += nonSpacingPlaceholder << 6;
+ }
+
+ return offset + totalCount;
+}
+
+
+void
+StringRenderer::clearActive(bool glyphOnly)
+{
+ for (auto& ctx : activeGlyphs_)
+ {
+ if (ctx.cacheNode)
+ FTC_Node_Unref(ctx.cacheNode, engine_->cacheManager());
+ else if (ctx.glyph)
+ FT_Done_Glyph(ctx.glyph); // when caching isn't used
+ ctx.cacheNode = NULL;
+ ctx.glyph = NULL;
+ }
+ if (!glyphOnly)
+ activeGlyphs_.clear();
+
+ glyphCacheValid_ = false;
+}
+
+
+int
+StringRenderer::convertCharEncoding(int charUcs4, FT_Encoding encoding)
+{
+ switch (encoding)
+ {
+ case FT_ENCODING_MS_SYMBOL:
+ case FT_ENCODING_UNICODE:
+ case FT_ENCODING_ADOBE_STANDARD: // These may be problematic...
+ case FT_ENCODING_ADOBE_EXPERT:
+ case FT_ENCODING_ADOBE_CUSTOM:
+ case FT_ENCODING_ADOBE_LATIN_1:
+ return charUcs4;
+ default:; // proceed
+ }
+
+ auto mib = -1;
+ switch (encoding)
+ {
+ case FT_ENCODING_SJIS:
+ mib = 17; // Shift_JIS
+ break;
+ case FT_ENCODING_PRC:
+ mib = 114; // GB 18030
+ break;
+ case FT_ENCODING_BIG5:
+ mib = 2026; // Big5
+ break;
+ case FT_ENCODING_WANSUNG:
+ mib = -949; // KS C 5601:1987, this is a fake mib value
+ break;
+ case FT_ENCODING_JOHAB:
+ mib = 38; // KS C 5601:1992 / EUC-KR
+ break;
+ case FT_ENCODING_APPLE_ROMAN:
+ mib = 2027;
+ break;
+ default:
+ return charUcs4; // Failed
+ }
+
+ if (mib == -1)
+ return charUcs4; // unsupported charmap
+ auto codec = QTextCodec::codecForMib(mib);
+ if (!codec)
+ return charUcs4; // unsupported
+
+ auto res = codec->fromUnicode(
+ QString::fromUcs4(reinterpret_cast<uint*>(&charUcs4), 1));
+ if (res.size() == 0)
+ return charUcs4;
+ if (res.size() == 1)
+ return res[0];
+ return ((static_cast<int>(res[0]) & 0xFF) << 8)
+ | (static_cast<int>(res[1]) & 0xFF);
+}
+
+
+// end of stringrenderer.cpp
diff --git a/src/ftinspect/engine/stringrenderer.hpp
b/src/ftinspect/engine/stringrenderer.hpp
new file mode 100644
index 0000000..d8aa323
--- /dev/null
+++ b/src/ftinspect/engine/stringrenderer.hpp
@@ -0,0 +1,241 @@
+// stringrenderer.hpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#pragma once
+
+#include <vector>
+#include <functional>
+
+#include <QString>
+
+#include <ft2build.h>
+#include <qslider.h>
+#include <freetype/freetype.h>
+#include <freetype/ftcache.h>
+#include <freetype/ftglyph.h>
+
+// adopted from `ftcommon.h`
+
+class Engine;
+struct GlyphContext
+{
+ int charCode = 0;
+ int charCodeUcs4 = 0;
+ int glyphIndex = 0;
+ FT_Glyph glyph = NULL;
+ FTC_Node cacheNode = NULL;
+
+ FT_Pos lsbDelta = 0; // delta caused by hinting
+ FT_Pos rsbDelta = 0; // delta caused by hinting
+ FT_Vector hadvance = { 0, 0 }; // kerned horizontal advance
+
+ FT_Vector vvector = { 0, 0 }; // vert. origin => hori. origin
+ FT_Vector vadvance = { 0, 0 }; // vertical advance
+};
+
+// Class to populate chars to render, to load and properly position glyphs.
+// Use callbacks to receive characters and lines. You should save the result
+// from the callbacks to a cache.
+class StringRenderer
+{
+public:
+ StringRenderer(Engine* engine);
+ ~StringRenderer();
+
+ enum KerningDegree // XXX: Not honored actually
+ {
+ KD_None = 0,
+ KD_Light,
+ KD_Medium,
+ KD_Tight
+ };
+
+ enum KerningMode
+ {
+ KM_None = 0,
+ KM_Normal,
+ KM_Smart
+ };
+
+ /*
+ * Called when outputting a glyph. The receiver is reponsible for rendering
+ * the glyph to bitmap.
+ *
+ * Need to pass the pen position because sometimes the outline vector
+ * contains no points, and thus can't be translated to the desired pen
+ * position.
+ */
+ using RenderCallback = std::function<void(FT_Glyph, // glyph
+ FT_Vector, // penPos
+ GlyphContext&)>;
+ /*
+ * Called when outputtng a glyph with bitmap pre-rendered.
+ * The receiver can simply use the bitmap, mainly for color layered fonts.
+ *
+ * TODO: Remove `RenderCallback` and do QImage creation in this class?
+ * The receiver is responsible for deleteing the `QImage`
+ * (ownership transfered).
+ */
+ using RenderImageCallback = std::function<void(QImage*, // bitmap
+ QRect, // bbox
+ FT_Vector, // penPos
+ FT_Vector, // advance
+ GlyphContext&)>;
+ /*
+ * Called right after the glyph is obtained from the font, before any other
+ * operation is done. The receiver can do pre-processing like slanting and
+ * emboldening in this function.
+ *
+ * The glyph pointer may be replaced. In that case, ownership is transfered
+ * to the renderer, and the new glyph will be eventually freed by
+ * the renderer. The callback is responsible to free the old glyph.
+ * This allows you to do the following:
+ * void callback(FT_Glyph* ptr) {
+ * ....
+ * auto oldPtr = *ptr;
+ * *ptr = ....;
+ * FT_Done_Glyph(olPtr);
+ * }
+ */
+ using PreprocessCallback = std::function<void(FT_Glyph*)>;
+ /*
+ * Called when a new line begins.
+ */
+ using LineBeginCallback = std::function<void(FT_Vector, // initial penPos
+ double)>; // size (points)
+
+ //////// Getters
+ bool isWaterfall() { return waterfall_; }
+ double position(){ return position_; }
+ int charMapIndex() { return charMapIndex_; }
+
+ //////// Callbacks
+ void
+ setCallback(RenderCallback cb)
+ {
+ renderCallback_ = std::move(cb);
+ }
+ void
+ setImageCallback(RenderImageCallback cb)
+ {
+ renderImageCallback_ = std::move(cb);
+ }
+ void
+ setPreprocessCallback(PreprocessCallback cb)
+ {
+ glyphPreprocessCallback_ = std::move(cb);
+ }
+ void
+ setLineBeginCallback(LineBeginCallback cb)
+ {
+ lineBeginCallback_ = std::move(cb);
+ }
+
+ //////// Setters for options
+ void setCharMapIndex(int charMapIndex, int limitIndex);
+ void setRepeated(bool repeated) { repeated_ = repeated; }
+ void setVertical(bool vertical) { vertical_ = vertical; }
+ void setRotation(double rotation);
+ void setWaterfall(bool waterfall) { waterfall_ = waterfall; }
+ void setWaterfallParameters(double start, double end)
+ {
+ waterfallStart_ = start;
+ waterfallEnd_ = end;
+ }
+ void setPosition(double pos) { position_ = pos; }
+ void setLsbRsbDelta(bool enabled) { lsbRsbDeltaEnabled_ = enabled; }
+ void setKerning(bool kerning);
+
+ // Need to be called when font or charMap changes
+ void setUseString(QString const& string);
+ void setUseAllGlyphs();
+
+ //////// Actions
+ int render(int width,
+ int height,
+ int offset);
+ int renderLine(int x, int y,
+ int width, int height,
+ int offset,
+ bool handleMultiLine = false);
+
+ void reloadAll(); // text/font/charmap changes, will call
+ // `reloadGlyphs`
+ void reloadGlyphs(); // any other parameter changes
+
+private:
+ Engine* engine_;
+
+ // Generally, rendering has those steps:
+ // 1. If in string mode, the string is load into `activeGlyphs_`
+ // (in `updateString`)
+ // 2. The char codes in contexts are converted to glyph indices
+ // (in `reloadGlyphIndices`)
+ // 3. If in string mode, glyphs are loaded into contexts.
+ // (in `loadStringGlyphs`)
+ // 4. In `render` function, according to mode, `renderLine` is called line
+ // by line (as well as `prepareRendering`).
+ // 5. In `renderLine`, if in all glyphs mode, glyphs from the begin index
+ // are loaded until the line is full (if the glyph already exists, it will
+ // be reused). If in string mode, it will directly use the prepared
glyphs.
+ // Preprocessing is done within this step, such as emboldening or
stroking.
+ // Eventually the `FT_Glyph` pointer is passed to the callback.
+
+ GlyphContext tempGlyphContext_;
+ // This vector stores all active glyphs for rendering. When rendering
strings,
+ // this is the container for chars, so DO NOT directly clear it to flush
+ // cache, you should clean glyph objects only. However when rendering all
+ // glyphs, it's generally to directly wipe the vector because it's
dynamically
+ // generated in `render` function (see above).
+ //
+ // Note: Because of kerning, this list must be ordered and allow duplicate
+ // characters.
+ //
+ // Actually this means 3 parts of storage: string charcode, glyph indices and
+ // glyph (+ all related info). Different parameter changes will trigger
+ // different levels of flushing.
+ std::vector<GlyphContext> activeGlyphs_;
+ bool glyphCacheValid_ = false;
+
+ int charMapIndex_ = 0;
+ int limitIndex_ = 0;
+ bool usingString_ = false;
+ bool repeated_ = false;
+ bool vertical_ = false;
+ double position_ = 0.5;
+ double rotation_ = 0;
+ int kerningDegree_ = KD_None;
+ KerningMode kerningMode_ = KM_None;
+ FT_Pos trackingKerning_ = 0;
+ FT_Matrix matrix_ = {};
+ bool matrixEnabled_ = false;
+ bool lsbRsbDeltaEnabled_ = true;
+
+ bool waterfall_ = false;
+ double waterfallStart_ = -1;
+ double waterfallEnd_ = -1; // -1 = Auto
+
+ RenderCallback renderCallback_;
+ RenderImageCallback renderImageCallback_;
+ PreprocessCallback glyphPreprocessCallback_;
+ LineBeginCallback lineBeginCallback_;
+
+ void reloadGlyphIndices(); // for string rendering
+ void prepareRendering();
+ void loadSingleContext(GlyphContext* ctx, GlyphContext* prev);
+ // Need to be called when font, charMap or size changes;
+ void loadStringGlyphs();
+ // Returns total line count
+ int prepareLine(int offset,
+ int lineWidth,
+ FT_Vector& outActualLineWidth,
+ int nonSpacingPlaceholder,
+ bool handleMultiLine = false);
+ void clearActive(bool glyphOnly = false);
+
+ int convertCharEncoding(int charUcs4, FT_Encoding encoding);
+};
+
+
+// end of stringrenderer.hpp
diff --git a/src/ftinspect/glyphcomponents/glyphcontinuous.cpp
b/src/ftinspect/glyphcomponents/glyphcontinuous.cpp
new file mode 100644
index 0000000..64e46ce
--- /dev/null
+++ b/src/ftinspect/glyphcomponents/glyphcontinuous.cpp
@@ -0,0 +1,602 @@
+// glyphcontinuous.cpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#include "glyphcontinuous.hpp"
+
+#include "../engine/engine.hpp"
+
+#include <QPainter>
+#include <QWheelEvent>
+
+#include <freetype/ftbitmap.h>
+
+
+GlyphCacheEntry::~GlyphCacheEntry()
+{
+ delete image;
+}
+
+
+GlyphCacheEntry::GlyphCacheEntry(GlyphCacheEntry&& other) noexcept
+{
+ *this = std::move(other);
+}
+
+
+GlyphCacheEntry&
+GlyphCacheEntry::operator=(GlyphCacheEntry&& other) noexcept
+{
+ if (this == &other)
+ return *this;
+
+ auto oldImage = image;
+ image = other.image;
+ basePosition = other.basePosition;
+ penPos = other.penPos;
+ charCode = other.charCode;
+ glyphIndex = other.glyphIndex;
+ nonSpacingPlaceholder = other.nonSpacingPlaceholder;
+ advance = other.advance;
+ other.image = oldImage;
+ return *this;
+}
+
+
+GlyphContinuous::GlyphContinuous(QWidget* parent, Engine* engine)
+: QWidget(parent),
+ engine_(engine),
+ stringRenderer_(engine)
+{
+ setAcceptDrops(false);
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+
+ flashTimer_ = new QTimer(this);
+ flashTimer_->setInterval(FlashIntervalMs);
+ connect(flashTimer_, &QTimer::timeout,
+ this, &GlyphContinuous::flashTimerFired);
+
+ FT_Stroker_New(engine_->ftLibrary(), &stroker_);
+}
+
+
+GlyphContinuous::~GlyphContinuous()
+{
+ FT_Stroker_Done(stroker_);
+}
+
+
+void
+GlyphContinuous::setSource(Source source)
+{
+ source_ = source;
+ switch (source)
+ {
+ case SRC_AllGlyphs:
+ stringRenderer_.setUseAllGlyphs();
+ positionDelta_ = {};
+ break;
+
+ case SRC_TextStringRepeated:
+ positionDelta_ = {};
+ /* fall through */
+ case SRC_TextString:
+ updateRendererText();
+ break;
+ }
+}
+
+
+void
+GlyphContinuous::setSourceText(QString text)
+{
+ text_ = std::move(text);
+ updateRendererText();
+}
+
+
+void
+GlyphContinuous::flashOnGlyph(int glyphIndex)
+{
+ flashTimer_->stop();
+
+ flashGlyphIndex_ = glyphIndex;
+ flashRemainingCount_ = FlashDurationMs / FlashIntervalMs;
+ flashTimer_->start();
+}
+
+
+void
+GlyphContinuous::stopFlashing()
+{
+ flashGlyphIndex_ = -1;
+ flashTimer_->stop();
+}
+
+
+void
+GlyphContinuous::purgeCache()
+{
+ glyphCache_.clear();
+ backgroundColorCache_ = engine_->renderingEngine()->background();
+ currentWritingLine_ = NULL;
+}
+
+
+void
+GlyphContinuous::resetPositionDelta()
+{
+ positionDelta_ = {};
+ repaint();
+}
+
+
+void
+GlyphContinuous::paintEvent(QPaintEvent* event)
+{
+ QPainter painter(this);
+ painter.fillRect(rect(), backgroundColorCache_);
+ painter.scale(scale_, scale_);
+
+ if (glyphCache_.empty())
+ fillCache();
+ paintCache(&painter);
+}
+
+
+void
+GlyphContinuous::wheelEvent(QWheelEvent* event)
+{
+ int numSteps = event->angleDelta().y() / 120;
+ if (event->modifiers() & Qt::ShiftModifier)
+ emit wheelResize(numSteps);
+ else if (event->modifiers() & Qt::ControlModifier)
+ emit wheelZoom(numSteps);
+ else if (event->modifiers() == 0)
+ emit wheelNavigate(-numSteps);
+}
+
+
+void
+GlyphContinuous::resizeEvent(QResizeEvent* event)
+{
+ purgeCache();
+ QWidget::resizeEvent(event);
+}
+
+
+void
+GlyphContinuous::mousePressEvent(QMouseEvent* event)
+{
+ if (!mouseOperationEnabled_)
+ return;
+ if (event->button() == Qt::LeftButton)
+ {
+ prevPositionDelta_ = positionDelta_;
+ mouseDownPostition_ = event->pos();
+ prevHoriPosition_ = stringRenderer_.position();
+ prevIndex_ = beginIndex_;
+ // We need to precalculate this value because after the first change of
+ // the begin index, the average line count would change. If we don't use
the
+ // old value, then moving up/down for the same distance would not return
+ // to the original index which is confusing.
+ averageLineCount_ = calculateAverageLineCount();
+ }
+}
+
+
+void
+GlyphContinuous::mouseMoveEvent(QMouseEvent* event)
+{
+ if (!mouseOperationEnabled_)
+ return;
+ if (event->buttons() != Qt::LeftButton)
+ return;
+ auto delta = event->pos() - mouseDownPostition_;
+ delta /= scale_;
+ if (source_ == SRC_AllGlyphs)
+ {
+ auto deltaIndex = -delta.x() / HorizontalUnitLength
+ - delta.y() / VerticalUnitLength * averageLineCount_;
+ if (prevIndex_ + deltaIndex != beginIndex_)
+ emit beginIndexChangeRequest(beginIndex_ + deltaIndex);
+ }
+ else if (source_ == SRC_TextString)
+ {
+ positionDelta_ = prevPositionDelta_ + delta;
+ positionDelta_.setX(0); // Don't move horizontally
+
+ // but use the renderer
+ // purpose for two scale_: one for undoing the `delta /= scale_`
+ // the other for effectively dividing width by the scale
+ auto horiPos = delta.x() * scale_ * scale_ / static_cast<double>(width());
+ horiPos += prevHoriPosition_;
+ horiPos = qBound(0.0, horiPos, 1.0);
+ stringRenderer_.setPosition(horiPos);
+
+ purgeCache();
+ repaint();
+ }
+}
+
+
+void
+GlyphContinuous::paintByRenderer()
+{
+ purgeCache();
+
+ stringRenderer_.setRepeated(source_ == SRC_TextStringRepeated);
+ stringRenderer_.setCallback(
+ [&](FT_Glyph glyph, FT_Vector penPos, GlyphContext& ctx)
+ {
+ saveSingleGlyph(glyph, penPos, ctx);
+ });
+ stringRenderer_.setImageCallback(
+ [&](QImage* image,
+ QRect pos,
+ FT_Vector penPos, FT_Vector advance,
+ GlyphContext& ctx)
+ {
+ saveSingleGlyphImage(image, pos, penPos, advance, ctx);
+ });
+ stringRenderer_.setPreprocessCallback(
+ [&](FT_Glyph* ptr)
+ {
+ preprocessGlyph(ptr);
+ });
+ stringRenderer_.setLineBeginCallback(
+ [&](FT_Vector pos, double size)
+ {
+ beginSaveLine(pos, size);
+ });
+ auto count = stringRenderer_.render(static_cast<int>(width() / scale_),
+ static_cast<int>(height() / scale_),
+ beginIndex_);
+ if (source_ == SRC_AllGlyphs)
+ displayingCount_ = count;
+ else
+ displayingCount_ = 0;
+}
+
+
+void
+GlyphContinuous::transformGlyphFancy(FT_Glyph glyph)
+{
+ auto& metrics = engine_->currentFontMetrics();
+ auto emboldeningX = (FT_Pos)(metrics.y_ppem * 64 * boldX_);
+ auto emboldeningY = (FT_Pos)(metrics.y_ppem * 64 * boldY_);
+ // adopted from ftview.c:289
+ if (glyph->format == FT_GLYPH_FORMAT_OUTLINE)
+ {
+ auto outline = reinterpret_cast<FT_OutlineGlyph>(glyph)->outline;
+ FT_Glyph_Transform(glyph, &shearMatrix_, NULL);
+ if (FT_Outline_EmboldenXY(&outline, emboldeningX, emboldeningY))
+ {
+ // XXX error handling?
+ return;
+ }
+
+ if (glyph->advance.x)
+ glyph->advance.x += emboldeningX;
+
+ if (glyph->advance.y)
+ glyph->advance.y += emboldeningY;
+ }
+ else if (glyph->format == FT_GLYPH_FORMAT_BITMAP)
+ {
+ auto xstr = emboldeningX & ~63;
+ auto ystr = emboldeningY & ~63;
+
+ auto bitmap = &reinterpret_cast<FT_BitmapGlyph>(glyph)->bitmap;
+ // No shearing support for bitmap
+ FT_Bitmap_Embolden(engine_->ftLibrary(), bitmap,
+ xstr, ystr);
+ }
+ else
+ return; // XXX no support for SVG
+}
+
+
+FT_Glyph
+GlyphContinuous::transformGlyphStroked(FT_Glyph glyph)
+{
+ // Well, here only outline glyph is supported.
+ if (glyph->format != FT_GLYPH_FORMAT_OUTLINE)
+ return NULL;
+ auto error = FT_Glyph_Stroke(&glyph, stroker_, 0);
+ if (error)
+ return NULL;
+ return glyph;
+}
+
+
+void
+GlyphContinuous::paintCache(QPainter* painter)
+{
+ bool flashFlipFlop = false;
+ if (flashRemainingCount_ >= 0)
+ {
+ if (flashGlyphIndex_ >= 0) // only flash when the glyph index valid
+ flashFlipFlop = flashRemainingCount_ % 2 == 1;
+ else
+ {
+ flashTimer_->stop();
+ flashRemainingCount_ = 0;
+ }
+ flashRemainingCount_--;
+ }
+ else if (flashGlyphIndex_ >= 0)
+ {
+ flashGlyphIndex_ = -1;
+ flashTimer_->stop();
+ }
+
+ if (stringRenderer_.isWaterfall())
+ positionDelta_.setY(0);
+ for (auto& line : glyphCache_)
+ {
+ beginDrawCacheLine(painter, line);
+ for (auto& glyph : line.entries)
+ {
+ if (glyph.glyphIndex == flashGlyphIndex_ && flashFlipFlop)
+ drawCacheGlyph(painter, glyph, true);
+ else
+ drawCacheGlyph(painter, glyph);
+ }
+ }
+}
+
+
+void
+GlyphContinuous::fillCache()
+{
+ prePaint();
+ paintByRenderer();
+ emit displayingCountUpdated(displayingCount_);
+}
+
+
+void
+GlyphContinuous::prePaint()
+{
+ displayingCount_ = 0;
+
+ // Used by fancy:
+ // adopted from ftview.c:289
+ /***************************************************************/
+ /* */
+ /* 2*2 affine transformation matrix, 16.16 fixed float format */
+ /* */
+ /* Shear matrix: */
+ /* */
+ /* | x' | | 1 k | | x | x' = x + ky */
+ /* | | = | | * | | <==> */
+ /* | y' | | 0 1 | | y | y' = y */
+ /* */
+ /* outline' shear outline */
+ /* */
+ /***************************************************************/
+
+
+ shearMatrix_.xx = 1 << 16;
+ shearMatrix_.xy = static_cast<FT_Fixed>(slant_ * (1 << 16));
+ shearMatrix_.yx = 0;
+ shearMatrix_.yy = 1 << 16;
+}
+
+
+void
+GlyphContinuous::updateStroke()
+{
+ if (mode_ != M_Stroked || !engine_->renderReady())
+ return;
+
+ auto& metrics = engine_->currentFontMetrics();
+ auto radius = static_cast<FT_Fixed>(metrics.y_ppem * 64 * strokeRadius_);
+ strokeRadiusForSize_ = radius;
+ FT_Stroker_Set(stroker_, radius,
+ FT_STROKER_LINECAP_ROUND,
+ FT_STROKER_LINEJOIN_ROUND,
+ 0);
+}
+
+
+void
+GlyphContinuous::updateRendererText()
+{
+ stringRenderer_.setUseString(text_); // TODO this need to be called when
font,
+ // size or charmap change
+}
+
+
+void
+GlyphContinuous::preprocessGlyph(FT_Glyph* glyphPtr)
+{
+ auto glyph = *glyphPtr;
+ switch (mode_)
+ {
+ case M_Fancy:
+ transformGlyphFancy(glyph);
+ break;
+ case M_Stroked:
+ {
+ auto stroked = transformGlyphStroked(glyph);
+ if (stroked)
+ {
+ FT_Done_Glyph(glyph);
+ *glyphPtr = stroked;
+ }
+ }
+ break;
+ default:; // Nothing for M_NORMAL.
+ }
+}
+
+
+void
+GlyphContinuous::beginSaveLine(FT_Vector pos,
+ double sizePoint)
+{
+ glyphCache_.emplace_back();
+ currentWritingLine_ = &glyphCache_.back();
+ currentWritingLine_->nonSpacingPlaceholder
+ = engine_->currentFontMetrics().y_ppem / 2;
+ currentWritingLine_->sizePoint = sizePoint;
+ currentWritingLine_->basePosition = { static_cast<int>(pos.x),
+ static_cast<int>(pos.y) };
+}
+
+
+void
+GlyphContinuous::saveSingleGlyph(FT_Glyph glyph,
+ FT_Vector penPos,
+ GlyphContext gctx)
+{
+ if (!currentWritingLine_)
+ return;
+
+ QRect rect;
+ QImage* image = engine_->renderingEngine()->convertGlyphToQImage(glyph,
+ &rect,
+ true);
+ saveSingleGlyphImage(image, rect, penPos, glyph->advance, gctx);
+}
+
+
+void
+GlyphContinuous::saveSingleGlyphImage(QImage* image,
+ QRect rect,
+ FT_Vector penPos,
+ FT_Vector advance,
+ GlyphContext gctx)
+{
+ if (!currentWritingLine_)
+ return;
+
+ currentWritingLine_->entries.emplace_back();
+ auto& entry = currentWritingLine_->entries.back();
+
+ QPoint penPosPoint = { static_cast<int>(penPos.x),
+ static_cast<int>(penPos.y) };
+
+ rect.translate(penPosPoint);
+
+ entry.image = image;
+ entry.basePosition = rect;
+ entry.charCode = gctx.charCode;
+ entry.glyphIndex = gctx.glyphIndex;
+ entry.advance = advance;
+ entry.penPos = penPosPoint;
+ entry.nonSpacingPlaceholder = currentWritingLine_->nonSpacingPlaceholder;
+}
+
+
+void
+GlyphContinuous::beginDrawCacheLine(QPainter* painter,
+ GlyphCacheLine& line)
+{
+ // Now only used by waterfall mode to draw a size indicator.
+ if (!stringRenderer_.isWaterfall())
+ {
+ sizeIndicatorOffset_ = 0;
+ return;
+ }
+
+ auto oldFont = painter->font();
+ oldFont.setPointSizeF(line.sizePoint);
+ painter->setFont(oldFont);
+ auto metrics = painter->fontMetrics();
+
+ auto printSize = line.sizePoint;
+ if (engine_->currentFontBitmapOnly())
+ printSize = printSize * engine_->dpi() / 72.0; // convert back
+ auto sizePrefix = QString("%1: ").arg(printSize);
+ painter->drawText(line.basePosition, sizePrefix);
+
+ sizeIndicatorOffset_ = metrics.horizontalAdvance(sizePrefix);
+ line.sizeIndicatorOffset = sizeIndicatorOffset_;
+}
+
+
+void
+GlyphContinuous::drawCacheGlyph(QPainter* painter,
+ const GlyphCacheEntry& entry,
+ bool colorInverted)
+{
+ // ftview.c:557
+ // Well, metrics is also part of the cache...
+ int width = entry.advance.x ? entry.advance.x >> 16
+ : entry.nonSpacingPlaceholder;
+
+ if (entry.advance.x == 0
+ && !stringRenderer_.isWaterfall()
+ && source_ == SRC_AllGlyphs)
+ {
+ // Draw a red square to indicate non-spacing glyphs
+ auto squarePoint = entry.penPos;
+ squarePoint.setY(squarePoint.y() - width);
+ auto rect = QRect(squarePoint, QSize(width, width));
+ painter->fillRect(rect, Qt::red);
+ }
+
+ QRect rect = entry.basePosition;
+ rect.moveLeft(rect.x() + sizeIndicatorOffset_);
+ rect.translate(positionDelta_);
+
+ if (colorInverted)
+ {
+ auto inverted = entry.image->copy();
+ inverted.invertPixels();
+ painter->drawImage(rect.topLeft(), inverted);
+ }
+ else
+ painter->drawImage(rect.topLeft(), *entry.image);
+}
+
+
+GlyphCacheEntry*
+GlyphContinuous::findGlyphByMouse(QPoint position,
+ double* outSizePoint)
+{
+ position -= positionDelta_;
+ position /= scale_;
+ for (auto& line : glyphCache_)
+ for (auto& entry : line.entries)
+ {
+ auto rect = entry.basePosition;
+ rect.moveLeft(rect.x() + line.sizeIndicatorOffset);
+ if (rect.contains(position))
+ {
+ if (outSizePoint)
+ *outSizePoint = line.sizePoint;
+ return &entry;
+ }
+ }
+ return NULL;
+}
+
+
+int
+GlyphContinuous::calculateAverageLineCount()
+{
+ int averageLineCount = 0;
+ for (auto& line : glyphCache_)
+ {
+ // line.entries.size must < INT_MAX because the total glyph count in
+ // the renderer is below that
+ averageLineCount += static_cast<int>(line.entries.size());
+ }
+ if (!glyphCache_.empty())
+ averageLineCount /= static_cast<int>(glyphCache_.size());
+ return averageLineCount;
+}
+
+
+void
+GlyphContinuous::flashTimerFired()
+{
+ repaint();
+}
+
+
+// end of glyphcontinuous.cpp
diff --git a/src/ftinspect/glyphcomponents/glyphcontinuous.hpp
b/src/ftinspect/glyphcomponents/glyphcontinuous.hpp
new file mode 100644
index 0000000..7366de9
--- /dev/null
+++ b/src/ftinspect/glyphcomponents/glyphcontinuous.hpp
@@ -0,0 +1,204 @@
+// glyphcontinuous.hpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#pragma once
+
+#include "graphicsdefault.hpp"
+#include "../engine/stringrenderer.hpp"
+
+#include <utility>
+#include <vector>
+
+#include <QWidget>
+#include <QImage>
+#include <QTimer>
+
+#include <freetype/freetype.h>
+#include <freetype/ftglyph.h>
+#include <freetype/ftoutln.h>
+#include <freetype/ftstroke.h>
+
+
+// We store images in the cache so we don't need to render all glyphs every
time
+// when repainting the widget.
+struct GlyphCacheEntry
+{
+ QImage* image = NULL;
+ QRect basePosition = {};
+ QPoint penPos = {};
+ int charCode = -1;
+ int glyphIndex = -1;
+ unsigned nonSpacingPlaceholder = 0;
+
+ FT_Vector advance = {};
+
+ GlyphCacheEntry() {}
+ ~GlyphCacheEntry();
+ GlyphCacheEntry(const GlyphCacheEntry& other) = delete;
+ GlyphCacheEntry& operator=(const GlyphCacheEntry& other) = delete;
+ GlyphCacheEntry(GlyphCacheEntry&& other) noexcept;
+ GlyphCacheEntry& operator=(GlyphCacheEntry&& other) noexcept;
+};
+
+
+struct GlyphCacheLine
+{
+ QPoint basePosition = {};
+ double sizePoint = 0.0;
+ int sizeIndicatorOffset;
+ unsigned short nonSpacingPlaceholder;
+ std::vector<GlyphCacheEntry> entries;
+};
+
+
+class Engine;
+class GlyphContinuous
+: public QWidget
+{
+ Q_OBJECT
+public:
+ GlyphContinuous(QWidget* parent, Engine* engine);
+ ~GlyphContinuous() override;
+
+ enum Source : int
+ {
+ SRC_AllGlyphs,
+ SRC_TextString,
+ SRC_TextStringRepeated
+ };
+
+ enum Mode : int
+ {
+ M_Normal,
+ M_Fancy,
+ M_Stroked
+ };
+
+ int displayingCount() { return displayingCount_; }
+ StringRenderer& stringRenderer() { return stringRenderer_; }
+
+ // all those setters don't trigger repaint.
+ void setBeginIndex(int index) { beginIndex_ = index; }
+ void setSource(Source source);
+ void setMode(Mode mode) { mode_ = mode; }
+ void setScale(double scale) { scale_ = scale; }
+ void setFancyParams(double boldX, double boldY, double slant)
+ {
+ boldX_ = boldX;
+ boldY_ = boldY;
+ slant_ = slant;
+ }
+ void setStrokeRadius(double radius) { strokeRadius_ = radius; }
+ void setSourceText(QString text);
+ void setMouseOperationEnabled(bool enabled)
+ {
+ mouseOperationEnabled_ = enabled;
+ }
+
+ void flashOnGlyph(int glyphIndex);
+ void stopFlashing();
+ void purgeCache();
+ void resetPositionDelta();
+
+signals:
+ void wheelNavigate(int steps);
+ void wheelResize(int steps);
+ void wheelZoom(int steps);
+ void beginIndexChangeRequest(int newIndex);
+ void displayingCountUpdated(int newCount);
+
+protected:
+ void paintEvent(QPaintEvent* event) override;
+ void wheelEvent(QWheelEvent* event) override;
+ void resizeEvent(QResizeEvent* event) override;
+ void mousePressEvent(QMouseEvent* event) override;
+ void mouseMoveEvent(QMouseEvent* event) override;
+
+private:
+ Engine* engine_;
+ StringRenderer stringRenderer_;
+
+ QTimer* flashTimer_;
+ int flashRemainingCount_ = 0;
+ int flashGlyphIndex_ = -1;
+
+ Source source_ = SRC_AllGlyphs;
+ Mode mode_ = M_Normal;
+ int beginIndex_;
+ double boldX_, boldY_, slant_;
+ double strokeRadius_;
+ QString text_;
+ int sizeIndicatorOffset_ = 0; // For Waterfall Rendering...
+
+ bool mouseOperationEnabled_ = true;
+ int displayingCount_ = 0;
+ FT_Fixed strokeRadiusForSize_ = 0;
+ double scale_ = 1.0;
+ FT_Matrix shearMatrix_;
+
+ FT_Stroker stroker_;
+
+ std::vector<GlyphCacheLine> glyphCache_;
+ QColor backgroundColorCache_;
+ GlyphCacheLine* currentWritingLine_ = NULL;
+
+ // Mouse operation related fields
+ QPoint positionDelta_; // For dragging on the text to move
+ double prevHoriPosition_;
+ QPoint prevPositionDelta_ = { 0, 0 };
+ QPoint mouseDownPostition_ = { 0, 0 };
+ int prevIndex_ = -1;
+ int averageLineCount_ = 0;
+
+ void paintByRenderer();
+
+ // These two assume ownership of glyphs, but don't free them.
+ // However, remember to free the glyph returned from `transformGlyphStroked`
+ void transformGlyphFancy(FT_Glyph glyph);
+ FT_Glyph transformGlyphStroked(FT_Glyph glyph);
+
+ void paintCache(QPainter* painter);
+ void fillCache();
+ void prePaint();
+ void updateStroke();
+ void updateRendererText();
+ void preprocessGlyph(FT_Glyph* glyphPtr);
+ // Callbacks
+ void beginSaveLine(FT_Vector pos,
+ double sizePoint);
+ void saveSingleGlyph(FT_Glyph glyph,
+ FT_Vector penPos,
+ GlyphContext gctx);
+ void saveSingleGlyphImage(QImage* image,
+ QRect rect,
+ FT_Vector penPos,
+ FT_Vector advance,
+ GlyphContext gctx);
+
+ // Funcs drawing from the cache
+ void beginDrawCacheLine(QPainter* painter,
+ GlyphCacheLine& line);
+ void drawCacheGlyph(QPainter* painter,
+ const GlyphCacheEntry& entry,
+ bool colorInverted = false);
+
+ // Mouse operations
+ GlyphCacheEntry* findGlyphByMouse(QPoint position,
+ double* outSizePoint);
+ int calculateAverageLineCount();
+
+ void flashTimerFired();
+
+ // Mouse constants
+ constexpr static int ClickDragThreshold = 10;
+ constexpr static int HorizontalUnitLength = 100;
+ constexpr static int VerticalUnitLength = 150;
+
+ // Flash Timer constants
+ constexpr static int FlashIntervalMs = 250;
+ constexpr static int FlashDurationMs = 3000;
+};
+
+
+// end of glyphcontinuous.hpp
diff --git a/src/ftinspect/maingui.cpp b/src/ftinspect/maingui.cpp
index 9fc4c01..b4a38dc 100644
--- a/src/ftinspect/maingui.cpp
+++ b/src/ftinspect/maingui.cpp
@@ -164,8 +164,14 @@ MainGUI::onTripletChanged()
void
MainGUI::switchTab()
{
+ auto current = tabWidget_->currentWidget();
reloadCurrentTabFont();
- lastTab_ = tabWidget_->currentWidget();
+
+ if (current == continuousTab_ && lastTab_ == singularTab_
+ && singularTab_->currentGlyph() >= 0)
+ continuousTab_->highlightGlyph(singularTab_->currentGlyph());
+
+ lastTab_ = current;
}
@@ -215,6 +221,7 @@ MainGUI::createLayout()
// right side
singularTab_ = new SingularTab(this, engine_);
+ continuousTab_ = new ContinuousTab(this, engine_);
tabWidget_ = new QTabWidget(this);
tabWidget_->setObjectName("mainTab"); // for stylesheet
@@ -222,10 +229,15 @@ MainGUI::createLayout()
// Note those two list must be in sync
tabs_.push_back(singularTab_);
tabWidget_->addTab(singularTab_, tr("Singular Grid View"));
+ tabs_.push_back(continuousTab_);
+ tabWidget_->addTab(continuousTab_, tr("Continuous View"));
lastTab_ = singularTab_;
tabWidget_->setTabToolTip(0, tr("View single glyph in grid view.\n"
"For pixelwise inspection of the glyphs."));
+ tabWidget_->setTabToolTip(1, tr("View a string of glyphs continuously.\n"
+ "Show all glyphs in the font or render "
+ "strings."));
tripletSelector_ = new TripletSelector(this, engine_);
rightLayout_ = new QVBoxLayout;
diff --git a/src/ftinspect/maingui.hpp b/src/ftinspect/maingui.hpp
index 18e3c98..44c6014 100644
--- a/src/ftinspect/maingui.hpp
+++ b/src/ftinspect/maingui.hpp
@@ -10,6 +10,7 @@
#include "panels/settingpanel.hpp"
#include "panels/abstracttab.hpp"
#include "panels/singular.hpp"
+#include "panels/continuous.hpp"
#include <vector>
#include <QAction>
@@ -88,6 +89,7 @@ private:
QTabWidget* tabWidget_;
std::vector<AbstractTab*> tabs_;
SingularTab* singularTab_;
+ ContinuousTab* continuousTab_;
QWidget* lastTab_ = NULL;
void openFonts(QStringList const& fileNames);
diff --git a/src/ftinspect/meson.build b/src/ftinspect/meson.build
index 405cff8..392ec8a 100644
--- a/src/ftinspect/meson.build
+++ b/src/ftinspect/meson.build
@@ -27,8 +27,11 @@ if qt5_dep.found()
'engine/paletteinfo.cpp',
'engine/mmgx.cpp',
'engine/fontinfo.cpp',
+ 'engine/stringrenderer.cpp',
+ 'engine/charmap.cpp',
'glyphcomponents/glyphbitmap.cpp',
+ 'glyphcomponents/glyphcontinuous.cpp',
'glyphcomponents/glyphoutline.cpp',
'glyphcomponents/glyphpointnumbers.cpp',
'glyphcomponents/glyphpoints.cpp',
@@ -39,12 +42,14 @@ if qt5_dep.found()
'widgets/tripletselector.cpp',
'widgets/glyphindexselector.cpp',
'widgets/fontsizeselector.cpp',
+ 'widgets/charmapcombobox.cpp',
'models/customcomboboxmodels.cpp',
'panels/settingpanel.cpp',
'panels/settingpanelmmgx.cpp',
'panels/singular.cpp',
+ 'panels/continuous.cpp',
'ftinspect.cpp',
'maingui.cpp',
@@ -54,15 +59,18 @@ if qt5_dep.found()
moc_files = qt5.preprocess(
moc_headers: [
'engine/fontfilemanager.hpp',
+ 'glyphcomponents/glyphcontinuous.hpp',
'widgets/customwidgets.hpp',
'widgets/tripletselector.hpp',
'widgets/glyphindexselector.hpp',
'widgets/fontsizeselector.hpp',
+ 'widgets/charmapcombobox.hpp',
'maingui.hpp',
'models/customcomboboxmodels.hpp',
'panels/settingpanel.hpp',
'panels/settingpanelmmgx.hpp',
'panels/singular.hpp',
+ 'panels/continuous.hpp',
],
dependencies: qt5_dep)
diff --git a/src/ftinspect/panels/continuous.cpp
b/src/ftinspect/panels/continuous.cpp
new file mode 100644
index 0000000..c10a7ef
--- /dev/null
+++ b/src/ftinspect/panels/continuous.cpp
@@ -0,0 +1,699 @@
+// continuous.cpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#include "continuous.hpp"
+
+#include "../uihelper.hpp"
+
+#include <climits>
+#include <QToolTip>
+#include <QVariant>
+
+
+ContinuousTab::ContinuousTab(QWidget* parent,
+ Engine* engine)
+: QWidget(parent),
+ engine_(engine)
+{
+ createLayout();
+
+ std::vector<CharMapInfo> tempCharMaps;
+ charMapSelector_->repopulate(tempCharMaps); // pass in an empty one
+
+ checkModeSource();
+ setDefaults();
+
+ createConnections();
+}
+
+
+void
+ContinuousTab::repaintGlyph()
+{
+ sizeSelector_->applyToEngine(engine_);
+
+ applySettings();
+ canvas_->stopFlashing();
+ canvas_->purgeCache();
+ canvas_->repaint();
+}
+
+
+void
+ContinuousTab::reloadFont()
+{
+ currentGlyphCount_ = engine_->currentFontNumberOfGlyphs();
+ {
+ QSignalBlocker blocker(sizeSelector_);
+ sizeSelector_->reloadFromFont(engine_);
+ }
+ setGlyphCount(qBound(0, currentGlyphCount_, INT_MAX));
+ checkModeSource();
+
+ charMapSelector_->repopulate();
+ canvas_->stopFlashing();
+ canvas_->stringRenderer().reloadAll();
+ canvas_->purgeCache();
+ repaintGlyph();
+}
+
+
+void
+ContinuousTab::applySettings()
+{
+ auto mode =
static_cast<GlyphContinuous::Mode>(modeSelector_->currentIndex());
+ auto src
+ = static_cast<GlyphContinuous::Source>(sourceSelector_->currentIndex());
+ canvas_->setMode(mode);
+ canvas_->setSource(src);
+ canvas_->setBeginIndex(indexSelector_->currentIndex());
+ canvas_->setScale(sizeSelector_->zoomFactor());
+ auto& sr = canvas_->stringRenderer();
+ sr.setWaterfall(waterfallCheckBox_->isChecked());
+ sr.setVertical(verticalCheckBox_->isChecked());
+ sr.setKerning(kerningCheckBox_->isChecked());
+ sr.setRotation(rotationSpinBox_->value());
+
+ // -1: Glyph order, otherwise the char map index in the original list
+ sr.setCharMapIndex(charMapSelector_->currentCharMapIndex(),
glyphLimitIndex_);
+
+ if (sr.isWaterfall())
+ sr.setWaterfallParameters(wfConfigDialog_->startSize(),
+ wfConfigDialog_->endSize());
+
+ canvas_->setFancyParams(xEmboldeningSpinBox_->value(),
+ yEmboldeningSpinBox_->value(),
+ slantSpinBox_->value());
+ canvas_->setStrokeRadius(strokeRadiusSpinBox_->value());
+}
+
+
+void
+ContinuousTab::highlightGlyph(int index)
+{
+ canvas_->flashOnGlyph(index);
+}
+
+
+void
+ContinuousTab::setGlyphCount(int count)
+{
+ currentGlyphCount_ = count;
+ updateLimitIndex();
+}
+
+
+void
+ContinuousTab::setGlyphBeginindex(int index)
+{
+ indexSelector_->setCurrentIndex(index);
+}
+
+
+void
+ContinuousTab::updateLimitIndex()
+{
+ auto cMap = charMapSelector_->currentCharMapIndex();
+ if (cMap < 0)
+ glyphLimitIndex_ = currentGlyphCount_;
+ else
+ glyphLimitIndex_ = charMapSelector_->charMaps()[cMap].maxIndex + 1;
+ indexSelector_->setMinMax(0, glyphLimitIndex_ - 1);
+}
+
+
+void
+ContinuousTab::checkModeSource()
+{
+ auto isFancy = modeSelector_->currentIndex() == GlyphContinuous::M_Fancy;
+ auto isStroked = modeSelector_->currentIndex() == GlyphContinuous::M_Stroked;
+ xEmboldeningSpinBox_->setEnabled(isFancy);
+ yEmboldeningSpinBox_->setEnabled(isFancy);
+ slantSpinBox_->setEnabled(isFancy);
+ strokeRadiusSpinBox_->setEnabled(isStroked);
+
+ auto src
+ = static_cast<GlyphContinuous::Source>(sourceSelector_->currentIndex());
+ auto isTextStrict = src == GlyphContinuous::SRC_TextString;
+ auto isText = src == GlyphContinuous::SRC_TextString
+ || src == GlyphContinuous::SRC_TextStringRepeated;
+ indexSelector_->setEnabled(src == GlyphContinuous::SRC_AllGlyphs);
+ sourceTextEdit_->setEnabled(isText);
+ sampleStringSelector_->setEnabled(isText);
+
+ {
+ QSignalBlocker blocker(kerningCheckBox_);
+ kerningCheckBox_->setEnabled(isText);
+ if (!isText)
+ kerningCheckBox_->setChecked(false);
+ }
+
+ canvas_->setSource(src);
+
+ {
+ auto wf = waterfallCheckBox_->isChecked();
+ QSignalBlocker blocker(verticalCheckBox_);
+ if (wf || !isTextStrict)
+ verticalCheckBox_->setChecked(false);
+ verticalCheckBox_->setEnabled(!wf && isTextStrict);
+ }
+
+ {
+ auto vert = verticalCheckBox_->isChecked();
+ QSignalBlocker blocker(waterfallCheckBox_);
+ if (vert)
+ waterfallCheckBox_->setChecked(false);
+ waterfallCheckBox_->setEnabled(!vert);
+ }
+
+ waterfallConfigButton_->setEnabled(waterfallCheckBox_->isChecked()
+ && !engine_->currentFontBitmapOnly());
+}
+
+
+void
+ContinuousTab::checkModeSourceAndRepaint()
+{
+ checkModeSource();
+ repaintGlyph();
+}
+
+
+void
+ContinuousTab::charMapChanged()
+{
+ int newIndex = charMapSelector_->currentCharMapIndex();
+ if (newIndex != lastCharMapIndex_)
+ setGlyphBeginindex(charMapSelector_->defaultFirstGlyphIndex());
+ updateLimitIndex();
+
+ applySettings();
+ canvas_->stringRenderer().reloadAll();
+ repaintGlyph();
+ lastCharMapIndex_ = newIndex;
+}
+
+
+void
+ContinuousTab::sourceTextChanged()
+{
+ canvas_->setSourceText(sourceTextEdit_->toPlainText());
+ repaintGlyph();
+}
+
+
+void
+ContinuousTab::presetStringSelected()
+{
+ auto index = sampleStringSelector_->currentIndex();
+ if (index < 0)
+ return;
+
+ auto var = sampleStringSelector_->currentData();
+ if (var.isValid() && var.canConvert<QString>())
+ {
+ auto str = var.toString();
+ if (!str.isEmpty())
+ sourceTextEdit_->setPlainText(str);
+ }
+}
+
+
+void
+ContinuousTab::reloadGlyphsAndRepaint()
+{
+ canvas_->stringRenderer().reloadGlyphs();
+ repaintGlyph();
+}
+
+
+void
+ContinuousTab::openWaterfallConfig()
+{
+ wfConfigDialog_->setVisible(true); // no `exec`: modalless
+}
+
+
+void
+ContinuousTab::showToolTip()
+{
+ QToolTip::showText(mapToGlobal(helpButton_->pos()),
+ tr(
+R"(Shift + Scroll: Adjust Font Size
+Ctrl + Scroll: Adjust Zoom Factor
+Shift + Plus/Minus: Adjust Font Size
+Shift + 0: Reset Font Size to Default
+Left Click: Show Glyph Details Info
+Right Click: Inspect Glyph in Singular Grid View
+
+<All Glyphs Source>
+ Drag: Adjust Begin Index
+<Text String Source>
+ Drag: Move String Position)"),
+ helpButton_);
+}
+
+
+bool
+ContinuousTab::eventFilter(QObject* watched,
+ QEvent* event)
+{
+ if (event->type() == QEvent::KeyPress)
+ {
+ auto keyEvent = dynamic_cast<QKeyEvent*>(event);
+ if (sizeSelector_->handleKeyEvent(keyEvent))
+ return true;
+ }
+ return false;
+}
+
+
+void
+ContinuousTab::wheelNavigate(int steps)
+{
+ if (sourceSelector_->currentIndex() == GlyphContinuous::SRC_AllGlyphs)
+ setGlyphBeginindex(indexSelector_->currentIndex() + steps);
+}
+
+
+void
+ContinuousTab::wheelZoom(int steps)
+{
+ sizeSelector_->handleWheelZoomBySteps(steps);
+}
+
+
+void
+ContinuousTab::wheelResize(int steps)
+{
+ sizeSelector_->handleWheelResizeBySteps(steps);
+}
+
+
+void
+ContinuousTab::createLayout()
+{
+ canvasFrame_ = new QFrame(this);
+ canvasFrame_->setFrameStyle(QFrame::StyledPanel | QFrame::Plain);
+
+ canvas_ = new GlyphContinuous(canvasFrame_, engine_);
+ sizeSelector_ = new FontSizeSelector(this, false, true);
+
+ indexSelector_ = new GlyphIndexSelector(this);
+ indexSelector_->setSingleMode(false);
+ indexSelector_->setNumberRenderer([this](int index)
+ { return formatIndex(index); });
+ sourceTextEdit_ = new QPlainTextEdit(
+ tr("The quick brown fox jumps over the lazy dog."), this);
+
+ modeSelector_ = new QComboBox(this);
+ charMapSelector_ = new CharMapComboBox(this, engine_);
+ sourceSelector_ = new QComboBox(this);
+ sampleStringSelector_ = new QComboBox(this);
+
+ charMapSelector_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
+
+ // Note: in sync with the enum!!
+ modeSelector_->insertItem(GlyphContinuous::M_Normal, tr("Normal"));
+ modeSelector_->insertItem(GlyphContinuous::M_Fancy, tr("Fancy"));
+ modeSelector_->insertItem(GlyphContinuous::M_Stroked, tr("Stroked"));
+ modeSelector_->setCurrentIndex(GlyphContinuous::M_Normal);
+
+ // Note: in sync with the enum!!
+ sourceSelector_->insertItem(GlyphContinuous::SRC_AllGlyphs,
+ tr("All Glyphs"));
+ sourceSelector_->insertItem(GlyphContinuous::SRC_TextString,
+ tr("Text String"));
+ sourceSelector_->insertItem(GlyphContinuous::SRC_TextStringRepeated,
+ tr("Text String (Repeated)"));
+
+ verticalCheckBox_ = new QCheckBox(tr("Vertical"), this);
+ waterfallCheckBox_ = new QCheckBox(tr("Waterfall"), this);
+ kerningCheckBox_ = new QCheckBox(tr("Kerning"), this);
+
+ modeLabel_ = new QLabel(tr("Mode:"), this);
+ sourceLabel_ = new QLabel(tr("Text Source:"), this);
+ charMapLabel_ = new QLabel(tr("Char Map:"), this);
+ xEmboldeningLabel_ = new QLabel(tr("Horz. Emb.:"), this);
+ yEmboldeningLabel_ = new QLabel(tr("Vert. Emb.:"), this);
+ slantLabel_ = new QLabel(tr("Slanting:"), this);
+ strokeRadiusLabel_ = new QLabel(tr("Stroke Radius:"), this);
+ rotationLabel_ = new QLabel(tr("Rotation:"), this);
+
+ resetPositionButton_ = new QPushButton(tr("Reset Pos"), this);
+ waterfallConfigButton_ = new QPushButton(tr("WF Config"), this);
+ helpButton_ = new QPushButton(this);
+ helpButton_->setText(tr("?"));
+
+ xEmboldeningSpinBox_ = new QDoubleSpinBox(this);
+ yEmboldeningSpinBox_ = new QDoubleSpinBox(this);
+ slantSpinBox_ = new QDoubleSpinBox(this);
+ strokeRadiusSpinBox_ = new QDoubleSpinBox(this);
+ rotationSpinBox_ = new QDoubleSpinBox(this);
+
+ xEmboldeningSpinBox_->setSingleStep(0.005);
+ xEmboldeningSpinBox_->setMinimum(-0.1);
+ xEmboldeningSpinBox_->setMaximum(0.1);
+ yEmboldeningSpinBox_->setSingleStep(0.005);
+ yEmboldeningSpinBox_->setMinimum(-0.1);
+ yEmboldeningSpinBox_->setMaximum(0.1);
+ slantSpinBox_->setSingleStep(0.02);
+ slantSpinBox_->setMinimum(-1);
+ slantSpinBox_->setMaximum(1);
+ strokeRadiusSpinBox_->setSingleStep(0.005);
+ strokeRadiusSpinBox_->setMinimum(0);
+ strokeRadiusSpinBox_->setMaximum(0.05);
+ rotationSpinBox_->setSingleStep(5);
+ rotationSpinBox_->setMinimum(-180);
+ rotationSpinBox_->setMaximum(180);
+
+ wfConfigDialog_ = new WaterfallConfigDialog(this);
+
+ // Tooltips
+ sourceSelector_->setToolTip(tr("Choose what to display as the text
source."));
+ modeSelector_->setToolTip(
+ tr("Choose the special effect in which the text is displayed."));
+ strokeRadiusSpinBox_->setToolTip(
+ tr("Stroke corner radius (only available when mode set to Stroked)"));
+ rotationSpinBox_->setToolTip(tr("Rotation, in degrees"));
+ xEmboldeningSpinBox_->setToolTip(
+ tr("Horizontal Emboldening (only available when mode set to Fancy)"));
+ yEmboldeningSpinBox_->setToolTip(
+ tr("Vertical Emboldening (only available when mode set to Fancy)"));
+ slantSpinBox_->setToolTip(
+ tr("Slanting (only available when mode set to Fancy)"));
+ sourceTextEdit_->setToolTip(
+ tr("Source string (only available when source set to Text String)"));
+ waterfallConfigButton_->setToolTip(tr(
+ "Set waterfall start and end size. Not available when the font is not\n"
+ "scalable because in such case all available sizes would be displayed."));
+ sampleStringSelector_->setToolTip(
+ tr("Select preset sample strings (only available when source set to\nText "
+ "String)"));
+ resetPositionButton_->setToolTip(tr("Reset the position to the center (only "
+ "available when source set to\nText "
+ "String)"));
+ waterfallCheckBox_->setToolTip(tr(
+ "Enable waterfall mode: show the font output in different sizes.\nWill "
+ "show all available sizes when the font is not scalable."));
+ verticalCheckBox_->setToolTip(tr("Enable vertical rendering (only
available\n"
+ "when source set to Text String)"));
+ kerningCheckBox_->setToolTip(tr("Enable kerning (GPOS table unsupported)"));
+ helpButton_->setToolTip(tr("Get mouse helps"));
+
+ // Layouting
+ canvasFrameLayout_ = new QHBoxLayout;
+ canvasFrameLayout_->addWidget(canvas_);
+ canvasFrame_->setLayout(canvasFrameLayout_);
+ canvasFrameLayout_->setContentsMargins(2, 2, 2, 2);
+ canvasFrame_->setContentsMargins(2, 2, 2, 2);
+
+ sizeHelpLayout_ = new QHBoxLayout;
+ sizeHelpLayout_->addWidget(sizeSelector_, 1, Qt::AlignVCenter);
+ sizeHelpLayout_->addWidget(helpButton_, 0);
+
+ bottomLayout_ = new QGridLayout;
+ bottomLayout_->addWidget(sourceLabel_, 0, 0);
+ bottomLayout_->addWidget(modeLabel_, 1, 0);
+ bottomLayout_->addWidget(charMapLabel_, 2, 0);
+ bottomLayout_->addWidget(sourceSelector_, 0, 1);
+ bottomLayout_->addWidget(modeSelector_, 1, 1);
+ bottomLayout_->addWidget(charMapSelector_, 2, 1);
+
+ bottomLayout_->addWidget(xEmboldeningLabel_, 1, 2);
+ bottomLayout_->addWidget(yEmboldeningLabel_, 2, 2);
+ bottomLayout_->addWidget(slantLabel_, 3, 2);
+ bottomLayout_->addWidget(strokeRadiusLabel_, 3, 0);
+ bottomLayout_->addWidget(rotationLabel_, 0, 2);
+
+ bottomLayout_->addWidget(xEmboldeningSpinBox_, 1, 3);
+ bottomLayout_->addWidget(yEmboldeningSpinBox_, 2, 3);
+ bottomLayout_->addWidget(slantSpinBox_, 3, 3);
+ bottomLayout_->addWidget(strokeRadiusSpinBox_, 3, 1);
+ bottomLayout_->addWidget(rotationSpinBox_, 0, 3);
+
+ bottomLayout_->addWidget(indexSelector_, 0, 4, 1, 2);
+ bottomLayout_->addWidget(sourceTextEdit_, 1, 4, 3, 1);
+ bottomLayout_->addWidget(resetPositionButton_, 0, 6);
+ bottomLayout_->addWidget(waterfallCheckBox_, 1, 6);
+ bottomLayout_->addWidget(verticalCheckBox_, 2, 6);
+ bottomLayout_->addWidget(kerningCheckBox_, 3, 6);
+ bottomLayout_->addWidget(waterfallConfigButton_, 1, 5);
+ bottomLayout_->addWidget(sampleStringSelector_, 2, 5);
+
+ bottomLayout_->setColumnStretch(4, 1);
+
+ mainLayout_ = new QVBoxLayout;
+ mainLayout_->addWidget(canvasFrame_);
+ mainLayout_->addLayout(sizeHelpLayout_);
+ mainLayout_->addLayout(bottomLayout_);
+
+ setLayout(mainLayout_);
+}
+
+
+void
+ContinuousTab::createConnections()
+{
+ connect(sizeSelector_, &FontSizeSelector::valueChanged,
+ this, &ContinuousTab::reloadGlyphsAndRepaint);
+
+ connect(canvas_, &GlyphContinuous::wheelResize,
+ this, &ContinuousTab::wheelResize);
+ connect(canvas_, &GlyphContinuous::wheelNavigate,
+ this, &ContinuousTab::wheelNavigate);
+ connect(canvas_, &GlyphContinuous::wheelZoom,
+ this, &ContinuousTab::wheelZoom);
+ connect(canvas_, &GlyphContinuous::displayingCountUpdated,
+ indexSelector_, &GlyphIndexSelector::setShowingCount);
+ connect(canvas_, &GlyphContinuous::beginIndexChangeRequest,
+ this, &ContinuousTab::setGlyphBeginindex);
+
+ connect(indexSelector_, &GlyphIndexSelector::currentIndexChanged,
+ this, &ContinuousTab::repaintGlyph);
+ connect(modeSelector_, QOverload<int>::of(&QComboBox::currentIndexChanged),
+ this, &ContinuousTab::checkModeSourceAndRepaint);
+ connect(charMapSelector_,
+ QOverload<int>::of(&CharMapComboBox::currentIndexChanged),
+ this, &ContinuousTab::charMapChanged);
+ connect(charMapSelector_, &CharMapComboBox::forceUpdateLimitIndex,
+ this, &ContinuousTab::updateLimitIndex);
+ connect(sourceSelector_, QOverload<int>::of(&QComboBox::currentIndexChanged),
+ this, &ContinuousTab::checkModeSourceAndRepaint);
+
+ connect(resetPositionButton_, &QPushButton::clicked,
+ canvas_, &GlyphContinuous::resetPositionDelta);
+ connect(waterfallConfigButton_, &QPushButton::clicked,
+ this, &ContinuousTab::openWaterfallConfig);
+ connect(helpButton_, &QPushButton::clicked,
+ this, &ContinuousTab::showToolTip);
+ connect(wfConfigDialog_, &WaterfallConfigDialog::sizeUpdated,
+ this, &ContinuousTab::repaintGlyph);
+
+ connect(xEmboldeningSpinBox_,
+ QOverload<double>::of(&QDoubleSpinBox::valueChanged),
+ this, &ContinuousTab::repaintGlyph);
+ connect(yEmboldeningSpinBox_,
+ QOverload<double>::of(&QDoubleSpinBox::valueChanged),
+ this, &ContinuousTab::repaintGlyph);
+ connect(slantSpinBox_,
+ QOverload<double>::of(&QDoubleSpinBox::valueChanged),
+ this, &ContinuousTab::repaintGlyph);
+ connect(strokeRadiusSpinBox_,
+ QOverload<double>::of(&QDoubleSpinBox::valueChanged),
+ this, &ContinuousTab::repaintGlyph);
+ connect(rotationSpinBox_,
+ QOverload<double>::of(&QDoubleSpinBox::valueChanged),
+ this, &ContinuousTab::repaintGlyph);
+
+ connect(waterfallCheckBox_, &QCheckBox::clicked,
+ this, &ContinuousTab::checkModeSourceAndRepaint);
+ connect(verticalCheckBox_, &QCheckBox::clicked,
+ this, &ContinuousTab::checkModeSourceAndRepaint);
+ connect(kerningCheckBox_, &QCheckBox::clicked,
+ this, &ContinuousTab::reloadGlyphsAndRepaint);
+ connect(sourceTextEdit_, &QPlainTextEdit::textChanged,
+ this, &ContinuousTab::sourceTextChanged);
+ connect(sampleStringSelector_,
+ QOverload<int>::of(&QComboBox::currentIndexChanged),
+ this, &ContinuousTab::presetStringSelected);
+
+ sizeSelector_->installEventFilterForWidget(canvas_);
+ sizeSelector_->installEventFilterForWidget(this);
+}
+
+
+extern const char* StringSamples[];
+
+void
+ContinuousTab::setDefaults()
+{
+ xEmboldeningSpinBox_->setValue(0.04);
+ yEmboldeningSpinBox_->setValue(0.04);
+ slantSpinBox_->setValue(0.22);
+ strokeRadiusSpinBox_->setValue(0.02);
+ rotationSpinBox_->setValue(0);
+
+ canvas_->setSourceText(sourceTextEdit_->toPlainText());
+ canvas_->setSource(GlyphContinuous::SRC_AllGlyphs);
+
+ sampleStringSelector_->addItem(tr("<Sample>"));
+ sampleStringSelector_->addItem(tr("English"), QString(StringSamples[0]));
+ sampleStringSelector_->addItem(tr("Latin"), QString(StringSamples[1]));
+ sampleStringSelector_->addItem(tr("Greek"), QString(StringSamples[2]));
+ sampleStringSelector_->addItem(tr("Cyrillic"), QString(StringSamples[3]));
+ sampleStringSelector_->addItem(tr("Chinese"), QString(StringSamples[4]));
+ sampleStringSelector_->addItem(tr("Japanese"), QString(StringSamples[5]));
+ sampleStringSelector_->addItem(tr("Korean"), QString(StringSamples[6]));
+}
+
+
+QString
+ContinuousTab::formatIndex(int index)
+{
+ auto idx = charMapSelector_->currentCharMapIndex();
+ if (idx < 0) // glyph order
+ return QString::number(index);
+ return charMapSelector_->charMaps()[idx].stringifyIndexShort(index);
+}
+
+
+WaterfallConfigDialog::WaterfallConfigDialog(QWidget* parent)
+: QDialog(parent)
+{
+ setModal(false);
+ setWindowTitle(tr("Waterfall Config"));
+ setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
+
+ createLayout();
+ checkAutoStatus();
+ createConnections();
+}
+
+
+double
+WaterfallConfigDialog::startSize()
+{
+ if (autoBox_->isChecked())
+ return -1.0;
+ return startSpinBox_->value();
+}
+
+
+double
+WaterfallConfigDialog::endSize()
+{
+ if (autoBox_->isChecked())
+ return -1.0;
+ return endSpinBox_->value();
+}
+
+
+void
+WaterfallConfigDialog::createLayout()
+{
+ startLabel_ = new QLabel(tr("Start Size (pt):"), this);
+ endLabel_ = new QLabel(tr("End Size (pt):"), this);
+
+ startSpinBox_ = new QDoubleSpinBox(this);
+ endSpinBox_ = new QDoubleSpinBox(this);
+
+ startSpinBox_->setSingleStep(0.5);
+ startSpinBox_->setMinimum(0.5);
+ startSpinBox_->setValue(1);
+
+ endSpinBox_->setSingleStep(0.5);
+ endSpinBox_->setMinimum(0.5);
+ endSpinBox_->setValue(1);
+
+ autoBox_ = new QCheckBox(tr("Auto"), this);
+ autoBox_->setChecked(true);
+
+ // Tooltips
+ autoBox_->setToolTip(tr(
+ "Use the default value which will try to start from near zero and place "
+ "in the middle of the screen the size selected in the selector."));
+ startSpinBox_->setToolTip(tr("Start size, will be always guaranteed."));
+ endSpinBox_->setToolTip(tr(
+ "End size, may not be guaranteed due to rounding and precision issues."));
+
+ // Layouting
+ layout_ = new QGridLayout;
+ gridLayout2ColAddWidget(layout_, autoBox_);
+ gridLayout2ColAddWidget(layout_, startLabel_, startSpinBox_);
+ gridLayout2ColAddWidget(layout_, endLabel_, endSpinBox_);
+
+ setLayout(layout_);
+}
+
+
+void
+WaterfallConfigDialog::createConnections()
+{
+ connect(autoBox_, &QCheckBox::clicked,
+ this, &WaterfallConfigDialog::checkAutoStatus);
+ connect(startSpinBox_,
+ QOverload<double>::of(&QDoubleSpinBox::valueChanged),
+ this, &WaterfallConfigDialog::sizeUpdated);
+ connect(endSpinBox_,
+ QOverload<double>::of(&QDoubleSpinBox::valueChanged),
+ this, &WaterfallConfigDialog::sizeUpdated);
+}
+
+
+void
+WaterfallConfigDialog::checkAutoStatus()
+{
+ startSpinBox_->setEnabled(!autoBox_->isChecked());
+ endSpinBox_->setEnabled(!autoBox_->isChecked());
+
+ emit sizeUpdated();
+}
+
+
+const char* StringSamples[] = {
+ "The quick brown fox jumps over the lazy dog",
+
+ /* Luís argüia à Júlia que «brações, fé, chá, óxido, pôr, zângão» */
+ /* eram palavras do português */
+ "Lu\u00EDs arg\u00FCia \u00E0 J\u00FAlia que \u00ABbra\u00E7\u00F5es, "
+ "f\u00E9, ch\u00E1, \u00F3xido, p\u00F4r, z\u00E2ng\u00E3o\u00BB eram "
+ "palavras do portugu\u00EAs",
+
+ /* Ο καλύμνιος σφουγγαράς ψιθύρισε πως θα βουτήξει χωρίς να διστάζει */
+ "\u039F \u03BA\u03B1\u03BB\u03CD\u03BC\u03BD\u03B9\u03BF\u03C2 \u03C3"
+ "\u03C6\u03BF\u03C5\u03B3\u03B3\u03B1\u03C1\u03AC\u03C2 \u03C8\u03B9"
+ "\u03B8\u03CD\u03C1\u03B9\u03C3\u03B5 \u03C0\u03C9\u03C2 \u03B8\u03B1 "
+ "\u03B2\u03BF\u03C5\u03C4\u03AE\u03BE\u03B5\u03B9 \u03C7\u03C9\u03C1"
+ "\u03AF\u03C2 \u03BD\u03B1 \u03B4\u03B9\u03C3\u03C4\u03AC\u03B6\u03B5"
+ "\u03B9",
+
+ /* Съешь ещё этих мягких французских булок да выпей же чаю */
+ "\u0421\u044A\u0435\u0448\u044C \u0435\u0449\u0451 \u044D\u0442\u0438"
+ "\u0445 \u043C\u044F\u0433\u043A\u0438\u0445 \u0444\u0440\u0430\u043D"
+ "\u0446\u0443\u0437\u0441\u043A\u0438\u0445 \u0431\u0443\u043B\u043E"
+ "\u043A \u0434\u0430 \u0432\u044B\u043F\u0435\u0439 \u0436\u0435 "
+ "\u0447\u0430\u044E",
+
+ /* 天地玄黃,宇宙洪荒。日月盈昃,辰宿列張。寒來暑往,秋收冬藏。*/
+ "\u5929\u5730\u7384\u9EC3\uFF0C\u5B87\u5B99\u6D2A\u8352\u3002\u65E5"
+ "\u6708\u76C8\u6603\uFF0C\u8FB0\u5BBF\u5217\u5F35\u3002\u5BD2\u4F86"
+ "\u6691\u5F80\uFF0C\u79CB\u6536\u51AC\u85CF\u3002",
+
+ /* いろはにほへと ちりぬるを わかよたれそ つねならむ */
+ /* うゐのおくやま けふこえて あさきゆめみし ゑひもせす */
+ "\u3044\u308D\u306F\u306B\u307B\u3078\u3068 \u3061\u308A\u306C\u308B"
+ "\u3092 \u308F\u304B\u3088\u305F\u308C\u305D \u3064\u306D\u306A\u3089"
+ "\u3080 \u3046\u3090\u306E\u304A\u304F\u3084\u307E \u3051\u3075\u3053"
+ "\u3048\u3066 \u3042\u3055\u304D\u3086\u3081\u307F\u3057 \u3091\u3072"
+ "\u3082\u305B\u3059",
+
+ /* 키스의 고유조건은 입술끼리 만나야 하고 특별한 기술은 필요치 않다 */
+ "\uD0A4\uC2A4\uC758 \uACE0\uC720\uC870\uAC74\uC740 \uC785\uC220\uB07C"
+ "\uB9AC \uB9CC\uB098\uC57C \uD558\uACE0 \uD2B9\uBCC4\uD55C \uAE30"
+ "\uC220\uC740 \uD544\uC694\uCE58 \uC54A\uB2E4"
+};
+
+
+// end of continuous.cpp
diff --git a/src/ftinspect/panels/continuous.hpp
b/src/ftinspect/panels/continuous.hpp
new file mode 100644
index 0000000..deec32a
--- /dev/null
+++ b/src/ftinspect/panels/continuous.hpp
@@ -0,0 +1,153 @@
+// continuous.hpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#pragma once
+
+#include "abstracttab.hpp"
+#include "../widgets/customwidgets.hpp"
+#include "../widgets/glyphindexselector.hpp"
+#include "../widgets/fontsizeselector.hpp"
+#include "../widgets/charmapcombobox.hpp"
+#include "../glyphcomponents/graphicsdefault.hpp"
+#include "../glyphcomponents/glyphcontinuous.hpp"
+#include "../engine/engine.hpp"
+
+#include <vector>
+#include <QWidget>
+#include <QDialog>
+#include <QFrame>
+#include <QLabel>
+#include <QComboBox>
+#include <QGridLayout>
+#include <QBoxLayout>
+#include <QPlainTextEdit>
+#include <QCheckBox>
+
+class WaterfallConfigDialog;
+class ContinuousTab
+: public QWidget, public AbstractTab
+{
+ Q_OBJECT
+public:
+ ContinuousTab(QWidget* parent, Engine* engine);
+ ~ContinuousTab() override = default;
+
+ void repaintGlyph() override;
+ void reloadFont() override;
+ void highlightGlyph(int index);
+ void applySettings();
+
+protected:
+ bool eventFilter(QObject* watched, QEvent* event) override;
+
+private:
+ Engine* engine_;
+
+ int currentGlyphCount_;
+ int lastCharMapIndex_ = 0;
+ int glyphLimitIndex_ = 0;
+
+ GlyphContinuous* canvas_;
+ QFrame* canvasFrame_;
+ FontSizeSelector* sizeSelector_;
+
+ QComboBox* modeSelector_;
+ QComboBox* sourceSelector_;
+ CharMapComboBox* charMapSelector_ = NULL;
+ QComboBox* sampleStringSelector_;
+
+ QPushButton* resetPositionButton_;
+ QPushButton* waterfallConfigButton_;
+ QPushButton* helpButton_;
+
+ QLabel* modeLabel_;
+ QLabel* sourceLabel_;
+ QLabel* charMapLabel_;
+ QLabel* xEmboldeningLabel_;
+ QLabel* yEmboldeningLabel_;
+ QLabel* slantLabel_;
+ QLabel* strokeRadiusLabel_;
+ QLabel* rotationLabel_;
+
+ QDoubleSpinBox* xEmboldeningSpinBox_;
+ QDoubleSpinBox* yEmboldeningSpinBox_;
+ QDoubleSpinBox* slantSpinBox_;
+ QDoubleSpinBox* strokeRadiusSpinBox_;
+ QDoubleSpinBox* rotationSpinBox_;
+
+ QCheckBox* verticalCheckBox_;
+ QCheckBox* waterfallCheckBox_;
+ QCheckBox* kerningCheckBox_;
+
+ GlyphIndexSelector* indexSelector_;
+ QPlainTextEdit* sourceTextEdit_;
+
+ QHBoxLayout* canvasFrameLayout_;
+ QHBoxLayout* sizeHelpLayout_;
+ QGridLayout* bottomLayout_;
+ QVBoxLayout* mainLayout_;
+
+ WaterfallConfigDialog* wfConfigDialog_;
+
+ void createLayout();
+ void createConnections();
+
+ void updateLimitIndex();
+ void checkModeSource();
+
+ // This doesn't trigger immediate repaint
+ void setGlyphCount(int count);
+
+ // But they do
+ void setGlyphBeginindex(int index);
+ void checkModeSourceAndRepaint();
+ void charMapChanged();
+ void sourceTextChanged();
+ void presetStringSelected();
+ void reloadGlyphsAndRepaint();
+ void openWaterfallConfig();
+ void showToolTip();
+
+ void wheelNavigate(int steps);
+ void wheelZoom(int steps);
+ void wheelResize(int steps);
+
+ void setDefaults();
+ QString formatIndex(int index);
+};
+
+
+class WaterfallConfigDialog
+: public QDialog
+{
+ Q_OBJECT
+
+public:
+ WaterfallConfigDialog(QWidget* parent);
+
+ double startSize();
+ double endSize();
+
+signals:
+ void sizeUpdated();
+
+private:
+ QLabel* startLabel_;
+ QLabel* endLabel_;
+
+ QDoubleSpinBox* startSpinBox_;
+ QDoubleSpinBox* endSpinBox_;
+
+ QCheckBox* autoBox_;
+
+ QGridLayout* layout_;
+
+ void createLayout();
+ void createConnections();
+
+ void checkAutoStatus();
+};
+
+
+// end of continuous.hpp
diff --git a/src/ftinspect/panels/settingpanel.cpp
b/src/ftinspect/panels/settingpanel.cpp
index 33345f7..403be29 100644
--- a/src/ftinspect/panels/settingpanel.cpp
+++ b/src/ftinspect/panels/settingpanel.cpp
@@ -363,9 +363,9 @@ SettingPanel::applySettings()
engine_->setUseColorLayer(colorLayerCheckBox_->isChecked());
engine_->renderingEngine()->setLCDUsesBGR(aaSettings.isBGR);
- //engine_->renderingEngine()->setLCDSubPixelPositioning(
- // antiAliasingComboBox_->currentIndex()
- // == AntiAliasingComboBoxModel::AntiAliasing_Light_SubPixel);
+ engine_->setLCDSubPixelPositioning(
+ antiAliasingComboBox_->currentIndex()
+ == AntiAliasingComboBoxModel::AntiAliasing_Light_SubPixel);
engine_->renderingEngine()->setForeground(foregroundColor_.rgba());
engine_->renderingEngine()->setBackground(backgroundColor_.rgba());
diff --git a/src/ftinspect/widgets/charmapcombobox.cpp
b/src/ftinspect/widgets/charmapcombobox.cpp
new file mode 100644
index 0000000..b9caa91
--- /dev/null
+++ b/src/ftinspect/widgets/charmapcombobox.cpp
@@ -0,0 +1,118 @@
+// charmapcombobox.cpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#include "charmapcombobox.hpp"
+
+#include "../engine/engine.hpp"
+
+
+CharMapComboBox::CharMapComboBox(QWidget* parent,
+ Engine* engine,
+ bool haveGlyphOrder)
+: QComboBox(parent),
+ haveGlyphOrder_(haveGlyphOrder),
+ engine_(engine)
+{
+ setToolTip("Set current charmap.");
+ connect(this, QOverload<int>::of(&CharMapComboBox::currentIndexChanged),
+ this, &CharMapComboBox::updateToolTip);
+}
+
+
+int
+CharMapComboBox::currentCharMapIndex()
+{
+ auto index = haveGlyphOrder_ ? currentIndex() - 1 : currentIndex();
+ if (index < 0 || charMaps_.size() <= static_cast<unsigned>(index))
+ return -1;
+ return index;
+}
+
+
+int
+CharMapComboBox::defaultFirstGlyphIndex()
+{
+ auto newIndex = currentCharMapIndex();
+ if (newIndex < 0)
+ return 0;
+ if (charMaps_[newIndex].maxIndex <= 20)
+ return charMaps_[newIndex].maxIndex - 1;
+ return 0x20;
+}
+
+
+void
+CharMapComboBox::repopulate()
+{
+ repopulate(engine_->currentFontCharMaps());
+}
+
+
+#define EncodingRole (Qt::UserRole + 10)
+void
+CharMapComboBox::repopulate(std::vector<CharMapInfo>& charMaps)
+{
+ if (charMaps_ == charMaps)
+ {
+ charMaps_ = charMaps; // Still need to substitute because ptr may differ
+ return;
+ }
+ charMaps_ = charMaps;
+ int oldIndex = currentIndex();
+ unsigned oldEncoding = 0u;
+
+ // Using additional UserRole to store encoding id
+ auto oldEncodingV = itemData(oldIndex, EncodingRole);
+ if (oldEncodingV.isValid() && oldEncodingV.canConvert<unsigned>())
+ oldEncoding = oldEncodingV.value<unsigned>();
+
+ { // This brace isn't for the `if` statement!
+ // suppress events during updating
+ QSignalBlocker selectorBlocker(this);
+
+ clear();
+ if (haveGlyphOrder_)
+ {
+ 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 encoding = static_cast<unsigned>(map.encoding);
+ setItemData(haveGlyphOrder_ ? i + 1 : i, encoding, EncodingRole);
+
+ if (encoding == oldEncoding && i == oldIndex)
+ newIndex = i;
+
+ i++;
+ }
+
+ // this shouldn't emit any event either, because force repainting
+ // will happen later, so embrace it into blocker block
+ setCurrentIndex(newIndex);
+ updateToolTip();
+ }
+
+ emit forceUpdateLimitIndex();
+}
+
+
+void
+CharMapComboBox::updateToolTip()
+{
+ auto index = currentIndex();
+ if (index >= 0 && index < count())
+ setToolTip(this->currentText());
+}
+
+
+// end of charmapcombobox.cpp
diff --git a/src/ftinspect/widgets/charmapcombobox.hpp
b/src/ftinspect/widgets/charmapcombobox.hpp
new file mode 100644
index 0000000..bebdbf8
--- /dev/null
+++ b/src/ftinspect/widgets/charmapcombobox.hpp
@@ -0,0 +1,40 @@
+// charmapcombobox.hpp
+
+// Copyright (C) 2022 by Charlie Jiang.
+
+#pragma once
+
+#include "../engine/charmap.hpp"
+
+#include <vector>
+#include <QComboBox>
+
+class Engine;
+class CharMapComboBox
+: public QComboBox
+{
+ Q_OBJECT
+public:
+ CharMapComboBox(QWidget* parent, Engine* engine, bool haveGlyphOrder = true);
+ ~CharMapComboBox() override = default;
+
+ bool haveGlyphOrder_;
+
+ std::vector<CharMapInfo>& charMaps() { return charMaps_; }
+ int currentCharMapIndex();
+ int defaultFirstGlyphIndex();
+ void repopulate();
+ void repopulate(std::vector<CharMapInfo>& charMaps);
+
+signals:
+ void forceUpdateLimitIndex();
+
+private:
+ Engine* engine_;
+ std::vector<CharMapInfo> charMaps_;
+
+ void updateToolTip();
+};
+
+
+// charmapcombobox.hpp
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [freetype2-demos] master e9aa456 22/41: # This is a combination of 2 commits.,
Werner Lemberg <=