├── create ├── create-icons-128.ps1 ├── create-icons-32.ps1 ├── create-text-16.ps1 ├── create-digits-30.ps1 ├── pack-font.py └── create-font.py ├── display ├── text-16.pf ├── digits-30.pf ├── icons-128.pf ├── icons-32.pf ├── display-test.py ├── main.py ├── packed_font.py ├── PiicoDev_Unified.py ├── enhanced_display.py └── PiicoDev_SSD1306.py ├── screenshots ├── Welcome.bmp ├── right-aligned-text.bmp └── temperature-screen.bmp ├── fonts ├── icons-128 │ ├── star.bmp │ ├── icons-128.pf │ └── icons-128.json ├── icons-32 │ ├── icons-32.pf │ ├── humidity.bmp │ ├── pressure.bmp │ ├── temperature.bmp │ └── icons-32.json ├── text-16 │ ├── text-16.pf │ └── text-16.json └── digits-30 │ ├── digits-30.pf │ └── digits-30.json ├── LICENSE ├── .gitignore └── README.md /create/create-icons-128.ps1: -------------------------------------------------------------------------------- 1 | python .\pack-font.py ..\fonts\icons-128\icons-128.json -------------------------------------------------------------------------------- /create/create-icons-32.ps1: -------------------------------------------------------------------------------- 1 | python .\pack-font.py ..\fonts\icons-32\icons-32.json -------------------------------------------------------------------------------- /display/text-16.pf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mark-gladding/packed-font/HEAD/display/text-16.pf -------------------------------------------------------------------------------- /display/digits-30.pf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mark-gladding/packed-font/HEAD/display/digits-30.pf -------------------------------------------------------------------------------- /display/icons-128.pf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mark-gladding/packed-font/HEAD/display/icons-128.pf -------------------------------------------------------------------------------- /display/icons-32.pf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mark-gladding/packed-font/HEAD/display/icons-32.pf -------------------------------------------------------------------------------- /screenshots/Welcome.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mark-gladding/packed-font/HEAD/screenshots/Welcome.bmp -------------------------------------------------------------------------------- /fonts/icons-128/star.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mark-gladding/packed-font/HEAD/fonts/icons-128/star.bmp -------------------------------------------------------------------------------- /fonts/icons-32/icons-32.pf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mark-gladding/packed-font/HEAD/fonts/icons-32/icons-32.pf -------------------------------------------------------------------------------- /fonts/text-16/text-16.pf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mark-gladding/packed-font/HEAD/fonts/text-16/text-16.pf -------------------------------------------------------------------------------- /fonts/digits-30/digits-30.pf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mark-gladding/packed-font/HEAD/fonts/digits-30/digits-30.pf -------------------------------------------------------------------------------- /fonts/icons-128/icons-128.pf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mark-gladding/packed-font/HEAD/fonts/icons-128/icons-128.pf -------------------------------------------------------------------------------- /fonts/icons-32/humidity.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mark-gladding/packed-font/HEAD/fonts/icons-32/humidity.bmp -------------------------------------------------------------------------------- /fonts/icons-32/pressure.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mark-gladding/packed-font/HEAD/fonts/icons-32/pressure.bmp -------------------------------------------------------------------------------- /fonts/icons-32/temperature.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mark-gladding/packed-font/HEAD/fonts/icons-32/temperature.bmp -------------------------------------------------------------------------------- /screenshots/right-aligned-text.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mark-gladding/packed-font/HEAD/screenshots/right-aligned-text.bmp -------------------------------------------------------------------------------- /screenshots/temperature-screen.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mark-gladding/packed-font/HEAD/screenshots/temperature-screen.bmp -------------------------------------------------------------------------------- /create/create-text-16.ps1: -------------------------------------------------------------------------------- 1 | python .\create-font.py 'C:\Windows\Fonts\Arial.ttf' ..\fonts\text-16\text-16.json --size 16 --xoffsets 106:1,118:1,121:1 --verbose 2 | python .\pack-font.py ..\fonts\text-16\text-16.json -------------------------------------------------------------------------------- /create/create-digits-30.ps1: -------------------------------------------------------------------------------- 1 | python .\create-font.py 'C:\Windows\Fonts\GillSansNova.ttf' ..\fonts\digits-30\digits-30.json --chars 48-57,45-46,176 --size 42 --verbose 2 | python .\pack-font.py ..\fonts\digits-30\digits-30.json -------------------------------------------------------------------------------- /fonts/icons-128/icons-128.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "icons-128.pf", 3 | "Width": 128, 4 | "Height": 64, 5 | "DefaultCharacter": "s", 6 | "Characters": [ 7 | { 8 | "Code": "s", 9 | "Width": 128, 10 | "Height": 64, 11 | "Filename": "star.bmp" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /fonts/icons-32/icons-32.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "icons-32.pf", 3 | "Width": 32, 4 | "Height": 32, 5 | "DefaultCharacter": "h", 6 | "Characters": [ 7 | { 8 | "Code": "h", 9 | "Width": 24, 10 | "Height": 32, 11 | "Filename": "humidity.bmp" 12 | }, 13 | { 14 | "Code": "p", 15 | "Width": 32, 16 | "Height": 32, 17 | "Filename": "pressure.bmp" 18 | }, 19 | { 20 | "Code": "t", 21 | "Width": 16, 22 | "Height": 32, 23 | "Filename": "temperature.bmp" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 mark-gladding 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /display/display-test.py: -------------------------------------------------------------------------------- 1 | # Script to test the 2D drawing functions of the SSD1306 Display and Frame Buffer are still accessible. 2 | # 3 | # Copyright (C) Mark Gladding 2023. 4 | # 5 | # MIT License (see the accompanying license file) 6 | # 7 | # https://github.com/mark-gladding/packed-font 8 | # 9 | 10 | from enhanced_display import Enhanced_Display 11 | import time 12 | 13 | if __name__ == "__main__": 14 | 15 | display = Enhanced_Display() 16 | display.load_fonts(['digits-30', 'text-16', 'icons-32', 'icons-128']) 17 | 18 | display.clear() 19 | 20 | display.select_font('text-16') 21 | display.text('2D Drawing', 0, 0, 1, 1) 22 | 23 | display.show() 24 | time.sleep(1) 25 | 26 | display.fill(0) 27 | display.pixel(64, 10, 1) 28 | display.pixel(65, 10, 1) 29 | display.pixel(66, 10, 1) 30 | 31 | display.line(0, 0, display.width -1, display.height -1, 1) 32 | display.line(display.width -1, 0, 0, display.height -1, 1) 33 | 34 | display.hline(0, 8, display.width, 1) 35 | display.hline(0, 48, display.width, 1) 36 | 37 | display.vline(8, 0, display.height, 1) 38 | display.vline(120, 0, display.height, 1) 39 | 40 | display.rect(0, 0, display.width, display.height, 1) 41 | 42 | display.fill_rect(16, 2, 96, 2, 1) 43 | 44 | display.circ(display.width // 2, display.height // 2, 16, 0) 45 | display.arc(display.width // 2, display.height // 2, 24, 45, 90) 46 | 47 | display.show() 48 | 49 | time.sleep(1) 50 | display.invert(1) 51 | time.sleep(1) 52 | display.setContrast(0) 53 | time.sleep(1) 54 | display.setContrast(128) 55 | time.sleep(1) 56 | display.setContrast(255) 57 | time.sleep(1) 58 | display.invert(0) 59 | time.sleep(1) 60 | display.rotate(False) 61 | time.sleep(1) 62 | display.rotate(True) 63 | time.sleep(1) 64 | 65 | for i in range(16): 66 | display.scroll(8, 4) 67 | display.fill_rect(0, 0, display.width, 4, 0) 68 | display.fill_rect(0, 4, 8, display.height - 4, 0) 69 | display.show() 70 | 71 | 72 | -------------------------------------------------------------------------------- /fonts/digits-30/digits-30.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "digits-30.pf", 3 | "Height": 30, 4 | "Width": 24, 5 | "DefaultCharacter": ".", 6 | "Characters": [ 7 | { 8 | "Code": "-", 9 | "Width": 14, 10 | "Height": 29, 11 | "Filename": "U045.bmp" 12 | }, 13 | { 14 | "Code": ".", 15 | "Width": 9, 16 | "Height": 29, 17 | "Filename": "U046.bmp" 18 | }, 19 | { 20 | "Code": "0", 21 | "Width": 21, 22 | "Height": 29, 23 | "Filename": "U048.bmp" 24 | }, 25 | { 26 | "Code": "1", 27 | "Width": 21, 28 | "Height": 29, 29 | "Filename": "U049.bmp" 30 | }, 31 | { 32 | "Code": "2", 33 | "Width": 21, 34 | "Height": 29, 35 | "Filename": "U050.bmp" 36 | }, 37 | { 38 | "Code": "3", 39 | "Width": 21, 40 | "Height": 29, 41 | "Filename": "U051.bmp" 42 | }, 43 | { 44 | "Code": "4", 45 | "Width": 21, 46 | "Height": 29, 47 | "Filename": "U052.bmp" 48 | }, 49 | { 50 | "Code": "5", 51 | "Width": 21, 52 | "Height": 29, 53 | "Filename": "U053.bmp" 54 | }, 55 | { 56 | "Code": "6", 57 | "Width": 21, 58 | "Height": 29, 59 | "Filename": "U054.bmp" 60 | }, 61 | { 62 | "Code": "7", 63 | "Width": 21, 64 | "Height": 29, 65 | "Filename": "U055.bmp" 66 | }, 67 | { 68 | "Code": "8", 69 | "Width": 21, 70 | "Height": 29, 71 | "Filename": "U056.bmp" 72 | }, 73 | { 74 | "Code": "9", 75 | "Width": 21, 76 | "Height": 29, 77 | "Filename": "U057.bmp" 78 | }, 79 | { 80 | "Code": "\u00b0", 81 | "Width": 17, 82 | "Height": 29, 83 | "Filename": "U176.bmp" 84 | } 85 | ] 86 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /create/pack-font.py: -------------------------------------------------------------------------------- 1 | # Script which takes a series of font bitmaps and an associated definition file and 2 | # creates a packed font file (.pf) for use on a Pico Pi SSD1306 display. 3 | # 4 | # It depends on the Pillow (PIL) library, available here: https://pillow.readthedocs.io/en/stable/index.html# 5 | # 6 | # Copyright (C) Mark Gladding 2023. 7 | # 8 | # MIT License (see the accompanying license file) 9 | # 10 | # https://github.com/mark-gladding/packed-font 11 | # 12 | import argparse 13 | import json 14 | from PIL import Image 15 | import os 16 | import sys 17 | 18 | def len_in_bytes(pixels): 19 | return int((pixels + 7) / 8) 20 | 21 | def create_packed_font(font_info_filename, verbose): 22 | with open(font_info_filename) as f: 23 | font_info = json.load(f) 24 | 25 | name = font_info["Name"] 26 | default_width = font_info["Width"] 27 | default_height = font_info["Height"] 28 | default_character = font_info["DefaultCharacter"] 29 | character_count = len(font_info["Characters"]) 30 | 31 | print(f'Creating font {name}, size {default_width}x{default_height} with {character_count} characters.') 32 | 33 | # Packed font format 34 | # Header - 'PF' (2 bytes) 35 | # - Default Character (1 byte) 36 | # - Number of characters (1 byte) 37 | # - Character 1..n 38 | # Character code (1 byte) 39 | # Width (1 byte) 40 | # Height (1 byte) 41 | # StartIndex of character data (2 bytes) 42 | # Character data[bytes] 43 | 44 | header = [ord('P'), ord('F'), ord(default_character), character_count ] 45 | data = [] 46 | start_index = 0 47 | 48 | for character in font_info["Characters"]: 49 | code = character["Code"] 50 | width = character["Width"] if "Width" in character else default_width 51 | height = character["Height"] if "Height" in character else default_height 52 | filename = character["Filename"] if "Filename" in character else f"{code}.bmp" 53 | header.append(ord(code)) 54 | header.append(width) 55 | header.append(height) 56 | header.append(start_index % 256) 57 | header.append(start_index >> 8) 58 | 59 | if verbose: 60 | print(code) 61 | im = Image.open(filename) 62 | image_width = im.size[0] 63 | image_height = im.size[1] 64 | if image_width < width or image_height < height: 65 | print(f'Image {filename}, size {im.size} less than expected ({width},{height})') 66 | sys.exit(-1) 67 | 68 | 69 | image_data = list(im.getdata()) 70 | for i in range(height): 71 | row = image_data[i*image_width:i*image_width + image_width] 72 | row = [1 if b > 0 else 0 for b in row] 73 | if verbose: 74 | print(row) 75 | for b in range(len_in_bytes(width)): 76 | val = 0 77 | for c in range(8): 78 | val = val + (row[b*8 + c] << (7-c)) 79 | data.append(val) 80 | start_index += len_in_bytes(width) * height 81 | 82 | with open(name, 'wb') as f: 83 | f.write(bytes(header)) 84 | f.write(bytes(data)) 85 | print(f'Packed font {name} successfully.') 86 | 87 | if __name__ == "__main__": 88 | parser = argparse.ArgumentParser(description='Generate a Packed Font file.', formatter_class=argparse.ArgumentDefaultsHelpFormatter) 89 | parser.add_argument('--verbose', help='Output each character as an array of 0s and 1s.', action='store_true') 90 | parser.add_argument('fontPathname', help='The path to the json font definition file.') 91 | args = parser.parse_args() 92 | 93 | currentDir = os.path.curdir 94 | os.chdir(os.path.dirname(args.fontPathname)) 95 | try: 96 | create_packed_font(os.path.basename(args.fontPathname), args.verbose) 97 | finally: 98 | os.chdir(currentDir) # Ensure the current directory is restored, even when an exception is thrown 99 | 100 | 101 | -------------------------------------------------------------------------------- /display/main.py: -------------------------------------------------------------------------------- 1 | # Script to demonstrate the Enhanced_Display class and associated packed fonts. 2 | # 3 | # Copyright (C) Mark Gladding 2023. 4 | # 5 | # MIT License (see the accompanying license file) 6 | # 7 | # https://github.com/mark-gladding/packed-font 8 | # 9 | 10 | from enhanced_display import Enhanced_Display 11 | import time 12 | 13 | if __name__ == "__main__": 14 | 15 | display = Enhanced_Display() 16 | 17 | # Load the list of fonts to use 18 | display.load_fonts(['digits-30', 'text-16', 'icons-32', 'icons-128']) 19 | 20 | # Display the Welcome screen 21 | display.fill(0) # Clear the screen 22 | 23 | display.select_font('icons-128') 24 | display.text('s', 0, 0) # The 's' character is the Star icon 25 | display.select_font('text-16') 26 | display.text('Welcome', 0, 0, 1, 1) # Center the text both horizontally and vertically. 27 | 28 | display.save_screenshot("title.bmp") # Take a screenshot and save to file. 29 | 30 | display.show() 31 | time.sleep(3) 32 | 33 | # Display the Text Alignment intro screen 34 | display.fill(0) 35 | 36 | display.text('Text', 0, 16, 1) 37 | display.text('Alignment', 0, 32, 1) 38 | 39 | display.show() 40 | time.sleep(1) 41 | 42 | # Display the left aligned text screen 43 | display.fill(0) 44 | 45 | display.text('left, top', 0, 0) 46 | display.text('left, center', 0, 0, 0, 1) 47 | display.text('left, bottom', 0, 0, 0, 2) 48 | 49 | display.show() 50 | time.sleep(1) 51 | 52 | # Display the center aligned text screen 53 | display.fill(0) 54 | 55 | display.text('center, top', 0, 0, 1, 0) 56 | display.text('center, center', 0, 0, 1, 1) 57 | display.text('center, bottom', 0, 0, 1, 2) 58 | 59 | display.show() 60 | time.sleep(1) 61 | 62 | # Display the right aligned text screen 63 | display.fill(0) 64 | 65 | display.text('right, top', 0, 0, 2, 0) 66 | display.text('right, center', 0, 0, 2, 1) 67 | display.text('right, bottom', 0, 0, 2, 2) 68 | 69 | display.show() 70 | time.sleep(1) 71 | 72 | # Display the Text & Icons intro screen 73 | display.fill(0) 74 | 75 | display.text('Text', 0, 8, 1) 76 | display.text('&', 0, 24, 1) 77 | display.text('Icons', 0, 40, 1) 78 | 79 | display.show() 80 | time.sleep(1.5) 81 | 82 | # Display the Temperature screen 83 | display.fill(0) 84 | 85 | display.select_font('digits-30') 86 | degrees = '\u00b0' # Character code for the degrees symbol 87 | display.text(f'12.3{degrees}', 0, 0, 1, 1) 88 | display.select_font('icons-32') 89 | display.text('t', 0, 0, 2) # The 't' character contains the temperature icon 90 | display.select_font(None) # Select the built in 8 pixel font 91 | display.text('Temperature', 0, 0, 1, 2) 92 | 93 | display.show() 94 | time.sleep(2) 95 | 96 | # Display the Humidity screen 97 | display.fill(0) 98 | 99 | display.select_font('digits-30') 100 | display.text('76', 0, 0, 1, 1) 101 | display.select_font('icons-32') 102 | display.text('h', 0, 0, 2) # The 'h' character contains the humidity icon 103 | display.select_font(None) 104 | display.text('Humidity', 0, 0, 1, 2) 105 | 106 | display.show() 107 | time.sleep(2) 108 | 109 | # Display the Pressure screen 110 | display.fill(0) 111 | 112 | display.select_font('digits-30') 113 | display.text('985', 0, 0, 1, 1, display.width - 32) 114 | display.select_font('icons-32') 115 | display.text('p', 0, 0, 2) # The 'p' character contains the pressure icon 116 | display.select_font(None) 117 | display.text('Pressure', 0, 0, 1, 2) 118 | 119 | display.show() 120 | time.sleep(2) 121 | 122 | # Display the Thank you screen 123 | display.fill(0) 124 | 125 | display.select_font('icons-128') 126 | display.text('s', 0, 0) 127 | display.select_font('text-16') 128 | display.text('Thank you', 0, 0, 1, 1) 129 | display.show() 130 | -------------------------------------------------------------------------------- /create/create-font.py: -------------------------------------------------------------------------------- 1 | # Script to render a truetype font (or any font format supported by PIL) and 2 | # create a series of font bitmaps and an associated definition file which can then be 3 | # used as inputs to create a packed font for use on a Pico Pi SSD1306 display. 4 | # 5 | # It depends on the Pillow (PIL) library, available here: https://pillow.readthedocs.io/en/stable/index.html# 6 | # 7 | # Copyright (C) Mark Gladding 2023. 8 | # 9 | # MIT License (see the accompanying license file) 10 | # 11 | # https://github.com/mark-gladding/packed-font 12 | # 13 | 14 | import argparse 15 | import json 16 | from PIL import Image, ImageFont, ImageDraw 17 | import os 18 | import sys 19 | 20 | def len_in_bytes(pixels): 21 | return int((pixels + 7) / 8) 22 | 23 | def rounded_width(pixels): 24 | return len_in_bytes(pixels) * 8 25 | 26 | def ensure_folder_exists(folderName): 27 | folderName = os.path.normpath(folderName) 28 | if not folderName or os.path.exists(folderName): 29 | return 30 | head = os.path.dirname(folderName) 31 | ensure_folder_exists(head) 32 | os.mkdir(folderName) 33 | 34 | if __name__ == "__main__": 35 | parser = argparse.ArgumentParser(description='Create a series of font bitmaps and an associated definition file.', formatter_class=argparse.ArgumentDefaultsHelpFormatter) 36 | parser.add_argument('sourceFontPathname', help='The path to the source font file (e.g. TTF).') 37 | parser.add_argument('fontPathname', help='The path to the json font definition file.') 38 | parser.add_argument('--verbose', help='Output each character as an array of 0s and 1s.', action='store_true') 39 | parser.add_argument('--chars', help='Series of comma separated character ranges to include (e.g. 32-57,59,100-120).', default='32-126') 40 | parser.add_argument('--xoffsets', help='Series of comma separated x-offsets for specific characters (e.g. 106:1,113:-2 ).', default=None) 41 | parser.add_argument('--size', help='Font size in pixels.', type=int, default=16) 42 | args = parser.parse_args() 43 | 44 | print(args.sourceFontPathname) 45 | font = ImageFont.truetype(args.sourceFontPathname, args.size) 46 | 47 | char_array = [] 48 | char_ranges = args.chars.split(',') 49 | for char_range in char_ranges: 50 | bounds = char_range.split('-') 51 | min_code = max_code = int(bounds[0]) 52 | if len(bounds) > 1: 53 | max_code = int(bounds[1]) 54 | for code in range(min_code, max_code + 1): 55 | char_array.append(code) 56 | 57 | char_array = list(set(char_array)) 58 | 59 | char_xoffsets = [0] * len(char_array) 60 | if args.xoffsets: 61 | xoffsets = args.xoffsets.split(',') 62 | for xoffset in xoffsets: 63 | char_offset = xoffset.split(':') 64 | if len(char_offset) != 2: 65 | sys.exit(f'Unknown xoffset argument {char_offset}') 66 | code = int(char_offset[0]) 67 | offset = int(char_offset[1]) 68 | char_xoffsets[char_array.index(code)] = offset 69 | 70 | 71 | char_defns = [] 72 | char_tops = [] # Keep track of the top bounding box for each character. This is needed when shifting characters up. 73 | minMinTop = 0 # The minimum top value that characters must be shifted up to avoid clipping the bottom of descending characters (e.g. g, y, j, etc) 74 | minTop = args.size 75 | maxWidth = 0 76 | maxHeight = 0 77 | anchor = 'la' 78 | for index, code in enumerate(char_array): 79 | c = chr(code) 80 | aLeft, aTop, aRight, aBottom = font.getbbox(c, anchor=anchor) 81 | minTop = min(aTop, minTop) 82 | minMinTop = max(aBottom - args.size, minMinTop) 83 | height = aBottom - aTop + 1 84 | width = aRight 85 | maxWidth = max(width, maxWidth) 86 | maxHeight = max(height, maxHeight) 87 | char_tops.append(aTop) 88 | char_defns.append({ "Code" : f"{c}", "Width" : width, "Height" : aBottom, "Filename" : f'U{code:03d}.bmp' }) 89 | if args.verbose: 90 | print(f'U{code} {c}: Left={aLeft}, Right={aRight}, Top={aTop}, Bottom={aBottom}, width={width}, height={height}') 91 | 92 | # Ensure characters are shifted up enough to avoid clipping the bottom of descending characters (e.g. g, y, j, etc) 93 | minTop = max(minMinTop, minTop) 94 | 95 | if args.verbose: 96 | print(f'minTop={minTop}, maxWidth={maxWidth}, maxHeight={maxHeight}') 97 | 98 | 99 | # Adjust the height of each character by subtracking minTop, as this is the amount the entire 100 | # font will be shifted up when being rendered. 101 | for index, _ in enumerate(char_array): 102 | char_defns[index]["Height"] -= minTop 103 | 104 | destFolder = os.path.dirname(args.fontPathname) 105 | ensure_folder_exists(destFolder) 106 | 107 | packed_font_filename = os.path.basename(args.fontPathname) 108 | packed_font_filename, _ = os.path.splitext(packed_font_filename) 109 | packed_font = { 110 | "Name" : f"{packed_font_filename}.pf", 111 | "Height" : maxHeight, 112 | "Width" : rounded_width(maxWidth), 113 | "DefaultCharacter" : ".", 114 | "Characters" : char_defns 115 | } 116 | 117 | for index, code in enumerate(char_array): 118 | c = chr(code) 119 | with Image.new("1", (rounded_width(maxWidth), maxHeight)) as im: 120 | d = ImageDraw.Draw(im) 121 | # Don't shift characters up more than their top value. This avoids clipping the top of superscripts when shifting up 122 | # but will change their y start position in the packed font. 123 | y_shift_up = min(minTop, char_tops[index]) 124 | d.text((char_xoffsets[index], -y_shift_up), f'{c}', fill="white", anchor=anchor, font=font) 125 | im.save(os.path.join(destFolder, f'U{code:03d}.bmp')) 126 | 127 | with open(args.fontPathname, 'w') as f: 128 | json.dump(packed_font, f, indent=4) 129 | 130 | print(f'Font {packed_font_filename} successfully saved to {destFolder}.') -------------------------------------------------------------------------------- /display/packed_font.py: -------------------------------------------------------------------------------- 1 | 2 | # Module for rendering different sized fonts on the SSD1306 Pico Pi Display. 3 | # Fonts are stored in a memory efficient binary format. 4 | # Fonts can be proportional or monospaced. 5 | # A font can contain just the characters needed for a specific application. 6 | # 7 | # Copyright (C) Mark Gladding 2023. 8 | # 9 | # MIT License (see the accompanying license file) 10 | # 11 | 12 | _loaded_fonts = {} 13 | _current_font = None 14 | 15 | def load_font(font_name): 16 | """Load a packed font into memory for use. Once loaded, the font must be selected for use. 17 | 18 | Args: 19 | font_name (string): Name of the font, without the .pf extension. 20 | """ 21 | global _loaded_fonts 22 | 23 | if font_name in _loaded_fonts: 24 | return 25 | _loaded_fonts[font_name] = _load_packed_font(font_name) 26 | 27 | def _load_packed_font(font_name): 28 | font = None 29 | with open(f'{font_name}.pf', 'rb') as f: 30 | header = f.read(4) 31 | if len(header) < 4 or header[0] != ord('P') or header[1] != ord('F'): 32 | print(f'{font_name}.pf has an unknown file format') 33 | return 34 | font = { 'name' : font_name, 35 | 'default_character' : chr(header[2]), 36 | 'character_count': header[3], 37 | 'characters' : {}, 38 | 'data' : None 39 | } 40 | 41 | print(f'Reading font {font_name} with {font["character_count"]} characters.') 42 | 43 | remaining_header_size = font['character_count'] * 5 44 | header = f.read(remaining_header_size) 45 | 46 | index = 0 47 | characters = font['characters'] 48 | for i in range(font["character_count"]): 49 | character = chr(header[index]) 50 | index += 1 51 | char_width = header[index] 52 | index += 1 53 | char_height = header[index] 54 | index += 1 55 | start_index = header[index] + header[index + 1] * 256 56 | index += 2 57 | characters[character] = { 58 | 'char_width' : char_width, 59 | 'char_height' : char_height, 60 | 'start_index' : start_index 61 | } 62 | font['data'] = f.read() 63 | return font 64 | 65 | def unload_all_fonts(): 66 | """ Unload all fonts and select the built in font as the current font.""" 67 | global _loaded_fonts, _current_font 68 | _loaded_fonts = {} 69 | _current_font = None 70 | 71 | def select_font(font_name): 72 | """Select the font to use for subsequent calls to get_text_size() and text() 73 | 74 | Args: 75 | font_name (string): Name of the font to select or None to select the built in font. 76 | """ 77 | global _current_font 78 | 79 | if font_name == None: # Select the built in font 80 | _current_font = None 81 | return 82 | 83 | if _current_font and _current_font['name'] == font_name: 84 | return 85 | 86 | if not font_name in _loaded_fonts: 87 | print(f'Cannot select unknown font {font_name}.') 88 | return 89 | _current_font = _loaded_fonts[font_name] 90 | 91 | 92 | def get_text_size(text): 93 | """Calculate the width and height of the rendered text using the currently selected font. 94 | 95 | Args: 96 | text (string): The text string to measure 97 | 98 | Returns: 99 | (int, int): Tuple containing the width and height of the rendered text. 100 | """ 101 | if not _current_font: 102 | return len(text) * 8, 8 # Built in font 103 | 104 | characters = _current_font['characters'] 105 | default_character = _current_font['default_character'] 106 | width = 0 107 | height = 0 108 | for char in text: 109 | try: 110 | char_definition = characters[char] 111 | except KeyError: 112 | char_definition = characters[default_character] 113 | width += char_definition['char_width'] 114 | height = max(height, char_definition['char_height']) 115 | return width, height 116 | 117 | def text(display, text, x, y, max_width=0, horiz_align=0, max_height=0, vert_align=0, c=1): 118 | """Render a text string to the display in the currently selected font, with optional alignment. 119 | 120 | Args: 121 | display (PiicoDev_SSD): The display to render the text on 122 | text (string): Text to render 123 | x (int): X coordinate to begin text rendering 124 | y (int): Y coordinate to begin text rendering 125 | max_width (int, optional): Width of the box to align text horizontally within. Defaults to 0. 126 | horiz_align (int, optional): 0 = Left, 1 = Center, 2 = Right. Defaults to 0. 127 | max_height (int, optional): Height of the box to align text vertically within. Defaults to 0. 128 | vert_align (int, optional): 0 = Top, 1 = Center, 2 = Bottom. Defaults to 0. 129 | c (int, optional): Color to render text in. Defaults to 1. 130 | """ 131 | 132 | if (max_width > 0 and horiz_align > 0) or (max_height > 0 and vert_align > 0): 133 | total_text_width, text_height = get_text_size(text) 134 | if horiz_align == 1: # Center 135 | x += int((max_width - total_text_width) / 2) 136 | elif horiz_align == 2: # Right 137 | x += max_width - total_text_width 138 | if vert_align == 1: # Center 139 | y += int((max_height - text_height) / 2) 140 | elif vert_align == 2: # Bottom 141 | y += max_height - text_height 142 | 143 | if not _current_font: # Built in font 144 | display.text(text, x, y, c) 145 | return 146 | 147 | characters = _current_font['characters'] 148 | default_character = _current_font['default_character'] 149 | data = _current_font['data'] 150 | for char in text: 151 | try: 152 | char_definition = characters[char] 153 | except KeyError: 154 | char_definition = characters[default_character] 155 | start_index = char_definition['start_index'] 156 | width = char_definition['char_width'] 157 | height = char_definition['char_height'] 158 | width_in_bytes = int((width + 7) / 8) 159 | for i in range(height): 160 | for j in range(width): 161 | byte_index = int(j / 8) 162 | bit_index = j - byte_index * 8 163 | val = data[start_index + i * width_in_bytes + byte_index ] 164 | if (val >> (7-bit_index)) & 1: 165 | display.pixel(x + j, y + i, c) 166 | x += width 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Packed Font 2 | ## Memory efficient MicroPython fonts for the Pico Pi and SSD1306 OLED Display 3 | 4 | MicroPython comes with an 8 x 8 pixel font which allows for a surprising amount of text to be displayed on the tiny OLED display. However the font is very small, especially for those of us getting older. 5 | 6 | A _Packed Font_ has the following features: 7 | 8 | * Uses a binary format to encode font data in a memory efficent manner. 9 | * When creating a _Packed Font_ you can select just the characters you need for your application, further saving on memory. 10 | * Supports proportional width fonts (i.e. each character can have a different width) which is more appropriate for larger fonts. 11 | * Can be used to store icons as well as characters. 12 | * Can be loaded/unloaded at runtime. You don't need to build a new version of MicroPython from source to add a new font. 13 | 14 | ## Quick Start 15 | 16 | There is an example application in the `./display/` folder which you can copy to your Pico Pi and run using Thonny. It runs through a few different examples of rendering text and icons. 17 | 18 | 19 | | ![Welcome Screen](screenshots/Welcome.bmp) | 20 | |:--:| 21 | |*128 Pixel Icon and 16 Pixel Font*| 22 | 23 | The above screen is rendered as follows: 24 | 25 | ```python 26 | from enhanced_display import Enhanced_Display 27 | 28 | if __name__ == "__main__": 29 | 30 | display = Enhanced_Display() 31 | 32 | # Load the list of fonts to use 33 | display.load_fonts(['digits-30', 'text-16', 'icons-32', 'icons-128']) 34 | 35 | # Display the Welcome screen 36 | display.fill(0) # Clear the screen 37 | 38 | display.select_font('icons-128') 39 | display.text('s', 0, 0) # The 's' character is the Star icon 40 | display.select_font('text-16') 41 | display.text('Welcome', 0, 0, 1, 1) # Center the text both horizontally and vertically. 42 | 43 | display.save_screenshot("title.bmp") # Take a screenshot and save to file. 44 | 45 | display.show() 46 | ``` 47 | 48 | 49 | | ![Right-aligned Text](screenshots/right-aligned-text.bmp) | 50 | |:--:| 51 | |*16 Pixel Text Font right-aligned*| 52 | 53 | The above is rendered as follows: 54 | 55 | ```python 56 | # Display the right aligned text screen 57 | display.fill(0) 58 | 59 | display.text('right, top', 0, 0, 2, 0) 60 | display.text('right, center', 0, 0, 2, 1) 61 | display.text('right, bottom', 0, 0, 2, 2) 62 | 63 | display.show() 64 | ``` 65 | 66 | | ![Temperature Screen](screenshots/temperature-screen.bmp) | 67 | |:--:| 68 | |*30 Pixel Digits Font, 8 Pixel Built in Font and 32 Pixel Icon*| 69 | 70 | The above screen is rendered as follows: 71 | 72 | ```python 73 | # Display the Temperature screen 74 | display.fill(0) 75 | 76 | display.select_font('digits-30') 77 | degrees = '\u00b0' # Character code for the degrees symbol 78 | display.text(f'12.3{degrees}', 0, 0, 1, 1) 79 | display.select_font('icons-32') 80 | display.text('t', 0, 0, 2) # The 't' character contains the temperature icon 81 | display.select_font(None) # Select the built in 8 pixel font 82 | display.text('Temperature', 0, 0, 1, 2) 83 | 84 | display.show() 85 | ``` 86 | 87 | If you haven't programmed your Pico Pi using Thonny before, here is a great article to get you started. 88 | 89 | [How to setup a Raspberry Pi Pico and Code with Thonny](https://core-electronics.com.au/guides/how-to-setup-a-raspberry-pi-pico-and-code-with-thonny/) 90 | 91 | 92 | ## Example Fonts 93 | 94 | There are two example fonts available for immediate use: 95 | 96 | 1. A 'text-16.pf' font based on the Arial font with characters in the ASCII range 32-126. 97 | 2. A 'digits-30.pf' font based on the GillSansNova font containing the digits 0-9, the minus symbol and the degrees symbol. 98 | 99 | To use these in your application, you will need the following files from the `./display/` folder: 100 | 101 | * `digits-30.pf` 102 | * `enhanced_display.py` 103 | * `packed_font.py` 104 | * `text-16.pf` 105 | * `PiicoDev_SSD1306.py` 106 | * `PiicoDev_Unified.py` 107 | 108 | Have a look at `main.py` for examples on how to load, select and render packed fonts. 109 | 110 | ## Enhanced Display Class 111 | 112 | The easiest way to use packed fonts on the SSD1306 OLED Display, is to use the `Enhanced Display` class. This class has the same interface as the PiicoDev_SSD1306 class but has the following additional features: 113 | 114 | * Render aligned text using packed fonts and the built in 8 x 8 pixel font. 115 | * Detects if a display is present and performs NOPs if not present (i.e. code will still run without a display connected). 116 | * Take a screenshot of the display and save it to a .bmp file. 117 | 118 | ## Creating your own Fonts 119 | 120 | It's easy to create custom fonts for use in your own applications. The advantage of creating your own fonts is you can choose just the characters you need. There are two Python scripts in the `create/` folder used for this purpose. Note that both require the Python Image Processing Library [Pillow (PIL) library](https://pillow.readthedocs.io/en/stable/index.html) to be installed. 121 | 122 | Creating a font from an existing TrueType font on your PC is a two step process. 123 | 124 | 1. Run the `create-font.py` script to convert a TrueType font into a font definition file (`font_name.json`) and a series of bitmaps (1 per character). 125 | 2. Run the `pack-font.py` script to convert the font definition file and character bitmaps into a single `font_name.pf` packed font file. You can then copy the packed font file onto your Pico Pi for use in your application. 126 | 127 | There are some example PowerShell scripts in the `create/` folder which were used to create the example packed fonts used by the example application. 128 | 129 | #### Notes 130 | 131 | * If you want to create a packed font from a series of icons, you would create the font definition file and icon bitmaps by hand. You can then use the `pack-font.py` command to convert them into a single `font_name.pf` packed font file. 132 | * In the font definition file (`font_name.json`), you must specify a `DefaultCharacter` within your font. This character will be used when rendering any character that's not included in the font. 133 | * When creating a font of a particular size, you normally aim for a font that has a height no larger than a certain target (e.g. 16 pixels high). You can adjust the font size argument you pass to `create-font.py` to maximize the size of the font while still meeting this target. For example if you are only using digits or capital letters, you don't need to allow space for descenders (e.g. y, j, g, etc) and can probably render an 18-20 pixel font within this size. 134 | * If you create a font with both descenders and superscripts, `create-font.py` may shift the superscripts down a few pixels to fit within the target height. 135 | * It's worth reviewing each character bitmap generated by `create-font.py` to make sure each character in the font has not been incorrectly cropped. 136 | * Some characters such as 'y', 'v' and 'j' can have their left side cropped by a pixel or two. If you notice this is occurring, you can use the `--xoffsets` parameter to `create-font.py` to specify an xoffset adjustment for individual characters (see `create-text-16.ps1` for an example). 137 | -------------------------------------------------------------------------------- /display/PiicoDev_Unified.py: -------------------------------------------------------------------------------- 1 | ''' 2 | PiicoDev.py: Unifies I2C drivers for different builds of MicroPython 3 | Changelog: 4 | - 2021 M.Ruppe - Initial Unified Driver 5 | - 2022-10-13 P.Johnston - Add helptext to run i2csetup script on Raspberry Pi 6 | - 2022-10-14 M.Ruppe - Explicitly set default I2C initialisation parameters for machine-class (Raspberry Pi Pico + W) 7 | - 2023-01-31 L.Howell - Add minimal support for ESP32 8 | - 2023-05-17 M.Ruppe - Make I2CUnifiedMachine() more flexible on initialisation. Frequency is optional. 9 | ''' 10 | import os 11 | _SYSNAME = os.uname().sysname 12 | compat_ind = 1 13 | i2c_err_str = 'PiicoDev could not communicate with module at address 0x{:02X}, check wiring' 14 | setupi2c_str = ', run "sudo curl -L https://piico.dev/i2csetup | bash". Suppress this warning by setting suppress_warnings=True' 15 | 16 | if _SYSNAME == 'microbit': 17 | from microbit import i2c 18 | from utime import sleep_ms 19 | 20 | elif _SYSNAME == 'Linux': 21 | from smbus2 import SMBus, i2c_msg 22 | from time import sleep 23 | from math import ceil 24 | 25 | def sleep_ms(t): 26 | sleep(t/1000) 27 | 28 | else: 29 | from machine import I2C, Pin 30 | from utime import sleep_ms 31 | 32 | class I2CBase: 33 | def writeto_mem(self, addr, memaddr, buf, *, addrsize=8): 34 | raise NotImplementedError('writeto_mem') 35 | 36 | def readfrom_mem(self, addr, memaddr, nbytes, *, addrsize=8): 37 | raise NotImplementedError('readfrom_mem') 38 | 39 | def write8(self, addr, buf, stop=True): 40 | raise NotImplementedError('write') 41 | 42 | def read16(self, addr, nbytes, stop=True): 43 | raise NotImplementedError('read') 44 | 45 | def __init__(self, bus=None, freq=None, sda=None, scl=None): 46 | raise NotImplementedError('__init__') 47 | 48 | class I2CUnifiedMachine(I2CBase): 49 | def __init__(self, bus=None, freq=None, sda=None, scl=None): 50 | if _SYSNAME == 'esp32' and (bus is None or sda is None or scl is None): 51 | raise Exception('Please input bus, machine.pin SDA, and SCL objects to use ESP32') 52 | 53 | if freq is None: freq = 400_000 54 | if not isinstance(freq, (int)): 55 | raise ValueError("freq must be an Int") 56 | if freq < 400_000: print("\033[91mWarning: minimum freq 400kHz is recommended if using OLED module.\033[0m") 57 | if bus is not None and sda is not None and scl is not None: 58 | print('Using supplied bus, sda, and scl to create machine.I2C() with freq: {} Hz'.format(freq)) 59 | self.i2c = I2C(bus, freq=freq, sda=sda, scl=scl) 60 | elif bus is None and sda is None and scl is None: 61 | self.i2c = I2C(0, scl=Pin(9), sda=Pin(8), freq=freq) # RPi Pico in Expansion Board 62 | else: 63 | raise Exception("Please provide at least bus, sda, and scl") 64 | 65 | self.writeto_mem = self.i2c.writeto_mem 66 | self.readfrom_mem = self.i2c.readfrom_mem 67 | 68 | def write8(self, addr, reg, data): 69 | if reg is None: 70 | self.i2c.writeto(addr, data) 71 | else: 72 | self.i2c.writeto(addr, reg + data) 73 | 74 | def read16(self, addr, reg): 75 | self.i2c.writeto(addr, reg, False) 76 | return self.i2c.readfrom(addr, 2) 77 | 78 | class I2CUnifiedMicroBit(I2CBase): 79 | def __init__(self, freq=None): 80 | if freq is not None: 81 | print('Initialising I2C freq to {}'.format(freq)) 82 | microbit.i2c.init(freq=freq) 83 | 84 | def writeto_mem(self, addr, memaddr, buf, *, addrsize=8): 85 | ad = memaddr.to_bytes(addrsize // 8, 'big') # pad address for eg. 16 bit 86 | i2c.write(addr, ad + buf) 87 | 88 | def readfrom_mem(self, addr, memaddr, nbytes, *, addrsize=8): 89 | ad = memaddr.to_bytes(addrsize // 8, 'big') # pad address for eg. 16 bit 90 | i2c.write(addr, ad, repeat=True) 91 | return i2c.read(addr, nbytes) 92 | 93 | def write8(self, addr, reg, data): 94 | if reg is None: 95 | i2c.write(addr, data) 96 | else: 97 | i2c.write(addr, reg + data) 98 | 99 | def read16(self, addr, reg): 100 | i2c.write(addr, reg, repeat=True) 101 | return i2c.read(addr, 2) 102 | 103 | class I2CUnifiedLinux(I2CBase): 104 | def __init__(self, bus=None, suppress_warnings=True): 105 | if suppress_warnings == False: 106 | with open('/boot/config.txt') as config_file: 107 | if 'dtparam=i2c_arm=on' in config_file.read(): 108 | pass 109 | else: 110 | print('I2C is not enabled. To enable' + setupi2c_str) 111 | config_file.close() 112 | with open('/boot/config.txt') as config_file: 113 | if 'dtparam=i2c_arm_baudrate=400000' in config_file.read(): 114 | pass 115 | else: 116 | print('Slow baudrate detected. If glitching occurs' + setupi2c_str) 117 | config_file.close() 118 | if bus is None: 119 | bus = 1 120 | self.i2c = SMBus(bus) 121 | 122 | def readfrom_mem(self, addr, memaddr, nbytes, *, addrsize=8): 123 | data = [None] * nbytes # initialise empty list 124 | self.smbus_i2c_read(addr, memaddr, data, nbytes, addrsize=addrsize) 125 | return data 126 | 127 | def writeto_mem(self, addr, memaddr, buf, *, addrsize=8): 128 | self.smbus_i2c_write(addr, memaddr, buf, len(buf), addrsize=addrsize) 129 | 130 | def smbus_i2c_write(self, address, reg, data_p, length, addrsize=8): 131 | ret_val = 0 132 | data = [] 133 | for index in range(length): 134 | data.append(data_p[index]) 135 | if addrsize == 8: 136 | msg_w = i2c_msg.write(address, [reg] + data) 137 | elif addrsize == 16: 138 | msg_w = i2c_msg.write(address, [reg >> 8, reg & 0xff] + data) 139 | else: 140 | raise Exception('address must be 8 or 16 bits long only') 141 | self.i2c.i2c_rdwr(msg_w) 142 | return ret_val 143 | 144 | def smbus_i2c_read(self, address, reg, data_p, length, addrsize=8): 145 | ret_val = 0 146 | if addrsize == 8: 147 | msg_w = i2c_msg.write(address, [reg]) # warning this is set up for 16-bit addresses 148 | elif addrsize == 16: 149 | msg_w = i2c_msg.write(address, [reg >> 8, reg & 0xff]) # warning this is set up for 16-bit addresses 150 | else: 151 | raise Exception('address must be 8 or 16 bits long only') 152 | msg_r = i2c_msg.read(address, length) 153 | self.i2c.i2c_rdwr(msg_w, msg_r) 154 | if ret_val == 0: 155 | for index in range(length): 156 | data_p[index] = ord(msg_r.buf[index]) 157 | return ret_val 158 | 159 | def write8(self, addr, reg, data): 160 | if reg is None: 161 | d = int.from_bytes(data, 'big') 162 | self.i2c.write_byte(addr, d) 163 | else: 164 | r = int.from_bytes(reg, 'big') 165 | d = int.from_bytes(data, 'big') 166 | self.i2c.write_byte_data(addr, r, d) 167 | 168 | def read16(self, addr, reg): 169 | regInt = int.from_bytes(reg, 'big') 170 | return self.i2c.read_word_data(addr, regInt).to_bytes(2, byteorder='little', signed=False) 171 | 172 | def create_unified_i2c(bus=None, freq=None, sda=None, scl=None, suppress_warnings=True): 173 | if _SYSNAME == 'microbit': 174 | i2c = I2CUnifiedMicroBit(freq=freq) 175 | elif _SYSNAME == 'Linux': 176 | i2c = I2CUnifiedLinux(bus=bus, suppress_warnings=suppress_warnings) 177 | else: 178 | i2c = I2CUnifiedMachine(bus=bus, freq=freq, sda=sda, scl=scl) 179 | return i2c 180 | -------------------------------------------------------------------------------- /display/enhanced_display.py: -------------------------------------------------------------------------------- 1 | # Class used to render onto a SSD1306 Pico Pi Display with the following enhancements: 2 | # - Render different sized fonts using the packed_font module. 3 | # - Detects if a display is present and performs NOPs if not present (i.e. code will still run without a display connected) 4 | # - Take a screenshot of the display and save it to a .bmp file. 5 | # 6 | # Copyright (C) Mark Gladding 2023. 7 | # 8 | # MIT License (see the accompanying license file) 9 | # 10 | # https://github.com/mark-gladding/packed-font 11 | # 12 | 13 | from PiicoDev_SSD1306 import * 14 | import math 15 | import packed_font 16 | import struct 17 | 18 | class Enhanced_Display: 19 | def __init__(self, address=0x3C,bus=None, freq=None, sda=None, scl=None, asw=None): 20 | self._display = create_PiicoDev_SSD1306(address, bus, freq, sda, scl, asw) 21 | self.width = WIDTH 22 | self.height = HEIGHT 23 | self.is_present = False 24 | self.selected_font = None 25 | 26 | if self._display.comms_err: 27 | print('Display not detected.') 28 | self._display = None 29 | else: 30 | self.is_present = True 31 | print(f'Detected display of size {self.width} x {self.height} pixels.') 32 | 33 | # --------------- Enhanced functions -------------- 34 | 35 | def load_font(self, font_name): 36 | """Load a packed font into memory for use. Once loaded, the font must be selected for use. 37 | 38 | Args: 39 | font_name (string): Name of the font, without the .pf extension. 40 | """ 41 | if self.is_present: 42 | packed_font.load_font(font_name) 43 | 44 | def load_fonts(self, font_name_list): 45 | """Load a list of packed fonts into memory for use. Once loaded, a font must be selected for use. 46 | 47 | Args: 48 | font_name_list (list[string]): A list of font names (without the .pf extension) to load. 49 | """ 50 | if self.is_present: 51 | for font_name in font_name_list: 52 | packed_font.load_font(font_name) 53 | 54 | def unload_all_fonts(self): 55 | """ Unload all fonts and select the built in font as the current font.""" 56 | if self.is_present: 57 | packed_font.unload_all_fonts() 58 | self.selected_font = None 59 | 60 | def select_font(self, font_name): 61 | """Select the font to use for subsequent calls to get_text_size() and text() 62 | 63 | Args: 64 | font_name (string): Name of the font to select or None to select the built in font. 65 | """ 66 | self.selected_font = font_name 67 | 68 | def get_text_size(self, text): 69 | """Calculate the width and height of the rendered text using the currently selected font. 70 | 71 | Args: 72 | text (string): The text string to measure 73 | 74 | Returns: 75 | (int, int): Tuple containing the width and height of the rendered text. 76 | """ 77 | 78 | if self.is_present: 79 | packed_font.select_font(self.selected_font) 80 | return packed_font.get_text_size(text) 81 | return 0, 0 82 | 83 | def text(self, text, x, y, horiz_align=0, vert_align=0, max_width=WIDTH, max_height=HEIGHT, c=1): 84 | """Render a text string to the display in the currently selected font, with optional alignment. 85 | 86 | Args: 87 | text (string): Text to render. 88 | x (int): X coordinate to begin text rendering. 89 | y (int): Y coordinate to begin text rendering. 90 | horiz_align (int, optional): 0 = Left, 1 = Center, 2 = Right. Defaults to 0. 91 | vert_align (int, optional): 0 = Top, 1 = Center, 2 = Bottom. Defaults to 0. 92 | max_width (int, optional): Width of the box to align text horizontally within. Defaults to display width. 93 | max_height (int, optional): Height of the box to align text vertically within. Defaults to display height. 94 | c (int, optional): Color to render text in. Defaults to 1. 95 | """ 96 | if self.is_present: 97 | packed_font.select_font(self.selected_font) 98 | packed_font.text(self._display, text, x, y, max_width, horiz_align, max_height, vert_align, c) 99 | 100 | def clear(self): 101 | """Clear the display and show the blank screen. 102 | """ 103 | if self.is_present: 104 | self._display.fill(0) 105 | self._display.show() 106 | 107 | def save_screenshot(self, filename): 108 | """Save the current screen contents to file in .bmp format. 109 | 110 | Args: 111 | filename (string): The name of the file to save the screenshot to (e.g. screenshot.bmp) 112 | """ 113 | if self.is_present: 114 | 115 | # Rotate display buffer bytes into rows of bytes 116 | rows = [] 117 | for y in range(self.height): 118 | ybit = y % 8 119 | row = [] 120 | for x in range(self.width // 8): 121 | val = 0 122 | for b in range(8): 123 | yval = self._display.buffer[(y // 8) * self.width + x * 8 + b] 124 | val += (1 << (7-b)) if ((yval >> ybit) & 1) == 1 else 0 125 | 126 | row.append(val) 127 | rows.append(row) 128 | 129 | # Convert the rows to a .bmp blob 130 | data = self._bmp(rows, self.width) 131 | # Write the blob to file. 132 | with open(filename, "wb") as f: 133 | f.write(data) 134 | 135 | def _bmp(self, rows, width): 136 | """ Create a bitmap blob from a list of rows, each row containing a list of bytes. 137 | Code from https://stackoverflow.com/questions/8729459/how-do-i-create-a-bmp-file-with-pure-python 138 | """ 139 | 140 | mult4 = lambda n: int(math.ceil(n/4))*4 141 | mult8 = lambda n: int(math.ceil(n/8))*8 142 | lh = lambda n: struct.pack("> 3 59 | self.write_cmd(0x00 | c1) # lower start column address 60 | self.write_cmd(0x10 | c2) # upper start column address 61 | 62 | def fill(self, c=0): 63 | for i in range(0, 1024): 64 | if c > 0: 65 | self.buffer[i] = 0xFF 66 | else: 67 | self.buffer[i] = 0x00 68 | 69 | def pixel(self, x, y, color): 70 | x = x & (WIDTH - 1) 71 | y = y & (HEIGHT - 1) 72 | page, shift_page = divmod(y, 8) 73 | ind = x + page * 128 74 | b = self.buffer[ind] | (1 << shift_page) if color else self.buffer[ind] & ~ (1 << shift_page) 75 | pack_into(">B", self.buffer, ind, b) 76 | self._set_pos(x, page) 77 | 78 | def line(self, x1, y1, x2, y2, c): 79 | # bresenham 80 | steep = abs(y2-y1) > abs(x2-x1) 81 | 82 | if steep: 83 | # Swap x/y 84 | tmp = x1 85 | x1 = y1 86 | y1 = tmp 87 | 88 | tmp = y2 89 | y2 = x2 90 | x2 = tmp 91 | 92 | if x1 > x2: 93 | # Swap start/end 94 | tmp = x1 95 | x1 = x2 96 | x2 = tmp 97 | tmp = y1 98 | y1 = y2 99 | y2 = tmp 100 | 101 | dx = x2 - x1; 102 | dy = abs(y2-y1) 103 | 104 | err = dx/2 105 | 106 | if(y1 < y2): 107 | ystep = 1 108 | else: 109 | ystep = -1 110 | 111 | while x1 <= x2: 112 | if steep: 113 | self.pixel(y1, x1, c) 114 | else: 115 | self.pixel(x1, y1, c) 116 | err -= dy 117 | if err < 0: 118 | y1 += ystep 119 | err += dx 120 | x1 += 1 121 | 122 | def hline(self, x, y, l, c): 123 | self.line(x, y, x + l, y, c) 124 | 125 | def vline(self, x, y, h, c): 126 | self.line(x, y, x, y + h, c) 127 | 128 | def rect(self, x, y, w, h, c): 129 | self.hline(x, y, w, c) 130 | self.hline(x, y+h, w, c) 131 | self.vline(x, y, h, c) 132 | self.vline(x+w, y, h, c) 133 | 134 | def fill_rect(self, x, y, w, h, c): 135 | for i in range(y, y + h): 136 | self.hline(x, i, w, c) 137 | 138 | def text(self, text, x, y, c=1): 139 | fontFile = open("font-pet-me-128.dat", "rb") 140 | font = bytearray(fontFile.read()) 141 | for text_index in range(0, len(text)): 142 | ind = 0 143 | for col in range(8): 144 | fontDataPixelValues = font[(ord(text[text_index])-32)*8 + col] 145 | #ind = text_index * 8 + x * 8 + y * 128 + col 146 | for i in range(0,7): 147 | if fontDataPixelValues & 1 << i != 0: 148 | x_coordinate = x + col + text_index * 8 149 | y_coordinate = y+i 150 | if x_coordinate < WIDTH and y_coordinate < HEIGHT: 151 | self.pixel(x_coordinate, y_coordinate, c) 152 | 153 | 154 | class PiicoDev_SSD1306(framebuf.FrameBuffer): 155 | def init_display(self): 156 | self.width = WIDTH 157 | self.height = HEIGHT 158 | self.pages = HEIGHT // 8 159 | self.buffer = bytearray(self.pages * WIDTH) 160 | for cmd in ( 161 | _SET_DISP, # display off 162 | # address setting 163 | _SET_MEM_ADDR, 164 | 0x00, # horizontal 165 | # resolution and layout 166 | _SET_DISP_START_LINE, # start at line 0 167 | _SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0 168 | _SET_MUX_RATIO, 169 | HEIGHT - 1, 170 | _SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0 171 | _SET_DISP_OFFSET, 172 | 0x00, 173 | _SET_COM_PIN_CFG, 174 | 0x12, 175 | # timing and driving scheme 176 | _SET_DISP_CLK_DIV, 177 | 0x80, 178 | _SET_PRECHARGE, 179 | 0xF1, 180 | _SET_VCOM_DESEL, 181 | 0x30, # 0.83*Vcc 182 | # display 183 | _SET_CONTRAST, 184 | 0xFF,# maximum 185 | _SET_ENTIRE_ON, # output follows RAM contents 186 | _SET_NORM_INV, # not inverted 187 | _SET_IREF_SELECT, 188 | 0x30, # enable internal IREF during display on 189 | # charge pump 190 | _SET_CHARGE_PUMP, 191 | 0x14, 192 | _SET_DISP | 0x01, # display on 193 | ): # on 194 | self.write_cmd(cmd) 195 | 196 | def poweroff(self): 197 | self.write_cmd(_SET_DISP) 198 | 199 | def poweron(self): 200 | self.write_cmd(_SET_DISP | 0x01) 201 | 202 | def setContrast(self, contrast): 203 | self.write_cmd(_SET_CONTRAST) 204 | self.write_cmd(contrast) 205 | 206 | def invert(self, invert): 207 | self.write_cmd(_SET_NORM_INV | (invert & 1)) 208 | 209 | def rotate(self, rotate): 210 | self.write_cmd(_SET_COM_OUT_DIR | ((rotate & 1) << 3)) 211 | self.write_cmd(_SET_SEG_REMAP | (rotate & 1)) 212 | 213 | def show(self): 214 | x0 = 0 215 | x1 = WIDTH - 1 216 | self.write_cmd(_SET_COL_ADDR) 217 | self.write_cmd(x0) 218 | self.write_cmd(x1) 219 | self.write_cmd(_SET_PAGE_ADDR) 220 | self.write_cmd(0) 221 | self.write_cmd(self.pages - 1) 222 | self.write_data(self.buffer) 223 | 224 | def write_cmd(self, cmd): 225 | try: 226 | self.i2c.writeto_mem(self.addr, int.from_bytes(b'\x80','big'), bytes([cmd])) 227 | self.comms_err = False 228 | except: 229 | print(i2c_err_str.format(self.addr)) 230 | self.comms_err = True 231 | 232 | def write_data(self, buf): 233 | try: 234 | self.write_list[1] = buf 235 | self.i2c.writeto_mem(self.addr, int.from_bytes(self.write_list[0],'big'), self.write_list[1]) 236 | self.comms_err = False 237 | except: 238 | print(i2c_err_str.format(self.addr)) 239 | self.comms_err = True 240 | 241 | def circ(self,x,y,r,t=1,c=1): 242 | for i in range(x-r,x+r+1): 243 | for j in range(y-r,y+r+1): 244 | if t==1: 245 | if((i-x)**2 + (j-y)**2 < r**2): 246 | self.pixel(i,j,1) 247 | else: 248 | if((i-x)**2 + (j-y)**2 < r**2) and ((i-x)**2 + (j-y)**2 >= (r-r*t-1)**2): 249 | self.pixel(i,j,c) 250 | 251 | def arc(self,x,y,r,stAng,enAng,t=0,c=1): 252 | for i in range(r*(1-t)-1,r): 253 | for ta in range(stAng,enAng,1): 254 | X = int(i*cos(radians(ta))+ x) 255 | Y = int(i*sin(radians(ta))+ y) 256 | self.pixel(X,Y,c) 257 | 258 | def load_pbm(self, filename, c): 259 | with open(filename, 'rb') as f: 260 | line = f.readline() 261 | if line.startswith(b'P4') is False: 262 | print('Not a valid pbm P4 file') 263 | return 264 | line = f.readline() 265 | while line.startswith(b'#') is True: 266 | line = f.readline() 267 | data_piicodev = bytearray(f.read()) 268 | for byte in range(WIDTH // 8 * HEIGHT): 269 | for bit in range(8): 270 | if data_piicodev[byte] & 1 << bit != 0: 271 | x_coordinate = ((8-bit) + (byte * 8)) % WIDTH 272 | y_coordinate = byte * 8 // WIDTH 273 | if x_coordinate < WIDTH and y_coordinate < HEIGHT: 274 | self.pixel(x_coordinate, y_coordinate, c) 275 | 276 | class graph2D: 277 | def __init__(self, originX = 0, originY = HEIGHT-1, width = WIDTH, height = HEIGHT, minValue=0, maxValue=255, c = 1, bars = False): 278 | self.minValue = minValue 279 | self.maxValue = maxValue 280 | self.originX = originX 281 | self.originY = originY 282 | self.width = width 283 | self.height = height 284 | self.c = c 285 | self.m = (1-height)/(maxValue-minValue) 286 | self.offset = originY-self.m*minValue 287 | self.bars = bars 288 | self.data = [] 289 | 290 | def updateGraph2D(self, graph, value): 291 | graph.data.insert(0,value) 292 | if len(graph.data) > graph.width: 293 | graph.data.pop() 294 | x = graph.originX+graph.width-1 295 | m = graph.c 296 | for value in graph.data: 297 | y = round(graph.m*value + graph.offset) 298 | if graph.bars == True: 299 | for idx in range(y, graph.originY+1): 300 | if x >= graph.originX and x < graph.originX+graph.width and idx <= graph.originY and idx > graph.originY-graph.height: 301 | self.pixel(x,idx, m) 302 | else: 303 | if x >= graph.originX and x < graph.originX+graph.width and y <= graph.originY and y > graph.originY-graph.height: 304 | self.pixel(x,y, m) 305 | x -= 1 306 | 307 | class PiicoDev_SSD1306_MicroPython(PiicoDev_SSD1306): 308 | def __init__(self, bus=None, freq=None, sda=None, scl=None, addr=0x3C): 309 | self.i2c = create_unified_i2c(bus=bus, freq=freq, sda=sda, scl=scl) 310 | self.addr = addr 311 | self.temp = bytearray(2) 312 | self.write_list = [b'\x40', None] # Co=0, D/C#=1 313 | self.init_display() 314 | super().__init__(self.buffer, WIDTH, HEIGHT, framebuf.MONO_VLSB) 315 | self.fill(0) 316 | self.show() 317 | 318 | class PiicoDev_SSD1306_MicroBit(PiicoDev_SSD1306): 319 | def __init__(self, bus=None, freq=None, sda=None, scl=None, addr=0x3C): 320 | self.i2c = create_unified_i2c(bus=bus, freq=freq, sda=sda, scl=scl) 321 | self.addr = addr 322 | self.temp = bytearray(2) 323 | self.write_list = [b'\x40', None] # Co=0, D/C#=1 324 | self.init_display() 325 | self.fill(0) 326 | self.show() 327 | 328 | class PiicoDev_SSD1306_Linux(PiicoDev_SSD1306): 329 | def __init__(self, bus=None, freq=None, sda=None, scl=None, addr=0x3C): 330 | self.i2c = create_unified_i2c(bus=bus, freq=freq, sda=sda, scl=scl) 331 | self.addr = addr 332 | self.temp = bytearray(2) 333 | self.write_list = [b'\x40', None] # Co=0, D/C#=1 334 | self.init_display() 335 | self.fill(0) 336 | self.show() 337 | 338 | def create_PiicoDev_SSD1306(address=0x3C,bus=None, freq=None, sda=None, scl=None, asw=None): 339 | if asw == 0: _a = 0x3C 340 | elif asw == 1: _a = 0x3D 341 | else: _a = address # parse desired address from direct address input or asw switch position (0 or 1) 342 | try: 343 | if compat_ind >= 1: 344 | pass 345 | else: 346 | print(compat_str) 347 | except: 348 | print(compat_str) 349 | if _SYSNAME == 'microbit': 350 | display = PiicoDev_SSD1306_MicroBit(addr=_a, freq=freq) 351 | elif _SYSNAME == 'Linux': 352 | display = PiicoDev_SSD1306_Linux(addr=_a, freq=freq) 353 | else: 354 | display = PiicoDev_SSD1306_MicroPython(addr=_a, bus=bus, freq=freq, sda=sda, scl=scl) 355 | return display -------------------------------------------------------------------------------- /fonts/text-16/text-16.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "text-16.pf", 3 | "Height": 16, 4 | "Width": 16, 5 | "DefaultCharacter": ".", 6 | "Characters": [ 7 | { 8 | "Code": " ", 9 | "Width": 4, 10 | "Height": 13, 11 | "Filename": "U032.bmp" 12 | }, 13 | { 14 | "Code": "!", 15 | "Width": 4, 16 | "Height": 13, 17 | "Filename": "U033.bmp" 18 | }, 19 | { 20 | "Code": "\"", 21 | "Width": 6, 22 | "Height": 13, 23 | "Filename": "U034.bmp" 24 | }, 25 | { 26 | "Code": "#", 27 | "Width": 9, 28 | "Height": 13, 29 | "Filename": "U035.bmp" 30 | }, 31 | { 32 | "Code": "$", 33 | "Width": 9, 34 | "Height": 14, 35 | "Filename": "U036.bmp" 36 | }, 37 | { 38 | "Code": "%", 39 | "Width": 14, 40 | "Height": 13, 41 | "Filename": "U037.bmp" 42 | }, 43 | { 44 | "Code": "&", 45 | "Width": 11, 46 | "Height": 13, 47 | "Filename": "U038.bmp" 48 | }, 49 | { 50 | "Code": "'", 51 | "Width": 3, 52 | "Height": 13, 53 | "Filename": "U039.bmp" 54 | }, 55 | { 56 | "Code": "(", 57 | "Width": 5, 58 | "Height": 16, 59 | "Filename": "U040.bmp" 60 | }, 61 | { 62 | "Code": ")", 63 | "Width": 5, 64 | "Height": 16, 65 | "Filename": "U041.bmp" 66 | }, 67 | { 68 | "Code": "*", 69 | "Width": 6, 70 | "Height": 13, 71 | "Filename": "U042.bmp" 72 | }, 73 | { 74 | "Code": "+", 75 | "Width": 9, 76 | "Height": 13, 77 | "Filename": "U043.bmp" 78 | }, 79 | { 80 | "Code": ",", 81 | "Width": 4, 82 | "Height": 16, 83 | "Filename": "U044.bmp" 84 | }, 85 | { 86 | "Code": "-", 87 | "Width": 5, 88 | "Height": 13, 89 | "Filename": "U045.bmp" 90 | }, 91 | { 92 | "Code": ".", 93 | "Width": 4, 94 | "Height": 13, 95 | "Filename": "U046.bmp" 96 | }, 97 | { 98 | "Code": "/", 99 | "Width": 5, 100 | "Height": 13, 101 | "Filename": "U047.bmp" 102 | }, 103 | { 104 | "Code": "0", 105 | "Width": 9, 106 | "Height": 13, 107 | "Filename": "U048.bmp" 108 | }, 109 | { 110 | "Code": "1", 111 | "Width": 9, 112 | "Height": 13, 113 | "Filename": "U049.bmp" 114 | }, 115 | { 116 | "Code": "2", 117 | "Width": 9, 118 | "Height": 13, 119 | "Filename": "U050.bmp" 120 | }, 121 | { 122 | "Code": "3", 123 | "Width": 9, 124 | "Height": 13, 125 | "Filename": "U051.bmp" 126 | }, 127 | { 128 | "Code": "4", 129 | "Width": 9, 130 | "Height": 13, 131 | "Filename": "U052.bmp" 132 | }, 133 | { 134 | "Code": "5", 135 | "Width": 9, 136 | "Height": 13, 137 | "Filename": "U053.bmp" 138 | }, 139 | { 140 | "Code": "6", 141 | "Width": 9, 142 | "Height": 13, 143 | "Filename": "U054.bmp" 144 | }, 145 | { 146 | "Code": "7", 147 | "Width": 9, 148 | "Height": 13, 149 | "Filename": "U055.bmp" 150 | }, 151 | { 152 | "Code": "8", 153 | "Width": 9, 154 | "Height": 13, 155 | "Filename": "U056.bmp" 156 | }, 157 | { 158 | "Code": "9", 159 | "Width": 9, 160 | "Height": 13, 161 | "Filename": "U057.bmp" 162 | }, 163 | { 164 | "Code": ":", 165 | "Width": 4, 166 | "Height": 13, 167 | "Filename": "U058.bmp" 168 | }, 169 | { 170 | "Code": ";", 171 | "Width": 4, 172 | "Height": 16, 173 | "Filename": "U059.bmp" 174 | }, 175 | { 176 | "Code": "<", 177 | "Width": 9, 178 | "Height": 13, 179 | "Filename": "U060.bmp" 180 | }, 181 | { 182 | "Code": "=", 183 | "Width": 9, 184 | "Height": 13, 185 | "Filename": "U061.bmp" 186 | }, 187 | { 188 | "Code": ">", 189 | "Width": 9, 190 | "Height": 13, 191 | "Filename": "U062.bmp" 192 | }, 193 | { 194 | "Code": "?", 195 | "Width": 9, 196 | "Height": 13, 197 | "Filename": "U063.bmp" 198 | }, 199 | { 200 | "Code": "@", 201 | "Width": 16, 202 | "Height": 16, 203 | "Filename": "U064.bmp" 204 | }, 205 | { 206 | "Code": "A", 207 | "Width": 11, 208 | "Height": 13, 209 | "Filename": "U065.bmp" 210 | }, 211 | { 212 | "Code": "B", 213 | "Width": 11, 214 | "Height": 13, 215 | "Filename": "U066.bmp" 216 | }, 217 | { 218 | "Code": "C", 219 | "Width": 12, 220 | "Height": 13, 221 | "Filename": "U067.bmp" 222 | }, 223 | { 224 | "Code": "D", 225 | "Width": 12, 226 | "Height": 13, 227 | "Filename": "U068.bmp" 228 | }, 229 | { 230 | "Code": "E", 231 | "Width": 11, 232 | "Height": 13, 233 | "Filename": "U069.bmp" 234 | }, 235 | { 236 | "Code": "F", 237 | "Width": 10, 238 | "Height": 13, 239 | "Filename": "U070.bmp" 240 | }, 241 | { 242 | "Code": "G", 243 | "Width": 12, 244 | "Height": 13, 245 | "Filename": "U071.bmp" 246 | }, 247 | { 248 | "Code": "H", 249 | "Width": 12, 250 | "Height": 13, 251 | "Filename": "U072.bmp" 252 | }, 253 | { 254 | "Code": "I", 255 | "Width": 4, 256 | "Height": 13, 257 | "Filename": "U073.bmp" 258 | }, 259 | { 260 | "Code": "J", 261 | "Width": 8, 262 | "Height": 13, 263 | "Filename": "U074.bmp" 264 | }, 265 | { 266 | "Code": "K", 267 | "Width": 11, 268 | "Height": 13, 269 | "Filename": "U075.bmp" 270 | }, 271 | { 272 | "Code": "L", 273 | "Width": 9, 274 | "Height": 13, 275 | "Filename": "U076.bmp" 276 | }, 277 | { 278 | "Code": "M", 279 | "Width": 13, 280 | "Height": 13, 281 | "Filename": "U077.bmp" 282 | }, 283 | { 284 | "Code": "N", 285 | "Width": 12, 286 | "Height": 13, 287 | "Filename": "U078.bmp" 288 | }, 289 | { 290 | "Code": "O", 291 | "Width": 12, 292 | "Height": 13, 293 | "Filename": "U079.bmp" 294 | }, 295 | { 296 | "Code": "P", 297 | "Width": 11, 298 | "Height": 13, 299 | "Filename": "U080.bmp" 300 | }, 301 | { 302 | "Code": "Q", 303 | "Width": 12, 304 | "Height": 14, 305 | "Filename": "U081.bmp" 306 | }, 307 | { 308 | "Code": "R", 309 | "Width": 12, 310 | "Height": 13, 311 | "Filename": "U082.bmp" 312 | }, 313 | { 314 | "Code": "S", 315 | "Width": 11, 316 | "Height": 13, 317 | "Filename": "U083.bmp" 318 | }, 319 | { 320 | "Code": "T", 321 | "Width": 10, 322 | "Height": 13, 323 | "Filename": "U084.bmp" 324 | }, 325 | { 326 | "Code": "U", 327 | "Width": 12, 328 | "Height": 13, 329 | "Filename": "U085.bmp" 330 | }, 331 | { 332 | "Code": "V", 333 | "Width": 11, 334 | "Height": 13, 335 | "Filename": "U086.bmp" 336 | }, 337 | { 338 | "Code": "W", 339 | "Width": 15, 340 | "Height": 13, 341 | "Filename": "U087.bmp" 342 | }, 343 | { 344 | "Code": "X", 345 | "Width": 11, 346 | "Height": 13, 347 | "Filename": "U088.bmp" 348 | }, 349 | { 350 | "Code": "Y", 351 | "Width": 11, 352 | "Height": 13, 353 | "Filename": "U089.bmp" 354 | }, 355 | { 356 | "Code": "Z", 357 | "Width": 10, 358 | "Height": 13, 359 | "Filename": "U090.bmp" 360 | }, 361 | { 362 | "Code": "[", 363 | "Width": 5, 364 | "Height": 16, 365 | "Filename": "U091.bmp" 366 | }, 367 | { 368 | "Code": "\\", 369 | "Width": 5, 370 | "Height": 13, 371 | "Filename": "U092.bmp" 372 | }, 373 | { 374 | "Code": "]", 375 | "Width": 4, 376 | "Height": 16, 377 | "Filename": "U093.bmp" 378 | }, 379 | { 380 | "Code": "^", 381 | "Width": 8, 382 | "Height": 13, 383 | "Filename": "U094.bmp" 384 | }, 385 | { 386 | "Code": "_", 387 | "Width": 10, 388 | "Height": 16, 389 | "Filename": "U095.bmp" 390 | }, 391 | { 392 | "Code": "`", 393 | "Width": 5, 394 | "Height": 13, 395 | "Filename": "U096.bmp" 396 | }, 397 | { 398 | "Code": "a", 399 | "Width": 9, 400 | "Height": 13, 401 | "Filename": "U097.bmp" 402 | }, 403 | { 404 | "Code": "b", 405 | "Width": 9, 406 | "Height": 13, 407 | "Filename": "U098.bmp" 408 | }, 409 | { 410 | "Code": "c", 411 | "Width": 8, 412 | "Height": 13, 413 | "Filename": "U099.bmp" 414 | }, 415 | { 416 | "Code": "d", 417 | "Width": 9, 418 | "Height": 13, 419 | "Filename": "U100.bmp" 420 | }, 421 | { 422 | "Code": "e", 423 | "Width": 9, 424 | "Height": 13, 425 | "Filename": "U101.bmp" 426 | }, 427 | { 428 | "Code": "f", 429 | "Width": 5, 430 | "Height": 13, 431 | "Filename": "U102.bmp" 432 | }, 433 | { 434 | "Code": "g", 435 | "Width": 9, 436 | "Height": 16, 437 | "Filename": "U103.bmp" 438 | }, 439 | { 440 | "Code": "h", 441 | "Width": 9, 442 | "Height": 13, 443 | "Filename": "U104.bmp" 444 | }, 445 | { 446 | "Code": "i", 447 | "Width": 4, 448 | "Height": 13, 449 | "Filename": "U105.bmp" 450 | }, 451 | { 452 | "Code": "j", 453 | "Width": 4, 454 | "Height": 16, 455 | "Filename": "U106.bmp" 456 | }, 457 | { 458 | "Code": "k", 459 | "Width": 8, 460 | "Height": 13, 461 | "Filename": "U107.bmp" 462 | }, 463 | { 464 | "Code": "l", 465 | "Width": 4, 466 | "Height": 13, 467 | "Filename": "U108.bmp" 468 | }, 469 | { 470 | "Code": "m", 471 | "Width": 13, 472 | "Height": 13, 473 | "Filename": "U109.bmp" 474 | }, 475 | { 476 | "Code": "n", 477 | "Width": 9, 478 | "Height": 13, 479 | "Filename": "U110.bmp" 480 | }, 481 | { 482 | "Code": "o", 483 | "Width": 9, 484 | "Height": 13, 485 | "Filename": "U111.bmp" 486 | }, 487 | { 488 | "Code": "p", 489 | "Width": 9, 490 | "Height": 16, 491 | "Filename": "U112.bmp" 492 | }, 493 | { 494 | "Code": "q", 495 | "Width": 9, 496 | "Height": 16, 497 | "Filename": "U113.bmp" 498 | }, 499 | { 500 | "Code": "r", 501 | "Width": 6, 502 | "Height": 13, 503 | "Filename": "U114.bmp" 504 | }, 505 | { 506 | "Code": "s", 507 | "Width": 8, 508 | "Height": 13, 509 | "Filename": "U115.bmp" 510 | }, 511 | { 512 | "Code": "t", 513 | "Width": 5, 514 | "Height": 13, 515 | "Filename": "U116.bmp" 516 | }, 517 | { 518 | "Code": "u", 519 | "Width": 9, 520 | "Height": 13, 521 | "Filename": "U117.bmp" 522 | }, 523 | { 524 | "Code": "v", 525 | "Width": 8, 526 | "Height": 13, 527 | "Filename": "U118.bmp" 528 | }, 529 | { 530 | "Code": "w", 531 | "Width": 12, 532 | "Height": 13, 533 | "Filename": "U119.bmp" 534 | }, 535 | { 536 | "Code": "x", 537 | "Width": 8, 538 | "Height": 13, 539 | "Filename": "U120.bmp" 540 | }, 541 | { 542 | "Code": "y", 543 | "Width": 8, 544 | "Height": 16, 545 | "Filename": "U121.bmp" 546 | }, 547 | { 548 | "Code": "z", 549 | "Width": 8, 550 | "Height": 13, 551 | "Filename": "U122.bmp" 552 | }, 553 | { 554 | "Code": "{", 555 | "Width": 5, 556 | "Height": 16, 557 | "Filename": "U123.bmp" 558 | }, 559 | { 560 | "Code": "|", 561 | "Width": 4, 562 | "Height": 16, 563 | "Filename": "U124.bmp" 564 | }, 565 | { 566 | "Code": "}", 567 | "Width": 5, 568 | "Height": 16, 569 | "Filename": "U125.bmp" 570 | }, 571 | { 572 | "Code": "~", 573 | "Width": 9, 574 | "Height": 13, 575 | "Filename": "U126.bmp" 576 | } 577 | ] 578 | } --------------------------------------------------------------------------------