1# -*- coding: utf-8 -*-
2
3# romiq.kh@gmail.com, 2014
4
5import array, struct, io
6
7from . import EngineError
8
9try:
10    from PIL import Image
11except ImportError:
12    Image = None
13
14try:
15    from PIL import FliImagePlugin
16except ImportError:
17    pass
18
19FLC_HEADER = [
20    ["fsize",        1, "I", False],
21    ["ftype",        1, "H", False],
22    ["frames_num",   1, "H", True],
23    ["width",        1, "H", True],
24    ["height",       1, "H", True],
25    ["depth",        1, "H", False],
26    ["flags",        1, "H", True],
27    ["speed",        1, "I", True],
28    ["reserved1",    1, "H", False],
29    ["created",      1, "I", True],
30    ["creator",      1, "I", True],
31    ["updated",      1, "I", True],
32    ["updater",      1, "I", True],
33    ["aspect_dx",    1, "H", True],
34    ["aspect_dy",    1, "H", True],
35    ["ext_flags",    1, "H", False],
36    ["keyframes",    1, "H", False],
37    ["totalframes",  1, "H", False],
38    ["req_memory",   1, "I", False],
39    ["max_regions",  1, "H", False],
40    ["transp_num",   1, "H", False],
41    ["reserved2",   24, "s", False],
42    ["oframe1",      1, "I", False],
43    ["oframe2",      1, "i", False],
44    ["reserved3",   40, "s", False],
45]
46
47class FLCLoader:
48    def __init__(self):
49        self.rgb = None
50        self.image = None
51        self.width = 0
52        self.height = 0
53        self.frame_num = 0
54        self.delay = 0
55
56
57    def load_info(self, f):
58        self.image = Image.open(f)
59        self.frame_num = 1
60        try:
61            while 1:
62                self.image.seek(self.image.tell() + 1)
63                self.frame_num += 1
64        except EOFError:
65            pass # end of sequence
66
67
68    def parseflcchunks(self, f, offset, limit, level = 0, maxchunks = None,):
69        def check_hdr(size, delta, name, offset):
70            if delta < size:
71                raise EngineError("Incorrect FLC %s chunk at 0x{:08x}".format(
72                    (name, offset)))
73
74        chunks = []
75        while True:
76            if limit is not None:
77                if offset >= limit:
78                    break
79            if maxchunks is not None:
80                if len(chunks) >= maxchunks:
81                    break
82            chunk = {"offset": offset}
83            temp = f.read(6)
84            sz, tp = struct.unpack_from("<IH", temp)
85            offset += 6
86
87            chunk["size"] = sz
88            chunk["type"] = tp
89            delta = sz - 6
90            if delta < 0:
91                raise EngineError("Incorrect FLC chunk at 0x{:08x}".format(
92                    chunk["offset"]))
93
94            raw_chunks = [
95                0x4, # COLOR_256
96                0x7, # DELTA_FLC
97                0xf, # BYTE_RUN
98                0xF100, # PREFIX_TYPE - mismaked, out destination ignore this
99            ]
100            #print("{}CHUNK 0x{:x}, size = {}".format("  "*level, tp, sz))
101            # do not parse 3rd level 0x12 chunk
102            if tp == 0x12 and level == 2:
103                tp = 0x4
104
105            # parse chunks
106            if tp in raw_chunks:
107                temp = f.read(delta)
108                offset += delta
109                chunk["data"] = temp
110            elif tp == 0x12:
111                # PSTAMP
112                check_hdr(6, delta, "PSTAMP", offset)
113                temp = f.read(6)
114                delta -= 6
115                height, width, xlate = struct.unpack_from("<3H", temp)
116                offset += 6
117                offset, subchunks = self.parseflcchunks(f, offset,
118                    offset + delta, level + 1, 1)
119                chunk["chunks"] = subchunks
120                #print(subchunks)
121            elif tp == 0xF1FA:
122                # FRAME_TYPE
123                check_hdr(10, delta, "FRAME_TYPE", offset)
124                temp = f.read(10)
125                delta -= 10
126                sub_num, delay, reserved, width, height = \
127                    struct.unpack_from("<5H", temp)
128                offset += 10
129                chunk["delay"] = delay
130                chunk["width"] = width
131                chunk["height"] = height
132                offset, subchunks = self.parseflcchunks(f, offset,
133                    offset + delta, level + 1, sub_num)
134                chunk["chunks"] = subchunks
135            else:
136                raise Exception("Unknown FLC chunk type 0x{:04x} at 0x{:x08x}".\
137                    format(tp, offset))
138
139            chunks.append(chunk)
140
141        return offset, chunks
142
143    def load_data(self, f):
144        # parse header
145        offset = 0
146        hdr_keys = []
147        hdr_struct = "<"
148        for hnam, hsz, htp, hed in FLC_HEADER:
149            hdr_keys.append(hnam)
150            if hsz == 1:
151                hdr_struct += htp
152            else:
153                hdr_struct += "%d" % hsz + htp
154
155        header = {}
156        temp = f.read(128)
157        hdr = struct.unpack_from(hdr_struct, temp)
158
159        offset += 128
160
161        if len(hdr) != len(hdr_keys):
162            raise EngineError("Incorrect FLC header {} != {}".format(
163                len(hdr), len(hdr_keys)))
164        for hid in range(len(hdr)):
165            header[hdr_keys[hid]] = hdr[hid]
166
167        if header["ftype"] != 0xAF12:
168            raise EngineError("Unsupported FLC type (0x{:04x})".format(
169                header["ftype"]))
170
171        # check if not EGI ext
172        if header["creator"] == 0x45474900:
173            if header["ext_flags"] != 0:
174                raise EngineError("Unsupported FLC EGI extension")
175
176        # NOTE: we recreate FLC to avoid Pilllow bug
177        #  1. remove 0xf100 chunk  (PREFIX, implementation specific)
178        #  2. remobe 0x12 subchunk (PSTAMP) from 1st frame
179
180        # read chunks
181        _, chunks = self.parseflcchunks(f, offset, header["fsize"])
182
183        f.seek(0)
184        buf = io.BytesIO()
185        buf.write(f.read(128)) # clone header
186        for chunk in chunks:
187            if chunk["type"] == 0xF100:
188                continue
189            elif chunk["type"] == 0xF1FA:
190                rebuild = False
191                nchunks = []
192                nsz = 16 # I6H - type, size, sub_num, delay,
193                         # reserved, width, height
194                for schunk in chunk["chunks"]:
195                    if schunk["type"] == 0x12: # detect mailformed PSTAMP
196                        rebuild = True
197                    elif rebuild:
198                        nchunks.append(schunk)
199                        nsz += schunk["size"]
200                if rebuild:
201                    buf.write(struct.pack("<I6H", nsz, 0xF1FA, len(nchunks),
202                        chunk["delay"], 0, chunk["width"], chunk["height"]))
203                    for schunk in nchunks:
204                        f.seek(schunk["offset"])
205                        buf.write(f.read(schunk["size"]))
206                    continue
207            # copy chunk
208            buf.write(f.read(chunk["size"]))
209
210        buf.seek(0)
211        self.image = Image.open(buf)
212