spiffsgen.py
1 #!/usr/bin/env python 2 # 3 # spiffsgen is a tool used to generate a spiffs image from a directory 4 # 5 # SPDX-FileCopyrightText: 2019-2022 Espressif Systems (Shanghai) CO LTD 6 # SPDX-License-Identifier: Apache-2.0 7 8 from __future__ import division, print_function 9 10 import argparse 11 import io 12 import math 13 import os 14 import struct 15 16 try: 17 import typing 18 19 TSP = typing.TypeVar('TSP', bound='SpiffsObjPageWithIdx') 20 ObjIdsItem = typing.Tuple[int, typing.Type[TSP]] 21 except ImportError: 22 pass 23 24 25 SPIFFS_PH_FLAG_USED_FINAL_INDEX = 0xF8 26 SPIFFS_PH_FLAG_USED_FINAL = 0xFC 27 28 SPIFFS_PH_FLAG_LEN = 1 29 SPIFFS_PH_IX_SIZE_LEN = 4 30 SPIFFS_PH_IX_OBJ_TYPE_LEN = 1 31 SPIFFS_TYPE_FILE = 1 32 33 # Based on typedefs under spiffs_config.h 34 SPIFFS_OBJ_ID_LEN = 2 # spiffs_obj_id 35 SPIFFS_SPAN_IX_LEN = 2 # spiffs_span_ix 36 SPIFFS_PAGE_IX_LEN = 2 # spiffs_page_ix 37 SPIFFS_BLOCK_IX_LEN = 2 # spiffs_block_ix 38 39 40 class SpiffsBuildConfig(object): 41 def __init__(self, 42 page_size, # type: int 43 page_ix_len, # type: int 44 block_size, # type: int 45 block_ix_len, # type: int 46 meta_len, # type: int 47 obj_name_len, # type: int 48 obj_id_len, # type: int 49 span_ix_len, # type: int 50 packed, # type: bool 51 aligned, # type: bool 52 endianness, # type: str 53 use_magic, # type: bool 54 use_magic_len, # type: bool 55 aligned_obj_ix_tables # type: bool 56 ): 57 if block_size % page_size != 0: 58 raise RuntimeError('block size should be a multiple of page size') 59 60 self.page_size = page_size 61 self.block_size = block_size 62 self.obj_id_len = obj_id_len 63 self.span_ix_len = span_ix_len 64 self.packed = packed 65 self.aligned = aligned 66 self.obj_name_len = obj_name_len 67 self.meta_len = meta_len 68 self.page_ix_len = page_ix_len 69 self.block_ix_len = block_ix_len 70 self.endianness = endianness 71 self.use_magic = use_magic 72 self.use_magic_len = use_magic_len 73 self.aligned_obj_ix_tables = aligned_obj_ix_tables 74 75 self.PAGES_PER_BLOCK = self.block_size // self.page_size 76 self.OBJ_LU_PAGES_PER_BLOCK = int(math.ceil(self.block_size / self.page_size * self.obj_id_len / self.page_size)) 77 self.OBJ_USABLE_PAGES_PER_BLOCK = self.PAGES_PER_BLOCK - self.OBJ_LU_PAGES_PER_BLOCK 78 79 self.OBJ_LU_PAGES_OBJ_IDS_LIM = self.page_size // self.obj_id_len 80 81 self.OBJ_DATA_PAGE_HEADER_LEN = self.obj_id_len + self.span_ix_len + SPIFFS_PH_FLAG_LEN 82 83 pad = 4 - (4 if self.OBJ_DATA_PAGE_HEADER_LEN % 4 == 0 else self.OBJ_DATA_PAGE_HEADER_LEN % 4) 84 85 self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED = self.OBJ_DATA_PAGE_HEADER_LEN + pad 86 self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED_PAD = pad 87 self.OBJ_DATA_PAGE_CONTENT_LEN = self.page_size - self.OBJ_DATA_PAGE_HEADER_LEN 88 89 self.OBJ_INDEX_PAGES_HEADER_LEN = (self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED + SPIFFS_PH_IX_SIZE_LEN + 90 SPIFFS_PH_IX_OBJ_TYPE_LEN + self.obj_name_len + self.meta_len) 91 if aligned_obj_ix_tables: 92 self.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED = (self.OBJ_INDEX_PAGES_HEADER_LEN + SPIFFS_PAGE_IX_LEN - 1) & ~(SPIFFS_PAGE_IX_LEN - 1) 93 self.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED_PAD = self.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED - self.OBJ_INDEX_PAGES_HEADER_LEN 94 else: 95 self.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED = self.OBJ_INDEX_PAGES_HEADER_LEN 96 self.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED_PAD = 0 97 98 self.OBJ_INDEX_PAGES_OBJ_IDS_HEAD_LIM = (self.page_size - self.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED) // self.block_ix_len 99 self.OBJ_INDEX_PAGES_OBJ_IDS_LIM = (self.page_size - self.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED) // self.block_ix_len 100 101 102 class SpiffsFullError(RuntimeError): 103 pass 104 105 106 class SpiffsPage(object): 107 _endianness_dict = { 108 'little': '<', 109 'big': '>' 110 } 111 112 _len_dict = { 113 1: 'B', 114 2: 'H', 115 4: 'I', 116 8: 'Q' 117 } 118 119 def __init__(self, bix, build_config): # type: (int, SpiffsBuildConfig) -> None 120 self.build_config = build_config 121 self.bix = bix 122 123 def to_binary(self): # type: () -> bytes 124 raise NotImplementedError() 125 126 127 class SpiffsObjPageWithIdx(SpiffsPage): 128 def __init__(self, obj_id, build_config): # type: (int, SpiffsBuildConfig) -> None 129 super(SpiffsObjPageWithIdx, self).__init__(0, build_config) 130 self.obj_id = obj_id 131 132 def to_binary(self): # type: () -> bytes 133 raise NotImplementedError() 134 135 136 class SpiffsObjLuPage(SpiffsPage): 137 def __init__(self, bix, build_config): # type: (int, SpiffsBuildConfig) -> None 138 SpiffsPage.__init__(self, bix, build_config) 139 140 self.obj_ids_limit = self.build_config.OBJ_LU_PAGES_OBJ_IDS_LIM 141 self.obj_ids = list() # type: typing.List[ObjIdsItem] 142 143 def _calc_magic(self, blocks_lim): # type: (int) -> int 144 # Calculate the magic value mirroring computation done by the macro SPIFFS_MAGIC defined in 145 # spiffs_nucleus.h 146 magic = 0x20140529 ^ self.build_config.page_size 147 if self.build_config.use_magic_len: 148 magic = magic ^ (blocks_lim - self.bix) 149 # narrow the result to build_config.obj_id_len bytes 150 mask = (2 << (8 * self.build_config.obj_id_len)) - 1 151 return magic & mask 152 153 def register_page(self, page): # type: (TSP) -> None 154 if not self.obj_ids_limit > 0: 155 raise SpiffsFullError() 156 157 obj_id = (page.obj_id, page.__class__) 158 self.obj_ids.append(obj_id) 159 self.obj_ids_limit -= 1 160 161 def to_binary(self): # type: () -> bytes 162 img = b'' 163 164 for (obj_id, page_type) in self.obj_ids: 165 if page_type == SpiffsObjIndexPage: 166 obj_id ^= (1 << ((self.build_config.obj_id_len * 8) - 1)) 167 img += struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] + 168 SpiffsPage._len_dict[self.build_config.obj_id_len], obj_id) 169 170 assert len(img) <= self.build_config.page_size 171 172 img += b'\xFF' * (self.build_config.page_size - len(img)) 173 174 return img 175 176 def magicfy(self, blocks_lim): # type: (int) -> None 177 # Only use magic value if no valid obj id has been written to the spot, which is the 178 # spot taken up by the last obj id on last lookup page. The parent is responsible 179 # for determining which is the last lookup page and calling this function. 180 remaining = self.obj_ids_limit 181 empty_obj_id_dict = { 182 1: 0xFF, 183 2: 0xFFFF, 184 4: 0xFFFFFFFF, 185 8: 0xFFFFFFFFFFFFFFFF 186 } 187 if remaining >= 2: 188 for i in range(remaining): 189 if i == remaining - 2: 190 self.obj_ids.append((self._calc_magic(blocks_lim), SpiffsObjDataPage)) 191 break 192 else: 193 self.obj_ids.append((empty_obj_id_dict[self.build_config.obj_id_len], SpiffsObjDataPage)) 194 self.obj_ids_limit -= 1 195 196 197 class SpiffsObjIndexPage(SpiffsObjPageWithIdx): 198 def __init__(self, obj_id, span_ix, size, name, build_config 199 ): # type: (int, int, int, str, SpiffsBuildConfig) -> None 200 super(SpiffsObjIndexPage, self).__init__(obj_id, build_config) 201 self.span_ix = span_ix 202 self.name = name 203 self.size = size 204 205 if self.span_ix == 0: 206 self.pages_lim = self.build_config.OBJ_INDEX_PAGES_OBJ_IDS_HEAD_LIM 207 else: 208 self.pages_lim = self.build_config.OBJ_INDEX_PAGES_OBJ_IDS_LIM 209 210 self.pages = list() # type: typing.List[int] 211 212 def register_page(self, page): # type: (SpiffsObjDataPage) -> None 213 if not self.pages_lim > 0: 214 raise SpiffsFullError 215 216 self.pages.append(page.offset) 217 self.pages_lim -= 1 218 219 def to_binary(self): # type: () -> bytes 220 obj_id = self.obj_id ^ (1 << ((self.build_config.obj_id_len * 8) - 1)) 221 img = struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] + 222 SpiffsPage._len_dict[self.build_config.obj_id_len] + 223 SpiffsPage._len_dict[self.build_config.span_ix_len] + 224 SpiffsPage._len_dict[SPIFFS_PH_FLAG_LEN], 225 obj_id, 226 self.span_ix, 227 SPIFFS_PH_FLAG_USED_FINAL_INDEX) 228 229 # Add padding before the object index page specific information 230 img += b'\xFF' * self.build_config.OBJ_DATA_PAGE_HEADER_LEN_ALIGNED_PAD 231 232 # If this is the first object index page for the object, add filname, type 233 # and size information 234 if self.span_ix == 0: 235 img += struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] + 236 SpiffsPage._len_dict[SPIFFS_PH_IX_SIZE_LEN] + 237 SpiffsPage._len_dict[SPIFFS_PH_FLAG_LEN], 238 self.size, 239 SPIFFS_TYPE_FILE) 240 241 img += self.name.encode() + (b'\x00' * ( 242 (self.build_config.obj_name_len - len(self.name)) 243 + self.build_config.meta_len 244 + self.build_config.OBJ_INDEX_PAGES_HEADER_LEN_ALIGNED_PAD)) 245 246 # Finally, add the page index of daa pages 247 for page in self.pages: 248 page = page >> int(math.log(self.build_config.page_size, 2)) 249 img += struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] + 250 SpiffsPage._len_dict[self.build_config.page_ix_len], page) 251 252 assert len(img) <= self.build_config.page_size 253 254 img += b'\xFF' * (self.build_config.page_size - len(img)) 255 256 return img 257 258 259 class SpiffsObjDataPage(SpiffsObjPageWithIdx): 260 def __init__(self, offset, obj_id, span_ix, contents, build_config 261 ): # type: (int, int, int, bytes, SpiffsBuildConfig) -> None 262 super(SpiffsObjDataPage, self).__init__(obj_id, build_config) 263 self.span_ix = span_ix 264 self.contents = contents 265 self.offset = offset 266 267 def to_binary(self): # type: () -> bytes 268 img = struct.pack(SpiffsPage._endianness_dict[self.build_config.endianness] + 269 SpiffsPage._len_dict[self.build_config.obj_id_len] + 270 SpiffsPage._len_dict[self.build_config.span_ix_len] + 271 SpiffsPage._len_dict[SPIFFS_PH_FLAG_LEN], 272 self.obj_id, 273 self.span_ix, 274 SPIFFS_PH_FLAG_USED_FINAL) 275 276 img += self.contents 277 278 assert len(img) <= self.build_config.page_size 279 280 img += b'\xFF' * (self.build_config.page_size - len(img)) 281 282 return img 283 284 285 class SpiffsBlock(object): 286 def _reset(self): # type: () -> None 287 self.cur_obj_index_span_ix = 0 288 self.cur_obj_data_span_ix = 0 289 self.cur_obj_id = 0 290 self.cur_obj_idx_page = None # type: typing.Optional[SpiffsObjIndexPage] 291 292 def __init__(self, bix, build_config): # type: (int, SpiffsBuildConfig) -> None 293 self.build_config = build_config 294 self.offset = bix * self.build_config.block_size 295 self.remaining_pages = self.build_config.OBJ_USABLE_PAGES_PER_BLOCK 296 self.pages = list() # type: typing.List[SpiffsPage] 297 self.bix = bix 298 299 lu_pages = list() 300 for i in range(self.build_config.OBJ_LU_PAGES_PER_BLOCK): 301 page = SpiffsObjLuPage(self.bix, self.build_config) 302 lu_pages.append(page) 303 304 self.pages.extend(lu_pages) 305 306 self.lu_page_iter = iter(lu_pages) 307 self.lu_page = next(self.lu_page_iter) 308 309 self._reset() 310 311 def _register_page(self, page): # type: (TSP) -> None 312 if isinstance(page, SpiffsObjDataPage): 313 assert self.cur_obj_idx_page is not None 314 self.cur_obj_idx_page.register_page(page) # can raise SpiffsFullError 315 316 try: 317 self.lu_page.register_page(page) 318 except SpiffsFullError: 319 self.lu_page = next(self.lu_page_iter) 320 try: 321 self.lu_page.register_page(page) 322 except AttributeError: # no next lookup page 323 # Since the amount of lookup pages is pre-computed at every block instance, 324 # this should never occur 325 raise RuntimeError('invalid attempt to add page to a block when there is no more space in lookup') 326 327 self.pages.append(page) 328 329 def begin_obj(self, obj_id, size, name, obj_index_span_ix=0, obj_data_span_ix=0 330 ): # type: (int, int, str, int, int) -> None 331 if not self.remaining_pages > 0: 332 raise SpiffsFullError() 333 self._reset() 334 335 self.cur_obj_id = obj_id 336 self.cur_obj_index_span_ix = obj_index_span_ix 337 self.cur_obj_data_span_ix = obj_data_span_ix 338 339 page = SpiffsObjIndexPage(obj_id, self.cur_obj_index_span_ix, size, name, self.build_config) 340 self._register_page(page) 341 342 self.cur_obj_idx_page = page 343 344 self.remaining_pages -= 1 345 self.cur_obj_index_span_ix += 1 346 347 def update_obj(self, contents): # type: (bytes) -> None 348 if not self.remaining_pages > 0: 349 raise SpiffsFullError() 350 page = SpiffsObjDataPage(self.offset + (len(self.pages) * self.build_config.page_size), 351 self.cur_obj_id, self.cur_obj_data_span_ix, contents, self.build_config) 352 353 self._register_page(page) 354 355 self.cur_obj_data_span_ix += 1 356 self.remaining_pages -= 1 357 358 def end_obj(self): # type: () -> None 359 self._reset() 360 361 def is_full(self): # type: () -> bool 362 return self.remaining_pages <= 0 363 364 def to_binary(self, blocks_lim): # type: (int) -> bytes 365 img = b'' 366 367 if self.build_config.use_magic: 368 for (idx, page) in enumerate(self.pages): 369 if idx == self.build_config.OBJ_LU_PAGES_PER_BLOCK - 1: 370 assert isinstance(page, SpiffsObjLuPage) 371 page.magicfy(blocks_lim) 372 img += page.to_binary() 373 else: 374 for page in self.pages: 375 img += page.to_binary() 376 377 assert len(img) <= self.build_config.block_size 378 379 img += b'\xFF' * (self.build_config.block_size - len(img)) 380 return img 381 382 383 class SpiffsFS(object): 384 def __init__(self, img_size, build_config): # type: (int, SpiffsBuildConfig) -> None 385 if img_size % build_config.block_size != 0: 386 raise RuntimeError('image size should be a multiple of block size') 387 388 self.img_size = img_size 389 self.build_config = build_config 390 391 self.blocks = list() # type: typing.List[SpiffsBlock] 392 self.blocks_lim = self.img_size // self.build_config.block_size 393 self.remaining_blocks = self.blocks_lim 394 self.cur_obj_id = 1 # starting object id 395 396 def _create_block(self): # type: () -> SpiffsBlock 397 if self.is_full(): 398 raise SpiffsFullError('the image size has been exceeded') 399 400 block = SpiffsBlock(len(self.blocks), self.build_config) 401 self.blocks.append(block) 402 self.remaining_blocks -= 1 403 return block 404 405 def is_full(self): # type: () -> bool 406 return self.remaining_blocks <= 0 407 408 def create_file(self, img_path, file_path): # type: (str, str) -> None 409 if len(img_path) > self.build_config.obj_name_len: 410 raise RuntimeError("object name '%s' too long" % img_path) 411 412 name = img_path 413 414 with open(file_path, 'rb') as obj: 415 contents = obj.read() 416 417 stream = io.BytesIO(contents) 418 419 try: 420 block = self.blocks[-1] 421 block.begin_obj(self.cur_obj_id, len(contents), name) 422 except (IndexError, SpiffsFullError): 423 block = self._create_block() 424 block.begin_obj(self.cur_obj_id, len(contents), name) 425 426 contents_chunk = stream.read(self.build_config.OBJ_DATA_PAGE_CONTENT_LEN) 427 428 while contents_chunk: 429 try: 430 block = self.blocks[-1] 431 try: 432 # This can fail because either (1) all the pages in block have been 433 # used or (2) object index has been exhausted. 434 block.update_obj(contents_chunk) 435 except SpiffsFullError: 436 # If its (1), use the outer exception handler 437 if block.is_full(): 438 raise SpiffsFullError 439 # If its (2), write another object index page 440 block.begin_obj(self.cur_obj_id, len(contents), name, 441 obj_index_span_ix=block.cur_obj_index_span_ix, 442 obj_data_span_ix=block.cur_obj_data_span_ix) 443 continue 444 except (IndexError, SpiffsFullError): 445 # All pages in the block have been exhausted. Create a new block, copying 446 # the previous state of the block to a new one for the continuation of the 447 # current object 448 prev_block = block 449 block = self._create_block() 450 block.cur_obj_id = prev_block.cur_obj_id 451 block.cur_obj_idx_page = prev_block.cur_obj_idx_page 452 block.cur_obj_data_span_ix = prev_block.cur_obj_data_span_ix 453 block.cur_obj_index_span_ix = prev_block.cur_obj_index_span_ix 454 continue 455 456 contents_chunk = stream.read(self.build_config.OBJ_DATA_PAGE_CONTENT_LEN) 457 458 block.end_obj() 459 460 self.cur_obj_id += 1 461 462 def to_binary(self): # type: () -> bytes 463 img = b'' 464 all_blocks = [] 465 for block in self.blocks: 466 all_blocks.append(block.to_binary(self.blocks_lim)) 467 bix = len(self.blocks) 468 if self.build_config.use_magic: 469 # Create empty blocks with magic numbers 470 while self.remaining_blocks > 0: 471 block = SpiffsBlock(bix, self.build_config) 472 all_blocks.append(block.to_binary(self.blocks_lim)) 473 self.remaining_blocks -= 1 474 bix += 1 475 else: 476 # Just fill remaining spaces FF's 477 all_blocks.append(b'\xFF' * (self.img_size - len(all_blocks) * self.build_config.block_size)) 478 img += b''.join([blk for blk in all_blocks]) 479 return img 480 481 482 class CustomHelpFormatter(argparse.HelpFormatter): 483 """ 484 Similar to argparse.ArgumentDefaultsHelpFormatter, except it 485 doesn't add the default value if "(default:" is already present. 486 This helps in the case of options with action="store_false", like 487 --no-magic or --no-magic-len. 488 """ 489 def _get_help_string(self, action): # type: (argparse.Action) -> str 490 if action.help is None: 491 return '' 492 if '%(default)' not in action.help and '(default:' not in action.help: 493 if action.default is not argparse.SUPPRESS: 494 defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE] 495 if action.option_strings or action.nargs in defaulting_nargs: 496 return action.help + ' (default: %(default)s)' 497 return action.help 498 499 500 def main(): # type: () -> None 501 parser = argparse.ArgumentParser(description='SPIFFS Image Generator', 502 formatter_class=CustomHelpFormatter) 503 504 parser.add_argument('image_size', 505 help='Size of the created image') 506 507 parser.add_argument('base_dir', 508 help='Path to directory from which the image will be created') 509 510 parser.add_argument('output_file', 511 help='Created image output file path') 512 513 parser.add_argument('--page-size', 514 help='Logical page size. Set to value same as CONFIG_SPIFFS_PAGE_SIZE.', 515 type=int, 516 default=256) 517 518 parser.add_argument('--block-size', 519 help="Logical block size. Set to the same value as the flash chip's sector size (g_rom_flashchip.sector_size).", 520 type=int, 521 default=4096) 522 523 parser.add_argument('--obj-name-len', 524 help='File full path maximum length. Set to value same as CONFIG_SPIFFS_OBJ_NAME_LEN.', 525 type=int, 526 default=32) 527 528 parser.add_argument('--meta-len', 529 help='File metadata length. Set to value same as CONFIG_SPIFFS_META_LENGTH.', 530 type=int, 531 default=4) 532 533 parser.add_argument('--use-magic', 534 dest='use_magic', 535 help='Use magic number to create an identifiable SPIFFS image. Specify if CONFIG_SPIFFS_USE_MAGIC.', 536 action='store_true') 537 538 parser.add_argument('--no-magic', 539 dest='use_magic', 540 help='Inverse of --use-magic (default: --use-magic is enabled)', 541 action='store_false') 542 543 parser.add_argument('--use-magic-len', 544 dest='use_magic_len', 545 help='Use position in memory to create different magic numbers for each block. Specify if CONFIG_SPIFFS_USE_MAGIC_LENGTH.', 546 action='store_true') 547 548 parser.add_argument('--no-magic-len', 549 dest='use_magic_len', 550 help='Inverse of --use-magic-len (default: --use-magic-len is enabled)', 551 action='store_false') 552 553 parser.add_argument('--follow-symlinks', 554 help='Take into account symbolic links during partition image creation.', 555 action='store_true') 556 557 parser.add_argument('--big-endian', 558 help='Specify if the target architecture is big-endian. If not specified, little-endian is assumed.', 559 action='store_true') 560 561 parser.add_argument('--aligned-obj-ix-tables', 562 action='store_true', 563 help='Use aligned object index tables. Specify if SPIFFS_ALIGNED_OBJECT_INDEX_TABLES is set.') 564 565 parser.set_defaults(use_magic=True, use_magic_len=True) 566 567 args = parser.parse_args() 568 569 if not os.path.exists(args.base_dir): 570 raise RuntimeError('given base directory %s does not exist' % args.base_dir) 571 572 with open(args.output_file, 'wb') as image_file: 573 image_size = int(args.image_size, 0) 574 spiffs_build_default = SpiffsBuildConfig(args.page_size, SPIFFS_PAGE_IX_LEN, 575 args.block_size, SPIFFS_BLOCK_IX_LEN, args.meta_len, 576 args.obj_name_len, SPIFFS_OBJ_ID_LEN, SPIFFS_SPAN_IX_LEN, 577 True, True, 'big' if args.big_endian else 'little', 578 args.use_magic, args.use_magic_len, args.aligned_obj_ix_tables) 579 580 spiffs = SpiffsFS(image_size, spiffs_build_default) 581 582 for root, dirs, files in os.walk(args.base_dir, followlinks=args.follow_symlinks): 583 for f in files: 584 full_path = os.path.join(root, f) 585 spiffs.create_file('/' + os.path.relpath(full_path, args.base_dir).replace('\\', '/'), full_path) 586 587 image = spiffs.to_binary() 588 589 image_file.write(image) 590 591 592 if __name__ == '__main__': 593 main()