diff --git a/.vscode/settings.json b/.vscode/settings.json index 81002b9..df6c7c1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "python.testing.unittestEnabled": false, "python.testing.nosetestsEnabled": false, "python.testing.pytestEnabled": true, - "python.envFile": "${workspaceRoot}/vscode.env" + "python.envFile": "${workspaceRoot}/vscode.env", + "python.pythonPath": "C:\\Users\\Chen\\AppData\\Local\\Programs\\Python\\Python39\\python.exe" } diff --git a/README.md b/README.md index 60c7bfa..5b7e34a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # BDF Parser -[![PyPI package](https://img.shields.io/badge/pip%20install-bdfparser-brightgreen)](https://pypi.org/project/example-pypi-package/) [![Actions Status](https://github.com/tomchen/bdfparser/workflows/Test/badge.svg)](https://github.com/tomchen/bdfparser/actions) [![License](https://img.shields.io/github/license/tomchen/bdfparser)](https://github.com/tomchen/bdfparser/blob/master/LICENSE) +[![PyPI package](https://img.shields.io/badge/pip%20install-bdfparser-brightgreen)](https://pypi.org/project/bdfparser/) [![Actions Status](https://github.com/tomchen/bdfparser/workflows/Test/badge.svg)](https://github.com/tomchen/bdfparser/actions) [![License](https://img.shields.io/github/license/tomchen/bdfparser)](https://github.com/tomchen/bdfparser/blob/master/LICENSE) BDF (Glyph Bitmap Distribution; [Wikipedia](https://en.wikipedia.org/wiki/Glyph_Bitmap_Distribution_Format); [Spec](https://font.tomchen.org/bdf_spec/)) format bitmap font file parser library in Python. It has [`Font`](https://font.tomchen.org/bdfparser_py/font), [`Glyph`](https://font.tomchen.org/bdfparser_py/glyph) and [`Bitmap`](https://font.tomchen.org/bdfparser_py/bitmap) classes providing more than 30 enriched API methods of parsing BDF fonts, getting their meta information, rendering text in any writing direction, adding special effects and manipulating bitmap images. It works seamlessly with [PIL / Pillow](https://pillow.readthedocs.io/en/stable/) and [NumPy](https://numpy.org/). diff --git a/src/bdfparser/bdfparser.py b/src/bdfparser/bdfparser.py index b5008a8..4989cc7 100644 --- a/src/bdfparser/bdfparser.py +++ b/src/bdfparser/bdfparser.py @@ -1,10 +1,12 @@ # BDF (Glyph Bitmap Distribution Format) Bitmap Font File Parser in Python # Copyright (c) 2017-2021 Tom CHEN (tomchen.org), MIT License +# https://font.tomchen.org/bdfparser_py/ import re import io import warnings +from sys import version_info as python_version def format_warning(message, category, filename, lineno, file=None, line=None): @@ -39,9 +41,16 @@ class Font(object): ] def __init__(self, *argv): + + if python_version < (3, 7, 0): + from collections import OrderedDict as ordered_dict + else: + ordered_dict = dict + self.glyphs = ordered_dict() + self.headers = {} self.props = {} - self.glyphs = {} + self.__glyph_count_to_check = None self.__curline_startchar = None self.__curline_chars = None @@ -291,7 +300,12 @@ def __prepare_glyphs_after(self): "The glyph count next to 'CHARS' keyword does not exist") else: warnings.warn( - f"The glyph count next to 'CHARS' keyword is {str(self.__glyph_count_to_check)}, which does not match the actual glyph count {str(l)}") + "The glyph count next to 'CHARS' keyword is " + + str(self.__glyph_count_to_check) + + ", which does not match the actual glyph count " + str(l) + ) + # Use old style for Python 3.5 support. For 3.6+: + # f"The glyph count next to 'CHARS' keyword is {str(self.__glyph_count_to_check)}, which does not match the actual glyph count {str(l)}" def length(self): return len(self.glyphs) @@ -300,13 +314,7 @@ def __len__(self): return self.length() def itercps(self, order=1, r=None): - # order: -1: reverse order in the BDF font file; 0: order in the BDF font file; - # 1: ascending codepoint order; 2: descending codepoint order - # r: codepoint range, accepts: - # * integer (examples: `128` (Basic Latin / ASCII), `0x100` (Basic Latin and Latin-1 Supplement / cp1250 / cp1251 / cp1252)) - # * tuple of integers (examples: `(0, 127)` (same as `128`), `(0, 0xff)` (same as `0x100`), `(48, 57)` (all numbers 0-9), `(65, 90)` (all uppercase basic latin letters A-Z), `(97, 122)` (all lowercase basic latin letters a-z), `(1328, 0x1032F)`) - # * list of tuples of integers (example: `[(65, 90), (97, 122)]` (all basic latin letters A-Za-z), `[(0x2E80, 0x9FFF), (0xA960, 0xA97F), (0xAC00, 0xD7FF), (0xF900, 0xFAFF), (0xFE30, 0xFE4F), (0xFF00, 0xFFEF), (0x20000, 0x3134F)]` (this is roughly all CJK characters in the Unicode)) - # see also https://en.wikipedia.org/wiki/Unicode_block + # https://font.tomchen.org/bdfparser_py/font/#iterglyphs ks = self.glyphs.keys() if order == 1: @@ -316,7 +324,10 @@ def itercps(self, order=1, r=None): elif order == 2: retiterator = iter(sorted(ks, reverse=True)) elif order == -1: - retiterator = reversed(ks) + try: + retiterator = reversed(ks) + except TypeError: + retiterator = reversed(list(ks)) # Python <=3.7 if r is not None: def f(cp): if isinstance(r, int): @@ -338,25 +349,18 @@ def iterglyphs(self, order=1, r=None): def glyphbycp(self, codepoint): if codepoint not in self.glyphs: raise Exception( - f"Glyph \"{chr(codepoint)}\" (codepoint {str(codepoint)}) does not exist in the font") + "Glyph \"" + chr(codepoint) + "\" (codepoint " + + str(codepoint) + ") does not exist in the font" + ) + # Use old style for Python 3.5 support. For 3.6+: + # f"Glyph \"{chr(codepoint)}\" (codepoint {str(codepoint)}) does not exist in the font" return Glyph(dict(zip(self.__META_TITLES, self.glyphs[codepoint])), self) def glyph(self, character): return self.glyphbycp(ord(character)) def drawcps(self, cps, linelimit=512, mode=1, direction='lrtb', usecurrentglyphspacing=False): - # `mode`: 0: ffb; 1: dwidth horizontally, dwidth1 vertically - # `direction`: - # * 'lrtb' or 'lr': left to right, lines from top to bottom (most common direction) - # * 'lrbt' : left to right, lines from bottom to top - # * 'rltb' or 'rl': right to left, lines from top to bottom (Arabic, Hebrew, Persian, Urdu) - # * 'rlbt' : right to left, lines from bottom to top - # * 'tbrl' or 'tb': top to bottom, lines from right to left (Chinese traditionally) - # * 'tblr' : top to bottom, lines from left to right - # * 'btrl' or 'bt': bottom to top, lines from right to left - # * 'btlr' : bottom to top, lines from left to right - # `usecurrentglyphspacing` is useful when direction='rl', example: see the difference between - # `font.draw('U的', direction='rl')` and `font.draw('U的', direction='rl', usecurrentglyphspacing=True)` + # https://font.tomchen.org/bdfparser_py/font#draw dire_shortcut_dict = { 'lr': 'lrtb', @@ -409,7 +413,9 @@ def drawcps(self, cps, linelimit=512, mode=1, direction='lrtb', usecurrentglyphs interglyph_global = self.headers[interglyph_str2] else: interglyph_global = None - # warnings.warn(f"The font do not have `{interglyph_keyword}`, glyph spacing adjustment could be skipped unless present in individual glyphs") + # warnings.warn("The font do not have `" + interglyph_keyword + "`, glyph spacing adjustment could be skipped unless present in individual glyphs") + # # Use old style for Python 3.5 support. For 3.6+: + # # warnings.warn(f"The font do not have `{interglyph_keyword}`, glyph spacing adjustment could be skipped unless present in individual glyphs") list_of_bitmaplist = [] bitmaplist = [] @@ -454,7 +460,11 @@ def append_bitmaplist_and_offsetlist(): else: if len(bitmaplist) == 0: raise Exception( - f"`linelimit` ({linelimit}) is too small the line can't even contain one glyph: \"{glyph.chr()}\" (codepoint {cp}, width: {w})") + "`linelimit` (" + linelimit + ") is too small the line can't even contain one glyph: \"" + + glyph.chr() + "\" (codepoint " + cp + ", width: " + w + ")" + ) + # Use old style for Python 3.5 support. For 3.6+: + # f"`linelimit` ({linelimit}) is too small the line can't even contain one glyph: \"{glyph.chr()}\" (codepoint {cp}, width: {w})" append_bitmaplist_and_offsetlist() size = 0 bitmaplist = [] @@ -494,6 +504,8 @@ def chr(self): return chr(self.cp()) def draw(self, mode=0, bb=None): + # https://font.tomchen.org/bdfparser_py/glyph#draw + if mode == 0: retbitmap = self.__draw_fbb() elif mode == 1: @@ -507,11 +519,6 @@ def draw(self, mode=0, bb=None): 'Parameter bb in draw() method must be set when mode=-1') return retbitmap - # 0 (default): area represented by the bitmap hex data, positioned and resized (cropped) (`fbbx` × `fbby`) according to `FONTBOUNDINGBOX` (the font's global bounding box) - # 1: area represented by the bitmap hex data, resized (cropped) according to `BBX` (`bbw` × `bbh`), which is the individual glyph bounding box, without unnecessary blank margin (but still possible to have blank margin) - # 2: area represented by the bitmap hex data, original, without resizing - # -1: user specified area. if this mode is chosen, you must specify `fbb`, which is a tuple (fbbx, fbby, fbbxoff, fbbyoff) representing your customized font bounding box. Similar to `FONTBOUNDINGBOX`, fbbx and fbby represent the size, fbbxoff and fbbyoff represent the relative position of the starting point to the origin - def __draw_user_specified(self, fbb): bbxoff = self.meta.get('bbxoff') bbyoff = self.meta.get('bbyoff') @@ -530,7 +537,11 @@ def __draw_bb(self): l = len(bindata) if l != bbh: raise Exception( - f"Glyph \"{str(self.meta.get('glyphname'))}\" (codepoint {str(self.meta.get('codepoint'))})'s bbh, {str(bbh)}, does not match its hexdata line count, {str(l)}") + "Glyph \"" + str(self.meta.get('glyphname')) + "\" (codepoint " + str(self.meta.get( + 'codepoint')) + ")'s bbh, " + str(bbh) + ", does not match its hexdata line count, " + str(l) + ) + # Use old style for Python 3.5 support. For 3.6+: + # f"Glyph \"{str(self.meta.get('glyphname'))}\" (codepoint {str(self.meta.get('codepoint'))})'s bbh, {str(bbh)}, does not match its hexdata line count, {str(l)}" bitmap.bindata = [b[0:bbw] for b in bindata] return bitmap @@ -538,8 +549,9 @@ def __draw_fbb(self): fh = self.font.headers return self.__draw_user_specified((fh['fbbx'], fh['fbby'], fh['fbbxoff'], fh['fbbyoff'])) - # get the relative position (displacement) of the origin from the left bottom corner of the bitmap drawn by method `draw()`, or vice versa (i.e. displacement of the left bottom corner of the bitmap from the origin) def origin(self, mode=0, fromorigin=False, xoff=None, yoff=None): + # https://font.tomchen.org/bdfparser_py/glyph#origin + bbxoff = self.meta.get('bbxoff') bbyoff = self.meta.get('bbyoff') if mode == 0: @@ -661,10 +673,7 @@ def overlay(self, bitmap): @classmethod def concatall(cls, bitmaplist, direction=1, align=1, offsetlist=None): - # offsetlist is spacing offset between two glyphs, len(offsetlist) should be len(bitmaplist)-1 - # direction: 1: right, 0: down, 2: left, -1: up - # if horizontal (1 right or 2 left): align: 1: bottom; 0: top - # if vertical (0 down or -1 up) : align: 1: left; 0: right + # https://font.tomchen.org/bdfparser_py/bitmap#bitmapconcatall if direction > 0: # horizontal @@ -809,11 +818,8 @@ def glow(self): return self def bytepad(self, bits=8): - # Do this before using the bitmap for a glyph in a BDF font. - # Per BDF spec, if the bit/pixel count in a line is not multiple of 8, - # the line needs to be padded on the right with 0s to the nearest byte (that is, multiple of 8) - # `bits` is 8 by default because 1 byte = 8 bits, - # you can change it if you want to use other unconventional, off-spec values, such as 4 (half a byte) + # https://font.tomchen.org/bdfparser_py/bitmap#bytepad + w = self.width() h = self.height() mod = w % bits @@ -822,14 +828,7 @@ def bytepad(self, bits=8): return self.crop(w + bits - mod, h) def todata(self, datatype=1): - # `datatype`: output data type - # * 0 : binary-encoded multi-line string e.g. '00010\n11010\n00201' - # * 1 : list of binary-encoded strings e.g. ['00010','11010','00201'] - # * 2 : list of lists of integers 0 or 1 (or 2 sometimes) e.g. [[0,0,0,1,0],[1,1,0,1,0],[0,0,2,0,1]] - # * 3 : list of integers 0 or 1 (or 2 sometimes) (to be used with .width() and .height()) e.g. [0,0,0,1,0,1,1,0,1,0,0,0,2,0,1] - # * 4 : list of lowercase hexadecimal-encoded strings (without '0x', padded with leading '0's according to width) e.g. ['02','1a'] - # * 5 : list of integers e.g. [2,26] - # NOTE: you can't have '2's when using datatypes 4 and 5 + # https://font.tomchen.org/bdfparser_py/bitmap#todata if datatype == 0: return '\n'.join(self.bindata) @@ -847,13 +846,7 @@ def todata(self, datatype=1): return [int(l, 2) for l in self.bindata] def tobytes(self, mode='RGB', bytesdict=None): - # output bytes to be used to create PIL / Pillow library image with `Image.frombytes()` - # `mode`: PIL image mode - # (see also https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes , but only the followings are supported) - # * '1' : 1-bit pixels, black and white, stored with one pixel per byte - # * 'L' : 8-bit pixels, black and white - # * 'RGB' : 3x8-bit pixels, true color - # * 'RGBA': 4x8-bit pixels, true color with transparency mask + # https://font.tomchen.org/bdfparser_py/bitmap#tobytes if mode == '1': diff --git a/tests/test_bitmap.py b/tests/test_bitmap.py index 927f8a1..ba95252 100644 --- a/tests/test_bitmap.py +++ b/tests/test_bitmap.py @@ -1,7 +1,5 @@ import unittest - from bdfparser import Font, Bitmap - from .info import specfont_path, bitmap_qr2_bindata, bitmap_qr3_bindata diff --git a/tests/test_font.py b/tests/test_font.py index 6372aad..2a65874 100644 --- a/tests/test_font.py +++ b/tests/test_font.py @@ -1,7 +1,5 @@ import unittest - from bdfparser import Font, Glyph - from .info import unifont_path, glyph_a_meta @@ -18,7 +16,8 @@ def test_load_file_path(self): self.assertIsInstance(self.font.load_file_path(unifont_path), Font) def test_load_file_obj(self): - self.assertIsInstance(self.font.load_file_obj(open(unifont_path)), Font) + self.assertIsInstance( + self.font.load_file_obj(open(unifont_path)), Font) class TestFont(unittest.TestCase): diff --git a/tests/test_glyph.py b/tests/test_glyph.py index fe55dac..52874c5 100644 --- a/tests/test_glyph.py +++ b/tests/test_glyph.py @@ -1,7 +1,5 @@ import unittest - from bdfparser import Font, Glyph - from .info import unifont_path, glyph_a_meta, bitmap_a_bindata, specfont_path