У меня есть некоторые данные, хранящиеся в битовом формате, которые я пытаюсь извлечь с помощью Python. В частности, это старые растровые шрифты, где:
Например, вот шестнадцатеричные данные с прямым порядком байтов для одного глифа шириной 9 бит:
1F 1F DB 79 3D BF FE F7 86 CE 3E
Если мы разобьем это вручную, мы увидим, что этот битовый шаблон представляет собой маленький смайлик:
8-bit bytes: 9-bit chunks: character (0='.', 1='$'):
0x1F = 00011111 000111110 ...$$$$$.
0x1F = 00011111 001111111 ..$$$$$$$
0xDB = 11011011 011011011 .$$.$$.$$
0x79 = 01111001 110010011 $$..$..$$
0x3D = 00111101 110110111 $$.$$.$$$
0xBF = 10111111 111111111 $$$$$$$$$
0xFE = 11111110 101111011 $.$$$$.$$
0xF7 = 11110111 110000110 $$....$$.
0x86 = 10000110 110011100 $$..$$$..
0xCE = 11001110 011111000 .$$$$$...
0x3E = 00111110
Поэтому я попытался сделать то же самое в Python, но не смог правильно выровнять данные при упаковке.
Вот что у меня есть на данный момент — всего лишь небольшой тестовый код, чтобы проверить, смогу ли я получить ожидаемый результат из этого конкретного фрагмента данных.
Обратите внимание на выбор field_type
, поскольку, похоже, это корень моих проблем:
import struct
import ctypes
field_type = ctypes.c_ulonglong
class PackedBitmap(ctypes.BigEndianStructure):
_fields_ = [ ('line00', field_type, 9),
('line01', field_type, 9),
('line02', field_type, 9),
('line03', field_type, 9),
('line04', field_type, 9),
('line05', field_type, 9),
('line06', field_type, 9),
('line07', field_type, 9),
('line08', field_type, 9),
('line09', field_type, 9) ]
bm = PackedBitmap()
struct.pack_into('>11s', bm, 0,
b'\x1F\x1F\xDB\x79\x3D\xBF\xFE\xF7\x86\xCE\x3E')
for field in bm._fields_:
bin_str = f'{getattr(bm, field[0]):09b}'
print(bin_str + ' ' + bin_str.replace('0','.').replace('1','$'))
Но независимо от того, какой тип C я выбираю для полей своего PackedBitmap, я не могу получить правильный результат. Ошибки есть всегда, и размер типа данных, похоже, определяет, где произойдет первая ошибка.
field_type = ctypes.c_ulonglong
:
000111110 ...$$$$$.
001111111 ..$$$$$$$
011011011 .$$.$$.$$
110010011 $$..$..$$
110110111 $$.$$.$$$
111111111 $$$$$$$$$
101111011 $.$$$$.$$
100001101 $....$$.$ # <- first error
100111000 $..$$$...
111110000 $$$$$....
field_type = ctypes.c_uint32
:
000111110 ...$$$$$.
001111111 ..$$$$$$$
011011011 .$$.$$.$$
001111011 ..$$$$.$$ # <- first error
011111111 .$$$$$$$$
111110111 $$$$$.$$$
100001101 $....$$.$
100111000 $..$$$...
111110000 $$$$$....
000000000 .........
field_type = ctypes.c_uint16
:
000111110 ...$$$$$.
110110110 $$.$$.$$. # <- first error
001111011 ..$$$$.$$
111111101 $$$$$$$.$
100001101 $....$$.$
001111100 ..$$$$$..
000000000 .........
000000000 .........
000000000 .........
000000000 .........
Я не уверен, что здесь происходит: длина в 9 бит удобно вписывается во все эти типы полей (64, 32 и 16 бит соответственно), поэтому не следует ли это упаковать так, как ожидалось? Чего мне не хватает и как это исправить?
Вы можете увидеть это, если распечатаете PackedBitmap.lineXY
. Он покажет вам смещение для байта и бита. В зависимости от field_type
вы можете увидеть, где что-то идет не так. Возможно, об этом стоит сообщить сопровождающим CPython. Но учтите, что в C
это тоже не работает.
@jasonharper Спасибо, я смотрел на bitstring
, но bitarray
может быть даже лучшим инструментом для этой работы.
Похоже, у ctypes.Strucuture
возникают проблемы при пересечении границ байтов (в зависимости от типа поля).
Мы можем напечатать смещение, которое используют поля:
print(PackedBitmap.line00)
...
print(PackedBitmap.line09)
Что дает нам
<Field type=c_ulonglong_be, ofs=0:55, bits=9> # 1
<Field type=c_ulonglong_be, ofs=0:46, bits=9> # 2
<Field type=c_ulonglong_be, ofs=0:37, bits=9> # 3
<Field type=c_ulonglong_be, ofs=0:28, bits=9> # 4
<Field type=c_ulonglong_be, ofs=0:19, bits=9> # 5
<Field type=c_ulonglong_be, ofs=0:10, bits=9> # 6
<Field type=c_ulonglong_be, ofs=0:1, bits=9> # 7
<Field type=c_ulonglong_be, ofs=8:55, bits=9> # 8
<Field type=c_ulonglong_be, ofs=8:46, bits=9> # 9
<Field type=c_ulonglong_be, ofs=8:37, bits=9> # 10
55
к 63
. Это отлично1
к 9
. Это тоже нормально55
к 63
во втором байте. Итак, нам не хватает бита 0
байта 0
.Это может быть ошибка или, по крайней мере, неожиданная. Когда вы переключитесь на другие типы данных, вы увидите, что ошибка возникает первой при переключении на следующий байт.
Возможно, стоит сообщить об этом разработчикам CPython. На мой взгляд, это должно быть упомянуто как минимум в документации. Я не нашел такой подсказки.
Однако вот реализация, которая не использует ни ctypes
, ни struct
:
from collections.abc import Iterator
from itertools import batched
raw = b'\x1F\x1F\xDB\x79\x3D\xBF\xFE\xF7\x86\xCE\x3E'
def regroup_bits(buffer: bytes, bits: int) -> Iterator[str]:
binary = ''.join(f'{byte:08b}' for byte in buffer)
return (''.join(batch) for batch in batched(binary, bits))
for i in regroup_bits(raw, 9):
print(i.replace('0', '.').replace('1', '$'))
Это можно оптимизировать, чтобы лучше использовать память, не создавая одну большую строку, а вместо этого используя столько бит, сколько вам нужно, а затем yield
9-битное значение.
Также обратите внимание, что эта реализация не дополняет последнее значение.
Битовые поля определяются реализацией в C. Обычно (я никогда не видел, чтобы это было реализовано по-другому) дополняется следующий элемент, если битовое поле не полностью помещается в элемент. ctypes
следует конвенции.
Спасибо за понимание границ элементов... да, я вижу, что там происходит. Подход с конкатенацией строк достаточно элегантен и, похоже, работает нормально! Я мог бы попробовать что-нибудь и с bitarray
, но на данный момент это кажется наиболее гибким решением для поддержки различной ширины.
Есть несколько способов решить эту проблему. Вот более удобный для разработчиков Python вариант, основанный на строковых операциях. Из-за этого он намного медленнее (и я бы сказал менее элегантен), чем немного манипулирующий аналог.
код00.py:
#!/usr/bin/env python
import string
import struct
import sys
def glyph(data, bit_width=9, display_chars = ".$"):
binary = "".join(f"{e:08b}" for e in data) # Convert to binary string
tbl = binary.maketrans("01", display_chars)
text = binary.translate(tbl) # Replace 0s and 1s by the given chars
rows, last = divmod(len(data) * 8, bit_width)
if last:
text += display_chars[0] * (bit_width - last) # Pad the string with 0s correspondent (if neccessary)
rows += 1
ret = []
for row_idx in range(rows):
ret.append(text[bit_width * row_idx: bit_width * (row_idx + 1)]) # Split the string in chunks with the required length
return tuple(ret)
def main(*argv):
data = b"\x1F\x1F\xDB\x79\x3D\xBF\xFE\xF7\x86\xCE\x3E"
print(data)
lines = glyph(data)
print("Glyph:")
for line in lines:
print(line)
if __name__ == "__main__":
print(
"Python {:s} {:03d}bit on {:s}\n".format(
" ".join(elem.strip() for elem in sys.version.split("\n")),
64 if sys.maxsize > 0x100000000 else 32,
sys.platform,
)
)
rc = main(*sys.argv[1:])
print("\nDone.\n")
sys.exit(rc)
Выход:
(qaic-env) [cfati@cfati-5510-0:/mnt/e/Work/Dev/StackExchange/StackOverflow/q078769490]> python ./code00.py Python 3.8.19 (default, Apr 6 2024, 17:58:10) [GCC 11.4.0] 064bit on linux b'\x1f\x1f\xdby=\xbf\xfe\xf7\x86\xce>' Glyph: ...$$$$$. ..$$$$$$$ .$$.$$.$$ $$..$..$$ $$.$$.$$$ $$$$$$$$$ $.$$$$.$$ $$....$$. $$..$$$.. .$$$$$... Done.
Вам лучше обрабатывать биты самостоятельно, в коде Python, чем полагаться на ctypes - в любом случае ctypes не будет намного быстрее для манипуляций с битами в своих структурах, и он будет «думать о типе данных контейнера» - это то есть ваши битовые поля должны содержаться в типе данных C, например int64 - вот почему вы получаете несовпадения при пересечении границы первых 8 байтов (64 бита) и почему это меняется при изменении типа контейнера на другой, чем ulong.
Оказывается, проще создать итератор, который будет выбирать каждый байт в последовательности и по очереди выдавать биты, а затем обернуть его другим итератором, который группирует эти биты в строки глифов.
class GlyphRender:
def __init__(self, byte_seq, width=9, ones = "#", zeros = " "):
self.byte_seq = byte_seq
self.width = width
self.ones = ones
self.zeros = zeros
def bit_iter(self):
for byte in self.byte_seq:
for power in range(7, -1, -1):
yield bool(byte & 2 ** power)
def __iter__(self):
line = ""
for bit in self.bit_iter():
line += self.ones if bit else self.zeros
if len(line) == self.width:
yield line
line = ""
if line:
yield line
def __str__(self):
return "\n".join(self)
И запускаем это на REPL:
In [49]: print(GlyphRender(b'\x1F\x1F\xDB\x79\x3D\xBF\xFE\xF7\x86\xCE\x3E'))
#####
#######
## ## ##
## # ##
## ## ###
#########
# #### ##
## ##
## ###
#####
Не то чтобы я рекомендовал поступать именно так, но вот способ сделать это в ctypes
. Используйте свойства для создания 9-битных полей. Вы могли бы еще более изощренно использовать собственные дескрипторы, чтобы избавиться от повторений, но я не собираюсь вдаваться в подробности. Обратите внимание, что другая реализация Python (это Python для 64-разрядной версии Windows) может не работать из-за того, что битовые поля определяются реализацией.
import ctypes as ct
class PackedBitmap(ct.BigEndianStructure):
_fields_ = (('_line01', ct.c_ubyte, 8),
('_line02', ct.c_ubyte, 1),
('_line11', ct.c_ubyte, 7),
('_line12', ct.c_ubyte, 2),
('_line21', ct.c_ubyte, 6),
('_line22', ct.c_ubyte, 3),
('_line31', ct.c_ubyte, 5),
('_line32', ct.c_ubyte, 4),
('_line41', ct.c_ubyte, 4),
('_line42', ct.c_ubyte, 5),
('_line51', ct.c_ubyte, 3),
('_line52', ct.c_ubyte, 6),
('_line61', ct.c_ubyte, 2),
('_line62', ct.c_ubyte, 7),
('_line71', ct.c_ubyte, 1),
('_line72', ct.c_ubyte, 8),
('_line81', ct.c_ubyte, 8),
('_line82', ct.c_ubyte, 1),
('_line91', ct.c_ubyte, 7))
@property
def line00(self):
return self._line01 << 1 | self._line02
@line00.setter
def line00(self, value):
self._line01 = value >> 1
self._line02 = value & 0x01
@property
def line01(self):
return self._line11 << 2 | self._line12
@line01.setter
def line01(self, value):
self._line11 = value >> 2
self._line12 = value & 0x03
@property
def line02(self):
return self._line21 << 3 | self._line22
@line02.setter
def line02(self, value):
self._line21 = value >> 3
self._line22 = value & 0x07
@property
def line03(self):
return self._line31 << 4 | self._line32
@line03.setter
def line03(self, value):
self._line31 = value >> 4
self._line32 = value & 0x0F
@property
def line04(self):
return self._line41 << 5 | self._line42
@line04.setter
def line04(self, value):
self._line41 = value >> 5
self._line42 = value & 0x1F
@property
def line05(self):
return self._line51 << 6 | self._line52
@line05.setter
def line05(self, value):
self._line51 = value >> 6
self._line52 = value & 0x3F
@property
def line06(self):
return self._line61 << 7 | self._line62
@line06.setter
def line06(self, value):
self._line61 = value >> 7
self._line62 = value & 0xEF
@property
def line07(self):
return self._line71 << 8 | self._line72
@line07.setter
def line07(self, value):
self._line71 = value >> 8
self._line72 = value
@property
def line08(self):
return self._line81 << 1 | self._line82
@line08.setter
def line08(self, value):
self._line81 = value >> 1
self._line82 = value & 0x01
@property
def line09(self):
return self._line91 << 2
@line09.setter
def line09(self, value):
self._line91 = value >> 2
self._line92 = 0
def __repr__(self):
return (f'PackedBitmap(0b{self.line00:09b},\n'
f' 0b{self.line01:09b},\n'
f' 0b{self.line02:09b},\n'
f' 0b{self.line03:09b},\n'
f' 0b{self.line04:09b},\n'
f' 0b{self.line05:09b},\n'
f' 0b{self.line06:09b},\n'
f' 0b{self.line07:09b},\n'
f' 0b{self.line08:09b},\n'
f' 0b{self.line09:09b})\n')
def __str__(self):
return (f'{self.line00:09b}\n'
f'{self.line01:09b}\n'
f'{self.line02:09b}\n'
f'{self.line03:09b}\n'
f'{self.line04:09b}\n'
f'{self.line05:09b}\n'
f'{self.line06:09b}\n'
f'{self.line07:09b}\n'
f'{self.line08:09b}\n'
f'{self.line09:09b}\n').translate(str.maketrans('01', ' \N{FULL BLOCK}'))
bm = PackedBitmap.from_buffer_copy(bytes.fromhex('1F 1F DB 79 3D BF FE F7 86 CE 3E'))
print(bytes(bm).hex(' '))
print(repr(bm))
print(bm)
bm.line00 = 0b100000000
bm.line01 = 0b010000000
bm.line02 = 0b001000000
bm.line03 = 0b000100000
bm.line04 = 0b000010000
bm.line05 = 0b000001000
bm.line06 = 0b000000100
bm.line07 = 0b000000010
bm.line08 = 0b000000001
bm.line09 = 0b111111111
# Structure is 88 bits (11 bytes), but 10 lines of 9 bits is 90.
# Last two bits of line09 are forced zero.
print(bytes(bm).hex(' '))
print(repr(bm))
print(bm)
Выход:
1f 1f db 79 3d bf fe f7 86 ce 3e
PackedBitmap(0b000111110,
0b001111111,
0b011011011,
0b110010011,
0b110110111,
0b111111111,
0b101111011,
0b110000110,
0b110011100,
0b011111000)
█████
███████
██ ██ ██
██ █ ██
██ ██ ███
█████████
█ ████ ██
██ ██
██ ███
█████
80 20 08 02 00 80 20 08 02 00 ff
PackedBitmap(0b100000000,
0b010000000,
0b001000000,
0b000100000,
0b000010000,
0b000001000,
0b000000100,
0b000000010,
0b000000001,
0b111111100)
█
█
█
█
█
█
█
█
█
███████
вы, вероятно, могли бы добиться большего, просто отказавшись от ctypes и поработав с «bytearray», а затем использовать класс с методами __getitem__
и __setitem__
, если вы хотите иметь возможность изменять содержимое структуры. Это позволяет избежать всех жестко запрограммированных свойств и даже работать для данных с различной разрядностью.
@jsbueno Как я уже сказал, я не рекомендую это, просто показываю способ реализации ОП с помощью ctypes
. Это было полезно для меня, когда у меня была странная структура C, поле которой пересекало естественную границу в реальной ситуации.
Да, я определенно не привязан к ctypes (кстати, мне не нужна возможность изменять содержимое - только для экспорта данных в более удобный формат).
В комментарии было упомянуто, что ОП хочет «...только экспортировать данные в более удобный формат».
Вот реализация класса для этого:
import ctypes as ct
class PackedBitmap:
def __init__(self, byte_data):
# store bytes in a Python int with two trailing pad bits
self._data = int.from_bytes(byte_data, 'big') << 2
def __getitem__(self, index):
# Support only integer indices (no slices) for 10 9-bit elements.
if index < 0 or index > 9:
raise IndexError('list index out of range')
# Shift and mask the 9 bits represented by the index
return self._data >> (81 - index * 9) & 0x1FF
def __str__(self):
'''Pretty display.'''
xlat = str.maketrans('01', ' \N{FULL BLOCK}')
return '\n'.join([f'{n:09b}'.translate(xlat) for n in self])
def __repr__(self):
'''Debug display.'''
return f'PackedBitmap({(bm._data >> 2).to_bytes(11)!r})'
def __iter__(self):
# Make object iterable
for i in range(len(self)):
yield self[i]
def __len__(self):
# Make object fixed length
return 10
bm = PackedBitmap(bytes([0x1f,0x1f,0xdb,0x79,0x3d,0xbf,0xfe,0xf7,0x86,0xce,0x3e]))
print(bm) # graphic display
print(repr(bm)) # debug display
print(list(bm)) # convert 9-bit data to a Python list
for n in bm: # iterate and custom display
print(f'0x{n:03X} {n:09b}')
Выход:
█████
███████
██ ██ ██
██ █ ██
██ ██ ███
█████████
█ ████ ██
██ ██
██ ███
█████
PackedBitmap(b'\x1f\x1f\xdby=\xbf\xfe\xf7\x86\xce>')
[62, 127, 219, 403, 439, 511, 379, 390, 412, 248]
0x03E 000111110
0x07F 001111111
0x0DB 011011011
0x193 110010011
0x1B7 110110111
0x1FF 111111111
0x17B 101111011
0x186 110000110
0x19C 110011100
0x0F8 011111000
Спасибо! Если немного уточнить, этот «более удобный формат» будет выровнен по байтам, а ширина символа может быть произвольной (в разумных пределах, скажем, <= 32); 9 был примером конкретного файла. Я выбрал вариант идеи Вомбаца, основанной на строках, который было тривиально адаптировать к этим потребностям, хотя в противном случае ваше решение, вероятно, было бы более эффективным и расширяемым.
Ctypes, очевидно, не позволяет битовому полю типа X охватывать более одного элемента типа X; например, с 64-битным X вы получаете семь 9-битных полей общей длиной 63 бита, затем один бит пропускается, так что следующее поле начинается в новом элементе ulonglong. Вы не сможете таким образом анализировать растровые данные. Вместо этого я бы предложил пакет
bitarray
: в нем есть метод.frombytes()
, который может напрямую читать ваши данные, а затем вы можете извлекать строки растрового изображения произвольного размера просто с помощью нарезки.