1#!/usr/local/bin/python3.7 2## 3## license:BSD-3-Clause 4## copyright-holders:Aaron Giles, Andrew Gardner 5## **************************************************************************** 6## 7## png2bdc.c 8## 9## Super-simple PNG to BDC file generator 10## 11## **************************************************************************** 12## 13## Format of PNG data: 14## 15## Multiple rows of characters. A black pixel means "on". All other colors 16## mean "off". Each row looks like this: 17## 18## * 8888 *** * 19## * 4444 * * ** 20## * 2222 * * * 21## * 1111 * * * 22## * * * * 23## ** *** *** 24## * 25## * 26## 27## ****** **** 28## 29## The column of pixels on the left-hand side (column 0) indicates the 30## character cell height. This column must be present on each row and 31## the height must be consistent for each row. 32## 33## Protruding one pixel into column 1 is the baseline indicator. There 34## should only be one row with a pixel in column 1 for each line, and 35## that pixel row index should be consistent for each row. 36## 37## In columns 2-5 are a 4-hex-digit starting character index number. This 38## is encoded as binary value. Each column is 4 pixels tall and represents 39## one binary digit. The character index number is the unicode character 40## number of the first character encoded in this row; subsequent 41## characters in the row are at increasing character indices. 42## 43## Starting in column 6 and beyond are the actual character bitmaps. 44## Below them, in the second row after the last row of the character, 45## is a solid line that indicates the width of the character, and also 46## where the character bitmap begins and ends. 47## 48## *************************************************************************** 49 50## 51## Python note: 52## This is a near-literal translation of the original C++ code. As such there 53## are some very non-pythonic things done throughout. The conversion was done 54## this way so as to insure compatibility as much as possible given the small 55## number of test cases. 56## 57 58import os 59import png 60import sys 61 62if sys.version_info >= (3,): 63 def b2p(v): 64 return bytes([v]) 65else: 66 def b2p(v): 67 return chr(v) 68 69 70######################################## 71## Helper classes 72######################################## 73class RenderFontChar: 74 """ 75 Contains information about a single character in a font. 76 """ 77 78 def __init__(self): 79 """ 80 """ 81 self.width = 0 # width from this character to the next 82 self.xOffs = 0 # X offset from baseline to top,left of bitmap 83 self.yOffs = 0 # Y offset from baseline to top,left of bitmap 84 self.bmWidth = 0 # width of bitmap 85 self.bmHeight = 0 # height of bitmap 86 self.bitmap = None # pointer to the bitmap containing the raw data 87 88 89class RenderFont: 90 """ 91 Contains information about a font 92 """ 93 94 def __init__(self): 95 self.height = 0 # height of the font, from ascent to descent 96 self.yOffs = 0 # y offset from baseline to descent 97 self.defChar = -1 # default character for glyphs not present 98 self.chars = list() # array of characters 99 for i in range(0, 65536): 100 self.chars.append(RenderFontChar()) 101 102 103 104######################################## 105## Helper functions 106######################################## 107def pixelIsSet(value): 108 return (value & 0xffffff) == 0 109 110 111def renderFontSaveCached(font, filename, length64, hash32): 112 """ 113 """ 114 fp = open(filename, "wb") 115 if not fp: 116 return 1 117 118 # Write the header 119 numChars = 0 120 for c in font.chars: 121 if c.width > 0: 122 numChars += 1 123 124 CACHED_CHAR_SIZE = 16 125 CACHED_HEADER_SIZE = 32 126 127 try: 128 fp.write(b'b') 129 fp.write(b'd') 130 fp.write(b'c') 131 fp.write(b'f') 132 fp.write(b'n') 133 fp.write(b't') 134 fp.write(b2p(1)) 135 fp.write(b2p(0)) 136 fp.write(b2p(length64 >> 56 & 0xff)) 137 fp.write(b2p(length64 >> 48 & 0xff)) 138 fp.write(b2p(length64 >> 40 & 0xff)) 139 fp.write(b2p(length64 >> 32 & 0xff)) 140 fp.write(b2p(length64 >> 24 & 0xff)) 141 fp.write(b2p(length64 >> 16 & 0xff)) 142 fp.write(b2p(length64 >> 8 & 0xff)) 143 fp.write(b2p(length64 >> 0 & 0xff)) 144 fp.write(b2p(hash32 >> 24 & 0xff)) 145 fp.write(b2p(hash32 >> 16 & 0xff)) 146 fp.write(b2p(hash32 >> 8 & 0xff)) 147 fp.write(b2p(hash32 >> 0 & 0xff)) 148 fp.write(b2p(numChars >> 24 & 0xff)) 149 fp.write(b2p(numChars >> 16 & 0xff)) 150 fp.write(b2p(numChars >> 8 & 0xff)) 151 fp.write(b2p(numChars >> 0 & 0xff)) 152 fp.write(b2p(font.height >> 8 & 0xff)) 153 fp.write(b2p(font.height >> 0 & 0xff)) 154 fp.write(b2p(font.yOffs >> 8 & 0xff)) 155 fp.write(b2p(font.yOffs >> 0 & 0xff)) 156 fp.write(b2p(font.defChar >> 24 & 0xff)) 157 fp.write(b2p(font.defChar >> 16 & 0xff)) 158 fp.write(b2p(font.defChar >> 8 & 0xff)) 159 fp.write(b2p(font.defChar >> 0 & 0xff)) 160 161 # Write a blank table at first (?) 162 charTable = [0]*(numChars * CACHED_CHAR_SIZE) 163 for i in range(numChars * CACHED_CHAR_SIZE): 164 fp.write(b2p(charTable[i])) 165 166 # Loop over all characters 167 tableIndex = 0 168 169 for i in range(len(font.chars)): 170 c = font.chars[i] 171 if c.width == 0: 172 continue 173 174 if c.bitmap: 175 dBuffer = list() 176 accum = 0 177 accbit = 7 178 179 # Bit-encode the character data 180 for y in range(0, c.bmHeight): 181 src = None 182 desty = y + font.height + font.yOffs - c.yOffs - c.bmHeight 183 if desty >= 0 and desty < font.height: 184 src = c.bitmap[desty] 185 for x in range(0, c.bmWidth): 186 if src is not None and src[x] != 0: 187 accum |= 1 << accbit 188 accbit -= 1 189 if accbit+1 == 0: 190 dBuffer.append(accum) 191 accum = 0 192 accbit = 7 193 194 # Flush any extra 195 if accbit != 7: 196 dBuffer.append(accum) 197 198 # Write the data 199 for j in range(len(dBuffer)): 200 fp.write(b2p(dBuffer[j])) 201 202 destIndex = tableIndex * CACHED_CHAR_SIZE 203 charTable[destIndex + 0] = i >> 24 & 0xff 204 charTable[destIndex + 1] = i >> 16 & 0xff 205 charTable[destIndex + 2] = i >> 8 & 0xff 206 charTable[destIndex + 3] = i >> 0 & 0xff 207 charTable[destIndex + 4] = c.width >> 8 & 0xff 208 charTable[destIndex + 5] = c.width >> 0 & 0xff 209 charTable[destIndex + 8] = c.xOffs >> 8 & 0xff 210 charTable[destIndex + 9] = c.xOffs >> 0 & 0xff 211 charTable[destIndex + 10] = c.yOffs >> 8 & 0xff 212 charTable[destIndex + 11] = c.yOffs >> 0 & 0xff 213 charTable[destIndex + 12] = c.bmWidth >> 8 & 0xff 214 charTable[destIndex + 13] = c.bmWidth >> 0 & 0xff 215 charTable[destIndex + 14] = c.bmHeight >> 8 & 0xff 216 charTable[destIndex + 15] = c.bmHeight >> 0 & 0xff 217 tableIndex += 1 218 219 # Seek back to the beginning and rewrite the table 220 fp.seek(CACHED_HEADER_SIZE, 0) 221 for i in range(numChars * CACHED_CHAR_SIZE): 222 fp.write(b2p(charTable[i])) 223 224 fp.close() 225 return 0 226 227 except: 228 print(sys.exc_info[1]) 229 return 1 230 231 232def bitmapToChars(pngObject, font): 233 """ 234 Convert a bitmap to characters in the given font 235 """ 236 # Just cache the bitmap into a list of lists since random access is paramount 237 bitmap = list() 238 width = pngObject.asRGBA8()[0] 239 height = pngObject.asRGBA8()[1] 240 rowGenerator = pngObject.asRGBA8()[2] 241 for row in rowGenerator: 242 cRow = list() 243 irpd = iter(row) 244 for r,g,b,a in zip(irpd, irpd, irpd, irpd): 245 cRow.append(a << 24 | r << 16 | g << 8 | b) 246 bitmap.append(cRow) 247 248 rowStart = 0 249 while rowStart < height: 250 # Find the top of the row 251 for i in range(rowStart, height): 252 if pixelIsSet(bitmap[rowStart][0]): 253 break 254 rowStart += 1 255 if rowStart >= height: 256 break 257 258 # Find the bottom of the row 259 rowEnd = rowStart + 1 260 for i in range(rowEnd, height): 261 if not pixelIsSet(bitmap[rowEnd][0]): 262 rowEnd -= 1 263 break 264 rowEnd += 1 265 266 # Find the baseline 267 baseline = rowStart 268 for i in range(rowStart, rowEnd+1): 269 if pixelIsSet(bitmap[baseline][1]): 270 break 271 baseline += 1 272 if baseline > rowEnd: 273 sys.stderr.write("No baseline found between rows %d-%d\n" % (rowStart, rowEnd)) 274 break 275 276 # Set or confirm the height 277 if font.height == 0: 278 font.height = rowEnd - rowStart + 1 279 font.yOffs = baseline - rowEnd 280 else: 281 if font.height != (rowEnd - rowStart + 1): 282 sys.stderr.write("Inconsistent font height at rows %d-%d\n" % (rowStart, rowEnd)) 283 break 284 if font.yOffs != (baseline - rowEnd): 285 sys.stderr.write("Inconsistent baseline at rows %d-%d\n" % (rowStart, rowEnd)) 286 break 287 288 # decode the starting character 289 chStart = 0 290 for x in range(0, 4): 291 for y in range(0, 4): 292 chStart = (chStart << 1) | pixelIsSet(bitmap[rowStart+y][2+x]) 293 294 # Print debug info 295 # print("Row %d-%d, baseline %d, character start %X" % (rowStart, rowEnd, baseline, chStart)) 296 297 # scan the column to find characters 298 colStart = 0 299 while colStart < width: 300 ch = RenderFontChar() 301 302 # Find the start of the character 303 for i in range(colStart, width): 304 if pixelIsSet(bitmap[rowEnd+2][colStart]): 305 break 306 colStart += 1 307 if colStart >= width: 308 break 309 310 # Find the end of the character 311 colEnd = colStart + 1 312 for i in range(colEnd, width): 313 if not pixelIsSet(bitmap[rowEnd+2][colEnd]): 314 colEnd -= 1 315 break 316 colEnd += 1 317 318 # Skip char which code is already registered 319 if ch.width <= 0: 320 # Print debug info 321 # print " Character %X - width = %d" % (chStart, colEnd - colStart + 1) 322 323 # Plot the character 324 ch.bitmap = list() 325 for y in range(rowStart, rowEnd+1): 326 ch.bitmap.append(list()) 327 for x in range(colStart, colEnd+1): 328 if pixelIsSet(bitmap[y][x]): 329 ch.bitmap[-1].append(0xffffffff) 330 else: 331 ch.bitmap[-1].append(0x00000000) 332 333 # Set the character parameters 334 ch.width = colEnd - colStart + 1 335 ch.xOffs = 0 336 ch.yOffs = font.yOffs 337 ch.bmWidth = len(ch.bitmap[0]) 338 ch.bmHeight = len(ch.bitmap) 339 340 # Insert the character into the list 341 font.chars[chStart] = ch 342 343 # Next character 344 chStart += 1 345 colStart = colEnd + 1 346 347 # Next row 348 rowStart = rowEnd + 1 349 350 # Return non-zero if we errored 351 return rowStart < height 352 353 354 355######################################## 356## Main 357######################################## 358def main(): 359 if len(sys.argv) < 3: 360 sys.stderr.write("Usage:\n%s <input.png> [<input2.png> [...]] <output.bdc>\n" % sys.argv[0]) 361 return 1 362 bdcName = sys.argv[-1] 363 364 font = RenderFont() 365 for i in range(1, len(sys.argv)-1): 366 filename = sys.argv[i] 367 if not os.path.exists(filename): 368 sys.stderr.write("Error attempting to open PNG file.\n") 369 return 1 370 371 pngObject = png.Reader(filename) 372 try: 373 pngObject.validate_signature() 374 except: 375 sys.stderr.write("Error reading PNG file.\n") 376 return 1 377 378 error = bitmapToChars(pngObject, font) 379 if error: 380 return 1 381 382 error = renderFontSaveCached(font, bdcName, 0, 0) 383 return error 384 385 386 387######################################## 388## Program entry point 389######################################## 390if __name__ == "__main__": 391 sys.exit(main()) 392