← index #6365PR #18486
Related · high · value 1.935
QUERY · ISSUE

extend framebuf.blit method with parameters to select a rectangle from the source

openby buzzwareopened 2020-08-23updated 2020-08-28

I am trying to implement font rendering. I have loaded a single bitmap of all letters, and now I want to blit each letter to the destination. However, there is currently no framebuf method for blitting a partial framebuf.

Currently its blit(fbuf, x, y, [key])

I suggest blit(fbuf, x, y, [key], [sx,sy,sw,sh]) although I'm not clear on whether width & height are better than right and bottom.

It would bring major performance gains to do partial blitting in C over Python, and I don't want to allocate a tiny framebuf for every letter of the alphabet.

CANDIDATE · PULL REQUEST

extmod/modframebuf: Add alpha blending and antialiased lines and polys.

openby corranwebsteropened 2025-11-27updated 2026-03-23
extmod

Summary

<img width="320" height="240" alt="poly_tests" src="https://github.com/user-attachments/assets/e33b41c3-1edd-4518-83f5-c2bf185b62a4" />

This change adds a number of related features:

  • an alpha parameter to most FrameBuffer drawing methods that performs the drawing alpha blended over the existing pixels in the buffer. The alpha value is an integer from 0 (transparent) to 255 (opaque).
  • the alpha parameter given to the blit() method can be a monochrome or greyscale format FrameBuffer of the same shape as the source image that is used as an alpha mask, but line drawing and polygon filling revert to the original versions.
  • replaces the line drawing algorithm from Bresenham's to Wu's and draws lines anti-aliased and alpha-blended.
  • replaces the polygon fill algorithm to use an A-buffer algorithm so filled polygons are drawn anti-aliased.
  • smaller changes to the ellipse and rectangle algorithms to avoid drawing pixels twice.
  • adds explicit little- and big-endian RGB565 formats, and a "byte- swapped" RGB565 format for backwards compatibility.

A compile-time switch (MICROPY_PY_FRAMEBUF_ALPHA) allows memory-constrained boards or developers to opt out of alpha support. With the opt-out the blit() method's alpha parameter remains active for monochrome FrameBuffers, allowing users to get transparency via "image + mask" in addition to "key color".

The change also includes appropriate changes to tests and updates the documentation.

This partially addresses #7253 and also fixes the issues I was seeing in #18361 (albeit with a sledgehammer).

Example code looks like the following:

# create a frame buffer with native RGB format
buf = framebuf.FrameBuffer(data, w, h, framebuf.RGB565)

# draw a filled rectangle with transparency
buf.rect(x, y, w, h, blue, True, 0xcf)

# blit a source with an alpha gradient
gradient_data = bytearray(w*h)
gradient_buf = framebuf.FrameBuffer(gradient_data, w, h, GS8)
for i in range(w):
    gradient_buf.vline(i, 0, 32, 255-((255 * i)//w))
buf.blit(source, x, y, -1, None, gradient_buf)

Testing

Testing has been largely done on the unix port and via the GitHub CI. Additional tests were added and, as part of the testing process, existing tests which were not being run on big-endian platforms are now being run.

Additionally I tested Tempe, a reasonably large drawing library based on framebuf, that could be easily adapted to the changes (I needed to change RGB565 format to RGB565_BS in a few places).

Trade-offs and Alternatives

Basic Alpha Blending

Adding the alpha support to most drawing methods was fairly straightforward. I chose to leave the additional alpha parameter in the signature if MICROPY_PY_FRAMEBUF_ALPHA is turned off, but it is ignored (except in the case of blit() as noted above). The rect() and ellipse() methods drew some pixels multiple times, which doesn't matter when drawing opaquely, but which cause dark artefacts when drawing with alpha, and so they were modified to ensure each pixel is only updated once. This required some fiddling with the ellipse algorithm in particular.

All alpha blending is done in a linear color space for simplicity.

RGB565 Color Channels

The biggest trade-off was in the handling of RGB565 format. To properly apply alpha to a RGB image, the alpha must be applied separately to each channel, which in the case of RGB565 requires knowing the endianness of the values. Currently it is common to be driving big-endian displays (eg. the ST7789 and similar) from little-endian hardware (eg. rp2-based boards) and in these cases the simplest approach is to simply represent colors as big-endian bit-patterns embedded in little-endian native ints (eg. to use 0bGGGBBBBB_RRRRRGGG for red instead of 0bRRRRRGGG_GGGBBBBB).

The choice made was the following:

  • RGB565 format means that the values are stored in the native format of the device running MicroPython. This is probably the only reasonable choice. So red is 0b11111_000000_00000, for example.
  • explicit big- and little-endian formats were added as RGB565_BE and RGB565_LE. In both these formats the color values are passed from Python in native format (so red is 0b11111_000000_00000 for example) and then stored in the appropriate endian format transparently to the user.
  • for backwards compatibility, a "byte-swapped" format RGB565_BS is provided which expects values to be provided in non-native byte order from MicroPython (so red is the color 0b00000000_11111000 and is stored in the underlying buffer in that order), and which byte-swaps appropraitely when applying alpha. This gives an easy way forward for code in the above example of a big-endian display for a little-endian device: just replace all uses of .RGB565 with RGB565_BS.

This will mean that unmodified code that is storing big-endian values in a little-endian buffer will not draw anti-aliased lines and polygons correctly even without specifying a transparency, possibly breaking existing code. Solutions are either to compile without alpha support, or to used RGB565_BS in place of RGB565.

I am open to suggestions about better ways to handle this, but I don't think there is a perfect solution.

Anti-aliasing

For anti-aliased polygons, a trade-off was made to store a little more information about the edges while rendering to reduce computational time. The A-buffer mask was chosen to by 2 scan-lines by 4 samples; it could probably be increased but I'd want to see real-world use of the current mask before doubling the number of samples.

The A-buffer algorithm uses popcount, and I ran into difficulties with __builtin_popcount not being available on some platforms and it being hard to tell when it is and isn't available, and similarly mp_popcount didn't work for some other platforms, so I ended up including the hand-coded version from the mp_popcount. Some bytes could possibly be saved by working out how to use mp_popcount universally, or perhaps just hard-coding a lookup table.

I couldn't find an anti-aliased algorithm for drawing an ellipse that didn't require a square root. So I didn't attempt that. It would be nice to have.

The other major trade-off was a compile-time switch vs. having separate anti-aliased methods (eg, line_aa() and poly_aa()) or switches between the behaviour based on parameters (eg. only use antialiasing if an alpha parameter is passed). The latter two options seemed to have too large a code-size to be worth considering: if you want anti-aliasing, you're probably going to want it all the time in any given application, so there's no need for the non-antialiased versions; and if you don't want anti-aliasing you don't want it all the time.

Other Notes

  • This is a fairly big PR with a number of moving parts, I've tried to structure it so the changes are easy to review.

  • I'm more of a Python programmer than a C programmer: suggestions on making the C code more idiomatic or otherwise tightening it up would be appreciated.

Keyboard

j / / n
next pair
k / / p
previous pair
1 / / h
show query pane
2 / / l
show candidate pane
c
copy suggested comment
r
toggle reasoning
g i
go to index
?
show this help
esc
close overlays

press ? or esc to close

copied