Skip to content

feat: Add 4bit bmp support#944

Merged
daveallie merged 3 commits intocrosspoint-reader:masterfrom
jpirnay:feat-4bit-bmp
Feb 19, 2026
Merged

feat: Add 4bit bmp support#944
daveallie merged 3 commits intocrosspoint-reader:masterfrom
jpirnay:feat-4bit-bmp

Conversation

@jpirnay
Copy link
Contributor

@jpirnay jpirnay commented Feb 17, 2026

Summary

  • What is the goal of this PR?

    • Allow users to create custom sleep screen images with standard tools (ImageMagick, GIMP, etc.) that render cleanly on the e-ink display without dithering artifacts. Previously, avoiding dithering required non-standard 2-bit BMPs that no standard image editor can produce. ( see issue How to render sleep-BMPs without dithering in 2-bit mode? #931 )
  • What changes are included?

    • Add 4-bit BMP format support to Bitmap.cpp (standard format, widely supported by image tools)
    • Auto-detect "native palette" images: if a BMP has ≤4 palette entries and all luminances map within ±21 of the display's native gray levels (0, 85, 170, 255), skip dithering entirely and direct-map pixels
    • Clarify pixel processing strategy with three distinct paths: error-diffusion dithering, simple quantization, or direct mapping
    • Add scripts/generate_test_bmps.py for generating test images across all supported BMP formats

Additional Context

  • The e-ink display has 4 native gray levels. When a BMP already uses exactly those levels, dithering adds noise to what should be clean output. The native palette detection uses a ±21 tolerance (~10%) to handle slight rounding from color space conversions in image tools.
    Users can now create a 4-color grayscale BMP with (imagemagic example):
convert input.png -colorspace Gray -colors 4 -depth 

AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? ** YES**

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 2026

No actionable comments were generated in the recent review. 🎉

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7b2412f and da49a5d.

📒 Files selected for processing (1)
  • lib/GfxRenderer/Bitmap.cpp
🧰 Additional context used
🧬 Code graph analysis (1)
lib/GfxRenderer/Bitmap.cpp (1)
lib/GfxRenderer/BitmapHelpers.cpp (4)
  • adjustPixel (40-51)
  • adjustPixel (40-40)
  • quantize (84-90)
  • quantize (84-84)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: cppcheck
  • GitHub Check: build
🔇 Additional comments (7)
lib/GfxRenderer/Bitmap.cpp (7)

7-12: Clearer inline documentation for dithering vs. native palettes.

Nice clarification of when dithering is applied and when direct mapping is used.


61-63: Error message updated for 4bpp support.

Good to see 4bpp reflected in the user-facing error.


105-109: 4bpp acceptance wired into header validation.

The valid-BPP gate now correctly permits 4bpp inputs.


116-119: Correct default palette size handling for paletted BMPs.

Defaulting colorsUsed to 2^bpp for bpp≤8 is in line with the spec.


147-168: Native-palette detection logic looks solid.

The luminance tolerance check and highColor gating are clear and consistent.


200-205: Consistent adjustPixel usage across mapping paths.

Applying global adjustments before mapping keeps output behavior aligned.


246-252: 4bpp nibble unpacking and palette lookup are correct.

High-nibble/low-nibble handling matches BMP 4bpp layout.


📝 Walkthrough

Walkthrough

Adds 4‑bpp BMP decoding support, palette luminance handling with native‑palette detection (skips dithering when palette maps to the display's 4 gray levels), conditional dithering paths (none / quantize / Atkinson / Floyd‑Steinberg), and a new script to generate BMP test images across bit depths.

Changes

Cohort / File(s) Summary
Bitmap Core Logic
lib/GfxRenderer/Bitmap.cpp
Enable 4‑bpp as valid BPP; read/store colorsUsed; compute palette luminances; detect nativePalette; conditional initialization of dithering; add 4‑bpp row unpack + mapping; branch per-pixel processing: native→direct 2‑bit mapping (with brightness/gamma), non-native→quantize or dither (Atkinson / Floyd‑Steinberg).
Bitmap Header
lib/GfxRenderer/Bitmap.h
Add private members colorsUsed (uint32_t) and nativePalette (bool); adjust dithering comment. No public API signature changes.
Test Utilities
scripts/generate_test_bmps.py
New script generating 480×800 BMP test images for 1‑, 2‑, 4‑, 8‑ (4/256 colors) and 24‑bit formats; includes BMP header/DIB/palette writers and pattern helpers to exercise dithering and palette behaviors.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • daveallie
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature addition: 4-bit BMP support, which aligns with the primary change across modified files.
Description check ✅ Passed The description clearly relates to the changeset, providing context about adding 4-bit BMP support, native palette detection, and the rationale for avoiding dithering artifacts.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 97c3314 and 09680a8.

📒 Files selected for processing (3)
  • lib/GfxRenderer/Bitmap.cpp
  • lib/GfxRenderer/Bitmap.h
  • scripts/generate_test_bmps.py
🧰 Additional context used
🧬 Code graph analysis (1)
lib/GfxRenderer/Bitmap.cpp (1)
lib/GfxRenderer/BitmapHelpers.cpp (4)
  • quantize (84-90)
  • quantize (84-84)
  • adjustPixel (40-51)
  • adjustPixel (40-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: cppcheck
🔇 Additional comments (13)
scripts/generate_test_bmps.py (7)

36-59: BMP header/palette helpers look solid.
Little‑endian packing and BI_RGB header fields are consistent for test output.


62-134: Pattern generators are well‑suited for dithering inspection.
The band layout and intermediate luminance values should expose artifacts clearly.


137-214: 1/2/4‑bit generators handle packing and padding correctly.
Row byte alignment and palette indexing look consistent with BMP specs.


217-260: 8‑bit generators align palette sizes with intent.
colorsUsed and palette entries match both the 4‑color and 256‑color cases.


263-285: 24‑bit generator writes correct BGR ordering with padding.


287-325: Main routine is clear and provides helpful usage guidance.


328-329: Standard entrypoint guard.

lib/GfxRenderer/Bitmap.h (1)

59-65: Private palette‑tracking additions are appropriate.
They keep palette state encapsulated for the new parsing logic.

lib/GfxRenderer/Bitmap.cpp (5)

7-12: Processing‑mode documentation is clear.


61-63: Error message now reflects 4bpp support.


108-119: 4bpp validation and default palette sizing look correct.


200-206: Direct‑mapping path for native palettes is clean.


246-252: 4bpp nibble unpacking looks correct.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/GfxRenderer/Bitmap.cpp`:
- Around line 147-168: The native-palette detection currently skips the
luminance check for palettes larger than 4 entries due to the "colorsUsed <= 4"
guard; remove that constraint so the loop that examines paletteLum[] runs for
any colorsUsed > 0 (still keep bpp <= 2 auto-native behavior). Concretely,
update the logic around nativePalette (the branch that sets nativePalette = true
and iterates over paletteLum[i]) to omit the colorsUsed <= 4 requirement,
ensuring the quantization check (lum >> 6, reconstructed, ±21 threshold) runs
for all palette sizes so highColor is computed correctly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 09680a8 and 7b2412f.

📒 Files selected for processing (1)
  • lib/GfxRenderer/Bitmap.cpp
🧰 Additional context used
🧬 Code graph analysis (1)
lib/GfxRenderer/Bitmap.cpp (1)
lib/GfxRenderer/BitmapHelpers.cpp (4)
  • quantize (84-90)
  • quantize (84-84)
  • adjustPixel (40-51)
  • adjustPixel (40-40)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: cppcheck
  • GitHub Check: build
🔇 Additional comments (5)
lib/GfxRenderer/Bitmap.cpp (5)

7-12: Processing-path documentation matches new behavior.

Clear and accurate description of the native-palette/direct-mapping behavior.


62-62: UnsupportedBpp message now reflects 4bpp support.


108-118: colorsUsed defaulting for paletted formats looks correct.

The colorsUsed == 0 fallback to 1u << bpp keeps BMP palette handling aligned with spec.


147-176: Native-palette detection and ditherer initialization look consistent.


246-252: 4bpp nibble unpacking matches BMP pixel order.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/GfxRenderer/Bitmap.cpp`:
- Around line 200-206: The native-palette branch currently computes color as
static_cast<uint8_t>(lum >> 6) and bypasses global pixel adjustments; change it
to apply adjustPixel before shifting so it matches other paths (use color =
static_cast<uint8_t>(adjustPixel(lum, currentX, prevRowY) >> 6) or equivalent).
Update the branch that checks nativePalette to call adjustPixel(lum, currentX,
prevRowY) prior to the >> 6 shift so nativePalette, ditherers, and quantize all
use the same brightness/contrast/gamma adjustments.

Copy link
Member

@daveallie daveallie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice add, thanks @jpirnay

@daveallie daveallie merged commit c4e3c24 into crosspoint-reader:master Feb 19, 2026
6 checks passed
saslv pushed a commit to saslv/crosspoint-reader that referenced this pull request Feb 19, 2026
## Summary

* What is the goal of this PR?
- Allow users to create custom sleep screen images with standard tools
(ImageMagick, GIMP, etc.) that render cleanly on the e-ink display
without dithering artifacts. Previously, avoiding dithering required
non-standard 2-bit BMPs that no standard image editor can produce. ( see
issue crosspoint-reader#931 )

* What changes are included?
- Add 4-bit BMP format support to Bitmap.cpp (standard format, widely
supported by image tools)
- Auto-detect "native palette" images: if a BMP has ≤4 palette entries
and all luminances map within ±21 of the display's native gray levels
(0, 85, 170, 255), skip dithering entirely and direct-map pixels
- Clarify pixel processing strategy with three distinct paths:
error-diffusion dithering, simple quantization, or direct mapping
- Add scripts/generate_test_bmps.py for generating test images across
all supported BMP formats

## Additional Context

* The e-ink display has 4 native gray levels. When a BMP already uses
exactly those levels, dithering adds noise to what should be clean
output. The native palette detection uses a ±21 tolerance (~10%) to
handle slight rounding from color space conversions in image tools.
Users can now create a 4-color grayscale BMP with (imagemagic example):
```
convert input.png -colorspace Gray -colors 4 -depth 
```
---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _** YES**_
@jpirnay jpirnay deleted the feat-4bit-bmp branch February 19, 2026 18:34
el pushed a commit to el/crosspoint-reader that referenced this pull request Feb 19, 2026
## Summary

* What is the goal of this PR?
- Allow users to create custom sleep screen images with standard tools
(ImageMagick, GIMP, etc.) that render cleanly on the e-ink display
without dithering artifacts. Previously, avoiding dithering required
non-standard 2-bit BMPs that no standard image editor can produce. ( see
issue crosspoint-reader#931 )

* What changes are included?
- Add 4-bit BMP format support to Bitmap.cpp (standard format, widely
supported by image tools)
- Auto-detect "native palette" images: if a BMP has ≤4 palette entries
and all luminances map within ±21 of the display's native gray levels
(0, 85, 170, 255), skip dithering entirely and direct-map pixels
- Clarify pixel processing strategy with three distinct paths:
error-diffusion dithering, simple quantization, or direct mapping
- Add scripts/generate_test_bmps.py for generating test images across
all supported BMP formats

## Additional Context

* The e-ink display has 4 native gray levels. When a BMP already uses
exactly those levels, dithering adds noise to what should be clean
output. The native palette detection uses a ±21 tolerance (~10%) to
handle slight rounding from color space conversions in image tools.
Users can now create a 4-color grayscale BMP with (imagemagic example):
```
convert input.png -colorspace Gray -colors 4 -depth 
```
---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _** YES**_
lukestein pushed a commit to lukestein/crosspoint-reader that referenced this pull request Feb 20, 2026
## Summary

* What is the goal of this PR?
- Allow users to create custom sleep screen images with standard tools
(ImageMagick, GIMP, etc.) that render cleanly on the e-ink display
without dithering artifacts. Previously, avoiding dithering required
non-standard 2-bit BMPs that no standard image editor can produce. ( see
issue crosspoint-reader#931 )

* What changes are included?
- Add 4-bit BMP format support to Bitmap.cpp (standard format, widely
supported by image tools)
- Auto-detect "native palette" images: if a BMP has ≤4 palette entries
and all luminances map within ±21 of the display's native gray levels
(0, 85, 170, 255), skip dithering entirely and direct-map pixels
- Clarify pixel processing strategy with three distinct paths:
error-diffusion dithering, simple quantization, or direct mapping
- Add scripts/generate_test_bmps.py for generating test images across
all supported BMP formats

## Additional Context

* The e-ink display has 4 native gray levels. When a BMP already uses
exactly those levels, dithering adds noise to what should be clean
output. The native palette detection uses a ±21 tolerance (~10%) to
handle slight rounding from color space conversions in image tools.
Users can now create a 4-color grayscale BMP with (imagemagic example):
```
convert input.png -colorspace Gray -colors 4 -depth 
```
---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _** YES**_
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants