Compare commits

...

35 commits

Author SHA1 Message Date
foamyguy
bcd7c653ec docstring fix for new sphinx 2021-01-07 18:40:36 -06:00
foamyguy
28b3879f5b fallback for builtin font 2021-01-07 18:21:25 -06:00
Jeff Epler
eaeed21773 label: Use new, optional 'ascent', 'descent' properties
Each time new glyphs have to be loaded from a font, it can take a long
time (hundreds of milliseconds).  In a parallel commit, 'ascent' and
'descent' properties will be added to BDF font objects, are essentially
free to compute, and are in any case much quicker than entering
load_glyphs.

This may change the layout of text slightly.  For instance, the height
of the "M" glyph in GothamBlack-50.bdf is 35, but the Ascent of the font
is 40 (and some characters, such as Å, are taller than the ascent at 45
pixels high)
2020-12-28 09:26:00 -06:00
Melissa LeBlanc-Williams
9bc4d2b5c8
Merge pull request #99 from FoamyGuy/wrapping
Wrapping helper function
2020-12-04 15:28:31 -07:00
Limor "Ladyada" Fried
f86e1afc94
Merge pull request #101 from FoamyGuy/add_magtag_example
Adding MagTag example
2020-11-22 15:43:32 -05:00
foamyguy
7fcd2065ac remove extra empty lines 2020-11-22 13:00:08 -06:00
foamyguy
62e7cdc584 adding magtag example 2020-11-22 12:59:37 -06:00
foamyguy
256380c71e fix words longer than max_chars 2020-11-21 19:10:48 -06:00
foamyguy
006fee4a6c black format 2020-11-16 22:03:05 -06:00
foamyguy
f3c565c359 add text wrapping helper function and example that uses it. 2020-11-16 21:47:53 -06:00
foamyguy
aedb89cc8c Merge remote-tracking branch 'origin/master' 2020-11-14 14:13:20 -06:00
foamyguy
ff7115131e
Merge pull request #98 from kmatch98/blank_text
Correct bug of modifying self rather than self.local_group when text …
2020-11-14 14:05:01 -06:00
Kevin Matocha
7f9dc63ed5 Fix long comment line for pylint 2020-11-14 13:30:44 -06:00
Kevin Matocha
1f42993525 Correct bug of modifying self rather than self.local_group when text is blank 2020-11-14 13:27:17 -06:00
foamyguy
0f337b892e
Merge pull request #2 from adafruit/master
merge from adafruit
2020-11-06 10:04:54 -06:00
foamyguy
957335cf9e
Merge pull request #96 from FoamyGuy/add_matrixportal_example
Add matrixportal example
2020-11-06 09:56:52 -06:00
foamyguy
856e6d2860 adding matrix portal example 2020-11-06 09:38:29 -06:00
foamyguy
2ab443f6e8
Merge pull request #1 from adafruit/master
merge from adafruit
2020-10-22 21:50:52 -05:00
foamyguy
0c57ae8728
Merge pull request #93 from eteq/add-init
add init
2020-10-19 18:08:56 -05:00
Erik Tollerud
6513a95867 add init 2020-10-17 23:59:24 -04:00
foamyguy
df94e2fbd9
Merge pull request #92 from kmatch98/bitmap_mutable
Correct documentation errors related to mutability
2020-09-05 13:25:14 -05:00
Kevin Matocha
444a8adb73 Correct documentation errors related to mutability 2020-09-05 11:35:28 -05:00
Scott Shawcroft
a836740924
Merge pull request #90 from kmatch98/bitmap_mutable
`bitmap_label`: Make text, line_spacing and scale mutable
2020-08-31 14:15:38 -07:00
Kevin Matocha
f23910cdce Breakout blit function, improve handling of builtin builtin bitmap.blit, add back kwargs passing to self Group instance 2020-08-28 15:34:01 -05:00
Kevin Matocha
4bab6cf4c0 Update docs file to include bitmap_label 2020-08-28 10:20:55 -05:00
Kevin Matocha
5d299cc0e8 ran black and pylint 2020-08-28 10:13:52 -05:00
Kevin Matocha
9ef676ef6d Fix scale bug in label.py, remove kwargs from both and add scale input parameter 2020-08-28 10:12:09 -05:00
Kevin Matocha
df1c8eafd2 fix bug with scale initialization 2020-08-27 16:48:06 -05:00
Kevin Matocha
98467964bf pylint fixes 2020-08-27 14:55:06 -05:00
Kevin Matocha
1a47464430 ran black and pylint updates 2020-08-27 14:45:35 -05:00
Kevin Matocha
7ca736f7e1 Add scale mutability, bug fix on blit range 2020-08-27 13:58:29 -05:00
Kevin Matocha
bece1ad28d Bug fix in try/except in _place_text 2020-08-26 14:07:49 -05:00
Kevin Matocha
a47afc06c2 Add try/except backward compatibility for bitmap.blit function in _place_text 2020-08-26 08:16:19 -05:00
Kevin Matocha
3e8f35f897 Added getter/setters for text, line_spacing and temporary fix for set_scale, some performance speedups by deleting duplication in bounding box calculations 2020-08-25 22:26:30 -05:00
Kevin Matocha
a404b09fd7 first commit with mutable text, font and line_spacing 2020-08-25 15:56:46 -05:00
7 changed files with 584 additions and 210 deletions

View file

@ -0,0 +1,69 @@
"""
Display Text module helper functions
"""
# The MIT License (MIT)
#
# Copyright (c) 2020 Tim C for Adafruit Industries LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
def wrap_text_to_lines(string, max_chars):
"""wrap_text_to_lines function
A helper that will return a list of lines with word-break wrapping
:param str string: The text to be wrapped
:param int max_chars: The maximum number of characters on a line before wrapping
:return list the_lines: A list of lines where each line is separated based on the amount
of max_chars provided
"""
def chunks(lst, n):
"""Yield successive n-sized chunks from lst."""
for i in range(0, len(lst), n):
yield lst[i : i + n]
string = string.replace("\n", "").replace("\r", "") # Strip confusing newlines
words = string.split(" ")
the_lines = []
the_line = ""
for w in words:
if len(w) > max_chars:
parts = []
for part in chunks(w, max_chars - 1):
parts.append("{}-".format(part))
the_lines.extend(parts[:-1])
the_line = parts[-1][:-1]
continue
if len(the_line + " " + w) <= max_chars:
the_line += " " + w
else:
the_lines.append(the_line)
the_line = "" + w
if the_line: # Last line remaining
the_lines.append(the_line)
# Remove first space from first line:
if the_lines[0][0] == " ":
the_lines[0] = the_lines[0][1:]
return the_lines

View file

@ -50,13 +50,12 @@ class Label(displayio.Group):
"""A label displaying a string of text that is stored in a bitmap. """A label displaying a string of text that is stored in a bitmap.
Note: This ``bitmap_label.py`` library utilizes a bitmap to display the text. Note: This ``bitmap_label.py`` library utilizes a bitmap to display the text.
This method is memory-conserving relative to ``label.py``. This method is memory-conserving relative to ``label.py``.
For the bitmap_label library, the font, text, and line_spacing must be set at The ``max_glyphs`` parameter is ignored and is present
instancing and are immutable. The ``max_glyphs`` parameter is ignored and is present
only for direct compatability with label.py. only for direct compatability with label.py.
For use cases where text changes are required after the initial instancing, please
use the `label.py` library. For further reduction in memory usage, set ``save_text=False`` (text string will not
For further reduction in memory usage, set save_text to False (text string will not be stored and ``line_spacing`` and ``font`` are immutable with ``save_text``
be stored). set to ``False``).
The origin point set by ``x`` and ``y`` The origin point set by ``x`` and ``y``
properties will be the left edge of the bounding box, and in the center of a M properties will be the left edge of the bounding box, and in the center of a M
@ -67,28 +66,28 @@ class Label(displayio.Group):
Must include a capital M for measuring character size. Must include a capital M for measuring character size.
:param str text: Text to display :param str text: Text to display
:param int max_glyphs: Unnecessary parameter (provided only for direct compability :param int max_glyphs: Unnecessary parameter (provided only for direct compability
with label.py) with label.py)
:param int color: Color of all text in RGB hex :param int color: Color of all text in RGB hex
:param int background_color: Color of the background, use `None` for transparent :param int background_color: Color of the background, use `None` for transparent
:param double line_spacing: Line spacing of text to display :param double line_spacing: Line spacing of text to display
:param boolean background_tight: Set `True` only if you want background box to tightly :param boolean background_tight: Set `True` only if you want background box to tightly
surround text surround text
:param int padding_top: Additional pixels added to background bounding box at top :param int padding_top: Additional pixels added to background bounding box at top
:param int padding_bottom: Additional pixels added to background bounding box at bottom :param int padding_bottom: Additional pixels added to background bounding box at bottom
:param int padding_left: Additional pixels added to background bounding box at left :param int padding_left: Additional pixels added to background bounding box at left
:param int padding_right: Additional pixels added to background bounding box at right :param int padding_right: Additional pixels added to background bounding box at right
:param (double,double) anchor_point: Point that anchored_position moves relative to. :param (double,double) anchor_point: Point that anchored_position moves relative to.
Tuple with decimal percentage of width and height. Tuple with decimal percentage of width and height.
(E.g. (0,0) is top left, (1.0, 0.5): is middle right.) (E.g. (0,0) is top left, (1.0, 0.5): is middle right.)
:param (int,int) anchored_position: Position relative to the anchor_point. Tuple :param (int,int) anchored_position: Position relative to the anchor_point. Tuple
containing x,y pixel coordinates. containing x,y pixel coordinates.
:param int scale: Integer value of the pixel scaling :param int scale: Integer value of the pixel scaling
:param bool save_text: Set True to save the text string as a constant in the :param bool save_text: Set True to save the text string as a constant in the
label structure. Set False to reduce memory use. label structure. Set False to reduce memory use.
""" """
# pylint: disable=unused-argument, too-many-instance-attributes, too-many-locals, too-many-arguments # pylint: disable=unused-argument, too-many-instance-attributes, too-many-locals, too-many-arguments
# pylint: disable=too-many-branches, no-self-use # pylint: disable=too-many-branches, no-self-use, too-many-statements
# Note: max_glyphs parameter is unnecessary, this is used for direct # Note: max_glyphs parameter is unnecessary, this is used for direct
# compatibility with label.py # compatibility with label.py
@ -111,128 +110,217 @@ class Label(displayio.Group):
anchor_point=None, anchor_point=None,
anchored_position=None, anchored_position=None,
save_text=True, # can reduce memory use if save_text = False save_text=True, # can reduce memory use if save_text = False
**kwargs scale=1,
**kwargs,
): ):
if text == "": # instance the Group
raise RuntimeError( # self Group will contain a single local_group which contains a Group (self.local_group)
"Please provide text string, or use label.py for mutable text" # which contains a TileGrid (self.tilegrid) which contains the text bitmap (self.bitmap)
) super().__init__(
max_size=1, x=x, y=y, scale=1, **kwargs,
)
# the self group scale should always remain at 1, the self.local_group will
# be used to set the scale
# **kwargs will pass any additional arguments provided to the Label
self.local_group = displayio.Group(
max_size=1, scale=scale
) # local_group holds the tileGrid and sets the scaling
self.append(
self.local_group
) # the local_group will always stay in the self Group
self._font = font self._font = font
self._text = text
# Scale will be passed to Group using kwargs. # Create the two-color palette
if "scale" in kwargs.keys(): self.palette = displayio.Palette(2)
self._scale = kwargs["scale"] self.color = color
else: self.background_color = background_color
self._scale = 1
self._line_spacing = line_spacing self._anchor_point = anchor_point
self._save_text = save_text self._anchored_position = anchored_position
# call the text updater with all the arguments.
self._reset_text(
font=font,
x=x,
y=y,
text=text,
line_spacing=line_spacing,
background_tight=background_tight,
padding_top=padding_top,
padding_bottom=padding_bottom,
padding_left=padding_left,
padding_right=padding_right,
anchor_point=anchor_point,
anchored_position=anchored_position,
save_text=save_text,
scale=scale,
)
def _reset_text(
self,
font=None,
x=None,
y=None,
text=None,
line_spacing=None,
background_tight=None,
padding_top=None,
padding_bottom=None,
padding_left=None,
padding_right=None,
anchor_point=None,
anchored_position=None,
save_text=None,
scale=None,
):
# Store all the instance variables
if font is not None:
self._font = font
if x is not None:
self.x = x
if y is not None:
self.y = y
if line_spacing is not None:
self._line_spacing = line_spacing
if background_tight is not None:
self._background_tight = background_tight
if padding_top is not None:
self._padding_top = max(0, padding_top)
if padding_bottom is not None:
self._padding_bottom = max(0, padding_bottom)
if padding_left is not None:
self._padding_left = max(0, padding_left)
if padding_right is not None:
self._padding_right = max(0, padding_right)
if anchor_point is not None:
self.anchor_point = anchor_point
if anchored_position is not None:
self._anchored_position = anchored_position
if save_text is not None:
self._save_text = save_text
if (
scale is not None
): # Scale will be defined in local_group (Note: self should have scale=1)
self.scale = scale # call the setter
# if text is not provided as a parameter (text is None), use the previous value.
if (text is None) and self._save_text:
text = self._text
if self._save_text: # text string will be saved if self._save_text: # text string will be saved
self._text = text self._text = text
else: else:
self._text = None # save a None value since text string is not saved self._text = None # save a None value since text string is not saved
# limit padding to >= 0 # Check for empty string
padding_top = max(0, padding_top) if (text == "") or (
padding_bottom = max(0, padding_bottom) text is None
padding_left = max(0, padding_left) ): # If empty string, just create a zero-sized bounding box and that's it.
padding_right = max(0, padding_right)
# Calculate the text bounding box self._bounding_box = (
0,
# Calculate tight box to provide bounding box dimensions to match label for 0,
# anchor_position calculations 0, # zero width with text == ""
( 0, # zero height with text == ""
tight_box_x,
tight_box_y,
tight_x_offset,
tight_y_offset,
) = self._text_bounding_box(
text, font, self._line_spacing, background_tight=True,
)
if background_tight:
box_x = tight_box_x
box_y = tight_box_y
y_offset = tight_y_offset
x_offset = tight_x_offset
else:
(box_x, box_y, x_offset, y_offset) = self._text_bounding_box(
text, font, self._line_spacing, background_tight=background_tight,
) )
# Calculate the background size including padding # Clear out any items in the self.local_group Group, in case this is an
box_x = box_x + padding_left + padding_right # update to the bitmap_label
box_y = box_y + padding_top + padding_bottom for _ in self.local_group:
self.local_group.pop(0)
# Create the two-color palette else: # The text string is not empty, so create the Bitmap and TileGrid and
self.palette = displayio.Palette(2) # append to the self Group
self.background_color = background_color # Calculate the text bounding box
self.color = color
# Create the bitmap and TileGrid # Calculate both "tight" and "loose" bounding box dimensions to match label for
self.bitmap = displayio.Bitmap(box_x, box_y, len(self.palette)) # anchor_position calculations
(
box_x,
tight_box_y,
x_offset,
tight_y_offset,
loose_box_y,
loose_y_offset,
) = self._text_bounding_box(
text, self._font, self._line_spacing,
) # calculate the box size for a tight and loose backgrounds
# Place the text into the Bitmap if self._background_tight:
self._place_text( box_y = tight_box_y
self.bitmap, y_offset = tight_y_offset
text,
font,
self._line_spacing,
padding_left - x_offset,
padding_top + y_offset,
)
label_position_yoffset = int( # To calibrate with label.py positioning else: # calculate the box size for a loose background
(font.get_glyph(ord("M")).height) / 2 box_y = loose_box_y
) y_offset = loose_y_offset
self.tilegrid = displayio.TileGrid( # Calculate the background size including padding
self.bitmap, box_x = box_x + self._padding_left + self._padding_right
pixel_shader=self.palette, box_y = box_y + self._padding_top + self._padding_bottom
width=1,
height=1,
tile_width=box_x,
tile_height=box_y,
default_tile=0,
x=-padding_left + x_offset,
y=label_position_yoffset - y_offset - padding_top,
)
# instance the Group # Create the bitmap and TileGrid
# this Group will contain just one TileGrid with one contained bitmap self.bitmap = displayio.Bitmap(box_x, box_y, len(self.palette))
super().__init__(
max_size=1, x=x, y=y, **kwargs
) # this will include any arguments, including scale
self.append(self.tilegrid) # add the bitmap's tilegrid to the group
# Update bounding_box values. Note: To be consistent with label.py, # Place the text into the Bitmap
# this is the bounding box for the text only, not including the background. self._place_text(
self.bitmap,
text,
self._font,
self._line_spacing,
self._padding_left - x_offset,
self._padding_top + y_offset,
)
self._bounding_box = ( label_position_yoffset = int( # To calibrate with label.py positioning
self.tilegrid.x, (self._font.get_glyph(ord("M")).height) / 2
self.tilegrid.y, )
tight_box_x,
tight_box_y, self.tilegrid = displayio.TileGrid(
) self.bitmap,
pixel_shader=self.palette,
width=1,
height=1,
tile_width=box_x,
tile_height=box_y,
default_tile=0,
x=-self._padding_left + x_offset,
y=label_position_yoffset - y_offset - self._padding_top,
)
# Clear out any items in the local_group Group, in case this is an update to
# the bitmap_label
for _ in self.local_group:
self.local_group.pop(0)
self.local_group.append(
self.tilegrid
) # add the bitmap's tilegrid to the group
# Update bounding_box values. Note: To be consistent with label.py,
# this is the bounding box for the text only, not including the background.
self._bounding_box = (
self.tilegrid.x,
self.tilegrid.y,
box_x,
tight_box_y,
)
self._anchored_position = anchored_position
self.anchor_point = anchor_point
self.anchored_position = ( self.anchored_position = (
self._anchored_position self._anchored_position
) # sets anchored_position with setter after bitmap is created ) # set the anchored_position with setter after bitmap is created, sets the
# x,y positions of the label
@staticmethod @staticmethod
def _line_spacing_ypixels(font, line_spacing): def _line_spacing_ypixels(font, line_spacing):
# Note: Scale is not implemented at this time, any scaling is pushed up to the Group level # Note: Scaling is provided at the Group level
return_value = int(line_spacing * font.get_bounding_box()[1]) return_value = int(line_spacing * font.get_bounding_box()[1])
return return_value return return_value
def _text_bounding_box(self, text, font, line_spacing, background_tight=False): def _text_bounding_box(self, text, font, line_spacing):
# This empirical approach checks several glyphs for maximum ascender and descender height # This empirical approach checks several glyphs for maximum ascender and descender height
# (consistent with label.py) # (consistent with label.py)
@ -240,7 +328,7 @@ class Label(displayio.Group):
# descender, will depend upon font used # descender, will depend upon font used
try: try:
self._font.load_glyphs(text + glyphs) font.load_glyphs(text + glyphs)
except AttributeError: except AttributeError:
# ignore if font does not have load_glyphs # ignore if font does not have load_glyphs
pass pass
@ -263,8 +351,6 @@ class Label(displayio.Group):
top = bottom = y_start top = bottom = y_start
y_offset_tight = int((font.get_glyph(ord("M")).height) / 2) y_offset_tight = int((font.get_glyph(ord("M")).height) / 2)
# this needs to be reviewed (also in label.py), since it doesn't respond
# properly to the number of newlines.
newline = False newline = False
@ -305,17 +391,25 @@ class Label(displayio.Group):
left = 0 left = 0
final_box_width = right - left final_box_width = right - left
if background_tight:
final_box_height = bottom - top
final_y_offset = -top + y_offset_tight
else: final_box_height_tight = bottom - top
final_box_height = (lines - 1) * self._line_spacing_ypixels( final_y_offset_tight = -top + y_offset_tight
font, line_spacing
) + (ascender_max + descender_max)
final_y_offset = ascender_max
return (final_box_width, final_box_height, left, final_y_offset) final_box_height_loose = (lines - 1) * self._line_spacing_ypixels(
font, line_spacing
) + (ascender_max + descender_max)
final_y_offset_loose = ascender_max
# return (final_box_width, final_box_height, left, final_y_offset)
return (
final_box_width,
final_box_height_tight,
left,
final_y_offset_tight,
final_box_height_loose,
final_y_offset_loose,
)
# pylint: disable=too-many-nested-blocks # pylint: disable=too-many-nested-blocks
def _place_text( def _place_text(
@ -328,20 +422,13 @@ class Label(displayio.Group):
yposition, yposition,
text_palette_index=1, text_palette_index=1,
background_palette_index=0, background_palette_index=0,
print_only_pixels=True, # print_only_pixels = True: only update the bitmap where the glyph skip_index=0, # set to None to write all pixels, other wise skip this palette index
# pixel color is > 0. This is especially useful for script fonts where glyph # when copying glyph bitmaps (this is important for slanted text
# bounding boxes overlap # where rectangulary glyph boxes overlap)
# Set `print_only_pixels=False` to write all pixels
): ):
# placeText - Writes text into a bitmap at the specified location. # placeText - Writes text into a bitmap at the specified location.
# #
# Verify paletteIndex is working properly with * operator, especially # Note: scale is pushed up to Group level
# if accommodating multicolored fonts
#
# Note: Scale is not implemented at this time, is pushed up to Group level
bitmap_width = bitmap.width
bitmap_height = bitmap.height
x_start = xposition # starting x position (left margin) x_start = xposition # starting x position (left margin)
y_start = yposition y_start = yposition
@ -385,47 +472,111 @@ class Label(displayio.Group):
) # for type BuiltinFont, this creates the x-offset in the glyph bitmap. ) # for type BuiltinFont, this creates the x-offset in the glyph bitmap.
# for BDF loaded fonts, this should equal 0 # for BDF loaded fonts, this should equal 0
for y in range(my_glyph.height): self._blit(
for x in range(my_glyph.width): bitmap,
x_placement = x + xposition + my_glyph.dx xposition + my_glyph.dx,
y_placement = y + yposition - my_glyph.height - my_glyph.dy yposition - my_glyph.height - my_glyph.dy,
my_glyph.bitmap,
if (bitmap_width > x_placement >= 0) and ( x_1=glyph_offset_x,
bitmap_height > y_placement >= 0 y_1=0,
): x_2=glyph_offset_x + my_glyph.width,
y_2=0 + my_glyph.height,
# Allows for remapping the bitmap indexes using paletteIndex skip_index=skip_index, # do not copy over any 0 background pixels
# for background and text. )
palette_indexes = (
background_palette_index,
text_palette_index,
)
this_pixel_color = palette_indexes[
my_glyph.bitmap[
y * my_glyph.bitmap.width + x + glyph_offset_x
]
]
if not print_only_pixels or this_pixel_color > 0:
# write all characters if printOnlyPixels = False,
# or if thisPixelColor is > 0
bitmap[
y_placement * bitmap_width + x_placement
] = this_pixel_color
elif y_placement > bitmap_height:
break
xposition = xposition + my_glyph.shift_x xposition = xposition + my_glyph.shift_x
return (left, top, right - left, bottom - top) # bounding_box return (left, top, right - left, bottom - top) # bounding_box
def _blit(
self,
bitmap, # target bitmap
x, # target x upper left corner
y, # target y upper left corner
source_bitmap, # source bitmap
x_1=0, # source x start
y_1=0, # source y start
x_2=None, # source x end
y_2=None, # source y end
skip_index=None, # palette index that will not be copied
# (for example: the background color of a glyph)
):
if hasattr(bitmap, "blit"): # if bitmap has a built-in blit function, call it
# this function should perform its own input checks
bitmap.blit(
x,
y,
source_bitmap,
x1=x_1,
y1=y_1,
x2=x_2,
y2=y_2,
skip_index=skip_index,
)
else: # perform pixel by pixel copy of the bitmap
# Perform input checks
if x_2 is None:
x_2 = source_bitmap.width
if y_2 is None:
y_2 = source_bitmap.height
# Rearrange so that x_1 < x_2 and y1 < y2
if x_1 > x_2:
x_1, x_2 = x_2, x_1
if y_1 > y_2:
y_1, y_2 = y_2, y_1
# Ensure that x2 and y2 are within source bitmap size
x_2 = min(x_2, source_bitmap.width)
y_2 = min(y_2, source_bitmap.height)
for y_count in range(y_2 - y_1):
for x_count in range(x_2 - x_1):
x_placement = x + x_count
y_placement = y + y_count
if (bitmap.width > x_placement >= 0) and (
bitmap.height > y_placement >= 0
): # ensure placement is within target bitmap
# get the palette index from the source bitmap
this_pixel_color = source_bitmap[
y_1
+ (
y_count * source_bitmap.width
) # Direct index into a bitmap array is speedier than [x,y] tuple
+ x_1
+ x_count
]
if (skip_index is None) or (this_pixel_color != skip_index):
bitmap[ # Direct index into a bitmap array is speedier than [x,y] tuple
y_placement * bitmap.width + x_placement
] = this_pixel_color
elif y_placement > bitmap.height:
break
@property @property
def bounding_box(self): def bounding_box(self):
"""An (x, y, w, h) tuple that completely covers all glyphs. The """An (x, y, w, h) tuple that completely covers all glyphs. The
first two numbers are offset from the x, y origin of this group""" first two numbers are offset from the x, y origin of this group"""
return self._bounding_box return self._bounding_box
@property
def scale(self):
"""Set the scaling of the label, in integer values"""
return self._scale
@scale.setter
def scale(self, new_scale):
self.local_group.scale = new_scale
self._scale = new_scale
self.anchored_position = self._anchored_position # update the anchored_position
@property @property
def line_spacing(self): def line_spacing(self):
"""The amount of space between lines of text, in multiples of the font's """The amount of space between lines of text, in multiples of the font's
@ -434,9 +585,10 @@ class Label(displayio.Group):
@line_spacing.setter @line_spacing.setter
def line_spacing(self, new_line_spacing): def line_spacing(self, new_line_spacing):
raise RuntimeError( if self._save_text:
"line_spacing is immutable for bitmap_label.py; use label.py for mutable line_spacing" self._reset_text(line_spacing=new_line_spacing)
) else:
raise RuntimeError("line_spacing is immutable when save_text is False")
@property @property
def color(self): def color(self):
@ -473,22 +625,22 @@ class Label(displayio.Group):
"""Text to displayed.""" """Text to displayed."""
return self._text return self._text
@text.setter @text.setter # Cannot set color or background color with text setter, use separate setter
def text(self, new_text): def text(self, new_text):
raise RuntimeError( self._reset_text(text=new_text)
"text is immutable for bitmap_label.py; use label.py library for mutable text"
)
@property @property
def font(self): def font(self):
"""Font to use for text display.""" """Font to use for text display."""
return self.font return self._font
@font.setter @font.setter
def font(self, new_font): def font(self, new_font):
raise RuntimeError( self._font = new_font
"font is immutable for bitmap_label.py; use label.py library for mutable font" if self._save_text:
) self._reset_text(font=new_font)
else:
raise RuntimeError("font is immutable when save_text is False")
@property @property
def anchor_point(self): def anchor_point(self):
@ -513,16 +665,15 @@ class Label(displayio.Group):
@anchored_position.setter @anchored_position.setter
def anchored_position(self, new_position): def anchored_position(self, new_position):
self._anchored_position = new_position self._anchored_position = new_position
# Set anchored_position # Set anchored_position
if (self._anchor_point is not None) and (self._anchored_position is not None): if (self._anchor_point is not None) and (self._anchored_position is not None):
self.x = int( self.x = int(
new_position[0] new_position[0]
- (self._bounding_box[0] * self._scale) - (self._bounding_box[0] * self.scale)
- round(self._anchor_point[0] * (self._bounding_box[2] * self._scale)) - round(self._anchor_point[0] * (self._bounding_box[2] * self.scale))
) )
self.y = int( self.y = int(
new_position[1] new_position[1]
- (self._bounding_box[1] * self._scale) - (self._bounding_box[1] * self.scale)
- round(self._anchor_point[1] * self._bounding_box[3] * self._scale) - round(self._anchor_point[1] * self._bounding_box[3] * self.scale)
) )

View file

@ -79,18 +79,25 @@ class Label(displayio.Group):
padding_right=0, padding_right=0,
anchor_point=None, anchor_point=None,
anchored_position=None, anchored_position=None,
scale=1,
**kwargs **kwargs
): ):
if "scale" in kwargs.keys():
self._scale = kwargs["scale"]
else:
self._scale = 1
if not max_glyphs and not text: if not max_glyphs and not text:
raise RuntimeError("Please provide a max size, or initial text") raise RuntimeError("Please provide a max size, or initial text")
if not max_glyphs: if not max_glyphs:
max_glyphs = len(text) max_glyphs = len(text)
# add one to max_size for the background bitmap tileGrid # add one to max_size for the background bitmap tileGrid
super().__init__(max_size=max_glyphs + 1, **kwargs)
# instance the Group
# self Group will contain a single local_group which contains a Group (self.local_group)
# which contains a TileGrid
super().__init__(
max_size=1, scale=1, **kwargs
) # The self scale should always be 1
self.local_group = displayio.Group(
max_size=max_glyphs + 1, scale=scale
) # local_group will set the scale
self.append(self.local_group)
self.width = max_glyphs self.width = max_glyphs
self._font = font self._font = font
@ -122,6 +129,8 @@ class Label(displayio.Group):
self._padding_left = padding_left self._padding_left = padding_left
self._padding_right = padding_right self._padding_right = padding_right
self._scale = scale
if text is not None: if text is not None:
self._update_text(str(text)) self._update_text(str(text))
if (anchored_position is not None) and (anchor_point is not None): if (anchored_position is not None) and (anchor_point is not None):
@ -138,28 +147,17 @@ class Label(displayio.Group):
y_box_offset = self._boundingbox[1] y_box_offset = self._boundingbox[1]
else: # draw a "loose" bounding box to include any ascenders/descenders. else: # draw a "loose" bounding box to include any ascenders/descenders.
ascent, descent = self._get_ascent_descent()
# check a few glyphs for maximum ascender and descender height
# Enhancement: it would be preferred to access the font "FONT_ASCENT" and
# "FONT_DESCENT" in the imported BDF file
glyphs = "M j'" # choose glyphs with highest ascender and lowest
# descender, will depend upon font used
ascender_max = descender_max = 0
for char in glyphs:
this_glyph = self._font.get_glyph(ord(char))
if this_glyph:
ascender_max = max(ascender_max, this_glyph.height + this_glyph.dy)
descender_max = max(descender_max, -this_glyph.dy)
box_width = self._boundingbox[2] + self._padding_left + self._padding_right box_width = self._boundingbox[2] + self._padding_left + self._padding_right
x_box_offset = -self._padding_left x_box_offset = -self._padding_left
box_height = ( box_height = (
(ascender_max + descender_max) (ascent + descent)
+ int((lines - 1) * self.height * self._line_spacing) + int((lines - 1) * self.height * self._line_spacing)
+ self._padding_top + self._padding_top
+ self._padding_bottom + self._padding_bottom
) )
y_box_offset = -ascender_max + y_offset - self._padding_top y_box_offset = -ascent + y_offset - self._padding_top
box_width = max(0, box_width) # remove any negative values box_width = max(0, box_width) # remove any negative values
box_height = max(0, box_height) # remove any negative values box_height = max(0, box_height) # remove any negative values
@ -174,12 +172,35 @@ class Label(displayio.Group):
return tile_grid return tile_grid
def _get_ascent_descent(self):
if hasattr(self.font, "ascent"):
return self.font.ascent, self.font.descent
# check a few glyphs for maximum ascender and descender height
glyphs = "M j'" # choose glyphs with highest ascender and lowest
try:
self._font.load_glyphs(glyphs)
except AttributeError:
# Builtin font doesn't have or need load_glyphs
pass
# descender, will depend upon font used
ascender_max = descender_max = 0
for char in glyphs:
this_glyph = self._font.get_glyph(ord(char))
if this_glyph:
ascender_max = max(ascender_max, this_glyph.height + this_glyph.dy)
descender_max = max(descender_max, -this_glyph.dy)
return ascender_max, descender_max
def _get_ascent(self):
return self._get_ascent_descent()[0]
def _update_background_color(self, new_color): def _update_background_color(self, new_color):
if new_color is None: if new_color is None:
self._background_palette.make_transparent(0) self._background_palette.make_transparent(0)
if self._added_background_tilegrid: if self._added_background_tilegrid:
self.pop(0) self.local_group.pop(0)
self._added_background_tilegrid = False self._added_background_tilegrid = False
else: else:
self._background_palette.make_opaque(0) self._background_palette.make_opaque(0)
@ -187,7 +208,7 @@ class Label(displayio.Group):
self._background_color = new_color self._background_color = new_color
lines = self._text.rstrip("\n").count("\n") + 1 lines = self._text.rstrip("\n").count("\n") + 1
y_offset = int((self._font.get_glyph(ord("M")).height) / 2) y_offset = self._get_ascent() // 2
if not self._added_background_tilegrid: # no bitmap is in the self Group if not self._added_background_tilegrid: # no bitmap is in the self Group
# add bitmap if text is present and bitmap sizes > 0 pixels # add bitmap if text is present and bitmap sizes > 0 pixels
@ -200,10 +221,16 @@ class Label(displayio.Group):
self._boundingbox[3] + self._padding_top + self._padding_bottom > 0 self._boundingbox[3] + self._padding_top + self._padding_bottom > 0
) )
): ):
if len(self) > 0: if (
self.insert(0, self._create_background_box(lines, y_offset)) len(self.local_group) > 0
): # This can be simplified in CP v6.0, when group.append(0) bug is corrected
self.local_group.insert(
0, self._create_background_box(lines, y_offset)
)
else: else:
self.append(self._create_background_box(lines, y_offset)) self.local_group.append(
self._create_background_box(lines, y_offset)
)
self._added_background_tilegrid = True self._added_background_tilegrid = True
else: # a bitmap is present in the self Group else: # a bitmap is present in the self Group
@ -217,9 +244,9 @@ class Label(displayio.Group):
self._boundingbox[3] + self._padding_top + self._padding_bottom > 0 self._boundingbox[3] + self._padding_top + self._padding_bottom > 0
) )
): ):
self[0] = self._create_background_box(lines, y_offset) self.local_group[0] = self._create_background_box(lines, y_offset)
else: # delete the existing bitmap else: # delete the existing bitmap
self.pop(0) self.local_group.pop(0)
self._added_background_tilegrid = False self._added_background_tilegrid = False
def _update_text( def _update_text(
@ -233,13 +260,7 @@ class Label(displayio.Group):
i = 0 i = 0
tilegrid_count = i tilegrid_count = i
try: y_offset = self._get_ascent() // 2
self._font.load_glyphs(new_text + "M")
except AttributeError:
# ignore if font does not have load_glyphs
pass
y_offset = int((self._font.get_glyph(ord("M")).height) / 2)
right = top = bottom = 0 right = top = bottom = 0
left = None left = None
@ -284,10 +305,10 @@ class Label(displayio.Group):
x=position_x, x=position_x,
y=position_y, y=position_y,
) )
if tilegrid_count < len(self): if tilegrid_count < len(self.local_group):
self[tilegrid_count] = face self.local_group[tilegrid_count] = face
else: else:
self.append(face) self.local_group.append(face)
tilegrid_count += 1 tilegrid_count += 1
x += glyph.shift_x x += glyph.shift_x
i += 1 i += 1
@ -296,8 +317,8 @@ class Label(displayio.Group):
if left is None: if left is None:
left = 0 left = 0
while len(self) > tilegrid_count: # i: while len(self.local_group) > tilegrid_count: # i:
self.pop() self.local_group.pop()
self._text = new_text self._text = new_text
self._boundingbox = (left, top, right - left, bottom - top) self._boundingbox = (left, top, right - left, bottom - top)
@ -319,6 +340,7 @@ class Label(displayio.Group):
@line_spacing.setter @line_spacing.setter
def line_spacing(self, spacing): def line_spacing(self, spacing):
self._line_spacing = spacing self._line_spacing = spacing
self.text = self._text # redraw the box
@property @property
def color(self): def color(self):
@ -358,6 +380,18 @@ class Label(displayio.Group):
except RuntimeError as run_error: except RuntimeError as run_error:
raise RuntimeError("Text length exceeds max_glyphs") from run_error raise RuntimeError("Text length exceeds max_glyphs") from run_error
@property
def scale(self):
"""Set the scaling of the label, in integer values"""
return self._scale
@scale.setter
def scale(self, new_scale):
current_anchored_position = self.anchored_position
self._scale = new_scale
self.local_group.scale = new_scale
self.anchored_position = current_anchored_position
@property @property
def font(self): def font(self):
"""Font to use for text display.""" """Font to use for text display."""

View file

@ -6,3 +6,6 @@
.. automodule:: adafruit_display_text.label .. automodule:: adafruit_display_text.label
:members: :members:
.. automodule:: adafruit_display_text.bitmap_label
:members:

View file

@ -0,0 +1,68 @@
"""
Basic display_text.label example script
adapted for use on MagTag.
"""
import time
import board
import displayio
import terminalio
from adafruit_display_text import label
# use built in display (PyPortal, PyGamer, PyBadge, CLUE, etc.)
# see guide for setting up external displays (TFT / OLED breakouts, RGB matrices, etc.)
# https://learn.adafruit.com/circuitpython-display-support-using-displayio/display-and-display-bus
display = board.DISPLAY
# wait until we can draw
time.sleep(display.time_to_refresh)
# main group to hold everything
main_group = displayio.Group()
# white background. Scaled to save RAM
bg_bitmap = displayio.Bitmap(display.width // 8, display.height // 8, 1)
bg_palette = displayio.Palette(1)
bg_palette[0] = 0xFFFFFF
bg_sprite = displayio.TileGrid(bg_bitmap, x=0, y=0, pixel_shader=bg_palette)
bg_group = displayio.Group(scale=8)
bg_group.append(bg_sprite)
main_group.append(bg_group)
# first example label
TEXT = "Hello world"
text_area = label.Label(
terminalio.FONT,
text=TEXT,
color=0xFFFFFF,
background_color=0x666666,
padding_top=1,
padding_bottom=3,
padding_right=4,
padding_left=4,
)
text_area.x = 10
text_area.y = 14
main_group.append(text_area)
# second example label
another_text = label.Label(
terminalio.FONT,
scale=2,
text="MagTag display_text\nexample",
color=0x000000,
background_color=0x999999,
padding_top=1,
padding_bottom=3,
padding_right=4,
padding_left=4,
)
# centered
another_text.anchor_point = (0.5, 0.5)
another_text.anchored_position = (display.width // 2, display.height // 2)
main_group.append(another_text)
# show the main group and refresh.
display.show(main_group)
display.refresh()
while True:
pass

View file

@ -0,0 +1,23 @@
"""
This example shows how to create a display_text label and show it
with a Matrix Portal
Requires:
adafruit_matrixportal - https://github.com/adafruit/Adafruit_CircuitPython_MatrixPortal
Copy it from the current libraries bundle into the lib folder on your device.
"""
import terminalio
from adafruit_matrixportal.matrix import Matrix
from adafruit_display_text import label
matrix = Matrix()
display = matrix.display
text = "Hello\nworld"
text_area = label.Label(terminalio.FONT, text=text)
text_area.x = 1
text_area.y = 4
display.show(text_area)
while True:
pass

View file

@ -0,0 +1,26 @@
"""
This example illustrates how to use the wrap_text_to_lines
helper function.
"""
import board
import terminalio
from adafruit_display_text import label, wrap_text_to_lines
# use built in display (PyPortal, PyGamer, PyBadge, CLUE, etc.)
# see guide for setting up external displays (TFT / OLED breakouts, RGB matrices, etc.)
# https://learn.adafruit.com/circuitpython-display-support-using-displayio/display-and-display-bus
display = board.DISPLAY
text = (
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, "
"sed do eiusmod tempor incididunt ut labore et dolore magna "
"aliqua. Ut enim ad minim veniam, quis nostrud exercitation "
"ullamco laboris nisi ut aliquip ex ea commodo consequat."
)
text = "\n".join(wrap_text_to_lines(text, 28))
text_area = label.Label(terminalio.FONT, text=text)
text_area.x = 10
text_area.y = 10
display.show(text_area)
while True:
pass