feat: blurred letterbox, portrait pairs, overlay metadata, button fixes

- Letterbox: extend edge pixels outward with blur + fade to solid color
- Portrait: detect and pair portraits side-by-side, fallback to solo
- Metadata: add overlay mode (corner badge), time field, custom date/time
  format strings (strftime), font size 1.5x, region-aware for pairs
- Buttons: swap G9/G10 to match physical layout, double-press random
  button for deep sleep, SLP indicator drawn before sleep
- Boot: remove redundant 'Loading photos' screen (saves 17s refresh)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 15:53:06 -04:00
parent 1d4f94426d
commit 0399573edf
13 changed files with 515 additions and 72 deletions

View File

@@ -123,55 +123,239 @@ static EdgeColor averageEdge(const uint8_t* rgb, uint16_t w, uint16_t h,
return {(uint8_t)(sumR / count), (uint8_t)(sumG / count), (uint8_t)(sumB / count)};
}
// Fill letterbox bars in the final buffer, matching each bar to its adjacent photo edge.
// fitted image is placed at (offsetX, offsetY) with size (fitW x fitH) inside (targetW x targetH).
// In-place horizontal box blur on a rectangular sub-region of a row-major RGB buffer.
// Operates on rows from y0..y1-1, columns x0..x1-1, within a buffer of stride `stride` pixels.
static void boxBlurH(uint8_t* buf, uint16_t stride,
uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, int radius) {
int width = x1 - x0;
if (width <= 0) return;
// Temp row buffer for one scanline (3 bytes per pixel)
uint8_t* tmp = (uint8_t*)malloc(width * 3);
if (tmp == nullptr) return;
int diam = radius * 2 + 1;
for (uint16_t y = y0; y < y1; y++) {
uint8_t* row = buf + ((size_t)y * stride + x0) * 3;
// Running sum initialization
int sumR = 0, sumG = 0, sumB = 0;
for (int i = -radius; i <= radius; i++) {
int xi = constrain(i, 0, width - 1);
sumR += row[xi * 3];
sumG += row[xi * 3 + 1];
sumB += row[xi * 3 + 2];
}
tmp[0] = sumR / diam;
tmp[1] = sumG / diam;
tmp[2] = sumB / diam;
for (int x = 1; x < width; x++) {
int addIdx = constrain(x + radius, 0, width - 1);
int remIdx = constrain(x - radius - 1, 0, width - 1);
sumR += row[addIdx * 3] - row[remIdx * 3];
sumG += row[addIdx * 3 + 1] - row[remIdx * 3 + 1];
sumB += row[addIdx * 3 + 2] - row[remIdx * 3 + 2];
tmp[x * 3] = sumR / diam;
tmp[x * 3 + 1] = sumG / diam;
tmp[x * 3 + 2] = sumB / diam;
}
memcpy(row, tmp, width * 3);
}
free(tmp);
}
// In-place vertical box blur on a rectangular sub-region.
static void boxBlurV(uint8_t* buf, uint16_t stride,
uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, int radius) {
int height = y1 - y0;
if (height <= 0) return;
uint8_t* tmp = (uint8_t*)malloc(height * 3);
if (tmp == nullptr) return;
int diam = radius * 2 + 1;
for (uint16_t x = x0; x < x1; x++) {
// Running sum initialization
int sumR = 0, sumG = 0, sumB = 0;
for (int i = -radius; i <= radius; i++) {
int yi = y0 + constrain(i, 0, height - 1);
size_t idx = ((size_t)yi * stride + x) * 3;
sumR += buf[idx];
sumG += buf[idx + 1];
sumB += buf[idx + 2];
}
tmp[0] = sumR / diam;
tmp[1] = sumG / diam;
tmp[2] = sumB / diam;
for (int y = 1; y < height; y++) {
int addY = y0 + constrain(y + radius, 0, height - 1);
int remY = y0 + constrain(y - radius - 1, 0, height - 1);
size_t addIdx = ((size_t)addY * stride + x) * 3;
size_t remIdx = ((size_t)remY * stride + x) * 3;
sumR += buf[addIdx] - buf[remIdx];
sumG += buf[addIdx + 1] - buf[remIdx + 1];
sumB += buf[addIdx + 2] - buf[remIdx + 2];
tmp[y * 3] = sumR / diam;
tmp[y * 3 + 1] = sumG / diam;
tmp[y * 3 + 2] = sumB / diam;
}
// Write back
for (int y = 0; y < height; y++) {
size_t idx = ((size_t)(y0 + y) * stride + x) * 3;
buf[idx] = tmp[y * 3];
buf[idx + 1] = tmp[y * 3 + 1];
buf[idx + 2] = tmp[y * 3 + 2];
}
}
free(tmp);
}
// 3-pass box blur (approximates Gaussian) on a sub-region
static void gaussianBlurRegion(uint8_t* buf, uint16_t stride,
uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1,
int radius) {
for (int pass = 0; pass < 3; pass++) {
boxBlurH(buf, stride, x0, y0, x1, y1, radius);
boxBlurV(buf, stride, x0, y0, x1, y1, radius);
}
}
// Fill letterbox bars with mirrored + blurred + faded content from the photo edge.
// The fitted image must already be placed in final_buf at (offsetX, offsetY).
static void fillLetterbox(uint8_t* final_buf, uint16_t targetW, uint16_t targetH,
const uint8_t* fitted, uint16_t fitW, uint16_t fitH,
uint16_t offsetX, uint16_t offsetY) {
static constexpr int BLUR_RADIUS = 6;
if (offsetY > 0) {
// Horizontal letterbox (top and bottom bars)
EdgeColor top = averageEdge(fitted, fitW, fitH, 0);
EdgeColor bot = averageEdge(fitted, fitW, fitH, 1);
// Horizontal bars (top and bottom)
EdgeColor topEdge = averageEdge(fitted, fitW, fitH, 0);
EdgeColor botEdge = averageEdge(fitted, fitW, fitH, 1);
// Fill top bar
for (uint16_t y = 0; y < offsetY; y++)
// --- Extend top edge into top bar ---
// Every row in the bar copies from the photo's topmost row (row 0)
for (uint16_t y = 0; y < offsetY; y++) {
for (uint16_t x = 0; x < targetW; x++) {
size_t idx = ((size_t)y * targetW + x) * 3;
final_buf[idx] = top.r; final_buf[idx+1] = top.g; final_buf[idx+2] = top.b;
int srcX = (int)x - (int)offsetX;
srcX = constrain(srcX, 0, (int)fitW - 1);
size_t srcIdx = ((size_t)0 * fitW + srcX) * 3; // always row 0
size_t dstIdx = ((size_t)y * targetW + x) * 3;
final_buf[dstIdx] = fitted[srcIdx];
final_buf[dstIdx + 1] = fitted[srcIdx + 1];
final_buf[dstIdx + 2] = fitted[srcIdx + 2];
}
// Fill bottom bar
}
// --- Extend bottom edge into bottom bar ---
// Every row copies from the photo's bottommost row (fitH - 1)
uint16_t botStart = offsetY + fitH;
for (uint16_t y = botStart; y < targetH; y++)
uint16_t lastRow = fitH - 1;
for (uint16_t y = botStart; y < targetH; y++) {
for (uint16_t x = 0; x < targetW; x++) {
int srcX = (int)x - (int)offsetX;
srcX = constrain(srcX, 0, (int)fitW - 1);
size_t srcIdx = ((size_t)lastRow * fitW + srcX) * 3;
size_t dstIdx = ((size_t)y * targetW + x) * 3;
final_buf[dstIdx] = fitted[srcIdx];
final_buf[dstIdx + 1] = fitted[srcIdx + 1];
final_buf[dstIdx + 2] = fitted[srcIdx + 2];
}
}
// --- Blur both bars ---
gaussianBlurRegion(final_buf, targetW, 0, 0, targetW, offsetY, BLUR_RADIUS);
gaussianBlurRegion(final_buf, targetW, 0, botStart, targetW, targetH, BLUR_RADIUS);
// --- Fade toward solid edge color ---
for (uint16_t y = 0; y < offsetY; y++) {
float alpha = (float)(offsetY - 1 - y) / (float)offsetY; // 0 at photo, 1 at screen edge
for (uint16_t x = 0; x < targetW; x++) {
size_t idx = ((size_t)y * targetW + x) * 3;
final_buf[idx] = bot.r; final_buf[idx+1] = bot.g; final_buf[idx+2] = bot.b;
final_buf[idx] = (uint8_t)(final_buf[idx] * (1.0f - alpha) + topEdge.r * alpha);
final_buf[idx + 1] = (uint8_t)(final_buf[idx + 1] * (1.0f - alpha) + topEdge.g * alpha);
final_buf[idx + 2] = (uint8_t)(final_buf[idx + 2] * (1.0f - alpha) + topEdge.b * alpha);
}
}
for (uint16_t y = botStart; y < targetH; y++) {
float alpha = (float)(y - botStart) / (float)(targetH - botStart);
for (uint16_t x = 0; x < targetW; x++) {
size_t idx = ((size_t)y * targetW + x) * 3;
final_buf[idx] = (uint8_t)(final_buf[idx] * (1.0f - alpha) + botEdge.r * alpha);
final_buf[idx + 1] = (uint8_t)(final_buf[idx + 1] * (1.0f - alpha) + botEdge.g * alpha);
final_buf[idx + 2] = (uint8_t)(final_buf[idx + 2] * (1.0f - alpha) + botEdge.b * alpha);
}
}
Serial.printf("[pipeline] Letterbox TB: top=#%02X%02X%02X bot=#%02X%02X%02X\n",
top.r, top.g, top.b, bot.r, bot.g, bot.b);
Serial.printf("[pipeline] Letterbox TB: bars %dpx, top=#%02X%02X%02X bot=#%02X%02X%02X\n",
offsetY, topEdge.r, topEdge.g, topEdge.b, botEdge.r, botEdge.g, botEdge.b);
}
if (offsetX > 0) {
// Vertical letterbox (left and right bars)
EdgeColor left = averageEdge(fitted, fitW, fitH, 2);
EdgeColor right = averageEdge(fitted, fitW, fitH, 3);
// Vertical bars (left and right)
EdgeColor leftEdge = averageEdge(fitted, fitW, fitH, 2);
EdgeColor rightEdge = averageEdge(fitted, fitW, fitH, 3);
// Fill left bar
for (uint16_t y = 0; y < targetH; y++)
// --- Extend left edge into left bar ---
// Every column in the bar copies from the photo's leftmost column (col 0)
for (uint16_t y = 0; y < targetH; y++) {
int srcY = (int)y - (int)offsetY;
srcY = constrain(srcY, 0, (int)fitH - 1);
size_t srcIdx = ((size_t)srcY * fitW + 0) * 3; // always column 0
for (uint16_t x = 0; x < offsetX; x++) {
size_t idx = ((size_t)y * targetW + x) * 3;
final_buf[idx] = left.r; final_buf[idx+1] = left.g; final_buf[idx+2] = left.b;
}
// Fill right bar
uint16_t rightStart = offsetX + fitW;
for (uint16_t y = 0; y < targetH; y++)
for (uint16_t x = rightStart; x < targetW; x++) {
size_t idx = ((size_t)y * targetW + x) * 3;
final_buf[idx] = right.r; final_buf[idx+1] = right.g; final_buf[idx+2] = right.b;
size_t dstIdx = ((size_t)y * targetW + x) * 3;
final_buf[dstIdx] = fitted[srcIdx];
final_buf[dstIdx + 1] = fitted[srcIdx + 1];
final_buf[dstIdx + 2] = fitted[srcIdx + 2];
}
}
Serial.printf("[pipeline] Letterbox LR: left=#%02X%02X%02X right=#%02X%02X%02X\n",
left.r, left.g, left.b, right.r, right.g, right.b);
// --- Extend right edge into right bar ---
// Every column copies from the photo's rightmost column (fitW - 1)
uint16_t rightStart = offsetX + fitW;
uint16_t lastCol = fitW - 1;
for (uint16_t y = 0; y < targetH; y++) {
int srcY = (int)y - (int)offsetY;
srcY = constrain(srcY, 0, (int)fitH - 1);
size_t srcIdx = ((size_t)srcY * fitW + lastCol) * 3;
for (uint16_t x = rightStart; x < targetW; x++) {
size_t dstIdx = ((size_t)y * targetW + x) * 3;
final_buf[dstIdx] = fitted[srcIdx];
final_buf[dstIdx + 1] = fitted[srcIdx + 1];
final_buf[dstIdx + 2] = fitted[srcIdx + 2];
}
}
// --- Blur both bars ---
gaussianBlurRegion(final_buf, targetW, 0, 0, offsetX, targetH, BLUR_RADIUS);
gaussianBlurRegion(final_buf, targetW, rightStart, 0, targetW, targetH, BLUR_RADIUS);
// --- Fade toward solid edge color ---
for (uint16_t y = 0; y < targetH; y++) {
for (uint16_t x = 0; x < offsetX; x++) {
float alpha = (float)(offsetX - 1 - x) / (float)offsetX;
size_t idx = ((size_t)y * targetW + x) * 3;
final_buf[idx] = (uint8_t)(final_buf[idx] * (1.0f - alpha) + leftEdge.r * alpha);
final_buf[idx + 1] = (uint8_t)(final_buf[idx + 1] * (1.0f - alpha) + leftEdge.g * alpha);
final_buf[idx + 2] = (uint8_t)(final_buf[idx + 2] * (1.0f - alpha) + leftEdge.b * alpha);
}
for (uint16_t x = rightStart; x < targetW; x++) {
float alpha = (float)(x - rightStart) / (float)(targetW - rightStart);
size_t idx = ((size_t)y * targetW + x) * 3;
final_buf[idx] = (uint8_t)(final_buf[idx] * (1.0f - alpha) + rightEdge.r * alpha);
final_buf[idx + 1] = (uint8_t)(final_buf[idx + 1] * (1.0f - alpha) + rightEdge.g * alpha);
final_buf[idx + 2] = (uint8_t)(final_buf[idx + 2] * (1.0f - alpha) + rightEdge.b * alpha);
}
}
Serial.printf("[pipeline] Letterbox LR: bars %dpx, left=#%02X%02X%02X right=#%02X%02X%02X\n",
offsetX, leftEdge.r, leftEdge.g, leftEdge.b,
rightEdge.r, rightEdge.g, rightEdge.b);
}
}