#!/usr/bin/env python3 ## ## license:BSD-3-Clause ## copyright-holders:Aaron Giles, Andrew Gardner ## **************************************************************************** ## ## png2bdc.c ## ## Super-simple PNG to BDC file generator ## ## **************************************************************************** ## ## Format of PNG data: ## ## Multiple rows of characters. A black pixel means "on". All other colors ## mean "off". Each row looks like this: ## ## * 8888 *** * ## * 4444 * * ** ## * 2222 * * * ## * 1111 * * * ## * * * * ## ** *** *** ## * ## * ## ## ****** **** ## ## The column of pixels on the left-hand side (column 0) indicates the ## character cell height. This column must be present on each row and ## the height must be consistent for each row. ## ## Protruding one pixel into column 1 is the baseline indicator. There ## should only be one row with a pixel in column 1 for each line, and ## that pixel row index should be consistent for each row. ## ## In columns 2-5 are a 4-hex-digit starting character index number. This ## is encoded as binary value. Each column is 4 pixels tall and represents ## one binary digit. The character index number is the unicode character ## number of the first character encoded in this row; subsequent ## characters in the row are at increasing character indices. ## ## Starting in column 6 and beyond are the actual character bitmaps. ## Below them, in the second row after the last row of the character, ## is a solid line that indicates the width of the character, and also ## where the character bitmap begins and ends. ## ## *************************************************************************** ## ## Python note: ## This is a near-literal translation of the original C++ code. As such there ## are some very non-pythonic things done throughout. The conversion was done ## this way so as to insure compatibility as much as possible given the small ## number of test cases. ## import os import png import sys def b2p(v): return bytes([v]) ######################################## ## Helper classes ######################################## class RenderFontChar: """ Contains information about a single character in a font. """ def __init__(self): """ """ self.width = 0 # width from this character to the next self.xOffs = 0 # X offset from baseline to top,left of bitmap self.yOffs = 0 # Y offset from baseline to top,left of bitmap self.bmWidth = 0 # width of bitmap self.bmHeight = 0 # height of bitmap self.bitmap = None # pointer to the bitmap containing the raw data class RenderFont: """ Contains information about a font """ def __init__(self): self.height = 0 # height of the font, from ascent to descent self.yOffs = 0 # y offset from baseline to descent self.defChar = -1 # default character for glyphs not present self.chars = list() # array of characters for i in range(0, 65536): self.chars.append(RenderFontChar()) ######################################## ## Helper functions ######################################## def pixelIsSet(value): return (value & 0xffffff) == 0 def renderFontSaveCached(font, filename, length64, hash32): """ """ fp = open(filename, "wb") if not fp: return 1 # Write the header numChars = 0 for c in font.chars: if c.width > 0: numChars += 1 CACHED_CHAR_SIZE = 16 CACHED_HEADER_SIZE = 32 try: fp.write(b'b') fp.write(b'd') fp.write(b'c') fp.write(b'f') fp.write(b'n') fp.write(b't') fp.write(b2p(1)) fp.write(b2p(0)) fp.write(b2p(length64 >> 56 & 0xff)) fp.write(b2p(length64 >> 48 & 0xff)) fp.write(b2p(length64 >> 40 & 0xff)) fp.write(b2p(length64 >> 32 & 0xff)) fp.write(b2p(length64 >> 24 & 0xff)) fp.write(b2p(length64 >> 16 & 0xff)) fp.write(b2p(length64 >> 8 & 0xff)) fp.write(b2p(length64 >> 0 & 0xff)) fp.write(b2p(hash32 >> 24 & 0xff)) fp.write(b2p(hash32 >> 16 & 0xff)) fp.write(b2p(hash32 >> 8 & 0xff)) fp.write(b2p(hash32 >> 0 & 0xff)) fp.write(b2p(numChars >> 24 & 0xff)) fp.write(b2p(numChars >> 16 & 0xff)) fp.write(b2p(numChars >> 8 & 0xff)) fp.write(b2p(numChars >> 0 & 0xff)) fp.write(b2p(font.height >> 8 & 0xff)) fp.write(b2p(font.height >> 0 & 0xff)) fp.write(b2p(font.yOffs >> 8 & 0xff)) fp.write(b2p(font.yOffs >> 0 & 0xff)) fp.write(b2p(font.defChar >> 24 & 0xff)) fp.write(b2p(font.defChar >> 16 & 0xff)) fp.write(b2p(font.defChar >> 8 & 0xff)) fp.write(b2p(font.defChar >> 0 & 0xff)) # Write a blank table at first (?) charTable = [0]*(numChars * CACHED_CHAR_SIZE) for i in range(numChars * CACHED_CHAR_SIZE): fp.write(b2p(charTable[i])) # Loop over all characters tableIndex = 0 for i in range(len(font.chars)): c = font.chars[i] if c.width == 0: continue if c.bitmap: dBuffer = list() accum = 0 accbit = 7 # Bit-encode the character data for y in range(0, c.bmHeight): src = None desty = y + font.height + font.yOffs - c.yOffs - c.bmHeight if desty >= 0 and desty < font.height: src = c.bitmap[desty] for x in range(0, c.bmWidth): if src is not None and src[x] != 0: accum |= 1 << accbit accbit -= 1 if accbit+1 == 0: dBuffer.append(accum) accum = 0 accbit = 7 # Flush any extra if accbit != 7: dBuffer.append(accum) # Write the data for j in range(len(dBuffer)): fp.write(b2p(dBuffer[j])) destIndex = tableIndex * CACHED_CHAR_SIZE charTable[destIndex + 0] = i >> 24 & 0xff charTable[destIndex + 1] = i >> 16 & 0xff charTable[destIndex + 2] = i >> 8 & 0xff charTable[destIndex + 3] = i >> 0 & 0xff charTable[destIndex + 4] = c.width >> 8 & 0xff charTable[destIndex + 5] = c.width >> 0 & 0xff charTable[destIndex + 8] = c.xOffs >> 8 & 0xff charTable[destIndex + 9] = c.xOffs >> 0 & 0xff charTable[destIndex + 10] = c.yOffs >> 8 & 0xff charTable[destIndex + 11] = c.yOffs >> 0 & 0xff charTable[destIndex + 12] = c.bmWidth >> 8 & 0xff charTable[destIndex + 13] = c.bmWidth >> 0 & 0xff charTable[destIndex + 14] = c.bmHeight >> 8 & 0xff charTable[destIndex + 15] = c.bmHeight >> 0 & 0xff tableIndex += 1 # Seek back to the beginning and rewrite the table fp.seek(CACHED_HEADER_SIZE, 0) for i in range(numChars * CACHED_CHAR_SIZE): fp.write(b2p(charTable[i])) fp.close() return 0 except: print(sys.exc_info[1]) return 1 def bitmapToChars(pngObject, font): """ Convert a bitmap to characters in the given font """ # Just cache the bitmap into a list of lists since random access is paramount bitmap = list() width = pngObject.asRGBA8()[0] height = pngObject.asRGBA8()[1] rowGenerator = pngObject.asRGBA8()[2] for row in rowGenerator: cRow = list() irpd = iter(row) for r,g,b,a in zip(irpd, irpd, irpd, irpd): cRow.append(a << 24 | r << 16 | g << 8 | b) bitmap.append(cRow) rowStart = 0 while rowStart < height: # Find the top of the row for i in range(rowStart, height): if pixelIsSet(bitmap[rowStart][0]): break rowStart += 1 if rowStart >= height: break # Find the bottom of the row rowEnd = rowStart + 1 for i in range(rowEnd, height): if not pixelIsSet(bitmap[rowEnd][0]): rowEnd -= 1 break rowEnd += 1 # Find the baseline baseline = rowStart for i in range(rowStart, rowEnd+1): if pixelIsSet(bitmap[baseline][1]): break baseline += 1 if baseline > rowEnd: sys.stderr.write("No baseline found between rows %d-%d\n" % (rowStart, rowEnd)) break # Set or confirm the height if font.height == 0: font.height = rowEnd - rowStart + 1 font.yOffs = baseline - rowEnd else: if font.height != (rowEnd - rowStart + 1): sys.stderr.write("Inconsistent font height at rows %d-%d\n" % (rowStart, rowEnd)) break if font.yOffs != (baseline - rowEnd): sys.stderr.write("Inconsistent baseline at rows %d-%d\n" % (rowStart, rowEnd)) break # decode the starting character chStart = 0 for x in range(0, 4): for y in range(0, 4): chStart = (chStart << 1) | pixelIsSet(bitmap[rowStart+y][2+x]) # Print debug info # print("Row %d-%d, baseline %d, character start %X" % (rowStart, rowEnd, baseline, chStart)) # scan the column to find characters colStart = 0 while colStart < width: ch = RenderFontChar() # Find the start of the character for i in range(colStart, width): if pixelIsSet(bitmap[rowEnd+2][colStart]): break colStart += 1 if colStart >= width: break # Find the end of the character colEnd = colStart + 1 for i in range(colEnd, width): if not pixelIsSet(bitmap[rowEnd+2][colEnd]): colEnd -= 1 break colEnd += 1 # Skip char which code is already registered if ch.width <= 0: # Print debug info # print " Character %X - width = %d" % (chStart, colEnd - colStart + 1) # Plot the character ch.bitmap = list() for y in range(rowStart, rowEnd+1): ch.bitmap.append(list()) for x in range(colStart, colEnd+1): if pixelIsSet(bitmap[y][x]): ch.bitmap[-1].append(0xffffffff) else: ch.bitmap[-1].append(0x00000000) # Set the character parameters ch.width = colEnd - colStart + 1 ch.xOffs = 0 ch.yOffs = font.yOffs ch.bmWidth = len(ch.bitmap[0]) ch.bmHeight = len(ch.bitmap) # Insert the character into the list font.chars[chStart] = ch # Next character chStart += 1 colStart = colEnd + 1 # Next row rowStart = rowEnd + 1 # Return non-zero if we errored return rowStart < height ######################################## ## Main ######################################## def main(): if len(sys.argv) < 3: sys.stderr.write("Usage:\n%s [ [...]] \n" % sys.argv[0]) return 1 bdcName = sys.argv[-1] font = RenderFont() for i in range(1, len(sys.argv)-1): filename = sys.argv[i] if not os.path.exists(filename): sys.stderr.write("Error attempting to open PNG file.\n") return 1 pngObject = png.Reader(filename) try: pngObject.validate_signature() except: sys.stderr.write("Error reading PNG file.\n") return 1 error = bitmapToChars(pngObject, font) if error: return 1 error = renderFontSaveCached(font, bdcName, 0, 0) return error ######################################## ## Program entry point ######################################## if __name__ == "__main__": sys.exit(main())