rle_compress_bitmap.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. #!/usr/bin/env python3
  2. #
  3. # Bitwise RLE compress a Marlin mono DOGM bitmap.
  4. # Input: An existing Marlin Marlin mono DOGM bitmap .cpp or .h file.
  5. # Output: A new file with the original and compressed data.
  6. #
  7. # Usage: rle_compress_bitmap.py INPUT_FILE OUTPUT_FILE
  8. #
  9. import sys, struct
  10. import re
  11. def addCompressedData(input_file, output_file):
  12. input_lines = input_file.readlines()
  13. input_file.close()
  14. ofile = open(output_file, 'wt')
  15. datatype = "uint8_t"
  16. bytewidth = 16
  17. raw_data = []
  18. arrname = ''
  19. c_data_section = False ; c_skip_data = False ; c_footer = False
  20. for line in input_lines:
  21. if not line: break
  22. if not c_footer:
  23. if not c_skip_data: ofile.write(line)
  24. mat = re.match(r'.+CUSTOM_BOOTSCREEN_BMPWIDTH\s+(\d+)', line)
  25. if mat: bytewidth = (int(mat[1]) + 7) // 8
  26. if "};" in line:
  27. c_skip_data = False
  28. c_data_section = False
  29. c_footer = True
  30. if c_data_section:
  31. cleaned = re.sub(r"\s|,|\n", "", line)
  32. mat = re.match(r'(0b|B)[01]{8}', cleaned)
  33. if mat:
  34. as_list = cleaned.split(mat[1])
  35. as_list.pop(0)
  36. raw_data += [int(x, 2) for x in as_list]
  37. else:
  38. as_list = cleaned.split("0x")
  39. as_list.pop(0)
  40. raw_data += [int(x, 16) for x in as_list]
  41. mat = re.match(r'const (uint\d+_t|unsigned char)', line)
  42. if mat:
  43. # e.g.: const unsigned char custom_start_bmp[] PROGMEM = {
  44. datatype = mat[0]
  45. if "_rle" in line:
  46. c_skip_data = True
  47. else:
  48. c_data_section = True
  49. arrname = line.split('[')[0].split(' ')[-1]
  50. print("Found data array", arrname)
  51. #print("\nRaw Bitmap Data", raw_data)
  52. #
  53. # Bitwise RLE (run length) encoding
  54. # Convert data from raw mono bitmap to a bitwise run-length-encoded format.
  55. # - The first nybble is the starting bit state. Changing this nybble inverts the bitmap.
  56. # - The following bytes provide the runs for alternating on/off bits.
  57. # - A value of 0-14 encodes a run of 1-15.
  58. # - A value of 16 indicates a run of 16-270 calculated using the next two bytes.
  59. #
  60. def bitwise_rle_encode(data):
  61. def get_bit(data, n): return 1 if (data[n // 8] & (0x80 >> (n & 7))) else 0
  62. def try_encode(data, isext):
  63. bitslen = len(data) * 8
  64. bitstate = get_bit(data, 0)
  65. rledata = [ bitstate ]
  66. bigrun = 256 if isext else 272
  67. medrun = False
  68. i = 0
  69. runlen = -1
  70. while i <= bitslen:
  71. if i < bitslen: b = get_bit(data, i)
  72. runlen += 1
  73. if bitstate != b or i == bitslen:
  74. if runlen >= bigrun:
  75. isext = True
  76. if medrun: return [], isext
  77. rem = runlen & 0xFF
  78. rledata += [ 15, 15, rem // 16, rem % 16 ]
  79. elif runlen >= 16:
  80. rledata += [ 15, runlen // 16 - 1, runlen % 16 ]
  81. if runlen >= 256: medrun = True
  82. else:
  83. rledata += [ runlen - 1 ]
  84. bitstate ^= 1
  85. runlen = 0
  86. i += 1
  87. #print("\nrledata", rledata)
  88. encoded = []
  89. ri = 0
  90. rlen = len(rledata)
  91. while ri < rlen:
  92. v = rledata[ri] << 4
  93. if (ri < rlen - 1): v |= rledata[ri + 1]
  94. encoded += [ v ]
  95. ri += 2
  96. #print("\nencoded", encoded)
  97. return encoded, isext
  98. # Try to encode with the original isext flag
  99. warn = "This may take a while" if len(data) > 300000 else ""
  100. print("Compressing image data...", warn)
  101. isext = False
  102. encoded, isext = try_encode(data, isext)
  103. if len(encoded) == 0:
  104. encoded, isext = try_encode(data, True)
  105. return encoded, isext
  106. def bitwise_rle_decode(isext, rledata, invert=0):
  107. expanded = []
  108. for n in rledata: expanded += [ n >> 4, n & 0xF ]
  109. decoded = []
  110. bitstate = 0 ; workbyte = 0 ; outindex = 0
  111. i = 0
  112. while i < len(expanded):
  113. c = expanded[i]
  114. i += 1
  115. if i == 1: bitstate = c ; continue
  116. if c == 15:
  117. d = expanded[i] ; e = expanded[i + 1]
  118. if isext and d == 15:
  119. c = 256 + 16 * e + expanded[i + 2] - 1
  120. i += 1
  121. else:
  122. c = 16 * d + e + 15
  123. i += 2
  124. for _ in range(c, -1, -1):
  125. bitval = 0x80 >> (outindex & 7)
  126. if bitstate: workbyte |= bitval
  127. if bitval == 1:
  128. decoded += [ workbyte ]
  129. workbyte = 0
  130. outindex += 1
  131. bitstate ^= 1
  132. print("\nDecoded RLE data:")
  133. pretty = [ '{0:08b}'.format(v) for v in decoded ]
  134. rows = [pretty[i:i+bytewidth] for i in range(0, len(pretty), bytewidth)]
  135. for row in rows: print(f"{''.join(row)}")
  136. return decoded
  137. def rle_emit(ofile, arrname, rledata, rawsize, isext):
  138. outstr = ''
  139. rows = [ rledata[i:i+16] for i in range(0, len(rledata), 16) ]
  140. for i in range(0, len(rows)):
  141. rows[i] = [ '0x{0:02X}'.format(v) for v in rows[i] ]
  142. outstr += f" {', '.join(rows[i])},\n"
  143. outstr = outstr[:-2]
  144. size = len(rledata)
  145. defname = 'COMPACT_CUSTOM_BOOTSCREEN_EXT' if isext else 'COMPACT_CUSTOM_BOOTSCREEN'
  146. ofile.write(f"\n// Saves {rawsize - size} bytes\n#define {defname}\n{datatype} {arrname}_rle[{size}] PROGMEM = {{\n{outstr}\n}};\n")
  147. # Encode the data, write it out, close the file
  148. rledata, isext = bitwise_rle_encode(raw_data)
  149. rle_emit(ofile, arrname, rledata, len(raw_data), isext)
  150. ofile.close()
  151. # Validate that code properly compressed (and decompressed) the data
  152. checkdata = bitwise_rle_decode(isext, rledata)
  153. for i in range(0, len(checkdata)):
  154. if raw_data[i] != checkdata[i]:
  155. print(f'Data mismatch at byte offset {i} (should be {raw_data[i]} but got {checkdata[i]})')
  156. break
  157. if len(sys.argv) <= 2:
  158. print('Usage: rle_compress_bitmap.py INPUT_FILE OUTPUT_FILE')
  159. exit(1)
  160. output_h = sys.argv[2]
  161. inname = sys.argv[1].replace('//', '/')
  162. try:
  163. input_h = open(inname)
  164. print("Processing", inname, "...")
  165. addCompressedData(input_h, output_h)
  166. except OSError:
  167. print("Can't find input file", inname)