Merge pull request #42 from jpirnay/chore-upstream
chore: Integrate upstream changes
This commit is contained in:
@@ -15,11 +15,12 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t cursorXFP = fp4::fromPixel(startX); // 12.4 fixed-point accumulator
|
||||
int lastBaseX = startX;
|
||||
int lastBaseLeft = 0;
|
||||
int lastBaseWidth = 0;
|
||||
int lastBaseTop = 0;
|
||||
int lastBaseAdvanceFP = 0; // 12.4 fixed-point
|
||||
int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap
|
||||
uint32_t cp;
|
||||
uint32_t prevCp = 0;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
|
||||
@@ -31,20 +32,22 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
|
||||
const EpdGlyph* glyph = getGlyph(cp);
|
||||
if (!glyph) {
|
||||
lastBaseX += fp4::toPixel(prevAdvanceFP); // flush pending advance before resetting
|
||||
prevCp = 0;
|
||||
prevAdvanceFP = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
const int raiseBy = isCombining ? combiningMark::raiseAboveBase(glyph->top, glyph->height, lastBaseTop) : 0;
|
||||
|
||||
if (!isCombining && prevCp != 0) {
|
||||
cursorXFP += getKerning(prevCp, cp); // 4.4 fixed-point kern
|
||||
const auto kernFP = getKerning(prevCp, cp); // 4.4 fixed-point kern
|
||||
lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP);
|
||||
}
|
||||
|
||||
const int cursorXPixels = fp4::toPixel(cursorXFP); // snap 12.4 fixed-point to nearest pixel
|
||||
const int glyphBaseX =
|
||||
isCombining ? combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, glyph->left, glyph->width)
|
||||
: cursorXPixels;
|
||||
: lastBaseX;
|
||||
const int glyphBaseY = startY - raiseBy;
|
||||
|
||||
*minX = std::min(*minX, glyphBaseX + glyph->left);
|
||||
@@ -53,11 +56,11 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
*maxY = std::max(*maxY, glyphBaseY + glyph->top);
|
||||
|
||||
if (!isCombining) {
|
||||
lastBaseX = cursorXPixels;
|
||||
lastBaseLeft = glyph->left;
|
||||
lastBaseWidth = glyph->width;
|
||||
lastBaseAdvanceFP = glyph->advanceX; // 12.4 fixed-point
|
||||
lastBaseTop = glyph->top;
|
||||
cursorXFP += glyph->advanceX; // 12.4 fixed-point advance
|
||||
prevAdvanceFP = lastBaseAdvanceFP;
|
||||
prevCp = cp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
/// Font metrics use "fixed-point 4" (4 fractional bits, i.e. 1/16-pixel
|
||||
/// resolution). Both the 12.4 glyph advances (uint16_t) and the 4.4 kern
|
||||
/// values (int8_t) share the same 4 fractional bits, so they can be freely
|
||||
/// added into a single int32_t accumulator during text layout. The
|
||||
/// accumulator is snapped to the nearest whole pixel only at render time,
|
||||
/// which avoids the per-character rounding errors that plagued integer-only
|
||||
/// layout.
|
||||
/// added before snapping to whole pixels.
|
||||
///
|
||||
/// Rendering and measurement use "differential rounding": each glyph step
|
||||
/// (previous advance + current kern) is combined in fixed-point and snapped
|
||||
/// to a pixel as one unit. This guarantees identical character pairs always
|
||||
/// produce the same pixel spacing, regardless of position on the line.
|
||||
///
|
||||
/// The helpers below eliminate the raw bit-shifts that would otherwise be
|
||||
/// scattered across every layout / measurement call site.
|
||||
|
||||
@@ -20,60 +20,37 @@ namespace {
|
||||
// The draw callback receives this via pDraw->pUser (set by setUserPointer()).
|
||||
// The file I/O callbacks receive the FsFile* via pFile->fHandle (set by jpegOpen()).
|
||||
struct JpegContext {
|
||||
GfxRenderer* renderer;
|
||||
const RenderConfig* config;
|
||||
int screenWidth;
|
||||
int screenHeight;
|
||||
GfxRenderer* renderer{nullptr};
|
||||
const RenderConfig* config{nullptr};
|
||||
int screenWidth{0};
|
||||
int screenHeight{0};
|
||||
|
||||
// Source dimensions after JPEGDEC's built-in scaling
|
||||
int scaledSrcWidth;
|
||||
int scaledSrcHeight;
|
||||
int scaledSrcWidth{0};
|
||||
int scaledSrcHeight{0};
|
||||
|
||||
// Final output dimensions
|
||||
int dstWidth;
|
||||
int dstHeight;
|
||||
int dstWidth{0};
|
||||
int dstHeight{0};
|
||||
|
||||
// Fine scale in 16.16 fixed-point (ESP32-C3 has no FPU)
|
||||
int32_t fineScaleFP; // src -> dst mapping
|
||||
int32_t invScaleFP; // dst -> src mapping
|
||||
int32_t fineScaleFP{1 << 16}; // src -> dst mapping
|
||||
int32_t invScaleFP{1 << 16}; // dst -> src mapping
|
||||
|
||||
PixelCache cache;
|
||||
bool caching;
|
||||
bool caching{false};
|
||||
|
||||
// See PngContext for the rationale: monochromeOutput requests a 1-bit Atkinson dither
|
||||
// emitting only 0/3 so the BW DirectPixelWriter (`pixelValue < 3` rule) maps cleanly.
|
||||
int oneBitDitherRow;
|
||||
Atkinson1BitDitherer* atkinson1BitDitherer;
|
||||
int oneBitDitherRow{-1};
|
||||
Atkinson1BitDitherer* atkinson1BitDitherer{nullptr};
|
||||
|
||||
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
|
||||
int currentDitherRow;
|
||||
AtkinsonDitherer* atkinsonDitherer;
|
||||
DiffusedBayerDitherer* diffusedBayerDitherer;
|
||||
int currentDitherRow{-1};
|
||||
AtkinsonDitherer* atkinsonDitherer{nullptr};
|
||||
DiffusedBayerDitherer* diffusedBayerDitherer{nullptr};
|
||||
#endif
|
||||
|
||||
JpegContext()
|
||||
: renderer(nullptr),
|
||||
config(nullptr),
|
||||
screenWidth(0),
|
||||
screenHeight(0),
|
||||
scaledSrcWidth(0),
|
||||
scaledSrcHeight(0),
|
||||
dstWidth(0),
|
||||
dstHeight(0),
|
||||
fineScaleFP(1 << 16),
|
||||
invScaleFP(1 << 16),
|
||||
caching(false),
|
||||
oneBitDitherRow(-1),
|
||||
atkinson1BitDitherer(nullptr)
|
||||
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
|
||||
,
|
||||
currentDitherRow(-1),
|
||||
atkinsonDitherer(nullptr),
|
||||
diffusedBayerDitherer(nullptr)
|
||||
#endif
|
||||
{
|
||||
}
|
||||
|
||||
~JpegContext() {
|
||||
delete atkinson1BitDitherer;
|
||||
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
|
||||
|
||||
@@ -20,62 +20,38 @@ namespace {
|
||||
// The draw callback receives this via pDraw->pUser (set by png.decode()).
|
||||
// The file I/O callbacks receive the FsFile* via pFile->fHandle (set by pngOpen()).
|
||||
struct PngContext {
|
||||
GfxRenderer* renderer;
|
||||
const RenderConfig* config;
|
||||
int screenWidth;
|
||||
int screenHeight;
|
||||
GfxRenderer* renderer{nullptr};
|
||||
const RenderConfig* config{nullptr};
|
||||
int screenWidth{0};
|
||||
int screenHeight{0};
|
||||
|
||||
// Scaling state
|
||||
float scale;
|
||||
int srcWidth;
|
||||
int srcHeight;
|
||||
int dstWidth;
|
||||
int dstHeight;
|
||||
int lastDstY; // Track last rendered destination Y to avoid duplicates
|
||||
float scale{1.f};
|
||||
int srcWidth{0};
|
||||
int srcHeight{0};
|
||||
int dstWidth{0};
|
||||
int dstHeight{0};
|
||||
int lastDstY{-1}; // Track last rendered destination Y to avoid duplicates
|
||||
|
||||
PixelCache cache;
|
||||
bool caching;
|
||||
bool caching{false};
|
||||
|
||||
uint8_t* grayLineBuffer;
|
||||
uint8_t* grayLineBuffer{nullptr};
|
||||
|
||||
// When the caller requests monochrome output (RenderConfig::monochromeOutput),
|
||||
// we run a proper 1-bit Atkinson dither (matching PngToBmpConverter's BW path)
|
||||
// and emit only values 0 or 3, which round-trip cleanly through the BW writer's
|
||||
// `pixelValue < 3` rule. The 4-level dither path collapses mid-grays to solid
|
||||
// black under that rule.
|
||||
int oneBitDitherRow;
|
||||
Atkinson1BitDitherer* atkinson1BitDitherer;
|
||||
int oneBitDitherRow{-1};
|
||||
Atkinson1BitDitherer* atkinson1BitDitherer{nullptr};
|
||||
|
||||
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
|
||||
int currentDitherRow;
|
||||
AtkinsonDitherer* atkinsonDitherer;
|
||||
DiffusedBayerDitherer* diffusedBayerDitherer;
|
||||
int currentDitherRow{-1};
|
||||
AtkinsonDitherer* atkinsonDitherer{nullptr};
|
||||
DiffusedBayerDitherer* diffusedBayerDitherer{nullptr};
|
||||
#endif
|
||||
|
||||
PngContext()
|
||||
: renderer(nullptr),
|
||||
config(nullptr),
|
||||
screenWidth(0),
|
||||
screenHeight(0),
|
||||
scale(1.0f),
|
||||
srcWidth(0),
|
||||
srcHeight(0),
|
||||
dstWidth(0),
|
||||
dstHeight(0),
|
||||
lastDstY(-1),
|
||||
caching(false),
|
||||
grayLineBuffer(nullptr),
|
||||
oneBitDitherRow(-1),
|
||||
atkinson1BitDitherer(nullptr)
|
||||
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
|
||||
,
|
||||
currentDitherRow(-1),
|
||||
atkinsonDitherer(nullptr),
|
||||
diffusedBayerDitherer(nullptr)
|
||||
#endif
|
||||
{
|
||||
}
|
||||
|
||||
~PngContext() {
|
||||
delete atkinson1BitDitherer;
|
||||
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
|
||||
|
||||
@@ -799,11 +799,21 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
||||
}
|
||||
|
||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||
if (!glyph) {
|
||||
lastBaseX += fp4::toPixel(prevAdvanceFP);
|
||||
prevCp = 0;
|
||||
prevAdvanceFP = 0;
|
||||
lastBaseLeft = 0;
|
||||
lastBaseWidth = 0;
|
||||
lastBaseTop = 0;
|
||||
lastBaseAdvanceFP = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
lastBaseLeft = glyph ? glyph->left : 0;
|
||||
lastBaseWidth = glyph ? glyph->width : 0;
|
||||
lastBaseTop = glyph ? glyph->top : 0;
|
||||
lastBaseAdvanceFP = glyph ? glyph->advanceX : 0;
|
||||
lastBaseLeft = glyph->left;
|
||||
lastBaseWidth = glyph->width;
|
||||
lastBaseTop = glyph->top;
|
||||
lastBaseAdvanceFP = glyph->advanceX;
|
||||
prevAdvanceFP = lastBaseAdvanceFP;
|
||||
|
||||
renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, lastBaseX, yPos, black, style);
|
||||
@@ -1773,7 +1783,13 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
||||
}
|
||||
|
||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||
prevAdvanceFP = glyph ? glyph->advanceX : 0;
|
||||
if (!glyph) {
|
||||
widthPx += fp4::toPixel(prevAdvanceFP);
|
||||
prevCp = 0;
|
||||
prevAdvanceFP = 0;
|
||||
continue;
|
||||
}
|
||||
prevAdvanceFP = glyph->advanceX;
|
||||
prevCp = cp;
|
||||
}
|
||||
widthPx += fp4::toPixel(prevAdvanceFP); // final glyph's advance
|
||||
@@ -1855,11 +1871,21 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
}
|
||||
|
||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||
if (!glyph) {
|
||||
lastBaseY -= fp4::toPixel(prevAdvanceFP);
|
||||
prevCp = 0;
|
||||
prevAdvanceFP = 0;
|
||||
lastBaseLeft = 0;
|
||||
lastBaseWidth = 0;
|
||||
lastBaseTop = 0;
|
||||
lastBaseAdvanceFP = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
lastBaseLeft = glyph ? glyph->left : 0;
|
||||
lastBaseWidth = glyph ? glyph->width : 0;
|
||||
lastBaseTop = glyph ? glyph->top : 0;
|
||||
lastBaseAdvanceFP = glyph ? glyph->advanceX : 0;
|
||||
lastBaseLeft = glyph->left;
|
||||
lastBaseWidth = glyph->width;
|
||||
lastBaseTop = glyph->top;
|
||||
lastBaseAdvanceFP = glyph->advanceX;
|
||||
prevAdvanceFP = lastBaseAdvanceFP;
|
||||
|
||||
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, x, lastBaseY, black, style);
|
||||
|
||||
@@ -286,7 +286,7 @@ STR_UPLOAD_PROMPT: "Отправить текущую позицию?"
|
||||
STR_UPLOAD_SUCCESS: "Прогресс отправлен!"
|
||||
STR_PULL_SUCCESS: "Удалённый прогресс применён!"
|
||||
STR_SYNC_FAILED_MSG: "Ошибка синхронизации"
|
||||
STR_SECTION_PREFIX: "Раздел"
|
||||
STR_SECTION_PREFIX: "Раздел "
|
||||
STR_UPLOAD: "Отправить"
|
||||
STR_BOOK_S_STYLE: "Стиль книги"
|
||||
STR_EMBEDDED_STYLE: "Встроенный стиль"
|
||||
|
||||
@@ -284,8 +284,8 @@ STR_UPLOAD: "Завантажити"
|
||||
STR_BOOK_S_STYLE: "Стиль книги"
|
||||
STR_EMBEDDED_STYLE: "Вбудований стиль"
|
||||
STR_OPDS_SERVER_URL: "URL сервера OPDS"
|
||||
STR_FOOTNOTES: "Зноски"
|
||||
STR_NO_FOOTNOTES: "На цій сторінці немає зносок"
|
||||
STR_FOOTNOTES: "Примітки"
|
||||
STR_NO_FOOTNOTES: "На цій сторінці немає приміток"
|
||||
STR_LINK: "[посилання]"
|
||||
STR_SCREENSHOT_BUTTON: "Знімок екрана"
|
||||
STR_AUTO_TURN_ENABLED: "Автоперегортання увімк.: "
|
||||
|
||||
+16
-26
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <HalClock.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
#define MAX_ENTRY_LEN 256
|
||||
@@ -40,44 +41,33 @@ void logPrintf(const char* level, const char* origin, const char* format, ...) {
|
||||
va_start(args, format);
|
||||
char buf[MAX_ENTRY_LEN];
|
||||
char* c = buf;
|
||||
// add the timestamp
|
||||
// add timestamp, wall clock, level and origin
|
||||
{
|
||||
unsigned long ms = millis();
|
||||
char wallClock[12];
|
||||
HalClock::formatLogTime(wallClock, sizeof(wallClock));
|
||||
int len;
|
||||
if (wallClock[0] != '\0') {
|
||||
len = snprintf(c, sizeof(buf), "[%lu %s] ", ms, wallClock);
|
||||
len = snprintf(c, sizeof(buf), "[%lu %s] [%s] [%s] ", ms, wallClock, level, origin);
|
||||
} else {
|
||||
len = snprintf(c, sizeof(buf), "[%lu] ", ms);
|
||||
len = snprintf(c, sizeof(buf), "[%lu] [%s] [%s] ", ms, level, origin);
|
||||
}
|
||||
// error while writing => return
|
||||
if (len < 0) {
|
||||
return; // encoding error, skip logging
|
||||
va_end(args);
|
||||
return;
|
||||
}
|
||||
c += len;
|
||||
}
|
||||
// add the level
|
||||
{
|
||||
const char* p = level;
|
||||
size_t remaining = sizeof(buf) - (c - buf);
|
||||
while (*p && remaining > 1) {
|
||||
*c++ = *p++;
|
||||
remaining--;
|
||||
}
|
||||
if (remaining > 1) {
|
||||
*c++ = ' ';
|
||||
}
|
||||
}
|
||||
// add the origin
|
||||
{
|
||||
int len = snprintf(c, sizeof(buf) - (c - buf), "[%s] ", origin);
|
||||
if (len < 0) {
|
||||
return; // encoding error, skip logging
|
||||
}
|
||||
c += len;
|
||||
// clamp c to be in buffer range
|
||||
c += std::min(len, MAX_ENTRY_LEN);
|
||||
}
|
||||
// add the user message
|
||||
vsnprintf(c, sizeof(buf) - (c - buf), format, args);
|
||||
{
|
||||
int len = vsnprintf(c, sizeof(buf) - (c - buf), format, args);
|
||||
if (len < 0) {
|
||||
va_end(args);
|
||||
return;
|
||||
}
|
||||
}
|
||||
va_end(args);
|
||||
if (logSerial) {
|
||||
logSerial.print(buf);
|
||||
|
||||
@@ -33,19 +33,19 @@ void logPrintf(const char* level, const char* origin, const char* format, ...);
|
||||
|
||||
#ifdef ENABLE_SERIAL_LOG
|
||||
#if LOG_LEVEL >= 0
|
||||
#define LOG_ERR(origin, format, ...) logPrintf("[ERR]", origin, format "\n", ##__VA_ARGS__)
|
||||
#define LOG_ERR(origin, format, ...) logPrintf("ERR", origin, format "\n", ##__VA_ARGS__)
|
||||
#else
|
||||
#define LOG_ERR(origin, format, ...)
|
||||
#endif
|
||||
|
||||
#if LOG_LEVEL >= 1
|
||||
#define LOG_INF(origin, format, ...) logPrintf("[INF]", origin, format "\n", ##__VA_ARGS__)
|
||||
#define LOG_INF(origin, format, ...) logPrintf("INF", origin, format "\n", ##__VA_ARGS__)
|
||||
#else
|
||||
#define LOG_INF(origin, format, ...)
|
||||
#endif
|
||||
|
||||
#if LOG_LEVEL >= 2
|
||||
#define LOG_DBG(origin, format, ...) logPrintf("[DBG]", origin, format "\n", ##__VA_ARGS__)
|
||||
#define LOG_DBG(origin, format, ...) logPrintf("DBG", origin, format "\n", ##__VA_ARGS__)
|
||||
#else
|
||||
#define LOG_DBG(origin, format, ...)
|
||||
#endif
|
||||
|
||||
+1
-2
@@ -51,7 +51,6 @@ board_build.partitions = partitions.csv
|
||||
extra_scripts =
|
||||
pre:scripts/build_html.py
|
||||
pre:scripts/gen_i18n.py
|
||||
pre:scripts/patch_jpegdec.py
|
||||
pre:scripts/git_branch.py
|
||||
|
||||
; Libraries
|
||||
@@ -63,7 +62,7 @@ lib_deps =
|
||||
bblanchon/ArduinoJson @ 7.4.2
|
||||
QRCode=symlink://lib/QRCode
|
||||
bitbank2/PNGdec @ ^1.0.0
|
||||
bitbank2/JPEGDEC @ ^1.8.0
|
||||
https://github.com/bitbank2/JPEGDEC.git#86282979224c8a32fd51e091ed5a35b0c699a52b
|
||||
links2004/WebSockets @ 2.7.3
|
||||
|
||||
[env:default]
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
"""
|
||||
PlatformIO pre-build script: patch JPEGDEC library for progressive JPEG support.
|
||||
|
||||
Two patches are applied:
|
||||
|
||||
1. JPEGMakeHuffTables: Skip AC Huffman table construction for progressive JPEGs.
|
||||
JPEGDEC 1.8.x fails to open progressive JPEGs because JPEGMakeHuffTables()
|
||||
cannot build AC tables with 11+-bit codes (the "slow tables" path is disabled).
|
||||
Since progressive decode only uses DC coefficients, AC tables are not needed.
|
||||
|
||||
2. JPEGDecodeMCU_P: Guard pMCU writes against MCU_SKIP (-8).
|
||||
The non-progressive JPEGDecodeMCU checks `iMCU >= 0` before writing to pMCU,
|
||||
but JPEGDecodeMCU_P does not. When EIGHT_BIT_GRAYSCALE mode skips chroma
|
||||
channels by passing MCU_SKIP, the unguarded write goes to a wild pointer
|
||||
(sMCUs[0xFFFFF8]) and crashes.
|
||||
|
||||
Both patches are applied idempotently so it is safe to run on every build.
|
||||
"""
|
||||
|
||||
Import("env")
|
||||
import os
|
||||
|
||||
def patch_jpegdec(env):
|
||||
# Find the JPEGDEC library in libdeps
|
||||
libdeps_dir = os.path.join(env["PROJECT_DIR"], ".pio", "libdeps")
|
||||
if not os.path.isdir(libdeps_dir):
|
||||
return
|
||||
for env_dir in os.listdir(libdeps_dir):
|
||||
jpeg_inl = os.path.join(libdeps_dir, env_dir, "JPEGDEC", "src", "jpeg.inl")
|
||||
if os.path.isfile(jpeg_inl):
|
||||
_apply_ac_table_patch(jpeg_inl)
|
||||
_apply_mcu_skip_patch(jpeg_inl)
|
||||
|
||||
def _apply_ac_table_patch(filepath):
|
||||
MARKER = "// CrossPoint patch: skip AC tables for progressive JPEG"
|
||||
with open(filepath, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
if MARKER in content:
|
||||
return # already patched
|
||||
|
||||
OLD = """\
|
||||
}
|
||||
// now do AC components (up to 4 tables of 16-bit codes)"""
|
||||
|
||||
NEW = """\
|
||||
}
|
||||
""" + MARKER + """
|
||||
// Progressive JPEG: only DC coefficients are decoded (first scan), so AC
|
||||
// Huffman tables are not needed. Skip building them to avoid failing on
|
||||
// 11+-bit AC codes that the optimized table builder cannot handle.
|
||||
if (pJPEG->ucMode == 0xc2)
|
||||
return 1;
|
||||
// now do AC components (up to 4 tables of 16-bit codes)"""
|
||||
|
||||
if OLD not in content:
|
||||
print("WARNING: JPEGDEC AC table patch target not found in %s — library may have been updated" % filepath)
|
||||
return
|
||||
|
||||
content = content.replace(OLD, NEW, 1)
|
||||
with open(filepath, "w") as f:
|
||||
f.write(content)
|
||||
print("Patched JPEGDEC: skip AC tables for progressive JPEG: %s" % filepath)
|
||||
|
||||
def _apply_mcu_skip_patch(filepath):
|
||||
MARKER = "// CrossPoint patch: guard pMCU write for MCU_SKIP"
|
||||
with open(filepath, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
if MARKER in content:
|
||||
return # already patched
|
||||
|
||||
# Patch 1: Guard the unconditional pMCU[0] write in JPEGDecodeMCU_P.
|
||||
# This is the DC coefficient store that crashes when iMCU = MCU_SKIP (-8).
|
||||
OLD_DC = """\
|
||||
pMCU[0] = (short)*iDCPredictor; // store in MCU[0]
|
||||
}
|
||||
// Now get the other 63 AC coefficients"""
|
||||
|
||||
NEW_DC = """\
|
||||
""" + MARKER + """
|
||||
if (iMCU >= 0)
|
||||
pMCU[0] = (short)*iDCPredictor; // store in MCU[0]
|
||||
}
|
||||
// Now get the other 63 AC coefficients"""
|
||||
|
||||
if OLD_DC not in content:
|
||||
print("WARNING: JPEGDEC MCU_SKIP patch target not found in %s — library may have been updated" % filepath)
|
||||
return
|
||||
|
||||
content = content.replace(OLD_DC, NEW_DC, 1)
|
||||
|
||||
# Patch 2: Guard the successive approximation pMCU[0] write.
|
||||
# This path is taken on subsequent scans (cApproxBitsHigh != 0), which we
|
||||
# don't normally hit (we only decode first scan), but guard it for safety.
|
||||
OLD_SA = """\
|
||||
pMCU[0] |= iPositive;
|
||||
}
|
||||
goto mcu_done; // that's it"""
|
||||
|
||||
NEW_SA = """\
|
||||
if (iMCU >= 0)
|
||||
pMCU[0] |= iPositive;
|
||||
}
|
||||
goto mcu_done; // that's it"""
|
||||
|
||||
if OLD_SA in content:
|
||||
content = content.replace(OLD_SA, NEW_SA, 1)
|
||||
|
||||
with open(filepath, "w") as f:
|
||||
f.write(content)
|
||||
print("Patched JPEGDEC: guard pMCU writes for MCU_SKIP in JPEGDecodeMCU_P: %s" % filepath)
|
||||
|
||||
# Apply patches immediately when this pre: script runs, before compilation starts.
|
||||
# Previously used env.AddPreAction("buildprog", ...) which deferred patching until
|
||||
# the link step — after the library was already compiled from unpatched source.
|
||||
patch_jpegdec(env)
|
||||
@@ -0,0 +1,381 @@
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "lib/EpdFont/EpdFont.h"
|
||||
#include "lib/EpdFont/EpdFontData.h"
|
||||
|
||||
static int testsPassed = 0;
|
||||
static int testsFailed = 0;
|
||||
|
||||
#define ASSERT_EQ(a, b) \
|
||||
do { \
|
||||
if ((a) != (b)) { \
|
||||
fprintf(stderr, " FAIL: %s:%d: %s == %d, expected %d\n", __FILE__, __LINE__, #a, (a), (b)); \
|
||||
testsFailed++; \
|
||||
return; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define ASSERT_TRUE(cond) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
fprintf(stderr, " FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
||||
testsFailed++; \
|
||||
return; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define PASS() testsPassed++
|
||||
|
||||
// ============================================================================
|
||||
// Synthetic test font
|
||||
//
|
||||
// Glyphs: 'T' (0x54), 'a' (0x61), 'o' (0x6F), 'x' (0x78)
|
||||
// - 'x' advance is 136 FP (8.5px) -- frac = 8, exactly at the rounding
|
||||
// boundary where absolute vs differential snapping diverges for "oo".
|
||||
// - No U+FFFD replacement glyph, so unknown codepoints trigger the
|
||||
// null-glyph path in getTextBounds.
|
||||
//
|
||||
// Kern pairs (4.4 fixed-point):
|
||||
// T->a: -5 (-0.3125px) T->o: -7 (-0.4375px)
|
||||
// o->a: -2 (-0.125px) o->o: -3 (-0.1875px)
|
||||
// ============================================================================
|
||||
|
||||
// clang-format off
|
||||
static const EpdGlyph kGlyphs[] = {
|
||||
// idx width height advanceX left top dataLength dataOffset
|
||||
/* 0 'T' */ { 8, 12, 137, 0, 12, 0, 0 },
|
||||
/* 1 'a' */ { 7, 8, 130, 0, 8, 0, 0 },
|
||||
/* 2 'o' */ { 8, 8, 145, 0, 8, 0, 0 },
|
||||
/* 3 'x' */ { 7, 8, 136, 0, 8, 0, 0 },
|
||||
};
|
||||
|
||||
static const EpdUnicodeInterval kIntervals[] = {
|
||||
{ 0x54, 0x54, 0 }, // 'T' -> glyph[0]
|
||||
{ 0x61, 0x61, 1 }, // 'a' -> glyph[1]
|
||||
{ 0x6F, 0x6F, 2 }, // 'o' -> glyph[2]
|
||||
{ 0x78, 0x78, 3 }, // 'x' -> glyph[3]
|
||||
};
|
||||
|
||||
static const EpdKernClassEntry kKernLeft[] = {
|
||||
{ 0x54, 1 }, // 'T' -> left class 1
|
||||
{ 0x6F, 2 }, // 'o' -> left class 2
|
||||
};
|
||||
|
||||
static const EpdKernClassEntry kKernRight[] = {
|
||||
{ 0x61, 1 }, // 'a' -> right class 1
|
||||
{ 0x6F, 2 }, // 'o' -> right class 2
|
||||
};
|
||||
|
||||
// Flat matrix: leftClassCount(2) x rightClassCount(2), 4.4 fixed-point
|
||||
// [L1,R1]=kern(T,a) [L1,R2]=kern(T,o) [L2,R1]=kern(o,a) [L2,R2]=kern(o,o)
|
||||
static const int8_t kKernMatrix[] = { -5, -7, -2, -3 };
|
||||
|
||||
static const EpdFontData kTestFontData = {
|
||||
.bitmap = nullptr,
|
||||
.glyph = kGlyphs,
|
||||
.intervals = kIntervals,
|
||||
.intervalCount = 4,
|
||||
.advanceY = 16,
|
||||
.ascender = 12,
|
||||
.descender = 0,
|
||||
.is2Bit = false,
|
||||
.groups = nullptr,
|
||||
.groupCount = 0,
|
||||
.glyphToGroup = nullptr,
|
||||
.kernLeftClasses = kKernLeft,
|
||||
.kernRightClasses = kKernRight,
|
||||
.kernMatrix = kKernMatrix,
|
||||
.kernLeftEntryCount = 2,
|
||||
.kernRightEntryCount = 2,
|
||||
.kernLeftClassCount = 2,
|
||||
.kernRightClassCount = 2,
|
||||
.ligaturePairs = nullptr,
|
||||
.ligaturePairCount = 0,
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
static EpdFont testFont(&kTestFontData);
|
||||
|
||||
// Helper: return width from getTextDimensions
|
||||
static int textWidth(const char* str) {
|
||||
int w = 0, h = 0;
|
||||
testFont.getTextDimensions(str, &w, &h);
|
||||
return w;
|
||||
}
|
||||
|
||||
static int textHeight(const char* str) {
|
||||
int w = 0, h = 0;
|
||||
testFont.getTextDimensions(str, &w, &h);
|
||||
return h;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Part 1: Pure fp4 math tests
|
||||
// ============================================================================
|
||||
|
||||
// Simulate the old absolute-snap gap for comparison
|
||||
static int absoluteGap(int32_t startFP, int32_t advanceFP, int32_t kernFP) {
|
||||
int32_t nextFP = startFP + advanceFP + kernFP;
|
||||
return fp4::toPixel(nextFP) - fp4::toPixel(startFP);
|
||||
}
|
||||
|
||||
void testFp4Basics() {
|
||||
printf("testFp4Basics...\n");
|
||||
|
||||
for (int px = 0; px < 500; px++) {
|
||||
ASSERT_EQ(fp4::toPixel(fp4::fromPixel(px)), px);
|
||||
}
|
||||
|
||||
ASSERT_EQ(fp4::toPixel(0), 0);
|
||||
ASSERT_EQ(fp4::toPixel(7), 0); // 0.4375 -> 0
|
||||
ASSERT_EQ(fp4::toPixel(8), 1); // 0.5 -> 1 (round half up)
|
||||
ASSERT_EQ(fp4::toPixel(15), 1); // 0.9375 -> 1
|
||||
ASSERT_EQ(fp4::toPixel(16), 1); // 1.0 -> 1
|
||||
ASSERT_EQ(fp4::toPixel(24), 2); // 1.5 -> 2
|
||||
ASSERT_EQ(fp4::toPixel(-8), 0); // -0.5 -> 0
|
||||
ASSERT_EQ(fp4::toPixel(-9), -1); // -0.5625 -> -1
|
||||
ASSERT_EQ(fp4::toPixel(-16), -1);
|
||||
|
||||
ASSERT_EQ(fp4::toPixel(137 + (-9)), 8); // 128 = 8.0 exact
|
||||
ASSERT_EQ(fp4::toPixel(137 + (-5)), 8); // 132 = 8.25
|
||||
ASSERT_EQ(fp4::toPixel(137 + (-1)), 9); // 136 = 8.5 (half rounds up)
|
||||
|
||||
printf(" All fp4 basics passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testOldApproachInconsistency() {
|
||||
printf("testOldApproachInconsistency...\n");
|
||||
|
||||
// 'oo' pair: advance=145 (9.0625px), kern=-3 (-0.1875px), combined=142 (8.875px)
|
||||
const int32_t advance = 145;
|
||||
const int32_t kern = -3;
|
||||
|
||||
int minGap = 999, maxGap = -999;
|
||||
for (int startPx = 0; startPx < 100; startPx++) {
|
||||
for (int frac = 0; frac < 16; frac++) {
|
||||
int32_t startFP = fp4::fromPixel(startPx) + frac;
|
||||
int gap = absoluteGap(startFP, advance, kern);
|
||||
if (gap < minGap) minGap = gap;
|
||||
if (gap > maxGap) maxGap = gap;
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_TRUE(maxGap - minGap >= 1);
|
||||
printf(" Old absolute gap range: [%d, %d] -- varies by %d px\n", minGap, maxGap, maxGap - minGap);
|
||||
|
||||
int diffStep = fp4::toPixel(advance + kern);
|
||||
printf(" Differential step: always %d px\n", diffStep);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testExhaustiveKernRange() {
|
||||
printf("testExhaustiveKernRange...\n");
|
||||
|
||||
const int32_t baseAdvance = 128;
|
||||
int checked = 0;
|
||||
|
||||
for (int advFrac = 0; advFrac < 16; advFrac++) {
|
||||
int32_t advance = baseAdvance + advFrac;
|
||||
for (int kern = -128; kern <= 127; kern++) {
|
||||
int step = fp4::toPixel(advance + static_cast<int32_t>(kern));
|
||||
float idealPx = fp4::toFloat(advance + kern);
|
||||
if (std::abs(step - idealPx) >= 1.0f) {
|
||||
fprintf(stderr, " FAIL: advance=%d, kern=%d, step=%d, ideal=%.4f\n", advance, kern, step, idealPx);
|
||||
testsFailed++;
|
||||
return;
|
||||
}
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
|
||||
printf(" Checked %d (advance, kern) combinations -- all within 1px of ideal\n", checked);
|
||||
PASS();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Part 2: Integration tests using real EpdFont::getTextDimensions
|
||||
// ============================================================================
|
||||
|
||||
void testKernLookup() {
|
||||
printf("testKernLookup...\n");
|
||||
|
||||
ASSERT_EQ(testFont.getKerning('T', 'a'), -5);
|
||||
ASSERT_EQ(testFont.getKerning('T', 'o'), -7);
|
||||
ASSERT_EQ(testFont.getKerning('o', 'a'), -2);
|
||||
ASSERT_EQ(testFont.getKerning('o', 'o'), -3);
|
||||
ASSERT_EQ(testFont.getKerning('a', 'o'), 0); // 'a' has no left class
|
||||
ASSERT_EQ(testFont.getKerning('x', 'o'), 0); // 'x' has no left class
|
||||
ASSERT_EQ(testFont.getKerning('T', 'x'), 0); // 'x' has no right class
|
||||
ASSERT_EQ(testFont.getKerning('T', 'T'), 0); // 'T' has no right class
|
||||
|
||||
printf(" All kern lookups correct\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testGlyphLookup() {
|
||||
printf("testGlyphLookup...\n");
|
||||
|
||||
ASSERT_TRUE(testFont.getGlyph('T') != nullptr);
|
||||
ASSERT_TRUE(testFont.getGlyph('a') != nullptr);
|
||||
ASSERT_TRUE(testFont.getGlyph('o') != nullptr);
|
||||
ASSERT_TRUE(testFont.getGlyph('x') != nullptr);
|
||||
ASSERT_EQ(testFont.getGlyph('T')->advanceX, 137);
|
||||
ASSERT_EQ(testFont.getGlyph('a')->advanceX, 130);
|
||||
ASSERT_EQ(testFont.getGlyph('o')->advanceX, 145);
|
||||
ASSERT_EQ(testFont.getGlyph('x')->advanceX, 136);
|
||||
|
||||
// No U+FFFD in font, so unknown codepoints return nullptr
|
||||
ASSERT_TRUE(testFont.getGlyph('Z') == nullptr);
|
||||
ASSERT_TRUE(testFont.getGlyph('b') == nullptr);
|
||||
|
||||
printf(" All glyph lookups correct\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// Known-value regression tests. Expected widths are computed by hand using
|
||||
// differential rounding. If someone reverts to absolute snapping, specific
|
||||
// test cases will fail.
|
||||
//
|
||||
// Layout trace for each string (all glyphs have left=0):
|
||||
// width = max glyph right edge = lastBaseX + glyph.width
|
||||
//
|
||||
// Differential step from glyph A to glyph B:
|
||||
// step = fp4::toPixel(advanceA + kern(A,B))
|
||||
void testKnownWidths() {
|
||||
printf("testKnownWidths...\n");
|
||||
|
||||
// "o": single glyph at x=0, width=8
|
||||
// w = 0 + 8 = 8
|
||||
ASSERT_EQ(textWidth("o"), 8);
|
||||
|
||||
// "oo": step = toPixel(145 + (-3)) = toPixel(142) = 9
|
||||
// o1 at 0, o2 at 9. w = 9 + 8 = 17
|
||||
ASSERT_EQ(textWidth("oo"), 17);
|
||||
|
||||
// "ooo": two steps of 9
|
||||
// o1 at 0, o2 at 9, o3 at 18. w = 18 + 8 = 26
|
||||
ASSERT_EQ(textWidth("ooo"), 26);
|
||||
|
||||
// "To": step = toPixel(137 + (-7)) = toPixel(130) = 8
|
||||
// T at 0, o at 8. w = 8 + 8 = 16
|
||||
ASSERT_EQ(textWidth("To"), 16);
|
||||
|
||||
// "Ta": step = toPixel(137 + (-5)) = toPixel(132) = 8
|
||||
// T at 0, a at 8. w = 8 + 7 = 15
|
||||
ASSERT_EQ(textWidth("Ta"), 15);
|
||||
|
||||
// "oa": step = toPixel(145 + (-2)) = toPixel(143) = 9
|
||||
// o at 0, a at 9. w = 9 + 7 = 16
|
||||
ASSERT_EQ(textWidth("oa"), 16);
|
||||
|
||||
// "Too": T at 0.
|
||||
// step T->o = toPixel(137 + (-7)) = 8. o1 at 8.
|
||||
// step o->o = toPixel(145 + (-3)) = 9. o2 at 17.
|
||||
// w = 17 + 8 = 25
|
||||
ASSERT_EQ(textWidth("Too"), 25);
|
||||
|
||||
// "xo": step = toPixel(136 + 0) = toPixel(136) = 9 (no kern: x has no left class)
|
||||
// x at 0, o at 9. w = 9 + 8 = 17
|
||||
ASSERT_EQ(textWidth("xo"), 17);
|
||||
|
||||
printf(" All known widths correct\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// "oo" pair consistency: the pixel gap between two o's must be the same
|
||||
// regardless of what prefix precedes them. This is THE key property of
|
||||
// differential rounding. With absolute snapping, "xoo" would produce a
|
||||
// different oo gap than "oo" because 'x' advance (136 FP) puts the first
|
||||
// 'o' at fractional phase 8, crossing the rounding boundary differently.
|
||||
void testPairConsistencyViaFont() {
|
||||
printf("testPairConsistencyViaFont...\n");
|
||||
|
||||
// The oo gap = width(prefix + "oo") - width(prefix + "o")
|
||||
// This isolates the pixel distance contributed by the second 'o'.
|
||||
const int oo_gap_bare = textWidth("oo") - textWidth("o");
|
||||
const int oo_gap_after_x = textWidth("xoo") - textWidth("xo");
|
||||
const int oo_gap_after_T = textWidth("Too") - textWidth("To");
|
||||
const int oo_gap_after_o = textWidth("ooo") - textWidth("oo");
|
||||
|
||||
printf(" oo gap (bare): %d\n", oo_gap_bare);
|
||||
printf(" oo gap (after x): %d\n", oo_gap_after_x);
|
||||
printf(" oo gap (after T): %d\n", oo_gap_after_T);
|
||||
printf(" oo gap (after o): %d\n", oo_gap_after_o);
|
||||
|
||||
// All must be identical
|
||||
ASSERT_EQ(oo_gap_after_x, oo_gap_bare);
|
||||
ASSERT_EQ(oo_gap_after_T, oo_gap_bare);
|
||||
ASSERT_EQ(oo_gap_after_o, oo_gap_bare);
|
||||
|
||||
printf(" All oo gaps identical (%d px) regardless of prefix\n", oo_gap_bare);
|
||||
PASS();
|
||||
}
|
||||
|
||||
// Null-glyph handling: when a codepoint has no glyph (and no replacement
|
||||
// glyph), the pending advance from the previous glyph must still be flushed.
|
||||
// Without the flush fix, the glyph after the null would overlap the one before.
|
||||
void testNullGlyphAdvancePreserved() {
|
||||
printf("testNullGlyphAdvancePreserved...\n");
|
||||
|
||||
// 'Z' (0x5A) is not in our font and there's no U+FFFD, so getGlyph returns null.
|
||||
// "oZo" should lay out as: o1 at 0, Z skipped (advance flushed), o2 at 9.
|
||||
// toPixel(145) = 9 (o's advance, no kern since Z resets prevCp).
|
||||
// w = 9 + 8 = 17
|
||||
int w = textWidth("oZo");
|
||||
printf(" width(\"oZo\") = %d\n", w);
|
||||
|
||||
// Without the flush fix, o2 would land at 0 (overlapping o1), giving w = 8.
|
||||
ASSERT_TRUE(w > 8);
|
||||
ASSERT_EQ(w, 17);
|
||||
|
||||
// Multi-null: "oZZo" -- two consecutive nulls, advance still preserved.
|
||||
w = textWidth("oZZo");
|
||||
printf(" width(\"oZZo\") = %d\n", w);
|
||||
ASSERT_EQ(w, 17);
|
||||
|
||||
// Null at start: "Zo" -- no pending advance to flush, o renders at 0.
|
||||
w = textWidth("Zo");
|
||||
printf(" width(\"Zo\") = %d\n", w);
|
||||
ASSERT_EQ(w, 8);
|
||||
|
||||
printf(" Null-glyph advance correctly preserved\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testHeightCalculation() {
|
||||
printf("testHeightCalculation...\n");
|
||||
|
||||
// 'T' is tallest: top=12, height=12 -> extent [0, 12)
|
||||
// 'o' and 'a': top=8, height=8 -> extent [0, 8)
|
||||
ASSERT_EQ(textHeight("o"), 8);
|
||||
ASSERT_EQ(textHeight("T"), 12);
|
||||
ASSERT_EQ(textHeight("To"), 12);
|
||||
ASSERT_EQ(textHeight("oo"), 8);
|
||||
|
||||
printf(" All heights correct\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
printf("=== Differential Rounding Tests ===\n\n");
|
||||
|
||||
// Part 1: Pure fp4 math
|
||||
testFp4Basics();
|
||||
testOldApproachInconsistency();
|
||||
testExhaustiveKernRange();
|
||||
|
||||
// Part 2: Integration tests against real EpdFont
|
||||
testKernLookup();
|
||||
testGlyphLookup();
|
||||
testKnownWidths();
|
||||
testPairConsistencyViaFont();
|
||||
testNullGlyphAdvancePreserved();
|
||||
testHeightCalculation();
|
||||
|
||||
printf("\n=== Results: %d passed, %d failed ===\n", testsPassed, testsFailed);
|
||||
return testsFailed > 0 ? 1 : 0;
|
||||
}
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
BUILD_DIR="$ROOT_DIR/build/differential_rounding"
|
||||
BINARY="$BUILD_DIR/DifferentialRoundingTest"
|
||||
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
SOURCES=(
|
||||
"$ROOT_DIR/test/differential_rounding/DifferentialRoundingTest.cpp"
|
||||
"$ROOT_DIR/lib/EpdFont/EpdFont.cpp"
|
||||
"$ROOT_DIR/lib/Utf8/Utf8.cpp"
|
||||
)
|
||||
|
||||
CXXFLAGS=(
|
||||
-std=c++20
|
||||
-O2
|
||||
-Wall
|
||||
-Wextra
|
||||
-pedantic
|
||||
-I"$ROOT_DIR"
|
||||
-I"$ROOT_DIR/lib"
|
||||
-I"$ROOT_DIR/lib/EpdFont"
|
||||
-I"$ROOT_DIR/lib/Utf8"
|
||||
)
|
||||
|
||||
c++ "${CXXFLAGS[@]}" "${SOURCES[@]}" -o "$BINARY"
|
||||
|
||||
"$BINARY" "$@"
|
||||
Reference in New Issue
Block a user