1# -*- coding: utf-8 -*-
2# imageio is distributed under the terms of the (new) BSD License.
3
4""" Read/Write SWF files.
5
6Backend: internal
7
8Shockwave flash (SWF) is a media format designed for rich and
9interactive animations. This plugin makes use of this format to
10store a series of images in a lossless format with good compression
11(zlib). The resulting images can be shown as an animation using
12a flash player (such as the browser).
13
14SWF stores images in RGBA format. RGB or grayscale images are
15automatically converted. SWF does not support meta data.
16
17Parameters for reading
18----------------------
19loop : bool
20 If True, the video will rewind as soon as a frame is requested
21 beyond the last frame. Otherwise, IndexError is raised. Default False.
22
23Parameters for saving
24---------------------
25fps : int
26 The speed to play the animation. Default 12.
27loop : bool
28 If True, add a tag to the end of the file to play again from
29 the first frame. Most flash players will then play the movie
30 in a loop. Note that the imageio SWF Reader does not check this
31 tag. Default True.
32html : bool
33 If the output is a file on the file system, write an html file
34 (in HTML5) that shows the animation. Default False.
35compress : bool
36 Whether to compress the swf file. Default False. You probably don't
37 want to use this. This does not decrease the file size since
38 the images are already compressed. It will result in slower
39 read and write time. The only purpose of this feature is to
40 create compressed SWF files, so that we can test the
41 functionality to read them.
42
43"""
44
45import os
46import zlib
47import logging
48from io import BytesIO
49
50import numpy as np
51
52from ..core import Format, read_n_bytes, image_as_uint
53
54
55logger = logging.getLogger(__name__)
56
57_swf = None # lazily loaded in lib()
58
59
60def load_lib():
61 global _swf
62 from . import _swf
63
64 return _swf
65
66
67class SWFFormat(Format):
68 """See :mod:`imageio.plugins.swf`"""
69
70 def _can_read(self, request):
71 tmp = request.firstbytes[0:3].decode("ascii", "ignore")
72 if tmp in ("FWS", "CWS"):
73 return True
74
75 def _can_write(self, request):
76 if request.extension in self.extensions:
77 return True
78
79 # -- reader
80
81 class Reader(Format.Reader):
82 def _open(self, loop=False):
83 if not _swf:
84 load_lib()
85
86 self._arg_loop = bool(loop)
87
88 self._fp = self.request.get_file()
89
90 # Check file ...
91 tmp = self.request.firstbytes[0:3].decode("ascii", "ignore")
92 if tmp == "FWS":
93 pass # OK
94 elif tmp == "CWS":
95 # Compressed, we need to decompress
96 bb = self._fp.read()
97 bb = bb[:8] + zlib.decompress(bb[8:])
98 # Wrap up in a file object
99 self._fp = BytesIO(bb)
100 else:
101 raise IOError("This does not look like a valid SWF file")
102
103 # Skip first bytes. This also tests support got seeking ...
104 try:
105 self._fp.seek(8)
106 self._streaming_mode = False
107 except Exception:
108 self._streaming_mode = True
109 self._fp_read(8)
110
111 # Skip header
112 # Note that the number of frames is there, which we could
113 # potentially use, but the number of frames does not necessarily
114 # correspond to the number of images.
115 nbits = _swf.bits2int(self._fp_read(1), 5)
116 nbits = 5 + nbits * 4
117 Lrect = nbits / 8.0
118 if Lrect % 1:
119 Lrect += 1
120 Lrect = int(Lrect)
121 self._fp_read(Lrect + 3)
122
123 # Now the rest is basically tags ...
124 self._imlocs = [] # tuple (loc, sze, T, L1)
125 if not self._streaming_mode:
126 # Collect locations of frame, while skipping through the data
127 # This does not read any of the tag *data*.
128 try:
129 while True:
130 isimage, sze, T, L1 = self._read_one_tag()
131 loc = self._fp.tell()
132 if isimage:
133 # Still need to check if the format is right
134 format = ord(self._fp_read(3)[2:])
135 if format == 5: # RGB or RGBA lossless
136 self._imlocs.append((loc, sze, T, L1))
137 self._fp.seek(loc + sze) # Skip over tag
138 except IndexError:
139 pass # done reading
140
141 def _fp_read(self, n):
142 return read_n_bytes(self._fp, n)
143
144 def _close(self):
145 pass
146
147 def _get_length(self):
148 if self._streaming_mode:
149 return np.inf
150 else:
151 return len(self._imlocs)
152
153 def _get_data(self, index):
154 # Check index
155 if index < 0:
156 raise IndexError("Index in swf file must be > 0")
157 if not self._streaming_mode:
158 if self._arg_loop and self._imlocs:
159 index = index % len(self._imlocs)
160 if index >= len(self._imlocs):
161 raise IndexError("Index out of bounds")
162
163 if self._streaming_mode:
164 # Walk over tags until we find an image
165 while True:
166 isimage, sze, T, L1 = self._read_one_tag()
167 bb = self._fp_read(sze) # always read data
168 if isimage:
169 im = _swf.read_pixels(bb, 0, T, L1) # can be None
170 if im is not None:
171 return im, {}
172
173 else:
174 # Go to corresponding location, read data, and convert to image
175 loc, sze, T, L1 = self._imlocs[index]
176 self._fp.seek(loc)
177 bb = self._fp_read(sze)
178 # Read_pixels should return ndarry, since we checked format
179 im = _swf.read_pixels(bb, 0, T, L1)
180 return im, {}
181
182 def _read_one_tag(self):
183 """
184 Return (True, loc, size, T, L1) if an image that we can read.
185 Return (False, loc, size, T, L1) if any other tag.
186 """
187
188 # Get head
189 head = self._fp_read(6)
190 if not head: # pragma: no cover
191 raise IndexError("Reached end of swf movie")
192
193 # Determine type and length
194 T, L1, L2 = _swf.get_type_and_len(head)
195 if not L2: # pragma: no cover
196 raise RuntimeError("Invalid tag length, could not proceed")
197
198 # Read data
199 isimage = False
200 sze = L2 - 6
201 # bb = self._fp_read(L2 - 6)
202
203 # Parse tag
204 if T == 0:
205 raise IndexError("Reached end of swf movie")
206 elif T in [20, 36]:
207 isimage = True
208 # im = _swf.read_pixels(bb, 0, T, L1) # can be None
209 elif T in [6, 21, 35, 90]: # pragma: no cover
210 logger.warning("Ignoring JPEG image: cannot read JPEG.")
211 else:
212 pass # Not an image tag
213
214 # Done. Return image. Can be None
215 # return im
216 return isimage, sze, T, L1
217
218 def _get_meta_data(self, index):
219 return {} # This format does not support meta data
220
221 # -- writer
222
223 class Writer(Format.Writer):
224 def _open(self, fps=12, loop=True, html=False, compress=False):
225 if not _swf:
226 load_lib()
227
228 self._arg_fps = int(fps)
229 self._arg_loop = bool(loop)
230 self._arg_html = bool(html)
231 self._arg_compress = bool(compress)
232
233 self._fp = self.request.get_file()
234 self._framecounter = 0
235 self._framesize = (100, 100)
236
237 # For compress, we use an in-memory file object
238 if self._arg_compress:
239 self._fp_real = self._fp
240 self._fp = BytesIO()
241
242 def _close(self):
243 self._complete()
244 # Get size of (uncompressed) file
245 sze = self._fp.tell()
246 # set nframes, this is in the potentially compressed region
247 self._fp.seek(self._location_to_save_nframes)
248 self._fp.write(_swf.int2uint16(self._framecounter))
249 # Compress body?
250 if self._arg_compress:
251 bb = self._fp.getvalue()
252 self._fp = self._fp_real
253 self._fp.write(bb[:8])
254 self._fp.write(zlib.compress(bb[8:]))
255 sze = self._fp.tell() # renew sze value
256 # set size
257 self._fp.seek(4)
258 self._fp.write(_swf.int2uint32(sze))
259 self._fp = None # Disable
260
261 # Write html?
262 if self._arg_html and os.path.isfile(self.request.filename):
263 dirname, fname = os.path.split(self.request.filename)
264 filename = os.path.join(dirname, fname[:-4] + ".html")
265 w, h = self._framesize
266 html = HTML % (fname, w, h, fname)
267 with open(filename, "wb") as f:
268 f.write(html.encode("utf-8"))
269
270 def _write_header(self, framesize, fps):
271 self._framesize = framesize
272 # Called as soon as we know framesize; when we get first frame
273 bb = b""
274 bb += "FC"[self._arg_compress].encode("ascii")
275 bb += "WS".encode("ascii") # signature bytes
276 bb += _swf.int2uint8(8) # version
277 bb += "0000".encode("ascii") # FileLength (leave open for now)
278 bb += (
279 _swf.Tag().make_rect_record(0, framesize[0], 0, framesize[1]).tobytes()
280 )
281 bb += _swf.int2uint8(0) + _swf.int2uint8(fps) # FrameRate
282 self._location_to_save_nframes = len(bb)
283 bb += "00".encode("ascii") # nframes (leave open for now)
284 self._fp.write(bb)
285
286 # Write some initial tags
287 taglist = _swf.FileAttributesTag(), _swf.SetBackgroundTag(0, 0, 0)
288 for tag in taglist:
289 self._fp.write(tag.get_tag())
290
291 def _complete(self):
292 # What if no images were saved?
293 if not self._framecounter:
294 self._write_header((10, 10), self._arg_fps)
295 # Write stop tag if we do not loop
296 if not self._arg_loop:
297 self._fp.write(_swf.DoActionTag("stop").get_tag())
298 # finish with end tag
299 self._fp.write("\x00\x00".encode("ascii"))
300
301 def _append_data(self, im, meta):
302 # Correct shape and type
303 if im.ndim == 3 and im.shape[-1] == 1:
304 im = im[:, :, 0]
305 im = image_as_uint(im, bitdepth=8)
306 # Get frame size
307 wh = im.shape[1], im.shape[0]
308 # Write header on first frame
309 isfirstframe = False
310 if self._framecounter == 0:
311 isfirstframe = True
312 self._write_header(wh, self._arg_fps)
313 # Create tags
314 bm = _swf.BitmapTag(im)
315 sh = _swf.ShapeTag(bm.id, (0, 0), wh)
316 po = _swf.PlaceObjectTag(1, sh.id, move=(not isfirstframe))
317 sf = _swf.ShowFrameTag()
318 # Write tags
319 for tag in [bm, sh, po, sf]:
320 self._fp.write(tag.get_tag())
321 self._framecounter += 1
322
323 def set_meta_data(self, meta):
324 pass
325
326
327HTML = """
328<!DOCTYPE html>
329<html>
330<head>
331 <title>Show Flash animation %s</title>
332</head>
333<body>
334 <embed width="%i" height="%i" src="%s">
335</html>
336"""