2025-12-03 22:00:29 +11:00
|
|
|
#include "EpdFont.h"
|
|
|
|
|
|
|
|
|
|
#include <Utf8.h>
|
|
|
|
|
|
2025-12-30 23:18:51 +11:00
|
|
|
#include <algorithm>
|
2025-12-03 22:00:29 +11:00
|
|
|
|
|
|
|
|
void EpdFont::getTextBounds(const char* string, const int startX, const int startY, int* minX, int* minY, int* maxX,
|
|
|
|
|
int* maxY) const {
|
|
|
|
|
*minX = startX;
|
|
|
|
|
*minY = startY;
|
|
|
|
|
*maxX = startX;
|
|
|
|
|
*maxY = startY;
|
|
|
|
|
|
|
|
|
|
if (*string == '\0') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 10:43:37 -06:00
|
|
|
int32_t cursorXFP = fp4::fromPixel(startX); // 12.4 fixed-point accumulator
|
2026-02-22 03:11:07 +01:00
|
|
|
int lastBaseX = startX;
|
2026-03-01 10:43:37 -06:00
|
|
|
int lastBaseAdvanceFP = 0; // 12.4 fixed-point
|
2026-02-22 03:11:07 +01:00
|
|
|
int lastBaseTop = 0;
|
|
|
|
|
constexpr int MIN_COMBINING_GAP_PX = 1;
|
2025-12-03 22:00:29 +11:00
|
|
|
uint32_t cp;
|
2026-02-24 02:31:43 -06:00
|
|
|
uint32_t prevCp = 0;
|
2025-12-03 22:00:29 +11:00
|
|
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
|
2026-02-24 02:31:43 -06:00
|
|
|
const bool isCombining = utf8IsCombiningMark(cp);
|
|
|
|
|
|
|
|
|
|
if (!isCombining) {
|
|
|
|
|
cp = applyLigatures(cp, string);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:00:29 +11:00
|
|
|
const EpdGlyph* glyph = getGlyph(cp);
|
|
|
|
|
if (!glyph) {
|
2026-02-24 02:31:43 -06:00
|
|
|
prevCp = 0;
|
2025-12-03 22:00:29 +11:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 03:11:07 +01:00
|
|
|
int raiseBy = 0;
|
2026-02-24 02:31:43 -06:00
|
|
|
if (isCombining) {
|
2026-02-22 03:11:07 +01:00
|
|
|
const int currentGap = glyph->top - glyph->height - lastBaseTop;
|
|
|
|
|
if (currentGap < MIN_COMBINING_GAP_PX) {
|
|
|
|
|
raiseBy = MIN_COMBINING_GAP_PX - currentGap;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 02:31:43 -06:00
|
|
|
if (!isCombining && prevCp != 0) {
|
2026-03-01 10:43:37 -06:00
|
|
|
cursorXFP += getKerning(prevCp, cp); // 4.4 fixed-point kern
|
2026-02-24 02:31:43 -06:00
|
|
|
}
|
|
|
|
|
|
2026-03-01 10:43:37 -06:00
|
|
|
const int cursorXPixels = fp4::toPixel(cursorXFP); // snap 12.4 fixed-point to nearest pixel
|
|
|
|
|
const int glyphBaseX = isCombining ? (lastBaseX + fp4::toPixel(lastBaseAdvanceFP / 2)) : cursorXPixels;
|
|
|
|
|
const int glyphBaseY = startY - raiseBy;
|
2026-02-22 03:11:07 +01:00
|
|
|
|
|
|
|
|
*minX = std::min(*minX, glyphBaseX + glyph->left);
|
|
|
|
|
*maxX = std::max(*maxX, glyphBaseX + glyph->left + glyph->width);
|
|
|
|
|
*minY = std::min(*minY, glyphBaseY + glyph->top - glyph->height);
|
|
|
|
|
*maxY = std::max(*maxY, glyphBaseY + glyph->top);
|
|
|
|
|
|
|
|
|
|
if (!isCombining) {
|
2026-03-01 10:43:37 -06:00
|
|
|
lastBaseX = cursorXPixels;
|
|
|
|
|
lastBaseAdvanceFP = glyph->advanceX; // 12.4 fixed-point
|
2026-02-22 03:11:07 +01:00
|
|
|
lastBaseTop = glyph->top;
|
2026-03-01 10:43:37 -06:00
|
|
|
cursorXFP += glyph->advanceX; // 12.4 fixed-point advance
|
2026-02-24 02:31:43 -06:00
|
|
|
prevCp = cp;
|
2026-02-22 03:11:07 +01:00
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EpdFont::getTextDimensions(const char* string, int* w, int* h) const {
|
|
|
|
|
int minX = 0, minY = 0, maxX = 0, maxY = 0;
|
|
|
|
|
|
|
|
|
|
getTextBounds(string, 0, 0, &minX, &minY, &maxX, &maxY);
|
|
|
|
|
|
|
|
|
|
*w = maxX - minX;
|
|
|
|
|
*h = maxY - minY;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 02:31:43 -06:00
|
|
|
static uint8_t lookupKernClass(const EpdKernClassEntry* entries, const uint16_t count, const uint32_t cp) {
|
|
|
|
|
if (!entries || count == 0 || cp > 0xFFFF) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2026-03-01 10:28:15 -06:00
|
|
|
|
2026-02-24 02:31:43 -06:00
|
|
|
const auto target = static_cast<uint16_t>(cp);
|
2026-03-01 10:28:15 -06:00
|
|
|
const auto* end = entries + count;
|
|
|
|
|
|
|
|
|
|
// lower_bound: exact-key lookup. Finds the first entry with codepoint >= target,
|
|
|
|
|
// then the equality check confirms an exact match exists.
|
|
|
|
|
const auto it = std::lower_bound(
|
|
|
|
|
entries, end, target, [](const EpdKernClassEntry& entry, uint16_t value) { return entry.codepoint < value; });
|
|
|
|
|
|
|
|
|
|
if (it != end && it->codepoint == target) {
|
|
|
|
|
return it->classId;
|
2026-02-24 02:31:43 -06:00
|
|
|
}
|
2026-03-01 10:28:15 -06:00
|
|
|
|
2026-02-24 02:31:43 -06:00
|
|
|
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;
|
2026-03-01 10:28:15 -06:00
|
|
|
const auto* end = pairs + count;
|
2026-02-24 02:31:43 -06:00
|
|
|
|
2026-03-01 10:28:15 -06:00
|
|
|
// lower_bound: exact-key lookup. Finds the first entry with pair >= key,
|
|
|
|
|
// then the equality check confirms an exact match exists.
|
|
|
|
|
const auto it =
|
|
|
|
|
std::lower_bound(pairs, end, key, [](const EpdLigaturePair& pair, uint32_t value) { return pair.pair < value; });
|
|
|
|
|
|
|
|
|
|
if (it != end && it->pair == key) {
|
|
|
|
|
return it->ligatureCp;
|
2026-02-24 02:31:43 -06:00
|
|
|
}
|
2026-03-01 10:28:15 -06:00
|
|
|
|
2026-02-24 02:31:43 -06:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 22:00:29 +11:00
|
|
|
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
|
2025-12-26 09:46:17 +09:00
|
|
|
const int count = data->intervalCount;
|
|
|
|
|
if (count == 0) return nullptr;
|
|
|
|
|
|
2026-03-01 10:28:15 -06:00
|
|
|
const EpdUnicodeInterval* intervals = data->intervals;
|
|
|
|
|
const auto* end = intervals + count;
|
2025-12-26 09:46:17 +09:00
|
|
|
|
2026-03-01 10:28:15 -06:00
|
|
|
// upper_bound: range lookup. Finds the first interval with first > cp, so the
|
|
|
|
|
// interval just before it is the last one with first <= cp. That's the only
|
|
|
|
|
// candidate that could contain cp. Then we verify cp <= candidate.last.
|
|
|
|
|
const auto it = std::upper_bound(
|
|
|
|
|
intervals, end, cp, [](uint32_t value, const EpdUnicodeInterval& interval) { return value < interval.first; });
|
2025-12-26 09:46:17 +09:00
|
|
|
|
2026-03-01 10:28:15 -06:00
|
|
|
if (it != intervals) {
|
|
|
|
|
const auto& interval = *(it - 1);
|
|
|
|
|
if (cp <= interval.last) {
|
|
|
|
|
return &data->glyph[interval.offset + (cp - interval.first)];
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-01 10:28:15 -06:00
|
|
|
|
2026-02-23 06:32:50 -06:00
|
|
|
if (cp != REPLACEMENT_GLYPH) {
|
|
|
|
|
return getGlyph(REPLACEMENT_GLYPH);
|
|
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
return nullptr;
|
|
|
|
|
}
|