1# -*- coding: utf-8 -*- 2# imageio is distributed under the terms of the (new) BSD License. 3# This code was taken from visvis/vvmovy/images2swf.py 4 5# styletest: ignore E261 6 7""" 8Provides a function (write_swf) to store a series of numpy arrays in an 9SWF movie, that can be played on a wide range of OS's. 10 11In desperation of wanting to share animated images, and then lacking a good 12writer for animated gif or .avi, I decided to look into SWF. This format 13is very well documented. 14 15This is a pure python module to create an SWF file that shows a series 16of images. The images are stored using the DEFLATE algorithm (same as 17PNG and ZIP and which is included in the standard Python distribution). 18As this compression algorithm is much more effective than that used in 19GIF images, we obtain better quality (24 bit colors + alpha channel) 20while still producesing smaller files (a test showed ~75%). Although 21SWF also allows for JPEG compression, doing so would probably require 22a third party library for the JPEG encoding/decoding, we could 23perhaps do this via Pillow or freeimage. 24 25sources and tools: 26 27- SWF on wikipedia 28- Adobes "SWF File Format Specification" version 10 29 (http://www.adobe.com/devnet/swf/pdf/swf_file_format_spec_v10.pdf) 30- swftools (swfdump in specific) for debugging 31- iwisoft swf2avi can be used to convert swf to avi/mpg/flv with really 32 good quality, while file size is reduced with factors 20-100. 33 A good program in my opinion. The free version has the limitation 34 of a watermark in the upper left corner. 35 36""" 37 38import os 39import zlib 40import time # noqa 41import logging 42 43import numpy as np 44 45 46logger = logging.getLogger(__name__) 47 48# todo: use Pillow to support reading JPEG images from SWF? 49 50 51## Base functions and classes 52 53 54class BitArray: 55 """ Dynamic array of bits that automatically resizes 56 with factors of two. 57 Append bits using .append() or += 58 You can reverse bits using .reverse() 59 """ 60 61 def __init__(self, initvalue=None): 62 self.data = np.zeros((16,), dtype=np.uint8) 63 self._len = 0 64 if initvalue is not None: 65 self.append(initvalue) 66 67 def __len__(self): 68 return self._len # self.data.shape[0] 69 70 def __repr__(self): 71 return self.data[: self._len].tostring().decode("ascii") 72 73 def _checkSize(self): 74 # check length... grow if necessary 75 arraylen = self.data.shape[0] 76 if self._len >= arraylen: 77 tmp = np.zeros((arraylen * 2,), dtype=np.uint8) 78 tmp[: self._len] = self.data[: self._len] 79 self.data = tmp 80 81 def __add__(self, value): 82 self.append(value) 83 return self 84 85 def append(self, bits): 86 87 # check input 88 if isinstance(bits, BitArray): 89 bits = str(bits) 90 if isinstance(bits, int): # pragma: no cover - we dont use it 91 bits = str(bits) 92 if not isinstance(bits, str): # pragma: no cover 93 raise ValueError("Append bits as strings or integers!") 94 95 # add bits 96 for bit in bits: 97 self.data[self._len] = ord(bit) 98 self._len += 1 99 self._checkSize() 100 101 def reverse(self): 102 """ In-place reverse. """ 103 tmp = self.data[: self._len].copy() 104 self.data[: self._len] = tmp[::-1] 105 106 def tobytes(self): 107 """ Convert to bytes. If necessary, 108 zeros are padded to the end (right side). 109 """ 110 bits = str(self) 111 112 # determine number of bytes 113 nbytes = 0 114 while nbytes * 8 < len(bits): 115 nbytes += 1 116 # pad 117 bits = bits.ljust(nbytes * 8, "0") 118 119 # go from bits to bytes 120 bb = bytes() 121 for i in range(nbytes): 122 tmp = int(bits[i * 8 : (i + 1) * 8], 2) 123 bb += int2uint8(tmp) 124 125 # done 126 return bb 127 128 129def int2uint32(i): 130 return int(i).to_bytes(4, "little") 131 132 133def int2uint16(i): 134 return int(i).to_bytes(2, "little") 135 136 137def int2uint8(i): 138 return int(i).to_bytes(1, "little") 139 140 141def int2bits(i, n=None): 142 """ convert int to a string of bits (0's and 1's in a string), 143 pad to n elements. Convert back using int(ss,2). """ 144 ii = i 145 146 # make bits 147 bb = BitArray() 148 while ii > 0: 149 bb += str(ii % 2) 150 ii = ii >> 1 151 bb.reverse() 152 153 # justify 154 if n is not None: 155 if len(bb) > n: # pragma: no cover 156 raise ValueError("int2bits fail: len larger than padlength.") 157 bb = str(bb).rjust(n, "0") 158 159 # done 160 return BitArray(bb) 161 162 163def bits2int(bb, n=8): 164 # Init 165 value = "" 166 167 # Get value in bits 168 for i in range(len(bb)): 169 b = bb[i : i + 1] 170 tmp = bin(ord(b))[2:] 171 # value += tmp.rjust(8,'0') 172 value = tmp.rjust(8, "0") + value 173 174 # Make decimal 175 return int(value[:n], 2) 176 177 178def get_type_and_len(bb): 179 """ bb should be 6 bytes at least 180 Return (type, length, length_of_full_tag) 181 """ 182 # Init 183 value = "" 184 185 # Get first 16 bits 186 for i in range(2): 187 b = bb[i : i + 1] 188 tmp = bin(ord(b))[2:] 189 # value += tmp.rjust(8,'0') 190 value = tmp.rjust(8, "0") + value 191 192 # Get type and length 193 type = int(value[:10], 2) 194 L = int(value[10:], 2) 195 L2 = L + 2 196 197 # Long tag header? 198 if L == 63: # '111111' 199 value = "" 200 for i in range(2, 6): 201 b = bb[i : i + 1] # becomes a single-byte bytes() 202 tmp = bin(ord(b))[2:] 203 # value += tmp.rjust(8,'0') 204 value = tmp.rjust(8, "0") + value 205 L = int(value, 2) 206 L2 = L + 6 207 208 # Done 209 return type, L, L2 210 211 212def signedint2bits(i, n=None): 213 """ convert signed int to a string of bits (0's and 1's in a string), 214 pad to n elements. Negative numbers are stored in 2's complement bit 215 patterns, thus positive numbers always start with a 0. 216 """ 217 218 # negative number? 219 ii = i 220 if i < 0: 221 # A negative number, -n, is represented as the bitwise opposite of 222 ii = abs(ii) - 1 # the positive-zero number n-1. 223 224 # make bits 225 bb = BitArray() 226 while ii > 0: 227 bb += str(ii % 2) 228 ii = ii >> 1 229 bb.reverse() 230 231 # justify 232 bb = "0" + str(bb) # always need the sign bit in front 233 if n is not None: 234 if len(bb) > n: # pragma: no cover 235 raise ValueError("signedint2bits fail: len larger than padlength.") 236 bb = bb.rjust(n, "0") 237 238 # was it negative? (then opposite bits) 239 if i < 0: 240 bb = bb.replace("0", "x").replace("1", "0").replace("x", "1") 241 242 # done 243 return BitArray(bb) 244 245 246def twits2bits(arr): 247 """ Given a few (signed) numbers, store them 248 as compactly as possible in the wat specifief by the swf format. 249 The numbers are multiplied by 20, assuming they 250 are twits. 251 Can be used to make the RECT record. 252 """ 253 254 # first determine length using non justified bit strings 255 maxlen = 1 256 for i in arr: 257 tmp = len(signedint2bits(i * 20)) 258 if tmp > maxlen: 259 maxlen = tmp 260 261 # build array 262 bits = int2bits(maxlen, 5) 263 for i in arr: 264 bits += signedint2bits(i * 20, maxlen) 265 266 return bits 267 268 269def floats2bits(arr): 270 """ Given a few (signed) numbers, convert them to bits, 271 stored as FB (float bit values). We always use 16.16. 272 Negative numbers are not (yet) possible, because I don't 273 know how the're implemented (ambiguity). 274 """ 275 bits = int2bits(31, 5) # 32 does not fit in 5 bits! 276 for i in arr: 277 if i < 0: # pragma: no cover 278 raise ValueError("Dit not implement negative floats!") 279 i1 = int(i) 280 i2 = i - i1 281 bits += int2bits(i1, 15) 282 bits += int2bits(i2 * 2 ** 16, 16) 283 return bits 284 285 286## Base Tag 287 288 289class Tag: 290 def __init__(self): 291 self.bytes = bytes() 292 self.tagtype = -1 293 294 def process_tag(self): 295 """ Implement this to create the tag. """ 296 raise NotImplementedError() 297 298 def get_tag(self): 299 """ Calls processTag and attaches the header. """ 300 self.process_tag() 301 302 # tag to binary 303 bits = int2bits(self.tagtype, 10) 304 305 # complete header uint16 thing 306 bits += "1" * 6 # = 63 = 0x3f 307 # make uint16 308 bb = int2uint16(int(str(bits), 2)) 309 310 # now add 32bit length descriptor 311 bb += int2uint32(len(self.bytes)) 312 313 # done, attach and return 314 bb += self.bytes 315 return bb 316 317 def make_rect_record(self, xmin, xmax, ymin, ymax): 318 """ Simply uses makeCompactArray to produce 319 a RECT Record. """ 320 return twits2bits([xmin, xmax, ymin, ymax]) 321 322 def make_matrix_record(self, scale_xy=None, rot_xy=None, trans_xy=None): 323 324 # empty matrix? 325 if scale_xy is None and rot_xy is None and trans_xy is None: 326 return "0" * 8 327 328 # init 329 bits = BitArray() 330 331 # scale 332 if scale_xy: 333 bits += "1" 334 bits += floats2bits([scale_xy[0], scale_xy[1]]) 335 else: 336 bits += "0" 337 338 # rotation 339 if rot_xy: 340 bits += "1" 341 bits += floats2bits([rot_xy[0], rot_xy[1]]) 342 else: 343 bits += "0" 344 345 # translation (no flag here) 346 if trans_xy: 347 bits += twits2bits([trans_xy[0], trans_xy[1]]) 348 else: 349 bits += twits2bits([0, 0]) 350 351 # done 352 return bits 353 354 355## Control tags 356 357 358class ControlTag(Tag): 359 def __init__(self): 360 Tag.__init__(self) 361 362 363class FileAttributesTag(ControlTag): 364 def __init__(self): 365 ControlTag.__init__(self) 366 self.tagtype = 69 367 368 def process_tag(self): 369 self.bytes = "\x00".encode("ascii") * (1 + 3) 370 371 372class ShowFrameTag(ControlTag): 373 def __init__(self): 374 ControlTag.__init__(self) 375 self.tagtype = 1 376 377 def process_tag(self): 378 self.bytes = bytes() 379 380 381class SetBackgroundTag(ControlTag): 382 """ Set the color in 0-255, or 0-1 (if floats given). """ 383 384 def __init__(self, *rgb): 385 self.tagtype = 9 386 if len(rgb) == 1: 387 rgb = rgb[0] 388 self.rgb = rgb 389 390 def process_tag(self): 391 bb = bytes() 392 for i in range(3): 393 clr = self.rgb[i] 394 if isinstance(clr, float): # pragma: no cover - not used 395 clr = clr * 255 396 bb += int2uint8(clr) 397 self.bytes = bb 398 399 400class DoActionTag(Tag): 401 def __init__(self, action="stop"): 402 Tag.__init__(self) 403 self.tagtype = 12 404 self.actions = [action] 405 406 def append(self, action): # pragma: no cover - not used 407 self.actions.append(action) 408 409 def process_tag(self): 410 bb = bytes() 411 412 for action in self.actions: 413 action = action.lower() 414 if action == "stop": 415 bb += "\x07".encode("ascii") 416 elif action == "play": # pragma: no cover - not used 417 bb += "\x06".encode("ascii") 418 else: # pragma: no cover 419 logger.warning("unkown action: %s" % action) 420 421 bb += int2uint8(0) 422 self.bytes = bb 423 424 425## Definition tags 426class DefinitionTag(Tag): 427 counter = 0 # to give automatically id's 428 429 def __init__(self): 430 Tag.__init__(self) 431 DefinitionTag.counter += 1 432 self.id = DefinitionTag.counter # id in dictionary 433 434 435class BitmapTag(DefinitionTag): 436 def __init__(self, im): 437 DefinitionTag.__init__(self) 438 self.tagtype = 36 # DefineBitsLossless2 439 440 # convert image (note that format is ARGB) 441 # even a grayscale image is stored in ARGB, nevertheless, 442 # the fabilous deflate compression will make it that not much 443 # more data is required for storing (25% or so, and less than 10% 444 # when storing RGB as ARGB). 445 446 if len(im.shape) == 3: 447 if im.shape[2] in [3, 4]: 448 tmp = np.ones((im.shape[0], im.shape[1], 4), dtype=np.uint8) * 255 449 for i in range(3): 450 tmp[:, :, i + 1] = im[:, :, i] 451 if im.shape[2] == 4: 452 tmp[:, :, 0] = im[:, :, 3] # swap channel where alpha is 453 else: # pragma: no cover 454 raise ValueError("Invalid shape to be an image.") 455 456 elif len(im.shape) == 2: 457 tmp = np.ones((im.shape[0], im.shape[1], 4), dtype=np.uint8) * 255 458 for i in range(3): 459 tmp[:, :, i + 1] = im[:, :] 460 else: # pragma: no cover 461 raise ValueError("Invalid shape to be an image.") 462 463 # we changed the image to uint8 4 channels. 464 # now compress! 465 self._data = zlib.compress(tmp.tostring(), zlib.DEFLATED) 466 self.imshape = im.shape 467 468 def process_tag(self): 469 470 # build tag 471 bb = bytes() 472 bb += int2uint16(self.id) # CharacterID 473 bb += int2uint8(5) # BitmapFormat 474 bb += int2uint16(self.imshape[1]) # BitmapWidth 475 bb += int2uint16(self.imshape[0]) # BitmapHeight 476 bb += self._data # ZlibBitmapData 477 478 self.bytes = bb 479 480 481class PlaceObjectTag(ControlTag): 482 def __init__(self, depth, idToPlace=None, xy=(0, 0), move=False): 483 ControlTag.__init__(self) 484 self.tagtype = 26 485 self.depth = depth 486 self.idToPlace = idToPlace 487 self.xy = xy 488 self.move = move 489 490 def process_tag(self): 491 # retrieve stuff 492 depth = self.depth 493 xy = self.xy 494 id = self.idToPlace 495 496 # build PlaceObject2 497 bb = bytes() 498 if self.move: 499 bb += "\x07".encode("ascii") 500 else: 501 # (8 bit flags): 4:matrix, 2:character, 1:move 502 bb += "\x06".encode("ascii") 503 bb += int2uint16(depth) # Depth 504 bb += int2uint16(id) # character id 505 bb += self.make_matrix_record(trans_xy=xy).tobytes() # MATRIX record 506 self.bytes = bb 507 508 509class ShapeTag(DefinitionTag): 510 def __init__(self, bitmapId, xy, wh): 511 DefinitionTag.__init__(self) 512 self.tagtype = 2 513 self.bitmapId = bitmapId 514 self.xy = xy 515 self.wh = wh 516 517 def process_tag(self): 518 """ Returns a defineshape tag. with a bitmap fill """ 519 520 bb = bytes() 521 bb += int2uint16(self.id) 522 xy, wh = self.xy, self.wh 523 tmp = self.make_rect_record(xy[0], wh[0], xy[1], wh[1]) # ShapeBounds 524 bb += tmp.tobytes() 525 526 # make SHAPEWITHSTYLE structure 527 528 # first entry: FILLSTYLEARRAY with in it a single fill style 529 bb += int2uint8(1) # FillStyleCount 530 bb += "\x41".encode("ascii") # FillStyleType (0x41 or 0x43 unsmoothed) 531 bb += int2uint16(self.bitmapId) # BitmapId 532 # bb += '\x00' # BitmapMatrix (empty matrix with leftover bits filled) 533 bb += self.make_matrix_record(scale_xy=(20, 20)).tobytes() 534 535 # # first entry: FILLSTYLEARRAY with in it a single fill style 536 # bb += int2uint8(1) # FillStyleCount 537 # bb += '\x00' # solid fill 538 # bb += '\x00\x00\xff' # color 539 540 # second entry: LINESTYLEARRAY with a single line style 541 bb += int2uint8(0) # LineStyleCount 542 # bb += int2uint16(0*20) # Width 543 # bb += '\x00\xff\x00' # Color 544 545 # third and fourth entry: NumFillBits and NumLineBits (4 bits each) 546 # I each give them four bits, so 16 styles possible. 547 bb += "\x44".encode("ascii") 548 549 self.bytes = bb 550 551 # last entries: SHAPERECORDs ... (individual shape records not aligned) 552 # STYLECHANGERECORD 553 bits = BitArray() 554 bits += self.make_style_change_record(0, 1, moveTo=(self.wh[0], self.wh[1])) 555 # STRAIGHTEDGERECORD 4x 556 bits += self.make_straight_edge_record(-self.wh[0], 0) 557 bits += self.make_straight_edge_record(0, -self.wh[1]) 558 bits += self.make_straight_edge_record(self.wh[0], 0) 559 bits += self.make_straight_edge_record(0, self.wh[1]) 560 561 # ENDSHAPRECORD 562 bits += self.make_end_shape_record() 563 564 self.bytes += bits.tobytes() 565 566 # done 567 # self.bytes = bb 568 569 def make_style_change_record(self, lineStyle=None, fillStyle=None, moveTo=None): 570 571 # first 6 flags 572 # Note that we use FillStyle1. If we don't flash (at least 8) does not 573 # recognize the frames properly when importing to library. 574 575 bits = BitArray() 576 bits += "0" # TypeFlag (not an edge record) 577 bits += "0" # StateNewStyles (only for DefineShape2 and Defineshape3) 578 if lineStyle: 579 bits += "1" # StateLineStyle 580 else: 581 bits += "0" 582 if fillStyle: 583 bits += "1" # StateFillStyle1 584 else: 585 bits += "0" 586 bits += "0" # StateFillStyle0 587 if moveTo: 588 bits += "1" # StateMoveTo 589 else: 590 bits += "0" 591 592 # give information 593 # todo: nbits for fillStyle and lineStyle is hard coded. 594 595 if moveTo: 596 bits += twits2bits([moveTo[0], moveTo[1]]) 597 if fillStyle: 598 bits += int2bits(fillStyle, 4) 599 if lineStyle: 600 bits += int2bits(lineStyle, 4) 601 602 return bits 603 604 def make_straight_edge_record(self, *dxdy): 605 if len(dxdy) == 1: 606 dxdy = dxdy[0] 607 608 # determine required number of bits 609 xbits = signedint2bits(dxdy[0] * 20) 610 ybits = signedint2bits(dxdy[1] * 20) 611 nbits = max([len(xbits), len(ybits)]) 612 613 bits = BitArray() 614 bits += "11" # TypeFlag and StraightFlag 615 bits += int2bits(nbits - 2, 4) 616 bits += "1" # GeneralLineFlag 617 bits += signedint2bits(dxdy[0] * 20, nbits) 618 bits += signedint2bits(dxdy[1] * 20, nbits) 619 620 # note: I do not make use of vertical/horizontal only lines... 621 622 return bits 623 624 def make_end_shape_record(self): 625 bits = BitArray() 626 bits += "0" # TypeFlag: no edge 627 bits += "0" * 5 # EndOfShape 628 return bits 629 630 631def read_pixels(bb, i, tagType, L1): 632 """ With pf's seed after the recordheader, reads the pixeldata. 633 """ 634 635 # Get info 636 charId = bb[i : i + 2] # noqa 637 i += 2 638 format = ord(bb[i : i + 1]) 639 i += 1 640 width = bits2int(bb[i : i + 2], 16) 641 i += 2 642 height = bits2int(bb[i : i + 2], 16) 643 i += 2 644 645 # If we can, get pixeldata and make numpy array 646 if format != 5: 647 logger.warning("Can only read 24bit or 32bit RGB(A) lossless images.") 648 else: 649 # Read byte data 650 offset = 2 + 1 + 2 + 2 # all the info bits 651 bb2 = bb[i : i + (L1 - offset)] 652 653 # Decompress and make numpy array 654 data = zlib.decompress(bb2) 655 a = np.frombuffer(data, dtype=np.uint8) 656 657 # Set shape 658 if tagType == 20: 659 # DefineBitsLossless - RGB data 660 try: 661 a.shape = height, width, 3 662 except Exception: 663 # Byte align stuff might cause troubles 664 logger.warning("Cannot read image due to byte alignment") 665 if tagType == 36: 666 # DefineBitsLossless2 - ARGB data 667 a.shape = height, width, 4 668 # Swap alpha channel to make RGBA 669 b = a 670 a = np.zeros_like(a) 671 a[:, :, 0] = b[:, :, 1] 672 a[:, :, 1] = b[:, :, 2] 673 a[:, :, 2] = b[:, :, 3] 674 a[:, :, 3] = b[:, :, 0] 675 676 return a 677 678 679## Last few functions 680 681 682# These are the original public functions, we don't use them, but we 683# keep it so that in principle this module can be used stand-alone. 684 685 686def checkImages(images): # pragma: no cover 687 """ checkImages(images) 688 Check numpy images and correct intensity range etc. 689 The same for all movie formats. 690 """ 691 # Init results 692 images2 = [] 693 694 for im in images: 695 if isinstance(im, np.ndarray): 696 # Check and convert dtype 697 if im.dtype == np.uint8: 698 images2.append(im) # Ok 699 elif im.dtype in [np.float32, np.float64]: 700 theMax = im.max() 701 if 128 < theMax < 300: 702 pass # assume 0:255 703 else: 704 im = im.copy() 705 im[im < 0] = 0 706 im[im > 1] = 1 707 im *= 255 708 images2.append(im.astype(np.uint8)) 709 else: 710 im = im.astype(np.uint8) 711 images2.append(im) 712 # Check size 713 if im.ndim == 2: 714 pass # ok 715 elif im.ndim == 3: 716 if im.shape[2] not in [3, 4]: 717 raise ValueError("This array can not represent an image.") 718 else: 719 raise ValueError("This array can not represent an image.") 720 else: 721 raise ValueError("Invalid image type: " + str(type(im))) 722 723 # Done 724 return images2 725 726 727def build_file( 728 fp, taglist, nframes=1, framesize=(500, 500), fps=10, version=8 729): # pragma: no cover 730 """ Give the given file (as bytes) a header. """ 731 732 # compose header 733 bb = bytes() 734 bb += "F".encode("ascii") # uncompressed 735 bb += "WS".encode("ascii") # signature bytes 736 bb += int2uint8(version) # version 737 bb += "0000".encode("ascii") # FileLength (leave open for now) 738 bb += Tag().make_rect_record(0, framesize[0], 0, framesize[1]).tobytes() 739 bb += int2uint8(0) + int2uint8(fps) # FrameRate 740 bb += int2uint16(nframes) 741 fp.write(bb) 742 743 # produce all tags 744 for tag in taglist: 745 fp.write(tag.get_tag()) 746 747 # finish with end tag 748 fp.write("\x00\x00".encode("ascii")) 749 750 # set size 751 sze = fp.tell() 752 fp.seek(4) 753 fp.write(int2uint32(sze)) 754 755 756def write_swf(filename, images, duration=0.1, repeat=True): # pragma: no cover 757 """Write an swf-file from the specified images. If repeat is False, 758 the movie is finished with a stop action. Duration may also 759 be a list with durations for each frame (note that the duration 760 for each frame is always an integer amount of the minimum duration.) 761 762 Images should be a list consisting numpy arrays with values between 763 0 and 255 for integer types, and between 0 and 1 for float types. 764 765 """ 766 767 # Check images 768 images2 = checkImages(images) 769 770 # Init 771 taglist = [FileAttributesTag(), SetBackgroundTag(0, 0, 0)] 772 773 # Check duration 774 if hasattr(duration, "__len__"): 775 if len(duration) == len(images2): 776 duration = [d for d in duration] 777 else: 778 raise ValueError("len(duration) doesn't match amount of images.") 779 else: 780 duration = [duration for im in images2] 781 782 # Build delays list 783 minDuration = float(min(duration)) 784 delays = [round(d / minDuration) for d in duration] 785 delays = [max(1, int(d)) for d in delays] 786 787 # Get FPS 788 fps = 1.0 / minDuration 789 790 # Produce series of tags for each image 791 # t0 = time.time() 792 nframes = 0 793 for im in images2: 794 bm = BitmapTag(im) 795 wh = (im.shape[1], im.shape[0]) 796 sh = ShapeTag(bm.id, (0, 0), wh) 797 po = PlaceObjectTag(1, sh.id, move=nframes > 0) 798 taglist.extend([bm, sh, po]) 799 for i in range(delays[nframes]): 800 taglist.append(ShowFrameTag()) 801 nframes += 1 802 803 if not repeat: 804 taglist.append(DoActionTag("stop")) 805 806 # Build file 807 # t1 = time.time() 808 fp = open(filename, "wb") 809 try: 810 build_file(fp, taglist, nframes=nframes, framesize=wh, fps=fps) 811 except Exception: 812 raise 813 finally: 814 fp.close() 815 # t2 = time.time() 816 817 # logger.warning("Writing SWF took %1.2f and %1.2f seconds" % (t1-t0, t2-t1) ) 818 819 820def read_swf(filename): # pragma: no cover 821 """Read all images from an SWF (shockwave flash) file. Returns a list 822 of numpy arrays. 823 824 Limitation: only read the PNG encoded images (not the JPG encoded ones). 825 """ 826 827 # Check whether it exists 828 if not os.path.isfile(filename): 829 raise IOError("File not found: " + str(filename)) 830 831 # Init images 832 images = [] 833 834 # Open file and read all 835 fp = open(filename, "rb") 836 bb = fp.read() 837 838 try: 839 # Check opening tag 840 tmp = bb[0:3].decode("ascii", "ignore") 841 if tmp.upper() == "FWS": 842 pass # ok 843 elif tmp.upper() == "CWS": 844 # Decompress movie 845 bb = bb[:8] + zlib.decompress(bb[8:]) 846 else: 847 raise IOError("Not a valid SWF file: " + str(filename)) 848 849 # Set filepointer at first tag (skipping framesize RECT and two uin16's 850 i = 8 851 nbits = bits2int(bb[i : i + 1], 5) # skip FrameSize 852 nbits = 5 + nbits * 4 853 Lrect = nbits / 8.0 854 if Lrect % 1: 855 Lrect += 1 856 Lrect = int(Lrect) 857 i += Lrect + 4 858 859 # Iterate over the tags 860 counter = 0 861 while True: 862 counter += 1 863 864 # Get tag header 865 head = bb[i : i + 6] 866 if not head: 867 break # Done (we missed end tag) 868 869 # Determine type and length 870 T, L1, L2 = get_type_and_len(head) 871 if not L2: 872 logger.warning("Invalid tag length, could not proceed") 873 break 874 # logger.warning(T, L2) 875 876 # Read image if we can 877 if T in [20, 36]: 878 im = read_pixels(bb, i + 6, T, L1) 879 if im is not None: 880 images.append(im) 881 elif T in [6, 21, 35, 90]: 882 logger.warning("Ignoring JPEG image: cannot read JPEG.") 883 else: 884 pass # Not an image tag 885 886 # Detect end tag 887 if T == 0: 888 break 889 890 # Next tag! 891 i += L2 892 893 finally: 894 fp.close() 895 896 # Done 897 return images 898 899 900# Backward compatibility; same public names as when this was images2swf. 901writeSwf = write_swf 902readSwf = read_swf 903