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