Extended bitmap viewer by option to togglie between grayscale and bw mode
This commit is contained in:
@@ -601,23 +601,13 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const BookOver
|
||||
constexpr int sectionSpacing = 10;
|
||||
const int availableWidth = pageWidth - 20;
|
||||
|
||||
int textBlockHeight = 0;
|
||||
int textBlockHeight = lineHeight10; // progress line (always present here)
|
||||
if (hasTitle) {
|
||||
textBlockHeight += lineHeight12;
|
||||
if (hasAuthor) {
|
||||
textBlockHeight += lineSpacing;
|
||||
} else if (hasProgress) {
|
||||
textBlockHeight += sectionSpacing;
|
||||
}
|
||||
textBlockHeight += hasAuthor ? lineSpacing : sectionSpacing;
|
||||
}
|
||||
if (hasAuthor) {
|
||||
textBlockHeight += lineHeight10;
|
||||
if (hasProgress) {
|
||||
textBlockHeight += sectionSpacing;
|
||||
}
|
||||
}
|
||||
if (hasProgress) {
|
||||
textBlockHeight += lineHeight10;
|
||||
textBlockHeight += lineHeight10 + sectionSpacing;
|
||||
}
|
||||
|
||||
const bool textBlack = (overlayMode != 3);
|
||||
@@ -638,8 +628,7 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const BookOver
|
||||
const std::string titleStr =
|
||||
renderer.truncatedText(BOOKERLY_12_FONT_ID, overlayInfo.title.c_str(), availableWidth, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(BOOKERLY_12_FONT_ID, currentY, titleStr.c_str(), textBlack, EpdFontFamily::BOLD);
|
||||
const int spacingAfterTitle = hasAuthor ? lineSpacing : (hasProgress ? sectionSpacing : lineSpacing);
|
||||
currentY += lineHeight12 + spacingAfterTitle;
|
||||
currentY += lineHeight12 + (hasAuthor ? lineSpacing : sectionSpacing);
|
||||
}
|
||||
|
||||
if (hasAuthor) {
|
||||
@@ -648,23 +637,20 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const BookOver
|
||||
currentY += lineHeight10 + sectionSpacing;
|
||||
}
|
||||
|
||||
if (hasProgress) {
|
||||
std::string progressStr;
|
||||
if (!overlayInfo.chapterName.empty()) {
|
||||
const std::string prefix = "";
|
||||
const int prefixWidth = renderer.getTextWidth(UI_10_FONT_ID, prefix.c_str());
|
||||
const int suffixWidth = renderer.getTextWidth(UI_10_FONT_ID, overlayInfo.progressSuffix.c_str());
|
||||
const int maxChapterWidth = availableWidth - prefixWidth - suffixWidth;
|
||||
const std::string truncatedChapter =
|
||||
maxChapterWidth > 0
|
||||
? renderer.truncatedText(UI_10_FONT_ID, overlayInfo.chapterName.c_str(), maxChapterWidth)
|
||||
: "";
|
||||
progressStr = prefix + truncatedChapter + overlayInfo.progressSuffix;
|
||||
} else {
|
||||
progressStr = renderer.truncatedText(UI_10_FONT_ID, overlayInfo.progressText.c_str(), availableWidth);
|
||||
}
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, currentY, progressStr.c_str(), textBlack);
|
||||
std::string progressStr;
|
||||
if (!overlayInfo.chapterName.empty()) {
|
||||
const std::string prefix = "";
|
||||
const int prefixWidth = renderer.getTextWidth(UI_10_FONT_ID, prefix.c_str());
|
||||
const int suffixWidth = renderer.getTextWidth(UI_10_FONT_ID, overlayInfo.progressSuffix.c_str());
|
||||
const int maxChapterWidth = availableWidth - prefixWidth - suffixWidth;
|
||||
const std::string truncatedChapter =
|
||||
maxChapterWidth > 0 ? renderer.truncatedText(UI_10_FONT_ID, overlayInfo.chapterName.c_str(), maxChapterWidth)
|
||||
: "";
|
||||
progressStr = prefix + truncatedChapter + overlayInfo.progressSuffix;
|
||||
} else {
|
||||
progressStr = renderer.truncatedText(UI_10_FONT_ID, overlayInfo.progressText.c_str(), availableWidth);
|
||||
}
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, currentY, progressStr.c_str(), textBlack);
|
||||
};
|
||||
|
||||
drawOverlay();
|
||||
|
||||
@@ -150,14 +150,44 @@ bool BmpViewerActivity::renderBmpImage(const bool showControls) {
|
||||
|
||||
GUI.fillPopupProgress(renderer, popupRect, 50);
|
||||
|
||||
bmpHasGreyscale = bitmap.hasGreyscale();
|
||||
// Only render in grayscale when the bitmap actually carries greyscale data AND the user has it enabled.
|
||||
const bool renderGrayscale = bmpHasGreyscale && grayscaleDisplay;
|
||||
|
||||
// Draw control hints. btn2 only shows the BW/Gray toggle when the bitmap supports greyscale —
|
||||
// pure 1-bit BMPs have nothing to toggle. The label shows the *target* mode (what pressing it switches to).
|
||||
const auto drawHints = [&]() {
|
||||
if (!showControls) return;
|
||||
const char* modeLabel =
|
||||
bmpHasGreyscale ? (grayscaleDisplay ? tr(STR_IMAGE_DISPLAY_BW) : tr(STR_IMAGE_DISPLAY_GRAYSCALE)) : "";
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), modeLabel, "", tr(STR_SET_SLEEP_SCREEN));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
};
|
||||
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
renderer.clearScreen();
|
||||
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, 0, 0);
|
||||
if (showControls) {
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", tr(STR_SET_SLEEP_SCREEN));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
}
|
||||
drawHints();
|
||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||
|
||||
if (renderGrayscale) {
|
||||
// Multi-pass 4-level grayscale render — mirrors SleepActivity::renderBitmapSleepScreen.
|
||||
bitmap.rewindToData();
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, 0, 0);
|
||||
renderer.copyGrayscaleLsbBuffers();
|
||||
|
||||
bitmap.rewindToData();
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, 0, 0);
|
||||
renderer.copyGrayscaleMsbBuffers();
|
||||
|
||||
renderer.displayGrayBuffer();
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
}
|
||||
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
@@ -182,7 +212,6 @@ bool BmpViewerActivity::renderDecodedImage(const bool showControls) {
|
||||
computeCenteredImagePlacement(dims.width, dims.height, pageWidth, pageHeight, x, y, renderWidth, renderHeight);
|
||||
|
||||
GUI.fillPopupProgress(renderer, popupRect, 50);
|
||||
renderer.clearScreen();
|
||||
|
||||
RenderConfig config{};
|
||||
config.x = x;
|
||||
@@ -198,23 +227,74 @@ bool BmpViewerActivity::renderDecodedImage(const bool showControls) {
|
||||
config.ditherMode = ImageDitherMode::Bayer;
|
||||
#endif
|
||||
|
||||
// Helper to draw the on-screen control hints. The btn3 label shows the *other* mode
|
||||
// (i.e. what pressing it would switch to).
|
||||
const auto drawHints = [&]() {
|
||||
if (!showControls) return;
|
||||
const char* modeLabel = grayscaleDisplay ? tr(STR_IMAGE_DISPLAY_BW) : tr(STR_IMAGE_DISPLAY_GRAYSCALE);
|
||||
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
|
||||
const char* btn3Label = grayscaleDisplay ? I18N.get(getCurrentDitherModeLabel()) : modeLabel;
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), modeLabel, btn3Label, tr(STR_SET_SLEEP_SCREEN));
|
||||
#else
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), modeLabel, "", tr(STR_SET_SLEEP_SCREEN));
|
||||
#endif
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
};
|
||||
|
||||
if (!grayscaleDisplay) {
|
||||
// Pure black-and-white path: single decode with 1-bit Atkinson dither.
|
||||
config.monochromeOutput = true;
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
renderer.clearScreen();
|
||||
if (!decoder->decodeToFramebuffer(filePath, renderer, config)) {
|
||||
return false;
|
||||
}
|
||||
drawHints();
|
||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Grayscale path: three decode passes (BW + LSB + MSB), 4-level dither in all of them.
|
||||
// Mirrors SleepActivity::renderCustomSleepScreen so the BW plane carries the same
|
||||
// 4-level quantization as the gray planes layered on top.
|
||||
config.monochromeOutput = false;
|
||||
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
renderer.clearScreen();
|
||||
if (!decoder->decodeToFramebuffer(filePath, renderer, config)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (showControls) {
|
||||
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
|
||||
const auto labels =
|
||||
mappedInput.mapLabels(tr(STR_BACK), "", I18N.get(getCurrentDitherModeLabel()), tr(STR_SET_SLEEP_SCREEN));
|
||||
#else
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", tr(STR_SET_SLEEP_SCREEN));
|
||||
#endif
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
}
|
||||
drawHints();
|
||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
if (!decoder->decodeToFramebuffer(filePath, renderer, config)) {
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
return false;
|
||||
}
|
||||
renderer.copyGrayscaleLsbBuffers();
|
||||
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||
if (!decoder->decodeToFramebuffer(filePath, renderer, config)) {
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
return false;
|
||||
}
|
||||
renderer.copyGrayscaleMsbBuffers();
|
||||
|
||||
renderer.displayGrayBuffer();
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
return true;
|
||||
}
|
||||
|
||||
void BmpViewerActivity::toggleDisplayMode() {
|
||||
grayscaleDisplay = !grayscaleDisplay;
|
||||
if (!renderCurrentImage()) {
|
||||
renderError(tr(STR_COULD_NOT_RENDER_IMAGE));
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
|
||||
StrId BmpViewerActivity::getCurrentDitherModeLabel() const {
|
||||
switch (imageDitherModeFromSetting(imageDitherMode)) {
|
||||
@@ -312,8 +392,17 @@ void BmpViewerActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm: toggle between 1-bit B&W and 4-level grayscale display.
|
||||
// For decoded images this always applies; for BMPs it only makes sense when the bitmap
|
||||
// actually carries greyscale data (1-bit BMPs have nothing to toggle).
|
||||
const bool toggleSupported = isBmpFile(filePath) ? bmpHasGreyscale : true;
|
||||
if (toggleSupported && mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
toggleDisplayMode();
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
|
||||
if (!isBmpFile(filePath) && mappedInput.wasReleased(MappedInputManager::Button::Left)) {
|
||||
if (!isBmpFile(filePath) && grayscaleDisplay && mappedInput.wasReleased(MappedInputManager::Button::Left)) {
|
||||
cycleDitherMode();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -24,9 +24,16 @@ class BmpViewerActivity final : public Activity {
|
||||
uint8_t initialImageDitherMode;
|
||||
bool imageDitherSettingsDirty;
|
||||
#endif
|
||||
// Per-session toggle: monochrome (1-bit Atkinson, single decode) vs grayscale (4-level dither, multipass).
|
||||
// Not persisted — defaults to grayscale every time the viewer opens.
|
||||
bool grayscaleDisplay = true;
|
||||
// True after a successful BMP render iff the bitmap actually carries greyscale data.
|
||||
// Used to gate the BW/Gray toggle in loop() — pure 1-bit BMPs cannot be toggled.
|
||||
bool bmpHasGreyscale = false;
|
||||
bool renderCurrentImage(bool showControls = true);
|
||||
bool renderBmpImage(bool showControls = true);
|
||||
bool renderDecodedImage(bool showControls = true);
|
||||
void toggleDisplayMode();
|
||||
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
|
||||
void cycleDitherMode();
|
||||
StrId getCurrentDitherModeLabel() const;
|
||||
|
||||
Reference in New Issue
Block a user