mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-04-29 10:26:52 -07:00
feat: Support for kerning and ligatures (#873)
## Summary **What is the goal of this PR?** Improved typesetting, including [kerning](https://en.wikipedia.org/wiki/Kerning) and [ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet). **What changes are included?** - The script to convert built-in fonts now adds kerning and ligature information to the generated font headers. - Epub page layout calculates proper kerning spaces and makes ligature substitutions according to the selected font.    ## Additional Context - I am not a typography expert. - The implementation has been reworked from the earlier version, so it is no longer necessary to omit Open Dyslexic, and kerning data now covers all fonts, styles, and codepoints for which we include bitmap data. - Claude Opus 4.6 helped with a lot of this. - There's an included test epub document with lots of kerning and ligature examples, shown in the photos. **_After some time to mature, I think this change is in decent shape to merge and get people testing._** After opening this PR I came across #660, which overlaps in adding ligature support. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_ --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+92
-5
@@ -20,26 +20,36 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
int lastBaseX = startX;
|
||||
int lastBaseAdvance = 0;
|
||||
int lastBaseTop = 0;
|
||||
bool hasBaseGlyph = false;
|
||||
constexpr int MIN_COMBINING_GAP_PX = 1;
|
||||
uint32_t cp;
|
||||
uint32_t prevCp = 0;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
|
||||
const bool isCombining = utf8IsCombiningMark(cp);
|
||||
|
||||
if (!isCombining) {
|
||||
cp = applyLigatures(cp, string);
|
||||
}
|
||||
|
||||
const EpdGlyph* glyph = getGlyph(cp);
|
||||
if (!glyph) {
|
||||
// TODO: Better handle this?
|
||||
prevCp = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool isCombining = utf8IsCombiningMark(cp);
|
||||
int raiseBy = 0;
|
||||
if (isCombining && hasBaseGlyph) {
|
||||
if (isCombining) {
|
||||
const int currentGap = glyph->top - glyph->height - lastBaseTop;
|
||||
if (currentGap < MIN_COMBINING_GAP_PX) {
|
||||
raiseBy = MIN_COMBINING_GAP_PX - currentGap;
|
||||
}
|
||||
}
|
||||
|
||||
const int glyphBaseX = (isCombining && hasBaseGlyph) ? (lastBaseX + lastBaseAdvance / 2) : cursorX;
|
||||
if (!isCombining && prevCp != 0) {
|
||||
cursorX += getKerning(prevCp, cp);
|
||||
}
|
||||
|
||||
const int glyphBaseX = isCombining ? (lastBaseX + lastBaseAdvance / 2) : cursorX;
|
||||
const int glyphBaseY = cursorY - raiseBy;
|
||||
|
||||
*minX = std::min(*minX, glyphBaseX + glyph->left);
|
||||
@@ -51,8 +61,8 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
lastBaseX = cursorX;
|
||||
lastBaseAdvance = glyph->advanceX;
|
||||
lastBaseTop = glyph->top;
|
||||
hasBaseGlyph = true;
|
||||
cursorX += glyph->advanceX;
|
||||
prevCp = cp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,6 +76,83 @@ void EpdFont::getTextDimensions(const char* string, int* w, int* h) const {
|
||||
*h = maxY - minY;
|
||||
}
|
||||
|
||||
static uint8_t lookupKernClass(const EpdKernClassEntry* entries, const uint16_t count, const uint32_t cp) {
|
||||
if (!entries || count == 0 || cp > 0xFFFF) {
|
||||
return 0;
|
||||
}
|
||||
const auto target = static_cast<uint16_t>(cp);
|
||||
int left = 0;
|
||||
int right = static_cast<int>(count) - 1;
|
||||
while (left <= right) {
|
||||
const int mid = left + (right - left) / 2;
|
||||
const uint16_t midCp = entries[mid].codepoint;
|
||||
if (midCp == target) {
|
||||
return entries[mid].classId;
|
||||
}
|
||||
if (midCp < target) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
right = mid - 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int8_t EpdFont::getKerning(const uint32_t leftCp, const uint32_t rightCp) const {
|
||||
if (!data->kernMatrix) {
|
||||
return 0;
|
||||
}
|
||||
const uint8_t lc = lookupKernClass(data->kernLeftClasses, data->kernLeftEntryCount, leftCp);
|
||||
if (lc == 0) return 0;
|
||||
const uint8_t rc = lookupKernClass(data->kernRightClasses, data->kernRightEntryCount, rightCp);
|
||||
if (rc == 0) return 0;
|
||||
return data->kernMatrix[(lc - 1) * data->kernRightClassCount + (rc - 1)];
|
||||
}
|
||||
|
||||
uint32_t EpdFont::getLigature(const uint32_t leftCp, const uint32_t rightCp) const {
|
||||
const auto* pairs = data->ligaturePairs;
|
||||
const auto count = data->ligaturePairCount;
|
||||
if (!pairs || count == 0 || leftCp > 0xFFFF || rightCp > 0xFFFF) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint32_t key = (leftCp << 16) | rightCp;
|
||||
int left = 0;
|
||||
int right = static_cast<int>(count) - 1;
|
||||
|
||||
while (left <= right) {
|
||||
const int mid = left + (right - left) / 2;
|
||||
const uint32_t midKey = pairs[mid].pair;
|
||||
if (midKey == key) {
|
||||
return pairs[mid].ligatureCp;
|
||||
}
|
||||
if (midKey < key) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
right = mid - 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t EpdFont::applyLigatures(uint32_t cp, const char*& text) const {
|
||||
if (!data->ligaturePairs || data->ligaturePairCount == 0) {
|
||||
return cp;
|
||||
}
|
||||
while (true) {
|
||||
const auto saved = reinterpret_cast<const uint8_t*>(text);
|
||||
const uint32_t nextCp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text));
|
||||
if (nextCp == 0) break;
|
||||
const uint32_t lig = getLigature(cp, nextCp);
|
||||
if (lig == 0) {
|
||||
text = reinterpret_cast<const char*>(saved);
|
||||
break;
|
||||
}
|
||||
cp = lig;
|
||||
}
|
||||
return cp;
|
||||
}
|
||||
|
||||
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
|
||||
const EpdUnicodeInterval* intervals = data->intervals;
|
||||
const int count = data->intervalCount;
|
||||
|
||||
@@ -11,4 +11,16 @@ class EpdFont {
|
||||
void getTextDimensions(const char* string, int* w, int* h) const;
|
||||
|
||||
const EpdGlyph* getGlyph(uint32_t cp) const;
|
||||
|
||||
/// Returns the kerning adjustment (in pixels) between two codepoints.
|
||||
/// Returns 0 if no kerning data exists for the pair.
|
||||
int8_t getKerning(uint32_t leftCp, uint32_t rightCp) const;
|
||||
|
||||
/// Returns the ligature codepoint for a pair, or 0 if no ligature exists.
|
||||
uint32_t getLigature(uint32_t leftCp, uint32_t rightCp) const;
|
||||
|
||||
/// Greedily applies ligature substitutions starting from cp, consuming
|
||||
/// as many following codepoints from text as possible. Returns the
|
||||
/// (possibly substituted) codepoint; advances text past consumed chars.
|
||||
uint32_t applyLigatures(uint32_t cp, const char*& text) const;
|
||||
};
|
||||
|
||||
@@ -31,6 +31,20 @@ typedef struct {
|
||||
uint32_t offset; ///< Index of the first code point into the glyph array
|
||||
} EpdUnicodeInterval;
|
||||
|
||||
/// Maps a codepoint to a kerning class ID, sorted by codepoint for binary search.
|
||||
/// Class IDs are 1-based; codepoints not in the table have implicit class 0 (no kerning).
|
||||
typedef struct {
|
||||
uint16_t codepoint; ///< Unicode codepoint
|
||||
uint8_t classId; ///< 1-based kerning class ID
|
||||
} __attribute__((packed)) EpdKernClassEntry;
|
||||
|
||||
/// Ligature substitution for a specific glyph pair, sorted by `pair` for binary search.
|
||||
/// `pair` encodes (leftCodepoint << 16 | rightCodepoint) for single-key lookup.
|
||||
typedef struct {
|
||||
uint32_t pair; ///< Packed codepoint pair (left << 16 | right)
|
||||
uint32_t ligatureCp; ///< Codepoint of the replacement ligature glyph
|
||||
} __attribute__((packed)) EpdLigaturePair;
|
||||
|
||||
/// Data stored for FONT AS A WHOLE
|
||||
typedef struct {
|
||||
const uint8_t* bitmap; ///< Glyph bitmaps, concatenated
|
||||
@@ -41,6 +55,15 @@ typedef struct {
|
||||
int ascender; ///< Maximal height of a glyph above the base line
|
||||
int descender; ///< Maximal height of a glyph below the base line
|
||||
bool is2Bit;
|
||||
const EpdFontGroup* groups; ///< NULL for uncompressed fonts
|
||||
uint16_t groupCount; ///< 0 for uncompressed fonts
|
||||
const EpdFontGroup* groups; ///< NULL for uncompressed fonts
|
||||
uint16_t groupCount; ///< 0 for uncompressed fonts
|
||||
const EpdKernClassEntry* kernLeftClasses; ///< Sorted left-side class map (nullptr if none)
|
||||
const EpdKernClassEntry* kernRightClasses; ///< Sorted right-side class map (nullptr if none)
|
||||
const int8_t* kernMatrix; ///< Flat leftClassCount x rightClassCount matrix
|
||||
uint16_t kernLeftEntryCount; ///< Entries in kernLeftClasses
|
||||
uint16_t kernRightEntryCount; ///< Entries in kernRightClasses
|
||||
uint8_t kernLeftClassCount; ///< Number of distinct left classes (matrix rows)
|
||||
uint8_t kernRightClassCount; ///< Number of distinct right classes (matrix cols)
|
||||
const EpdLigaturePair* ligaturePairs; ///< Sorted ligature pair table (nullptr if none)
|
||||
uint32_t ligaturePairCount; ///< Number of entries in ligaturePairs
|
||||
} EpdFontData;
|
||||
|
||||
@@ -26,4 +26,12 @@ const EpdFontData* EpdFontFamily::getData(const Style style) const { return getF
|
||||
|
||||
const EpdGlyph* EpdFontFamily::getGlyph(const uint32_t cp, const Style style) const {
|
||||
return getFont(style)->getGlyph(cp);
|
||||
};
|
||||
}
|
||||
|
||||
int8_t EpdFontFamily::getKerning(const uint32_t leftCp, const uint32_t rightCp, const Style style) const {
|
||||
return getFont(style)->getKerning(leftCp, rightCp);
|
||||
}
|
||||
|
||||
uint32_t EpdFontFamily::applyLigatures(const uint32_t cp, const char*& text, const Style style) const {
|
||||
return getFont(style)->applyLigatures(cp, text);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ class EpdFontFamily {
|
||||
void getTextDimensions(const char* string, int* w, int* h, Style style = REGULAR) const;
|
||||
const EpdFontData* getData(Style style = REGULAR) const;
|
||||
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;
|
||||
int8_t getKerning(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const;
|
||||
uint32_t applyLigatures(uint32_t cp, const char*& text, Style style = REGULAR) const;
|
||||
|
||||
private:
|
||||
const EpdFont* regular;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user