feat: Support for Korean line breaks and glyph spacing (#2288)
Co-authored-by: Uri Tauber <uritaube@gmail.com>
This commit is contained in:
co-authored by
Uri Tauber
parent
d4069aeae5
commit
22f3575064
@@ -101,6 +101,9 @@ static uint8_t lookupKernClass(const EpdKernClassEntry* entries, const uint16_t
|
||||
}
|
||||
|
||||
int8_t EpdFont::getKerning(const uint32_t leftCp, const uint32_t rightCp) const {
|
||||
if (utf8IsCjkBreakable(leftCp) || utf8IsCjkBreakable(rightCp)) {
|
||||
return 0;
|
||||
}
|
||||
if (!data->kernMatrix) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
+81
-41
@@ -115,6 +115,9 @@ void SdCardFont::freeStyleAll(PerStyle& s) {
|
||||
freeStyleMiniData(s);
|
||||
delete[] s.fullIntervals;
|
||||
s.fullIntervals = nullptr;
|
||||
delete[] s.bmpIntervals;
|
||||
s.bmpIntervals = nullptr;
|
||||
s.intervalsAreBmp16 = false;
|
||||
freeStyleKernLigatureData(s);
|
||||
s.present = false;
|
||||
}
|
||||
@@ -516,59 +519,94 @@ bool SdCardFont::load(const char* path) {
|
||||
styleCount_ = styleCount;
|
||||
contentHash_ = hash;
|
||||
|
||||
// Load full intervals into RAM for each present style
|
||||
// Load full intervals into RAM for each present style. BMP-only fonts with
|
||||
// fewer than 65536 glyphs use a compact 6-byte interval table instead of the
|
||||
// on-disk 12-byte table; large sparse CJK subsets otherwise keep tens of KB
|
||||
// of always-resident heap just for lookup metadata.
|
||||
for (uint8_t i = 0; i < MAX_STYLES; i++) {
|
||||
auto& s = styles_[i];
|
||||
if (!s.present) continue;
|
||||
|
||||
s.fullIntervals = new (std::nothrow) EpdUnicodeInterval[s.header.intervalCount];
|
||||
if (!s.fullIntervals) {
|
||||
LOG_ERR("SDCF", "Failed to allocate %u intervals for style %u", s.header.intervalCount, i);
|
||||
freeAll();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file.seekSet(s.intervalsFileOffset)) {
|
||||
LOG_ERR("SDCF", "Failed to seek to intervals for style %u", i);
|
||||
freeAll();
|
||||
return false;
|
||||
}
|
||||
size_t intervalsBytes = s.header.intervalCount * sizeof(EpdUnicodeInterval);
|
||||
if (file.read(reinterpret_cast<uint8_t*>(s.fullIntervals), intervalsBytes) != static_cast<int>(intervalsBytes)) {
|
||||
LOG_ERR("SDCF", "Failed to read intervals for style %u", i);
|
||||
freeAll();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate interval contents before any later code (findGlobalGlyphIndex,
|
||||
// glyph reads) trusts them. A malformed file could otherwise drive
|
||||
// out-of-range glyph indices into bogus on-disk reads.
|
||||
{
|
||||
uint32_t expectedOffset = 0;
|
||||
uint32_t prevLast = 0;
|
||||
bool canUseBmp16 = s.header.glyphCount <= UINT16_MAX;
|
||||
uint32_t expectedOffset = 0;
|
||||
uint32_t prevLast = 0;
|
||||
EpdUnicodeInterval iv{};
|
||||
for (uint32_t j = 0; j < s.header.intervalCount; ++j) {
|
||||
if (file.read(reinterpret_cast<uint8_t*>(&iv), sizeof(iv)) != sizeof(iv)) {
|
||||
LOG_ERR("SDCF", "Failed to read interval %u for style %u", j, i);
|
||||
freeAll();
|
||||
return false;
|
||||
}
|
||||
if (iv.first > iv.last) {
|
||||
LOG_ERR("SDCF", "Style %u: invalid interval %u (first 0x%lX > last 0x%lX)", i, j,
|
||||
static_cast<unsigned long>(iv.first), static_cast<unsigned long>(iv.last));
|
||||
file.close();
|
||||
freeAll();
|
||||
return false;
|
||||
}
|
||||
const uint32_t span = iv.last - iv.first + 1;
|
||||
const bool overlapsPrev = (j > 0 && iv.first <= prevLast);
|
||||
const bool spanTooBig = (span > s.header.glyphCount);
|
||||
const bool offsetMismatch = (iv.offset != expectedOffset);
|
||||
const bool offsetOverruns = (iv.offset > s.header.glyphCount - span);
|
||||
if (overlapsPrev || spanTooBig || offsetMismatch || offsetOverruns) {
|
||||
LOG_ERR("SDCF", "Style %u: invalid interval layout at %u (overlap=%d span=%u offMis=%d offOver=%d)", i, j,
|
||||
overlapsPrev, span, offsetMismatch, offsetOverruns);
|
||||
file.close();
|
||||
freeAll();
|
||||
return false;
|
||||
}
|
||||
if (iv.first > UINT16_MAX || iv.last > UINT16_MAX || iv.offset > UINT16_MAX) {
|
||||
canUseBmp16 = false;
|
||||
}
|
||||
expectedOffset += span;
|
||||
prevLast = iv.last;
|
||||
}
|
||||
|
||||
if (!file.seekSet(s.intervalsFileOffset)) {
|
||||
LOG_ERR("SDCF", "Failed to seek back to intervals for style %u", i);
|
||||
freeAll();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (canUseBmp16) {
|
||||
s.bmpIntervals = new (std::nothrow) PerStyle::BmpInterval16[s.header.intervalCount];
|
||||
if (!s.bmpIntervals) {
|
||||
LOG_ERR("SDCF", "Failed to allocate compact intervals for style %u", i);
|
||||
freeAll();
|
||||
return false;
|
||||
}
|
||||
for (uint32_t j = 0; j < s.header.intervalCount; ++j) {
|
||||
const auto& iv = s.fullIntervals[j];
|
||||
if (iv.first > iv.last) {
|
||||
LOG_ERR("SDCF", "Style %u: invalid interval %u (first 0x%lX > last 0x%lX)", i, j,
|
||||
static_cast<unsigned long>(iv.first), static_cast<unsigned long>(iv.last));
|
||||
file.close();
|
||||
if (file.read(reinterpret_cast<uint8_t*>(&iv), sizeof(iv)) != sizeof(iv)) {
|
||||
LOG_ERR("SDCF", "Failed to read compact interval %u for style %u", j, i);
|
||||
freeAll();
|
||||
return false;
|
||||
}
|
||||
const uint32_t span = iv.last - iv.first + 1;
|
||||
const bool overlapsPrev = (j > 0 && iv.first <= prevLast);
|
||||
const bool spanTooBig = (span > s.header.glyphCount);
|
||||
const bool offsetMismatch = (iv.offset != expectedOffset);
|
||||
const bool offsetOverruns = (iv.offset > s.header.glyphCount - span);
|
||||
if (overlapsPrev || spanTooBig || offsetMismatch || offsetOverruns) {
|
||||
LOG_ERR("SDCF", "Style %u: invalid interval layout at %u (overlap=%d span=%u offMis=%d offOver=%d)", i, j,
|
||||
overlapsPrev, span, offsetMismatch, offsetOverruns);
|
||||
file.close();
|
||||
freeAll();
|
||||
return false;
|
||||
}
|
||||
expectedOffset += span;
|
||||
prevLast = iv.last;
|
||||
s.bmpIntervals[j] = {static_cast<uint16_t>(iv.first), static_cast<uint16_t>(iv.last),
|
||||
static_cast<uint16_t>(iv.offset)};
|
||||
}
|
||||
s.intervalsAreBmp16 = true;
|
||||
} else {
|
||||
s.fullIntervals = new (std::nothrow) EpdUnicodeInterval[s.header.intervalCount];
|
||||
if (!s.fullIntervals) {
|
||||
LOG_ERR("SDCF", "Failed to allocate %u intervals for style %u", s.header.intervalCount, i);
|
||||
freeAll();
|
||||
return false;
|
||||
}
|
||||
size_t intervalsBytes = s.header.intervalCount * sizeof(EpdUnicodeInterval);
|
||||
if (file.read(reinterpret_cast<uint8_t*>(s.fullIntervals), intervalsBytes) != static_cast<int>(intervalsBytes)) {
|
||||
LOG_ERR("SDCF", "Failed to read intervals for style %u", i);
|
||||
freeAll();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,13 +641,15 @@ int32_t SdCardFont::findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint)
|
||||
int right = static_cast<int>(s.header.intervalCount) - 1;
|
||||
while (left <= right) {
|
||||
int mid = left + (right - left) / 2;
|
||||
const auto& interval = s.fullIntervals[mid];
|
||||
if (codepoint < interval.first) {
|
||||
const uint32_t first = s.intervalsAreBmp16 ? s.bmpIntervals[mid].first : s.fullIntervals[mid].first;
|
||||
const uint32_t last = s.intervalsAreBmp16 ? s.bmpIntervals[mid].last : s.fullIntervals[mid].last;
|
||||
if (codepoint < first) {
|
||||
right = mid - 1;
|
||||
} else if (codepoint > interval.last) {
|
||||
} else if (codepoint > last) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
return static_cast<int32_t>(interval.offset + (codepoint - interval.first));
|
||||
const uint32_t offset = s.intervalsAreBmp16 ? s.bmpIntervals[mid].offset : s.fullIntervals[mid].offset;
|
||||
return static_cast<int32_t>(offset + (codepoint - first));
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
@@ -1257,7 +1297,7 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
|
||||
|
||||
if (!self->loaded_ || styleIdx >= MAX_STYLES || !self->styles_[styleIdx].present) return nullptr;
|
||||
const auto& s = self->styles_[styleIdx];
|
||||
if (!s.fullIntervals) return nullptr;
|
||||
if (!s.fullIntervals && !s.bmpIntervals) return nullptr;
|
||||
|
||||
// Check overflow cache first (matching both codepoint and style)
|
||||
for (uint32_t i = 0; i < self->overflowCount_; i++) {
|
||||
|
||||
@@ -58,9 +58,9 @@ class SdCardFont {
|
||||
// Returns true if advance table is populated for at least one style.
|
||||
bool hasAdvanceTable() const;
|
||||
|
||||
// Free mini data for all styles, restore stub EpdFontData.
|
||||
// Also clears the temporary advance table (built per layout pass) but
|
||||
// preserves the persistent advance cache (reused across passes).
|
||||
// Free mini data for all styles and restore stub EpdFontData.
|
||||
// Preserves the persistent advance cache so repeated layout passes can reuse
|
||||
// previously fetched metrics.
|
||||
void clearCache();
|
||||
|
||||
// Drop the persistent advance cache. Call when unloading the SD font or
|
||||
@@ -140,6 +140,14 @@ class SdCardFont {
|
||||
|
||||
// Full intervals loaded from file (kept in RAM for codepoint lookup)
|
||||
EpdUnicodeInterval* fullIntervals = nullptr;
|
||||
struct BmpInterval16 {
|
||||
uint16_t first;
|
||||
uint16_t last;
|
||||
uint16_t offset;
|
||||
} __attribute__((packed));
|
||||
static_assert(sizeof(BmpInterval16) == 6, "BmpInterval16 must remain compact");
|
||||
BmpInterval16* bmpIntervals = nullptr;
|
||||
bool intervalsAreBmp16 = false;
|
||||
|
||||
// Persistent kern-class + ligature tables (lazy-loaded on first prewarm).
|
||||
// The full kern MATRIX is NOT resident — on Literata-class fonts a single
|
||||
|
||||
@@ -34,19 +34,15 @@ bool SdCardFontManager::loadFamily(const SdCardFontFamilyInfo& family, GfxRender
|
||||
unloadAll(renderer);
|
||||
}
|
||||
|
||||
// Select by ordinal position: sort available sizes, then map the font size
|
||||
// enum (SMALL=0 .. EXTRA_LARGE=3) to the corresponding slot. When the
|
||||
// family has fewer sizes than 4, clamp to the last available size.
|
||||
auto sizes = family.availableSizes();
|
||||
if (sizes.empty()) {
|
||||
// Select the physical point size closest to the built-in reader sizes. Some
|
||||
// CJK font packs only ship larger sizes, so ordinal selection can make
|
||||
// MEDIUM load 18pt+ and produce oversized pages on small devices.
|
||||
const SdCardFontFileInfo* selected = family.findClosestReaderSize(fontSizeEnum);
|
||||
if (!selected) {
|
||||
LOG_ERR("SDMGR", "Family %s has no files to load", family.name.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t idx = fontSizeEnum;
|
||||
if (idx >= sizes.size()) idx = sizes.size() - 1;
|
||||
const SdCardFontFileInfo* selected = family.findFile(sizes[idx]);
|
||||
|
||||
auto* font = new (std::nothrow) SdCardFont();
|
||||
if (!font) {
|
||||
LOG_ERR("SDMGR", "Failed to allocate SdCardFont for %s", selected->path.c_str());
|
||||
|
||||
@@ -15,10 +15,10 @@ class SdCardFontManager {
|
||||
SdCardFontManager(const SdCardFontManager&) = delete;
|
||||
SdCardFontManager& operator=(const SdCardFontManager&) = delete;
|
||||
|
||||
// Load the font file matching fontSizeEnum (SMALL=0 .. EXTRA_LARGE=3) by
|
||||
// ordinal position in the family's sorted size list. Only one .cpfont file
|
||||
// is loaded; other sizes remain on disk. This keeps resident interval +
|
||||
// kern/ligature tables to one size's worth of memory.
|
||||
// Load the font file whose physical point size is closest to the reader
|
||||
// fontSizeEnum (SMALL=12, MEDIUM=14, LARGE=16, EXTRA_LARGE=18). Only one
|
||||
// .cpfont file is loaded; other sizes remain on disk. This keeps resident
|
||||
// interval + kern/ligature tables to one size's worth of memory.
|
||||
// Returns true on success.
|
||||
bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t fontSizeEnum);
|
||||
|
||||
@@ -32,7 +32,7 @@ class SdCardFontManager {
|
||||
// Get name of currently loaded family (empty if none).
|
||||
const std::string& currentFamilyName() const { return loadedFamilyName_; };
|
||||
|
||||
// Point size that was actually loaded (closest match to targetPtSize).
|
||||
// Point size that was actually loaded.
|
||||
// 0 if nothing loaded.
|
||||
uint8_t currentPointSize() const { return loadedPointSize_; };
|
||||
|
||||
|
||||
@@ -15,6 +15,40 @@ const SdCardFontFileInfo* SdCardFontFamilyInfo::findFile(uint8_t size, uint8_t s
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const SdCardFontFileInfo* SdCardFontFamilyInfo::findClosestReaderSize(const uint8_t fontSizeEnum,
|
||||
const uint8_t style) const {
|
||||
if (files.empty()) return nullptr;
|
||||
|
||||
uint8_t target = 14;
|
||||
switch (fontSizeEnum) {
|
||||
case 0:
|
||||
target = 12;
|
||||
break;
|
||||
case 2:
|
||||
target = 16;
|
||||
break;
|
||||
case 3:
|
||||
target = 18;
|
||||
break;
|
||||
case 1:
|
||||
default:
|
||||
target = 14;
|
||||
break;
|
||||
}
|
||||
|
||||
const SdCardFontFileInfo* best = nullptr;
|
||||
uint8_t bestDelta = 255;
|
||||
for (const auto& f : files) {
|
||||
if (f.style != style) continue;
|
||||
const uint8_t delta = f.pointSize > target ? f.pointSize - target : target - f.pointSize;
|
||||
if (!best || delta < bestDelta || (delta == bestDelta && f.pointSize < best->pointSize)) {
|
||||
best = &f;
|
||||
bestDelta = delta;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
bool SdCardFontFamilyInfo::hasSize(uint8_t size) const {
|
||||
for (const auto& f : files) {
|
||||
if (f.pointSize == size) return true;
|
||||
|
||||
@@ -18,6 +18,7 @@ struct SdCardFontFamilyInfo {
|
||||
std::vector<SdCardFontFileInfo> files;
|
||||
|
||||
const SdCardFontFileInfo* findFile(uint8_t size, uint8_t style = 0) const;
|
||||
const SdCardFontFileInfo* findClosestReaderSize(uint8_t fontSizeEnum, uint8_t style = 0) const;
|
||||
bool hasSize(uint8_t size) const;
|
||||
std::vector<uint8_t> availableSizes() const;
|
||||
};
|
||||
|
||||
@@ -248,7 +248,7 @@ unmerged_intervals = sorted(intervals + add_ints)
|
||||
intervals = []
|
||||
unvalidated_intervals = []
|
||||
for i_start, i_end in unmerged_intervals:
|
||||
if len(unvalidated_intervals) > 0 and i_start + 1 <= unvalidated_intervals[-1][1]:
|
||||
if len(unvalidated_intervals) > 0 and i_start <= unvalidated_intervals[-1][1] + 1:
|
||||
unvalidated_intervals[-1] = (unvalidated_intervals[-1][0], max(unvalidated_intervals[-1][1], i_end))
|
||||
continue
|
||||
unvalidated_intervals.append((i_start, i_end))
|
||||
|
||||
+241
-32
@@ -24,6 +24,7 @@ constexpr size_t RTL_PARAGRAPH_PROBE_WORDS = 3;
|
||||
// Per-word: scan enough chars to see through leading neutrals (quotes, numbers)
|
||||
// before giving up. 64 is a hedge for pathological cases like long numeric tokens.
|
||||
constexpr int RTL_PER_WORD_PROBE_DEPTH = 64;
|
||||
constexpr size_t MIN_JUSTIFY_GAPS = 1;
|
||||
|
||||
// Byte-level pre-check: Hebrew UTF-8 lead bytes 0xD6-0xD7, Arabic/Syriac 0xD8-0xDB.
|
||||
bool mayContainRtlBytes(const char* str) {
|
||||
@@ -57,6 +58,134 @@ uint32_t lastCodepoint(const std::string& word) {
|
||||
|
||||
bool containsSoftHyphen(const std::string& word) { return word.find(SOFT_HYPHEN_UTF8) != std::string::npos; }
|
||||
|
||||
bool isNoBreakBeforeCjkPunctuation(const uint32_t cp) {
|
||||
switch (cp) {
|
||||
case '.':
|
||||
case ',':
|
||||
case ':':
|
||||
case ';':
|
||||
case '!':
|
||||
case '?':
|
||||
case ')':
|
||||
case ']':
|
||||
case '}':
|
||||
case 0x00BB: // »
|
||||
case 0x2019: // ’
|
||||
case 0x201D: // ”
|
||||
case 0x3001: // 、
|
||||
case 0x3002: // 。
|
||||
case 0x3009: // 〉
|
||||
case 0x300B: // 》
|
||||
case 0x300D: // 」
|
||||
case 0x300F: // 』
|
||||
case 0x3011: // 】
|
||||
case 0x3015: // 〕
|
||||
case 0x3017: // 〗
|
||||
case 0x3019: // 〙
|
||||
case 0x301B: // 〛
|
||||
case 0xFF01: // !
|
||||
case 0xFF09: // )
|
||||
case 0xFF0C: // ,
|
||||
case 0xFF0E: // .
|
||||
case 0xFF1A: // :
|
||||
case 0xFF1B: // ;
|
||||
case 0xFF1F: // ?
|
||||
case 0xFF3D: // ]
|
||||
case 0xFF5D: // }
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool isNoBreakAfterCjkPunctuation(const uint32_t cp) {
|
||||
switch (cp) {
|
||||
case '(':
|
||||
case '[':
|
||||
case '{':
|
||||
case 0x00AB: // «
|
||||
case 0x2018: // ‘
|
||||
case 0x201C: // “
|
||||
case 0x3008: // 〈
|
||||
case 0x300A: // 《
|
||||
case 0x300C: // 「
|
||||
case 0x300E: // 『
|
||||
case 0x3010: // 【
|
||||
case 0x3014: // 〔
|
||||
case 0x3016: // 〖
|
||||
case 0x3018: // 〘
|
||||
case 0x301A: // 〚
|
||||
case 0xFF08: // (
|
||||
case 0xFF3B: // [
|
||||
case 0xFF5B: // {
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool containsCjkBreakableCodepoint(const std::string& text) {
|
||||
const auto* ptr = reinterpret_cast<const unsigned char*>(text.c_str());
|
||||
while (*ptr) {
|
||||
const uint32_t cp = utf8NextCodepoint(&ptr);
|
||||
if (utf8IsCjkBreakable(cp)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasCjkBreakOpportunityBetween(const uint32_t leftCp, const uint32_t rightCp) {
|
||||
if (!utf8IsCjkBreakable(leftCp) && !utf8IsCjkBreakable(rightCp)) return false;
|
||||
if (isNoBreakAfterCjkPunctuation(leftCp) || isNoBreakBeforeCjkPunctuation(rightCp)) return false;
|
||||
if (utf8IsCombiningMark(rightCp)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<size_t> cjkCharacterBreakByteOffsets(const std::string& text) {
|
||||
struct CodepointBoundary {
|
||||
uint32_t cp;
|
||||
size_t endOffset;
|
||||
};
|
||||
|
||||
std::vector<CodepointBoundary> codepoints;
|
||||
codepoints.reserve(text.size());
|
||||
bool hasCjkBreakable = false;
|
||||
|
||||
const auto* ptr = reinterpret_cast<const unsigned char*>(text.c_str());
|
||||
const auto* const start = ptr;
|
||||
while (*ptr) {
|
||||
const uint32_t cp = utf8NextCodepoint(&ptr);
|
||||
if (cp == 0) break;
|
||||
if (utf8IsCjkBreakable(cp)) {
|
||||
hasCjkBreakable = true;
|
||||
}
|
||||
codepoints.push_back({cp, static_cast<size_t>(ptr - start)});
|
||||
}
|
||||
|
||||
if (!hasCjkBreakable || codepoints.size() < 2) return {};
|
||||
|
||||
std::vector<size_t> allowedOffsets;
|
||||
allowedOffsets.reserve(codepoints.size() - 1);
|
||||
for (size_t i = 0; i + 1 < codepoints.size(); ++i) {
|
||||
const uint32_t current = codepoints[i].cp;
|
||||
const uint32_t next = codepoints[i + 1].cp;
|
||||
if (!hasCjkBreakOpportunityBetween(current, next)) continue;
|
||||
allowedOffsets.push_back(codepoints[i].endOffset);
|
||||
}
|
||||
return allowedOffsets;
|
||||
}
|
||||
|
||||
int computeJustifyExtra(const int spareSpace, const size_t gapCount) {
|
||||
if (gapCount < MIN_JUSTIFY_GAPS || spareSpace <= 0) return 0;
|
||||
// Distribute the spare space evenly across gaps. Do NOT bail out to 0 when the
|
||||
// per-gap stretch is large: a sparse line (few words on a wide page) legitimately
|
||||
// needs big gaps to reach the margin. Returning 0 there disables justification for
|
||||
// that line, leaving it right-aligned (RTL) / left-aligned (LTR) — the mismatched
|
||||
// alignment bug. Match the un-capped behavior of the old code.
|
||||
return spareSpace / static_cast<int>(gapCount);
|
||||
}
|
||||
|
||||
// Removes every soft hyphen in-place so rendered glyphs match measured widths.
|
||||
void stripSoftHyphensInPlace(std::string& word) {
|
||||
size_t pos = 0;
|
||||
@@ -132,12 +261,54 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
||||
const bool wordStartsRtl = !hasRtlWord && mayContainRtlBytes(word.c_str()) &&
|
||||
BidiUtils::startsWithRtl(word.c_str(), RTL_PER_WORD_PROBE_DEPTH);
|
||||
|
||||
const auto pushToken = [&](std::string token, const bool continues, const bool noSpaceBefore,
|
||||
const bool isFocusSuffix) {
|
||||
words.push_back(std::move(token));
|
||||
wordStyles.push_back(baseStyle);
|
||||
wordContinues.push_back(continues);
|
||||
wordNoSpaceBefore.push_back(noSpaceBefore);
|
||||
wordIsFocusSuffix.push_back(isFocusSuffix);
|
||||
};
|
||||
|
||||
bool effectiveAttachToPrevious = attachToPrevious;
|
||||
bool effectiveNoSpaceBefore = false;
|
||||
if (attachToPrevious && !words.empty() &&
|
||||
hasCjkBreakOpportunityBetween(lastCodepoint(words.back()), firstCodepoint(word))) {
|
||||
effectiveAttachToPrevious = false;
|
||||
effectiveNoSpaceBefore = true;
|
||||
}
|
||||
|
||||
if (auto breakOffsets = cjkCharacterBreakByteOffsets(word); !breakOffsets.empty()) {
|
||||
bool firstToken = true;
|
||||
size_t tokenStart = 0;
|
||||
for (const size_t breakOffset : breakOffsets) {
|
||||
if (breakOffset <= tokenStart || breakOffset > word.size()) continue;
|
||||
pushToken(word.substr(tokenStart, breakOffset - tokenStart), firstToken ? effectiveAttachToPrevious : false,
|
||||
firstToken ? effectiveNoSpaceBefore : true, false);
|
||||
firstToken = false;
|
||||
tokenStart = breakOffset;
|
||||
}
|
||||
if (tokenStart < word.size()) {
|
||||
pushToken(word.substr(tokenStart), firstToken ? effectiveAttachToPrevious : false,
|
||||
firstToken ? effectiveNoSpaceBefore : true, false);
|
||||
}
|
||||
if (wordStartsRtl) {
|
||||
hasRtlWord = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (containsCjkBreakableCodepoint(word)) {
|
||||
pushToken(std::move(word), effectiveAttachToPrevious, effectiveNoSpaceBefore, false);
|
||||
if (wordStartsRtl) {
|
||||
hasRtlWord = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Already-bold text should stay fully bold; focus splitting would make its suffix regular later.
|
||||
if (!this->focusReadingEnabled || (baseStyle & EpdFontFamily::BOLD) != 0) {
|
||||
words.push_back(std::move(word));
|
||||
wordStyles.push_back(baseStyle);
|
||||
wordContinues.push_back(attachToPrevious);
|
||||
wordIsFocusSuffix.push_back(false);
|
||||
pushToken(std::move(word), effectiveAttachToPrevious, effectiveNoSpaceBefore, false);
|
||||
if (wordStartsRtl) {
|
||||
hasRtlWord = true;
|
||||
}
|
||||
@@ -166,17 +337,19 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
||||
words.reserve(newCapacity);
|
||||
wordStyles.reserve(newCapacity);
|
||||
wordContinues.reserve(newCapacity);
|
||||
wordNoSpaceBefore.reserve(newCapacity);
|
||||
wordIsFocusSuffix.reserve(newCapacity);
|
||||
}
|
||||
|
||||
// Lambda helper to process and push individual sub-segments of the string
|
||||
// Use std::string_view to avoid heap allocations when slicing
|
||||
auto processSegment = [&](std::string_view segment, bool isWord, bool attach) {
|
||||
auto processSegment = [&](std::string_view segment, bool isWord, bool attach, bool noSpaceBefore) {
|
||||
if (!isWord) {
|
||||
// Punctuation and Numbers stay regular
|
||||
words.emplace_back(segment);
|
||||
wordStyles.push_back(baseStyle);
|
||||
wordContinues.push_back(attach);
|
||||
wordNoSpaceBefore.push_back(noSpaceBefore);
|
||||
wordIsFocusSuffix.push_back(false);
|
||||
} else {
|
||||
size_t charCount = 0;
|
||||
@@ -198,6 +371,7 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
||||
words.emplace_back(segment);
|
||||
wordStyles.push_back(static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::BOLD));
|
||||
wordContinues.push_back(attach);
|
||||
wordNoSpaceBefore.push_back(noSpaceBefore);
|
||||
wordIsFocusSuffix.push_back(false);
|
||||
} else {
|
||||
countPtr = reinterpret_cast<const unsigned char*>(segment.data());
|
||||
@@ -210,12 +384,14 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
||||
words.emplace_back(segment.substr(0, splitByteOffset));
|
||||
wordStyles.push_back(static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::BOLD));
|
||||
wordContinues.push_back(attach);
|
||||
wordNoSpaceBefore.push_back(noSpaceBefore);
|
||||
wordIsFocusSuffix.push_back(false);
|
||||
|
||||
// Regular suffix - marked so extractLine can merge it back into single TextBlock entry
|
||||
words.emplace_back(segment.substr(splitByteOffset));
|
||||
wordStyles.push_back(baseStyle);
|
||||
wordContinues.push_back(true);
|
||||
wordNoSpaceBefore.push_back(false);
|
||||
wordIsFocusSuffix.push_back(true);
|
||||
}
|
||||
}
|
||||
@@ -243,7 +419,8 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
||||
|
||||
// Only the very first segment inherits the original attachToPrevious flag.
|
||||
// Every subsequent segment MUST attach=true so it glues seamlessly to the prefix.
|
||||
processSegment(segment, inWordSegment, isFirstSegment ? attachToPrevious : true);
|
||||
processSegment(segment, inWordSegment, isFirstSegment ? effectiveAttachToPrevious : true,
|
||||
isFirstSegment ? effectiveNoSpaceBefore : false);
|
||||
|
||||
// Setup for the next segment
|
||||
segmentStart = currentCpStart;
|
||||
@@ -255,7 +432,8 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
||||
// Process the final remaining segment
|
||||
size_t segmentLen = end - segmentStart;
|
||||
std::string_view segment(reinterpret_cast<const char*>(segmentStart), segmentLen);
|
||||
processSegment(segment, inWordSegment, isFirstSegment ? attachToPrevious : true);
|
||||
processSegment(segment, inWordSegment, isFirstSegment ? effectiveAttachToPrevious : true,
|
||||
isFirstSegment ? effectiveNoSpaceBefore : false);
|
||||
if (wordStartsRtl) {
|
||||
hasRtlWord = true;
|
||||
}
|
||||
@@ -324,14 +502,16 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
||||
std::vector<size_t> lineBreakIndices;
|
||||
if (hyphenationEnabled) {
|
||||
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
|
||||
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues);
|
||||
lineBreakIndices =
|
||||
computeHyphenatedLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues, wordNoSpaceBefore);
|
||||
} else {
|
||||
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues);
|
||||
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues, wordNoSpaceBefore);
|
||||
}
|
||||
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
||||
|
||||
for (size_t i = 0; i < lineCount; ++i) {
|
||||
extractLine(i, pageWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer, fontId);
|
||||
extractLine(i, pageWidth, wordWidths, wordContinues, wordNoSpaceBefore, lineBreakIndices, processLine, renderer,
|
||||
fontId);
|
||||
}
|
||||
|
||||
// Remove consumed words so size() reflects only remaining words
|
||||
@@ -340,6 +520,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
||||
words.erase(words.begin(), words.begin() + consumed);
|
||||
wordStyles.erase(wordStyles.begin(), wordStyles.begin() + consumed);
|
||||
wordContinues.erase(wordContinues.begin(), wordContinues.begin() + consumed);
|
||||
wordNoSpaceBefore.erase(wordNoSpaceBefore.begin(), wordNoSpaceBefore.begin() + consumed);
|
||||
wordIsFocusSuffix.erase(wordIsFocusSuffix.begin(), wordIsFocusSuffix.begin() + consumed);
|
||||
}
|
||||
}
|
||||
@@ -356,7 +537,8 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
|
||||
}
|
||||
|
||||
std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth,
|
||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec) {
|
||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
|
||||
std::vector<bool>& noSpaceBeforeVec) {
|
||||
if (words.empty()) {
|
||||
return {};
|
||||
}
|
||||
@@ -395,7 +577,9 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
for (size_t j = i; j < totalWordCount; ++j) {
|
||||
// Add space before word j, unless it's the first word on the line or a continuation
|
||||
int gap = 0;
|
||||
if (j > static_cast<size_t>(i) && !continuesVec[j]) {
|
||||
if (j > static_cast<size_t>(i) && noSpaceBeforeVec[j]) {
|
||||
gap = 0;
|
||||
} else if (j > static_cast<size_t>(i) && !continuesVec[j]) {
|
||||
gap =
|
||||
renderer.getSpaceAdvance(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]);
|
||||
} else if (j > static_cast<size_t>(i) && continuesVec[j]) {
|
||||
@@ -470,7 +654,8 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
// Builds break indices while opportunistically splitting the word that would overflow the current line.
|
||||
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId,
|
||||
const int pageWidth, std::vector<uint16_t>& wordWidths,
|
||||
std::vector<bool>& continuesVec) {
|
||||
std::vector<bool>& continuesVec,
|
||||
std::vector<bool>& noSpaceBeforeVec) {
|
||||
const int firstLineIndent = resolveFirstLineIndent(true, renderer, fontId);
|
||||
|
||||
std::vector<size_t> lineBreakIndices;
|
||||
@@ -488,7 +673,9 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
while (currentIndex < wordWidths.size()) {
|
||||
const bool isFirstWord = currentIndex == lineStart;
|
||||
int spacing = 0;
|
||||
if (!isFirstWord && !continuesVec[currentIndex]) {
|
||||
if (!isFirstWord && noSpaceBeforeVec[currentIndex]) {
|
||||
spacing = 0;
|
||||
} else if (!isFirstWord && !continuesVec[currentIndex]) {
|
||||
spacing = renderer.getSpaceAdvance(fontId, lastCodepoint(words[currentIndex - 1]),
|
||||
firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
|
||||
} else if (!isFirstWord && continuesVec[currentIndex]) {
|
||||
@@ -618,6 +805,7 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
||||
// line, while "kilometer" moves to the next line.
|
||||
// wordContinues[wordIndex] is intentionally left unchanged — the prefix keeps its original attachment.
|
||||
wordContinues.insert(wordContinues.begin() + wordIndex + 1, false);
|
||||
wordNoSpaceBefore.insert(wordNoSpaceBefore.begin() + wordIndex + 1, false);
|
||||
|
||||
// Update cached widths to reflect the new prefix/remainder pairing.
|
||||
wordWidths[wordIndex] = static_cast<uint16_t>(chosenWidth);
|
||||
@@ -627,7 +815,8 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
||||
}
|
||||
|
||||
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const std::vector<uint16_t>& wordWidths,
|
||||
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
|
||||
const std::vector<bool>& continuesVec, const std::vector<bool>& noSpaceBeforeVec,
|
||||
const std::vector<size_t>& lineBreakIndices,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
||||
const GfxRenderer& renderer, const int fontId) {
|
||||
const size_t lineBreak = lineBreakIndices[breakIndex];
|
||||
@@ -660,7 +849,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
|
||||
lineWordWidthSum += wordWidths[lastBreakAt + wordIdx];
|
||||
// Count gaps: each word after the first creates a gap, unless it's a continuation
|
||||
if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) {
|
||||
if (wordIdx > 0 && noSpaceBeforeVec[lastBreakAt + wordIdx]) {
|
||||
// Unicode break opportunity with no inserted Latin-style space. It is still
|
||||
// a stretchable gap for justified CJK/Korean text.
|
||||
actualGapCount++;
|
||||
} else if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) {
|
||||
actualGapCount++;
|
||||
totalNaturalGaps += renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx - 1]),
|
||||
firstCodepoint(lineWords[wordIdx]), lineWordStyles[wordIdx - 1]);
|
||||
@@ -689,8 +882,8 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
|
||||
// For justified text, compute per-gap extra to distribute remaining space evenly
|
||||
const int spareSpace = effectivePageWidth - lineWordWidthSum - totalNaturalGaps;
|
||||
const int justifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine && actualGapCount >= 1)
|
||||
? spareSpace / static_cast<int>(actualGapCount)
|
||||
const int justifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
|
||||
? computeJustifyExtra(spareSpace, actualGapCount)
|
||||
: 0;
|
||||
|
||||
// BiDi processing: reorder words with UAX#9 in full-line context.
|
||||
@@ -709,11 +902,13 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
reorderedStylesScratch.clear();
|
||||
reorderedWidthsScratch.clear();
|
||||
reorderedContinuesScratch.clear();
|
||||
reorderedNoSpaceBeforeScratch.clear();
|
||||
reorderedFocusSuffixScratch.clear();
|
||||
reorderedWordsScratch.reserve(visualOrderScratch.size());
|
||||
reorderedStylesScratch.reserve(visualOrderScratch.size());
|
||||
reorderedWidthsScratch.reserve(visualOrderScratch.size());
|
||||
reorderedContinuesScratch.reserve(visualOrderScratch.size());
|
||||
reorderedNoSpaceBeforeScratch.reserve(visualOrderScratch.size());
|
||||
reorderedFocusSuffixScratch.reserve(visualOrderScratch.size());
|
||||
|
||||
for (size_t i = 0; i < visualOrderScratch.size(); ++i) {
|
||||
@@ -740,6 +935,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
}
|
||||
}
|
||||
reorderedContinuesScratch.push_back(continues);
|
||||
reorderedNoSpaceBeforeScratch.push_back(!continues && noSpaceBeforeVec[lastBreakAt + src]);
|
||||
}
|
||||
|
||||
int reorderedWordWidthSum = 0;
|
||||
@@ -747,7 +943,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
int reorderedNaturalGaps = 0;
|
||||
for (size_t wordIdx = 0; wordIdx < reorderedWidthsScratch.size(); wordIdx++) {
|
||||
reorderedWordWidthSum += reorderedWidthsScratch[wordIdx];
|
||||
if (wordIdx > 0 && !reorderedContinuesScratch[wordIdx]) {
|
||||
if (wordIdx > 0 && reorderedNoSpaceBeforeScratch[wordIdx]) {
|
||||
// Unicode break opportunity with no inserted Latin-style space. It is still
|
||||
// a stretchable gap for justified CJK/Korean text.
|
||||
reorderedGapCount++;
|
||||
} else if (wordIdx > 0 && !reorderedContinuesScratch[wordIdx]) {
|
||||
reorderedGapCount++;
|
||||
reorderedNaturalGaps += renderer.getSpaceAdvance(fontId, lastCodepoint(reorderedWordsScratch[wordIdx - 1]),
|
||||
firstCodepoint(reorderedWordsScratch[wordIdx]),
|
||||
@@ -763,10 +963,9 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
}
|
||||
|
||||
const int reorderedSpare = effectivePageWidth - reorderedWordWidthSum - reorderedNaturalGaps;
|
||||
const int reorderedJustifyExtra =
|
||||
(effectiveAlignment == CssTextAlign::Justify && !isLastLine && reorderedGapCount >= 1)
|
||||
? reorderedSpare / static_cast<int>(reorderedGapCount)
|
||||
: 0;
|
||||
const int reorderedJustifyExtra = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
|
||||
? computeJustifyExtra(reorderedSpare, reorderedGapCount)
|
||||
: 0;
|
||||
|
||||
const int justifyContribution = (effectiveAlignment == CssTextAlign::Justify && !isLastLine)
|
||||
? reorderedJustifyExtra * static_cast<int>(reorderedGapCount)
|
||||
@@ -805,9 +1004,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
}
|
||||
xpos += advance;
|
||||
} else if (wordIdx + 1 < reorderedWidthsScratch.size()) {
|
||||
int gap = renderer.getSpaceAdvance(fontId, lastCodepoint(reorderedWordsScratch[wordIdx]),
|
||||
firstCodepoint(reorderedWordsScratch[wordIdx + 1]),
|
||||
reorderedStylesScratch[wordIdx]);
|
||||
const bool nextNoSpace = reorderedNoSpaceBeforeScratch[wordIdx + 1];
|
||||
int gap = nextNoSpace ? 0
|
||||
: renderer.getSpaceAdvance(fontId, lastCodepoint(reorderedWordsScratch[wordIdx]),
|
||||
firstCodepoint(reorderedWordsScratch[wordIdx + 1]),
|
||||
reorderedStylesScratch[wordIdx]);
|
||||
if (effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
|
||||
gap += reorderedJustifyExtra;
|
||||
}
|
||||
@@ -846,11 +1047,15 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
xpos -= advance;
|
||||
} else {
|
||||
int gap = 0;
|
||||
bool nextNoSpace = false;
|
||||
if (wordIdx + 1 < lineWordCount) {
|
||||
gap = renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx]),
|
||||
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
|
||||
nextNoSpace = noSpaceBeforeVec[lastBreakAt + wordIdx + 1];
|
||||
gap = nextNoSpace
|
||||
? 0
|
||||
: renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx]),
|
||||
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
|
||||
}
|
||||
if (effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
|
||||
if (wordIdx + 1 < lineWordCount && effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
|
||||
gap += justifyExtra;
|
||||
}
|
||||
xpos -= gap;
|
||||
@@ -880,11 +1085,15 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
xpos += advance;
|
||||
} else {
|
||||
int gap = 0;
|
||||
bool nextNoSpace = false;
|
||||
if (wordIdx + 1 < lineWordCount) {
|
||||
gap = renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx]),
|
||||
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
|
||||
nextNoSpace = noSpaceBeforeVec[lastBreakAt + wordIdx + 1];
|
||||
gap = nextNoSpace
|
||||
? 0
|
||||
: renderer.getSpaceAdvance(fontId, lastCodepoint(lineWords[wordIdx]),
|
||||
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
|
||||
}
|
||||
if (effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
|
||||
if (wordIdx + 1 < lineWordCount && effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
|
||||
gap += justifyExtra;
|
||||
}
|
||||
xpos += wordWidths[lastBreakAt + wordIdx] + gap;
|
||||
|
||||
@@ -15,7 +15,8 @@ class GfxRenderer;
|
||||
class ParsedText {
|
||||
std::vector<std::string> words;
|
||||
std::vector<EpdFontFamily::Style> wordStyles;
|
||||
std::vector<bool> wordContinues; // true = word attaches to previous (no space before it)
|
||||
std::vector<bool> wordContinues; // true = word attaches to previous with no break
|
||||
std::vector<bool> wordNoSpaceBefore; // true = may break before token, but no synthetic space when joined
|
||||
std::vector<bool> wordIsFocusSuffix; // true = token is the regular tail of a focus bold-prefix split
|
||||
BlockStyle blockStyle;
|
||||
bool extraParagraphSpacing;
|
||||
@@ -27,18 +28,22 @@ class ParsedText {
|
||||
std::vector<EpdFontFamily::Style> reorderedStylesScratch;
|
||||
std::vector<uint16_t> reorderedWidthsScratch;
|
||||
std::vector<bool> reorderedContinuesScratch;
|
||||
std::vector<bool> reorderedNoSpaceBeforeScratch;
|
||||
std::vector<bool> reorderedFocusSuffixScratch;
|
||||
std::vector<uint16_t> visualOrderScratch;
|
||||
|
||||
int resolveFirstLineIndent(bool isFirstLine, const GfxRenderer& renderer, int fontId) const;
|
||||
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
|
||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
|
||||
std::vector<bool>& noSpaceBeforeVec);
|
||||
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
|
||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
|
||||
std::vector<bool>& noSpaceBeforeVec);
|
||||
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
|
||||
void extractLine(size_t breakIndex, int pageWidth, const std::vector<uint16_t>& wordWidths,
|
||||
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
|
||||
const std::vector<bool>& continuesVec, const std::vector<bool>& noSpaceBeforeVec,
|
||||
const std::vector<size_t>& lineBreakIndices,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const GfxRenderer& renderer,
|
||||
int fontId);
|
||||
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
||||
|
||||
@@ -1437,8 +1437,18 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
||||
int32_t widthFP = 0;
|
||||
const bool isSupSub = (style & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0;
|
||||
const uint8_t styleIdx = resolveSdCardStyle(*sdIt->second, style);
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
}
|
||||
const auto& font = fontIt->second;
|
||||
while (uint32_t cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text))) {
|
||||
int32_t advFP = sdIt->second->getAdvance(cp, styleIdx);
|
||||
if (advFP == 0 && !utf8IsCombiningMark(cp)) {
|
||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||
advFP = glyph ? glyph->advanceX : 0;
|
||||
}
|
||||
widthFP += isSupSub ? (advFP + 1) / 2 : advFP;
|
||||
}
|
||||
return fp4::toPixel(widthFP);
|
||||
|
||||
+4
-1
@@ -21,12 +21,15 @@ int utf8SafeTruncateBuffer(const char* buf, int len);
|
||||
// Covers CJK Unified Ideographs, Hiragana, Katakana, Hangul Syllables, CJK punctuation,
|
||||
// and fullwidth forms — the ranges where word boundaries are implicit per character.
|
||||
inline bool utf8IsCjkBreakable(const uint32_t cp) {
|
||||
return (cp >= 0x3000 && cp <= 0x303F) // CJK Symbols and Punctuation
|
||||
return (cp >= 0x1100 && cp <= 0x11FF) // Hangul Jamo
|
||||
|| (cp >= 0x3000 && cp <= 0x303F) // CJK Symbols and Punctuation
|
||||
|| (cp >= 0x3040 && cp <= 0x309F) // Hiragana
|
||||
|| (cp >= 0x30A0 && cp <= 0x30FF) // Katakana
|
||||
|| (cp >= 0x3130 && cp <= 0x318F) // Hangul Compatibility Jamo
|
||||
|| (cp >= 0x3400 && cp <= 0x4DBF) // CJK Extension A
|
||||
|| (cp >= 0x4E00 && cp <= 0x9FFF) // CJK Unified Ideographs
|
||||
|| (cp >= 0xAC00 && cp <= 0xD7AF) // Hangul Syllables
|
||||
|| (cp >= 0xD7B0 && cp <= 0xD7FF) // Hangul Jamo Extended-B
|
||||
|| (cp >= 0xF900 && cp <= 0xFAFF) // CJK Compatibility Ideographs
|
||||
|| (cp >= 0xFE30 && cp <= 0xFE4F) // CJK Compatibility Forms
|
||||
|| (cp >= 0xFF01 && cp <= 0xFF60) // Fullwidth Latin / Punctuation
|
||||
|
||||
+15
-18
@@ -397,37 +397,34 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
||||
|
||||
// Continue out of block with data set
|
||||
} else if (fileStat.method == ZIP_METHOD_DEFLATED) {
|
||||
// Read out deflated content from file
|
||||
const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize));
|
||||
if (deflatedData == nullptr) {
|
||||
LOG_ERR("ZIP", "Failed to allocate memory for decompression buffer");
|
||||
auto* fileReadBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||
if (!fileReadBuffer) {
|
||||
LOG_ERR("ZIP", "Failed to allocate memory for zip file read buffer");
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const size_t dataRead = file.read(deflatedData, deflatedDataSize);
|
||||
ZipInflateCtx ctx;
|
||||
ctx.file = &file;
|
||||
ctx.fileRemaining = deflatedDataSize;
|
||||
ctx.readBuf = fileReadBuffer;
|
||||
ctx.readBufSize = 1024;
|
||||
|
||||
if (dataRead != deflatedDataSize) {
|
||||
LOG_ERR("ZIP", "Failed to read data, expected %d got %d", deflatedDataSize, dataRead);
|
||||
free(deflatedData);
|
||||
if (!ctx.reader.init(true)) {
|
||||
LOG_ERR("ZIP", "Failed to init inflate reader");
|
||||
free(fileReadBuffer);
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
ctx.reader.setReadCallback(zipReadCallback);
|
||||
|
||||
bool success = false;
|
||||
{
|
||||
InflateReader r;
|
||||
r.init(false);
|
||||
r.setSource(deflatedData, deflatedDataSize);
|
||||
success = r.read(data, inflatedDataSize);
|
||||
}
|
||||
free(deflatedData);
|
||||
|
||||
if (!success) {
|
||||
if (!ctx.reader.read(data, inflatedDataSize)) {
|
||||
LOG_ERR("ZIP", "Failed to inflate file");
|
||||
free(fileReadBuffer);
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
free(fileReadBuffer);
|
||||
|
||||
// Continue out of block with data set
|
||||
} else {
|
||||
|
||||
@@ -5,12 +5,16 @@
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
|
||||
namespace {
|
||||
|
||||
static uint8_t fontSizeEnumFromSettings() {
|
||||
uint8_t e = SETTINGS.fontSize;
|
||||
if (e >= CrossPointSettings::FONT_SIZE_COUNT) e = 1; // default to MEDIUM
|
||||
return e;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SdCardFontSystem::begin(GfxRenderer& renderer) {
|
||||
registry_.discover();
|
||||
|
||||
@@ -74,10 +78,8 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
|
||||
SETTINGS.sdFontFamilyName[0] = '\0';
|
||||
return;
|
||||
}
|
||||
auto sizes = family->availableSizes();
|
||||
uint8_t idx = sizeEnum;
|
||||
if (idx >= sizes.size()) idx = sizes.size() - 1;
|
||||
uint8_t wantedPt = sizes.empty() ? 0 : sizes[idx];
|
||||
const auto* selected = family->findClosestReaderSize(sizeEnum);
|
||||
const uint8_t wantedPt = selected ? selected->pointSize : 0;
|
||||
if (!registryWasDirty && wantedPt == manager_.currentPointSize()) return;
|
||||
LOG_DBG("SDFS", "Reloading %s: size %u -> %u (enum %u)%s", wantedFamily, manager_.currentPointSize(), wantedPt,
|
||||
sizeEnum, registryWasDirty ? " [registry dirty]" : "");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "ActivityManager.h"
|
||||
|
||||
#include <FontCacheManager.h>
|
||||
#include <HalPowerManager.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Memory.h>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "Epub.h"
|
||||
@@ -29,7 +30,11 @@ std::unique_ptr<Epub> ReaderActivity::loadEpub(const std::string& path) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto epub = std::unique_ptr<Epub>(new Epub(path, "/.crosspoint"));
|
||||
auto epub = makeUniqueNoThrow<Epub>(path, "/.crosspoint");
|
||||
if (!epub) {
|
||||
LOG_ERR("READER", "Failed to allocate EPUB object");
|
||||
return nullptr;
|
||||
}
|
||||
if (epub->load(true, SETTINGS.embeddedStyle == 0)) {
|
||||
return epub;
|
||||
}
|
||||
@@ -44,7 +49,11 @@ std::unique_ptr<Xtc> ReaderActivity::loadXtc(const std::string& path) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto xtc = std::unique_ptr<Xtc>(new Xtc(path, "/.crosspoint"));
|
||||
auto xtc = makeUniqueNoThrow<Xtc>(path, "/.crosspoint");
|
||||
if (!xtc) {
|
||||
LOG_ERR("READER", "Failed to allocate XTC object");
|
||||
return nullptr;
|
||||
}
|
||||
if (xtc->load()) {
|
||||
return xtc;
|
||||
}
|
||||
@@ -59,7 +68,11 @@ std::unique_ptr<Txt> ReaderActivity::loadTxt(const std::string& path) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto txt = std::unique_ptr<Txt>(new Txt(path, "/.crosspoint"));
|
||||
auto txt = makeUniqueNoThrow<Txt>(path, "/.crosspoint");
|
||||
if (!txt) {
|
||||
LOG_ERR("READER", "Failed to allocate TXT object");
|
||||
return nullptr;
|
||||
}
|
||||
if (txt->load()) {
|
||||
return txt;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user