1#!/usr/bin/env python3
2import collections, optparse, re, sys
3
4formats_by_name = {}
5formats_list = []
6
7def read_color_h(filename):
8    """
9    Read in the list of formats.
10    """
11    formats = []
12    inside_enum = False
13    for line in open(filename):
14        line = line.strip()
15        if line == "{": continue
16        if line == "typedef enum ALLEGRO_PIXEL_FORMAT": inside_enum = True
17        elif inside_enum:
18            match = re.match(r"\s*ALLEGRO_PIXEL_FORMAT_(\w+)", line)
19            if match:
20                formats.append(match.group(1))
21            else:
22                break
23    return formats
24
25def parse_format(format):
26    """
27    Parse the format name into an info structure.
28    """
29    if format.startswith("ANY"): return None
30    if "DXT" in format: return None
31
32    separator = format.find("_")
33    class Info: pass
34    class Component: pass
35
36    Info = collections.namedtuple("Info", "components, name, little_endian,"
37        "float, single_channel, size")
38    Component = collections.namedtuple("Component", "color, size,"
39        "position_from_left, position")
40
41    pos = 0
42    info = Info(
43        components={},
44        name=format,
45        little_endian="_LE" in format,
46        float=False,
47        single_channel=False,
48        size=None,
49    )
50
51    if "F32" in format:
52        return info._replace(
53            float=True,
54            size=128,
55        )
56
57    if "SINGLE_CHANNEL" in format:
58        return info._replace(
59            single_channel=True,
60            size=8,
61        )
62
63    for i in range(separator):
64        c = Component(
65            color=format[i],
66            size=int(format[separator + 1 + i]),
67            position_from_left=pos,
68            position=None,
69        )
70        info.components[c.color] = c
71        pos += c.size
72    size = pos
73    return info._replace(
74        components={k: c._replace(position=size - c.position_from_left - c.size)
75                    for k, c in info.components.items()},
76        size=size,
77        float=False,
78    )
79
80def macro_lines(info_a, info_b):
81    """
82    Write out the lines of a conversion macro.
83    """
84    r = ""
85
86    names = list(info_b.components.keys())
87    names.sort()
88
89    if info_a.float:
90        if info_b.single_channel:
91            return "   (uint32_t)((x).r * 255)\n"
92        lines = []
93        for name in names:
94            if name == "X": continue
95            c = info_b.components[name]
96            mask = (1 << c.size) - 1
97            lines.append(
98                "((uint32_t)((x)." + name.lower() + " * " + str(mask) +
99                ") << " + str(c.position) + ")")
100        r += "   ("
101        r += " | \\\n    ".join(lines)
102        r += ")\n"
103        return r
104
105    if info_b.float:
106        if info_a.single_channel:
107            return "   al_map_rgb(x, 0, 0)\n"
108        lines = []
109        for name in "RGBA":
110            if name not in info_a.components: break
111            c = info_a.components[name]
112            mask = (1 << c.size) - 1
113            line = "((x) >> " + str(c.position) + ") & " + str(mask)
114            if c.size < 8:
115                line = "_al_rgb_scale_" + str(c.size) + "[" + line + "]"
116            lines.append(line)
117
118        r += "   al_map_rgba(" if len(lines) == 4 else "   al_map_rgb("
119        r += ",\\\n   ".join(lines)
120        r += ")\n"
121        return r
122
123    if info_a.single_channel:
124        lines = []
125        for name in names:
126           # Only map to R, and let A=1
127           if name in ["X", "G", "B"]:
128               continue
129           c = info_b.components[name]
130           shift = 8 - c.size - c.position
131           m = hex(((1 << c.size) - 1) << c.position)
132           if name == "A":
133               lines.append(m)
134               continue
135           if shift > 0:
136               lines.append("(((x) >> " + str(shift) + ") & " + m + ")")
137           elif shift < 0:
138               lines.append("(((x) << " + str(-shift) + ") & " + m + ")")
139           else:
140               lines.append("((x) & " + m + ")")
141        r += "   ("
142        r += " | \\\n   ".join(lines)
143        r += ")\n"
144        return r
145
146    if info_b.single_channel:
147        c = info_a.components["R"]
148        m = (1 << c.size) - 1
149        extract = "(((x) >> " + str(c.position) + ") & " + hex(m) + ")"
150        if (c.size != 8):
151            scale = "(_al_rgb_scale_" + str(c.size) + "[" + extract + "])"
152        else:
153            scale = extract
154        r += "   " + scale + "\n"
155        return r
156
157    # Generate a list of (mask, shift, add) tuples for all components.
158    ops = {}
159    for name in names:
160        if name == "X": continue # We simply ignore X components.
161        c_b = info_b.components[name]
162        if name not in info_a.components:
163            # Set A component to all 1 bits if the source doesn't have it.
164            if name == "A":
165                add = (1 << c_b.size) - 1
166                add <<= c_b.position
167                ops[name] = (0, 0, add, 0, 0, 0)
168            continue
169        c_a = info_a.components[name]
170        mask = (1 << c_b.size) - 1
171        shift_right = c_a.position
172        mask_pos = c_a.position
173        shift_left = c_b.position
174        bitdiff = c_a.size - c_b.size
175        if bitdiff > 0:
176            shift_right += bitdiff
177            mask_pos += bitdiff
178        else:
179            shift_left -= bitdiff
180            mask = (1 << c_a.size) - 1
181
182        mask <<= mask_pos
183        shift = shift_left - shift_right
184        ops[name] = (mask, shift, 0, c_a.size, c_b.size, mask_pos)
185
186    # Collapse multiple components if possible.
187    common_shifts = {}
188    for name, (mask, shift, add, size_a, size_b, mask_pos) in ops.items():
189        if not add and not (size_a != 8 and size_b == 8):
190            if shift in common_shifts: common_shifts[shift].append(name)
191            else: common_shifts[shift] = [name]
192    for newshift, colors in common_shifts.items():
193        if len(colors) == 1: continue
194        newname = ""
195        newmask = 0
196        colors.sort()
197        masks_pos = []
198        for name in colors:
199            mask, shift, add, size_a, size_b, mask_pos = ops[name]
200
201            names.remove(name)
202            newname += name
203            newmask |= mask
204            masks_pos.append(mask_pos)
205        names.append(newname)
206        ops[newname] = (newmask, newshift, 0, size_a, size_b, min(masks_pos))
207
208    # Write out a line for each remaining operation.
209    lines = []
210    add_format = "0x%0" + str(info_b.size >> 2) + "x"
211    mask_format = "0x%0" + str(info_a.size >> 2) + "x"
212    for name in names:
213        if not name in ops: continue
214        mask, shift, add, size_a, size_b, mask_pos = ops[name]
215        if add:
216            line = "(" + (add_format % add) + ")"
217            lines.append((line, name, 0, size_a, size_b, mask_pos))
218            continue
219        mask_string = "((x) & " + (mask_format % mask) + ")"
220        if (size_a != 8 and size_b == 8):
221            line = "(_al_rgb_scale_" + str(size_a) + "[" + mask_string + " >> %2d" % mask_pos + "]"
222        else:
223            if (shift > 0):
224                line = "(" + mask_string + " << %2d" % shift + ")"
225            elif (shift < 0):
226                line = "(" + mask_string + " >> %2d" % -shift + ")"
227            else:
228                line = mask_string + "      "
229        lines.append((line, name, shift, size_a, size_b, mask_pos))
230
231    # Concoct the macro.
232    for i in range(len(lines)):
233        line, name, shift, size_a, size_b, mask_pos = lines[i]
234        if i == 0: start = "   ("
235        else: start = "    "
236        if i == len(lines) - 1: cont = ")   "
237        else: cont = " | \\"
238        if size_a != 8 and size_b == 8:
239            shift = shift+(mask_pos-(8-size_a))
240            if shift > 0:
241                backshift = " << %2d)" % shift
242            elif shift < 0:
243                backshift = " >> %2d)" % -shift
244            else:
245                backshift = "       )"
246        else:
247            backshift = "       "
248        r += start + line + backshift + " /* " + name + " */" + cont + "\n"
249    return r
250
251
252def converter_macro(info_a, info_b):
253    """
254    Create a conversion macro.
255    """
256    if not info_a or not info_b: return None
257
258    name = "ALLEGRO_CONVERT_" + info_a.name + "_TO_" + info_b.name
259
260    r = ""
261
262    if info_a.little_endian or info_b.little_endian:
263        r += "#ifdef ALLEGRO_BIG_ENDIAN\n"
264        r += "#define " + name + "(x) \\\n"
265        if info_a.name == "ABGR_8888_LE":
266            r += macro_lines(formats_by_name["RGBA_8888"], info_b)
267        elif info_b.name == "ABGR_8888_LE":
268            r += macro_lines(info_a, formats_by_name["RGBA_8888"])
269        else:
270            r += "#error Conversion %s -> %s not understood by make_converters.py\n" % (
271                info_a.name, info_b.name)
272        r += "#else\n"
273        r += "#define " + name + "(x) \\\n"
274        r += macro_lines(info_a, info_b)
275        r += "#endif\n"
276    else:
277        r += "#define " + name + "(x) \\\n"
278        r += macro_lines(info_a, info_b)
279
280    return r
281
282def write_convert_h(filename):
283    """
284    Create the file with all the conversion macros.
285    """
286    f = open(filename, "w")
287    f.write("""\
288// Warning: This file was created by make_converters.py - do not edit.
289#ifndef __al_included_allegro5_aintern_convert_h
290#define __al_included_allegro5_aintern_convert_h
291
292#include "allegro5/allegro.h"
293#include "allegro5/internal/aintern_pixels.h"
294""")
295
296    for a in formats_list:
297        for b in formats_list:
298            if b == a: continue
299            macro = converter_macro(a, b)
300            if macro:
301                f.write(macro)
302
303    f.write("""\
304#endif
305// Warning: This file was created by make_converters.py - do not edit.
306""")
307
308def converter_function(info_a, info_b):
309    """
310    Create a string with one conversion function.
311    """
312    name = info_a.name.lower() + "_to_" + info_b.name.lower()
313    params = "const void *src, int src_pitch,\n"
314    params += "   void *dst, int dst_pitch,\n"
315    params += "   int sx, int sy, int dx, int dy, int width, int height"
316    declaration = "static void " + name + "(" + params + ")"
317
318    macro_name = "ALLEGRO_CONVERT_" + info_a.name + "_TO_" + info_b.name
319
320    types_and_sizes = {
321        8 : ("uint8_t", "", 1),
322        15 : ("uint16_t", "", 2),
323        16 : ("uint16_t", "", 2),
324        24: ("uint8_t", " * 3", 1),
325        32 : ("uint32_t", "", 4),
326        128 : ("ALLEGRO_COLOR", "", 16)}
327    a_type, a_count, a_size = types_and_sizes[info_a.size]
328    b_type, b_count, b_size = types_and_sizes[info_b.size]
329
330    if a_count == "" and b_count == "":
331        conversion = """\
332         *dst_ptr = %(macro_name)s(*src_ptr);
333         dst_ptr++;
334         src_ptr++;""" % locals()
335    else:
336
337        if a_count != "":
338            s_conversion = """\
339         #ifdef ALLEGRO_BIG_ENDIAN
340         int src_pixel = src_ptr[2] | (src_ptr[1] << 8) | (src_ptr[0] << 16);
341         #else
342         int src_pixel = src_ptr[0] | (src_ptr[1] << 8) | (src_ptr[2] << 16);
343         #endif
344""" % locals()
345
346        if b_count != "":
347            d_conversion = """\
348         #ifdef ALLEGRO_BIG_ENDIAN
349         dst_ptr[0] = dst_pixel >> 16;
350         dst_ptr[1] = dst_pixel >> 8;
351         dst_ptr[2] = dst_pixel;
352         #else
353         dst_ptr[0] = dst_pixel;
354         dst_ptr[1] = dst_pixel >> 8;
355         dst_ptr[2] = dst_pixel >> 16;
356         #endif
357""" % locals()
358
359        if a_count != "" and b_count != "":
360            conversion = s_conversion + ("""\
361         int dst_pixel = %(macro_name)s(src_pixel);
362""" % locals()) + d_conversion
363
364        elif a_count != "":
365            conversion = s_conversion + ("""\
366         *dst_ptr = %(macro_name)s(src_pixel);
367""" % locals())
368
369        else:
370            conversion = ("""\
371         int dst_pixel = %(macro_name)s(*src_ptr);
372""" % locals()) + d_conversion
373
374        conversion += """\
375         src_ptr += 1%(a_count)s;
376         dst_ptr += 1%(b_count)s;""" % locals()
377
378    r = declaration + "\n"
379    r += "{\n"
380    r += """\
381   int y;
382   const %(a_type)s *src_ptr = (const %(a_type)s *)((const char *)src + sy * src_pitch);
383   %(b_type)s *dst_ptr = (void *)((char *)dst + dy * dst_pitch);
384   int src_gap = src_pitch / %(a_size)d - width%(a_count)s;
385   int dst_gap = dst_pitch / %(b_size)d - width%(b_count)s;
386   src_ptr += sx%(a_count)s;
387   dst_ptr += dx%(b_count)s;
388   for (y = 0; y < height; y++) {
389      %(b_type)s *dst_end = dst_ptr + width%(b_count)s;
390      while (dst_ptr < dst_end) {
391%(conversion)s
392      }
393      src_ptr += src_gap;
394      dst_ptr += dst_gap;
395   }
396""" % locals()
397
398    r += "}\n"
399
400    return r
401
402def write_convert_c(filename):
403    """
404    Write out the file with the conversion functions.
405    """
406    f = open(filename, "w")
407    f.write("""\
408// Warning: This file was created by make_converters.py - do not edit.
409#include "allegro5/allegro.h"
410#include "allegro5/internal/aintern_bitmap.h"
411#include "allegro5/internal/aintern_convert.h"
412""")
413
414    for a in formats_list:
415        for b in formats_list:
416            if b == a: continue
417            if not a or not b: continue
418            function = converter_function(a, b)
419            f.write(function)
420
421    f.write("""\
422void (*_al_convert_funcs[ALLEGRO_NUM_PIXEL_FORMATS]
423   [ALLEGRO_NUM_PIXEL_FORMATS])(const void *, int, void *, int,
424   int, int, int, int, int, int) = {
425""")
426    for a in formats_list:
427        if not a:
428            f.write("   {NULL},\n")
429        else:
430            f.write("   {")
431            was_null = False
432            for b in formats_list:
433                if b and a != b:
434                    name = a.name.lower() + "_to_" + b.name.lower()
435                    f.write("\n      " + name + ",")
436                    was_null = False
437                else:
438                    if not was_null: f.write("\n     ")
439                    f.write(" NULL,")
440                    was_null = True
441            f.write("\n   },\n")
442
443    f.write("""\
444};
445
446// Warning: This file was created by make_converters.py - do not edit.
447""")
448
449def main(argv):
450    global options
451    p = optparse.OptionParser()
452    p.description = """\
453When run from the toplevel A5 folder, this will re-create the convert.h and
454convert.c files containing all the low-level color conversion macros and
455functions."""
456    options, args = p.parse_args()
457
458    # Read in color.h to get the available formats.
459    formats = read_color_h("include/allegro5/color.h")
460
461    print(formats)
462
463    # Parse the component info for each format.
464    for f in formats:
465        info = parse_format(f)
466        formats_by_name[f] = info
467        formats_list.append(info)
468
469    # Output a macro for each possible conversion.
470    write_convert_h("include/allegro5/internal/aintern_convert.h")
471
472    # Output a function for each possible conversion.
473    write_convert_c("src/convert.c")
474
475if __name__ == "__main__":
476    main(sys.argv)
477
478# vim: set sts=4 sw=4 et:
479