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