rle_compress_bitmap.py 6.6 KB

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