Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,22 @@ echo "Running clang-format fix before commit..."
# working tree.
if ((${#staged_files[@]})); then
git add -- "${staged_files[@]}"
fi
fi
# --- Round-10: Phase 1 placeholder-sentinel gate ---
# Block commits that still carry first-run sentinels in the host test sources.
# The Phase 1 commit MUST NOT land with these in place — they leave the tests
# vacuous. CI's ci.yml step is the mandatory gate; this hook is the fast local
# feedback path.
if git grep -l 'WILL_BE_REPLACED_AFTER_FIRST_RUN' -- test/ ; then
echo "ERROR: placeholder string WILL_BE_REPLACED_AFTER_FIRST_RUN still in test source." >&2
echo " Run the test, copy the actual values, and replace the sentinels." >&2
exit 1
fi
if git grep -l 'VISUAL_ORDER_WORD_' -- test/ ; then
echo "ERROR: placeholder string VISUAL_ORDER_WORD_ still in test source." >&2
exit 1
fi
if git grep -l 'static_cast<int16_t>(-32768)' -- test/parsed_text/ ; then
echo "ERROR: placeholder int16 sentinel -32768 still in BiDi test source." >&2
exit 1
fi
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,22 @@ jobs:
with:
submodules: recursive

- name: Verify no placeholder sentinels in test sources
run: |
if git grep -l 'WILL_BE_REPLACED_AFTER_FIRST_RUN' -- test/ ; then
echo "::error::placeholder string WILL_BE_REPLACED_AFTER_FIRST_RUN still in test source." >&2
echo "::error::Run the test, copy the actual values, and replace the sentinels." >&2
exit 1
fi
if git grep -l 'VISUAL_ORDER_WORD_' -- test/ ; then
echo "::error::placeholder string VISUAL_ORDER_WORD_ still in test source." >&2
exit 1
fi
if git grep -l 'static_cast<int16_t>(-32768)' -- test/parsed_text/ ; then
echo "::error::placeholder int16 sentinel -32768 still in BiDi test source." >&2
exit 1
fi

- name: Install build tools
run: |
sudo apt-get update
Expand Down
18 changes: 9 additions & 9 deletions lib/Epub/Epub/ParsedText.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#include "ParsedText.h"

#include <BidiUtils.h>
#include <GfxRenderer.h>
#include <ITextMetrics.h>
#include <Utf8.h>

#include <algorithm>
Expand Down Expand Up @@ -68,7 +68,7 @@ void stripSoftHyphensInPlace(std::string& word) {
// Returns the advance width for a word while ignoring soft hyphen glyphs and optionally appending a visible hyphen.
// Uses advance width (sum of glyph advances + kerning) rather than bounding box width so that italic glyph overhangs
// don't inflate inter-word spacing.
uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const std::string& word,
uint16_t measureWordWidth(const ITextMetrics& renderer, const int fontId, const std::string& word,
const EpdFontFamily::Style style, const bool appendHyphen = false) {
if (word.size() == 1 && word[0] == ' ' && !appendHyphen) {
return renderer.getSpaceWidth(fontId, style);
Expand Down Expand Up @@ -261,7 +261,7 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
}
}

int ParsedText::resolveFirstLineIndent(const bool isFirstLine, const GfxRenderer& renderer, const int fontId) const {
int ParsedText::resolveFirstLineIndent(const bool isFirstLine, const ITextMetrics& renderer, const int fontId) const {
if (!isFirstLine || !isNaturalAlign) {
return 0;
}
Expand All @@ -277,7 +277,7 @@ int ParsedText::resolveFirstLineIndent(const bool isFirstLine, const GfxRenderer
return 0;
}
// Consumes data to minimize memory usage
void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const uint16_t viewportWidth,
void ParsedText::layoutAndExtractLines(const ITextMetrics& renderer, const int fontId, const uint16_t viewportWidth,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
const bool includeLastLine) {
if (words.empty()) {
Expand Down Expand Up @@ -344,7 +344,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
}
}

std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& renderer, const int fontId) {
std::vector<uint16_t> ParsedText::calculateWordWidths(const ITextMetrics& renderer, const int fontId) {
std::vector<uint16_t> wordWidths;
wordWidths.reserve(words.size());

Expand All @@ -355,7 +355,7 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
return wordWidths;
}

std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth,
std::vector<size_t> ParsedText::computeLineBreaks(const ITextMetrics& renderer, const int fontId, const int pageWidth,
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec) {
if (words.empty()) {
return {};
Expand Down Expand Up @@ -468,7 +468,7 @@ 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,
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const ITextMetrics& renderer, const int fontId,
const int pageWidth, std::vector<uint16_t>& wordWidths,
std::vector<bool>& continuesVec) {
const int firstLineIndent = resolveFirstLineIndent(true, renderer, fontId);
Expand Down Expand Up @@ -540,7 +540,7 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r

// Splits words[wordIndex] into prefix (adding a hyphen only when needed) and remainder when a legal breakpoint fits the
// available width.
bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availableWidth, const GfxRenderer& renderer,
bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availableWidth, const ITextMetrics& renderer,
const int fontId, std::vector<uint16_t>& wordWidths,
const bool allowFallbackBreaks) {
// Guard against invalid indices or zero available width before attempting to split.
Expand Down Expand Up @@ -629,7 +629,7 @@ 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::function<void(std::shared_ptr<TextBlock>)>& processLine,
const GfxRenderer& renderer, const int fontId) {
const ITextMetrics& renderer, const int fontId) {
const size_t lineBreak = lineBreakIndices[breakIndex];
const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0;
const size_t lineWordCount = lineBreak - lastBreakAt;
Expand Down
16 changes: 8 additions & 8 deletions lib/Epub/Epub/ParsedText.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#include "blocks/BlockStyle.h"
#include "blocks/TextBlock.h"

class GfxRenderer;
class ITextMetrics;

class ParsedText {
std::vector<std::string> words;
Expand All @@ -30,18 +30,18 @@ class ParsedText {
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,
int resolveFirstLineIndent(bool isFirstLine, const ITextMetrics& renderer, int fontId) const;
std::vector<size_t> computeLineBreaks(const ITextMetrics& renderer, int fontId, int pageWidth,
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
std::vector<size_t> computeHyphenatedLineBreaks(const ITextMetrics& renderer, int fontId, int pageWidth,
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const ITextMetrics& 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::function<void(std::shared_ptr<TextBlock>)>& processLine, const GfxRenderer& renderer,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const ITextMetrics& renderer,
int fontId);
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
std::vector<uint16_t> calculateWordWidths(const ITextMetrics& renderer, int fontId);

public:
explicit ParsedText(const bool extraParagraphSpacing, const bool hyphenationEnabled = false,
Expand All @@ -59,7 +59,7 @@ class ParsedText {
BlockStyle& getBlockStyle() { return blockStyle; }
size_t size() const { return words.size(); }
bool isEmpty() const { return words.empty(); }
void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, uint16_t viewportWidth,
void layoutAndExtractLines(const ITextMetrics& renderer, int fontId, uint16_t viewportWidth,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
bool includeLastLine = true);
};
1 change: 1 addition & 0 deletions lib/Epub/Epub/blocks/TextBlock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <BidiUtils.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <Logging.h>
#include <Serialization.h>

Expand Down
5 changes: 4 additions & 1 deletion lib/Epub/Epub/blocks/TextBlock.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once
#include <EpdFontFamily.h>
#include <HalStorage.h>

class HalFile;

#include <memory>
#include <string>
Expand Down Expand Up @@ -42,6 +43,8 @@ class TextBlock final : public Block {
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
const BlockStyle& getBlockStyle() const { return blockStyle; }
const std::vector<std::string>& getWords() const { return words; }
const std::vector<EpdFontFamily::Style>& getWordStyles() const { return wordStyles; }
const std::vector<int16_t>& getWordXpos() const { return wordXpos; }
bool isEmpty() override { return words.empty(); }
size_t wordCount() const { return words.size(); }
// given a renderer works out where to break the words into lines
Expand Down
18 changes: 10 additions & 8 deletions lib/GfxRenderer/GfxRenderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#include <EpdFontFamily.h>
#include <HalDisplay.h>

#include "ITextMetrics.h"

namespace BidiUtils {
// Paragraph base direction for the Unicode BiDi algorithm (UAX#9).
// AUTO: scan text for first strong directional character (P2/P3 rules)
Expand All @@ -25,7 +27,7 @@ class SdCardFont;
// 0 = transparent, 1-16 = gray levels (white to black)
enum Color : uint8_t { Clear = 0x00, White = 0x01, LightGray = 0x05, DarkGray = 0x0A, Black = 0x10 };

class GfxRenderer {
class GfxRenderer : public ITextMetrics {
public:
enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };

Expand Down Expand Up @@ -110,13 +112,13 @@ class GfxRenderer {
void unregisterSdCardFont(int fontId) { removeFont(fontId); }
void clearSdCardFonts() { sdCardFonts_.clear(); }
const std::map<int, SdCardFont*>& getSdCardFonts() const { return sdCardFonts_; }
bool isSdCardFont(int fontId) const { return sdCardFonts_.count(fontId) > 0; }
bool isSdCardFont(int fontId) const override { return sdCardFonts_.count(fontId) > 0; }
// Ensure SD card font glyph data is loaded for the given text. Called from layout code
// (which holds a const GfxRenderer&) before measuring word widths. Safe to call on non-SD fonts (no-op).
// styleMask: bitmask of styles to prepare (bit 0=regular, 1=bold, 2=italic, 3=bold-italic).
void ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask = 0x0F) const;
void ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask = 0x0F) const override;
void ensureSdCardFontReady(int fontId, const std::vector<std::string>& words, bool includeHyphen,
uint8_t styleMask = 0x0F) const;
uint8_t styleMask = 0x0F) const override;

// Orientation control (affects logical width/height and coordinate transforms)
void setOrientation(const Orientation o) { orientation = o; }
Expand Down Expand Up @@ -192,14 +194,14 @@ class GfxRenderer {
void drawText(int fontId, int x, int y, const char* text, bool black = true,
EpdFontFamily::Style style = EpdFontFamily::REGULAR,
BidiUtils::BidiBaseDir baseDir = BidiUtils::BidiBaseDir::AUTO) const;
int getSpaceWidth(int fontId, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
int getSpaceWidth(int fontId, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const override;
/// Returns the total inter-word advance: fp4::toPixel(spaceAdvance + kern(leftCp,' ') + kern(' ',rightCp)).
/// Using a single snap avoids the +/-1 px rounding error that arises when space advance and kern are
/// snapped separately and then added as integers.
int getSpaceAdvance(int fontId, uint32_t leftCp, uint32_t rightCp, EpdFontFamily::Style style) const;
int getSpaceAdvance(int fontId, uint32_t leftCp, uint32_t rightCp, EpdFontFamily::Style style) const override;
/// Returns the kerning adjustment between two adjacent codepoints.
int getKerning(int fontId, uint32_t leftCp, uint32_t rightCp, EpdFontFamily::Style style) const;
int getTextAdvanceX(int fontId, const char* text, EpdFontFamily::Style style) const;
int getKerning(int fontId, uint32_t leftCp, uint32_t rightCp, EpdFontFamily::Style style) const override;
int getTextAdvanceX(int fontId, const char* text, EpdFontFamily::Style style) const override;
int getFontAscenderSize(int fontId) const;
int getLineHeight(int fontId) const;
std::string truncatedText(int fontId, const char* text, int maxWidth,
Expand Down
25 changes: 25 additions & 0 deletions lib/GfxRenderer/ITextMetrics.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#pragma once

#include <EpdFontFamily.h>

#include <cstdint>
#include <string>
#include <vector>

// Text-measurement surface used by layout code (ParsedText). Extracted from
// GfxRenderer so layout has no dependency on the E-ink HAL / Arduino, which
// keeps it host-testable. GfxRenderer implements this interface; production
// code keeps passing a GfxRenderer&, which up-casts implicitly.
class ITextMetrics {
public:
virtual ~ITextMetrics() = default;

virtual int getSpaceWidth(int fontId, EpdFontFamily::Style style) const = 0;
virtual int getSpaceAdvance(int fontId, uint32_t leftCp, uint32_t rightCp, EpdFontFamily::Style style) const = 0;
virtual int getKerning(int fontId, uint32_t leftCp, uint32_t rightCp, EpdFontFamily::Style style) const = 0;
virtual int getTextAdvanceX(int fontId, const char* text, EpdFontFamily::Style style) const = 0;
virtual bool isSdCardFont(int fontId) const = 0;
virtual void ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask) const = 0;
virtual void ensureSdCardFontReady(int fontId, const std::vector<std::string>& words, bool includeHyphen,
uint8_t styleMask) const = 0;
};
3 changes: 2 additions & 1 deletion test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16)
project(crosspoint_reader_tests CXX)
project(crosspoint_reader_tests CXX C)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand Down Expand Up @@ -46,3 +46,4 @@ add_subdirectory(cjk_ui_fallback)
add_subdirectory(cjk_font_parity)
add_subdirectory(font_resolver)
add_subdirectory(font_boundary)
add_subdirectory(parsed_text)
28 changes: 28 additions & 0 deletions test/parsed_text/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# IMPORTANT: TextBlock.cpp is intentionally NOT linked here. It pulls <GfxRenderer.h>,
# which chains into HalDisplay (Arduino/HAL). ParsedText only uses TextBlock through
# its header-inline ctor and getWords(), so the cpp body is not required to link.
# If a later refactor moves any of those out-of-line, add a HOST_TEST guard inside
# TextBlock.cpp around the render/serialize bodies and link it here with -DHOST_TEST.
add_executable(ParsedTextLayoutTest
ParsedTextLayoutTest.cpp
${REPO_ROOT}/lib/Epub/Epub/ParsedText.cpp
${REPO_ROOT}/lib/Epub/Epub/hyphenation/Hyphenator.cpp
${REPO_ROOT}/lib/Epub/Epub/hyphenation/LanguageRegistry.cpp
${REPO_ROOT}/lib/Epub/Epub/hyphenation/LiangHyphenation.cpp
${REPO_ROOT}/lib/Epub/Epub/hyphenation/HyphenationCommon.cpp
${REPO_ROOT}/lib/MiniBidi/BidiUtils.cpp
${REPO_ROOT}/lib/MiniBidi/minibidi.c
${REPO_ROOT}/lib/Utf8/Utf8.cpp
)
target_include_directories(ParsedTextLayoutTest PRIVATE
${REPO_ROOT}
${REPO_ROOT}/lib/Epub
${REPO_ROOT}/lib/GfxRenderer
${REPO_ROOT}/lib/EpdFont
${REPO_ROOT}/lib/Utf8
${REPO_ROOT}/lib/MiniBidi
${REPO_ROOT}/test
${REPO_ROOT}/test/support
)
target_link_libraries(ParsedTextLayoutTest PRIVATE crosspoint_test_common GTest::gtest_main)
gtest_discover_tests(ParsedTextLayoutTest)
Loading
Loading