From 324c6c13611d5ec737037fcdc02e8beba6eac3fa Mon Sep 17 00:00:00 2001 From: mikee47 Date: Mon, 23 Dec 2024 14:53:58 +0000 Subject: [PATCH] Crop glyphs --- README.rst | 28 +++++++++++++++++++++------- Tools/rc/rclib/font.py | 18 ++++++++++++++++-- Tools/rc/rclib/freetype.py | 22 +--------------------- 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/README.rst b/README.rst index b9687c3..887fdb7 100644 --- a/README.rst +++ b/README.rst @@ -179,6 +179,9 @@ Resource script entries look like this: "": { "codepoints": "", // Which character glyphs to include. See below. "chars": "", // List of required character codepoints + "alpha", // Optional: maximum number of bits per pixel (1, 2, 4 or 8). Default is 8. + "mono", // Optional: Set to *true* to use 1 bit per pixel. Default is *false*. + "size", // Optional parameter for freetype fonts "normal": "", "italic": "", "bold": "", @@ -188,7 +191,7 @@ Resource script entries look like this: Styles are optional but a font must have at least one typeface. -By default, all ASCII characters from 0x20 (space) to 0x7e (~). +By default, all ASCII characters from 0x20 (space) to 0x7e (~) are generated. The ``codepoints`` parameter is a comma-separated list of ranges: a-z,A-Z,0-9,0x4000-0x4050 @@ -197,6 +200,14 @@ This overrides the default and includes only characters and digits, plus unicode The ``chars`` parameter is a simple list of characters, e.g. "Include these chars". Both lists are combined, de-duplicated and sorted in ascending order. +The **alpha** parameter sets a limit on the number of bits per pixel used for the generated glyphs. +This cannot be used to convert monochrome fonts to grayscale, only to reduce grayscale resolution. + +The **mono** parameter is equivalent to **alpha: 1**, and is ignored if **alpha** is specified. + +Generated glyphs are cropped to remove any blank space around the image, then packed to minimise memory usage. +This means that the bitmap data is contiguous, without breaks, from top left to bottom right. + The following font classes are currently supported: GFX @@ -215,6 +226,10 @@ The following font classes are currently supported: These are from the `TFT_eSPI `__ library. See ``resource/fonts/VLW``. + Glyphs are defined using 8 bits per pixel, so have a default **alpha** of 8. + Changing the alpha to 4 or 2 reduces the glyph size (by 50% or 75%) in many cases without + affecting perceived quality. Conversion to monochrome (alpha 1) looks terrible. + Note that TTF/OTF scalable vector fonts are supported directly by this library so is the preferred format for new fonts. freetype @@ -228,12 +243,11 @@ The following font classes are currently supported: The ``freetype`` library supports other types so if required these are easily added. - These fonts have some additional parameters: - "mono": - Whether to produce monochrome (1-bit) or grayscale (8-bit alpha) glyphs. - If not specified, defaults to grayscale. - "size": - e.g. 16, 14.5 + An optional **size** parameter indicates the point size for the font to generate. + If omitted, the first typeface defined for that font is used. + + High quality glyphs of any size can be produced, by default with **alpha** of 8. + TrueType and OpenType fonts can produce good monochrome glyphs, good for larger sizes. Images diff --git a/Tools/rc/rclib/font.py b/Tools/rc/rclib/font.py index dd69f3a..2f4d23e 100644 --- a/Tools/rc/rclib/font.py +++ b/Tools/rc/rclib/font.py @@ -25,7 +25,7 @@ import sys import struct from .base import Resource, findFile, StructSize, fstrSize -from PIL import Image +from PIL import Image, ImageChops class FontStyle(enum.Enum): """Style is a set of these values, using strings here but bitfields in library""" @@ -67,10 +67,24 @@ def set_bitmap(self, img: Image): """ Convert bitmap to internal (GFX) format and set width, height. We remove space round the glyph, update glyph offsets and pack the resulting bits. """ + + # Find smallest bounding box and crop + w, h = img.width, img.height + bg = Image.new('L', (w, h), 0) + diff = ImageChops.difference(img, bg) + diff = ImageChops.add(diff, diff, 2.0, -100) + bbox = diff.getbbox() + + if bbox: + img = img.crop(bbox) + self.xOffset += bbox[0] + self.yOffset += bbox[1] + else: + img = img.crop((0,0,0,0)) w, h = img.width, img.height + self.width = w self.height = h - stride = w # All modes 1 byte per pixel alpha = 1 if img.mode == '1' else self.typeface.font.alpha self.alpha = alpha pixels_per_byte = 8 // alpha diff --git a/Tools/rc/rclib/freetype.py b/Tools/rc/rclib/freetype.py index 9046601..5935df0 100644 --- a/Tools/rc/rclib/freetype.py +++ b/Tools/rc/rclib/freetype.py @@ -26,33 +26,13 @@ import array import freetype from .font import Glyph, Typeface -from PIL import Image, ImageChops -from io import BytesIO +from PIL import Image def pointsToPixels(points26): return round(points26 / 64) -def print_bitmap_diff(bitmap: freetype.Bitmap): - if bitmap.width + bitmap.rows == 0: - return - mode = '1' if bitmap.pixel_mode == freetype.FT_PIXEL_MODE_MONO else 'L' - img = Image.frombuffer(mode, (bitmap.width, bitmap.rows), bytearray(bitmap.buffer)) - bg = Image.new(mode, (bitmap.width, bitmap.rows), 0) - diff = ImageChops.difference(img, bg) - diff = ImageChops.add(diff, diff, 2.0, -100) - bbox = diff.getbbox() - if bbox is None: - return - w, h = bbox[2] - bbox[0], bbox[3] - bbox[1] - size_orig = bitmap.width * bitmap.rows - size_new = w * h - pix_diff = size_orig - size_new - if pix_diff: - print(f'glyph = ({bitmap.width}, {bitmap.rows}), bbox = ({w}, {h}), pix. diff {pix_diff} ({size_orig} -> {size_new})') - - def parse_typeface(typeface: Typeface): face = freetype.Face(typeface.source)