_swf.py 25 KB

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