-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy path__init__.py
391 lines (326 loc) · 9.52 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
#! python3
"""This is an APNG module, which can create apng file from pngs
Reference:
http://littlesvr.ca/apng/
http://wiki.mozilla.org/APNG_Specification
https://www.w3.org/TR/PNG/
"""
import struct
import binascii
import itertools
import io
__version__ = "0.2.0"
try:
import PIL.Image
except ImportError:
# Without Pillow, apng can only handle PNG images
pass
PNG_SIGN = b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"
# http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.Summary-of-standard-chunks
CHUNK_BEFORE_IDAT = {
"cHRM", "gAMA", "iCCP", "sBIT", "sRGB", "bKGD", "hIST", "tRNS", "pHYs",
"sPLT", "tIME", "PLTE"
}
def is_png(png):
"""Test if ``png`` is a valid PNG file by checking the signature.
:arg png: If ``png`` is a :any:`path-like object` or :any:`file-like object`
object, read the content into bytes.
:type png: path-like, file-like, or bytes
:rtype: bool
"""
if isinstance(png, str) or hasattr(png, "__fspath__"):
with open(png, "rb") as f:
png_header = f.read(8)
elif hasattr(png, "read"):
position = png.tell()
png_header = png.read(8)
png.seek(position)
elif isinstance(png, bytes):
png_header = png[:8]
else:
raise TypeError("Must be file, bytes, or str but get {}"
.format(type(png)))
return png_header == PNG_SIGN
def chunks_read(b):
"""Parse PNG bytes into different chunks, yielding (type, data).
@type is a string of chunk type.
@data is the bytes of the chunk. Including length, type, data, and crc.
"""
# skip signature
i = 8
# yield chunks
while i < len(b):
data_len, = struct.unpack("!I", b[i:i+4])
type = b[i+4:i+8].decode("latin-1")
yield type, b[i:i+data_len+12]
i += data_len + 12
def chunks(png):
"""Yield ``(chunk_type, chunk_raw_data)`` from ``png``.
.. note::
``chunk_raw_data`` includes chunk length, type, and CRC.
:arg png: If ``png`` is a :any:`path-like object` or :any:`file-like object`
object, read the content into bytes.
:type png: path-like, file-like, or bytes
:rtype: Generator[tuple(str, bytes)]
"""
if not is_png(png):
# convert to png
if isinstance(png, bytes):
with io.BytesIO(png) as f:
with io.BytesIO() as f2:
PIL.Image.open(f).save(f2, "PNG", optimize=True)
png = f2.getvalue()
else:
with io.BytesIO() as f2:
PIL.Image.open(png).save(f2, "PNG", optimize=True)
png = f2.getvalue()
if isinstance(png, str) or hasattr(png, "__fspath__"):
# path like
with open(png, "rb") as f:
png = f.read()
elif hasattr(png, "read"):
# file like
png = png.read()
return chunks_read(png)
def make_chunk(type, data):
"""Create a raw chunk by composing chunk's ``type`` and ``data``. It
calculates chunk length and CRC for you.
:arg str type: PNG chunk type.
:arg bytes data: PNG chunk data, **excluding chunk length, type, and CRC**.
:rtype: bytes
"""
out = struct.pack("!I", len(data))
data = type.encode("latin-1") + data
out += data + struct.pack("!I", binascii.crc32(data))
return out
class PNG:
"""Represent PNG image. This class should only be initiated with
classmethods."""
def __init__(self):
self.hdr = None
self.end = None
self.width = None
self.height = None
self.chunks = []
def init(self):
"""Extract some info from chunks"""
for type, data in self.chunks:
if type == "IHDR":
self.hdr = data
elif type == "IEND":
self.end = data
if self.hdr:
# grab w, h info
self.width, self.height = struct.unpack("!II", self.hdr[8:16])
@classmethod
def open(cls, png):
"""Open a PNG file.
:arg png: See :func:`chunks`.
:rtype: :class:`PNG`
"""
o = cls()
o.chunks = list(chunks(png))
o.init()
return o
@classmethod
def from_chunks(cls, chunks):
"""Construct PNG from raw chunks.
:arg chunks: A list of ``(chunk_type, chunk_raw_data)``. Also see
:func:`chunks`.
:type chunks: list[tuple(str, bytes)]
"""
o = cls()
o.chunks = chunks
o.init()
return o
def to_bytes(self):
"""Convert entire image to bytes.
:rtype: bytes
"""
chunks = [PNG_SIGN]
chunks.extend(c[1] for c in self.chunks)
return b"".join(chunks)
def save(self, file):
"""Save entire image to a file.
:arg file: The destination.
:type file: path-like or file-like
"""
if isinstance(file, str) or hasattr(file, "__fspath__"):
with open(file, "wb") as f:
f.write(self.to_bytes())
else:
file.write(self.to_bytes())
class FrameControl:
"""A data class holding fcTL info."""
def __init__(self, width=None, height=None, x_offset=0, y_offset=0, delay=100, delay_den=1000, depose_op=1, blend_op=0):
"""Parameters are assigned as object members. See `https://wiki.mozilla.org/APNG_Specification <https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk>`_ for the detail of fcTL.
"""
self.width = width
self.height = height
self.x_offset = x_offset
self.y_offset = y_offset
self.delay = delay
self.delay_den = delay_den
self.depose_op = depose_op
self.blend_op = blend_op
def to_bytes(self):
"""Convert to bytes.
:rtype: bytes
"""
return struct.pack("!IIIIHHbb", self.width, self.height, self.x_offset, self.y_offset, self.delay, self.delay_den, self.depose_op, self.blend_op)
@classmethod
def from_bytes(cls, b):
"""Contruct fcTL info from bytes.
:arg bytes b: The length of ``b`` must be *28*, excluding sequence
number and CRC.
"""
return cls(*struct.unpack("!IIIIHHbb", b))
class APNG:
"""Represent APNG image."""
def __init__(self):
"""APNG is composed by multiple PNGs, which can be inserted with
:meth:`append`.
:var frames: Frames of APNG, a list of ``(png, control)`` tuple.
:vartype frames: list[tuple(PNG, FrameControl)]
"""
self.frames = []
def append(self, png, **options):
"""Read a PNG file and append one frame.
:arg png: See :meth:`PNG.open`.
:arg options: See :class:`FrameControl`.
"""
png = PNG.open(png)
control = FrameControl(**options)
if control.width is None:
control.width = png.width
if control.height is None:
control.height = png.height
self.frames.append((png, control))
def to_bytes(self):
"""Convert entire image to bytes.
:rtype: bytes
"""
# grab the chunks we needs
out = [PNG_SIGN]
# FIXME: it's tricky to define "other_chunks". HoneyView stop the
# animation if it sees chunks other than fctl or idat, so we put other
# chunks to the end of the file
other_chunks = []
seq = 0
# for first frame
png, control = self.frames[0]
# header
out.append(png.hdr)
# acTL
out.append(make_chunk("acTL", struct.pack("!II", len(self.frames), 0)))
# fcTL
if control:
out.append(make_chunk("fcTL", struct.pack("!I", seq) + control.to_bytes()))
seq += 1
# and others...
idat_chunks = []
for type, data in png.chunks:
if type in ("IHDR", "IEND"):
continue
if type == "IDAT":
# put at last
idat_chunks.append(data)
continue
out.append(data)
out.extend(idat_chunks)
# FIXME: we should do some optimization to frames...
# for other frames
for png, control in self.frames[1:]:
# fcTL
out.append(
make_chunk("fcTL", struct.pack("!I", seq) + control.to_bytes())
)
seq += 1
# and others...
for type, data in png.chunks:
if type in ("IHDR", "IEND") or type in CHUNK_BEFORE_IDAT:
continue
elif type == "IDAT":
# convert IDAT to fdAT
out.append(
make_chunk("fdAT", struct.pack("!I", seq) + data[8:-4])
)
seq += 1
else:
other_chunks.append(data)
# end
out.extend(other_chunks)
out.append(png.end)
return b"".join(out)
@classmethod
def from_files(cls, files, **options):
"""Create APNG from multiple files.
This is same as::
im = APNG()
for file in files:
im.append(file, **options)
:arg list files: A list of file. See :meth:`PNG.open`.
:arg options: Options for :class:`FrameControl`.
:rtype: APNG
"""
o = cls()
for file in files:
o.append(file, **options)
return o
@classmethod
def open(cls, file):
"""Open an APNG file.
:arg file: See :func:`chunks`.
:rtype: APNG
"""
hdr = None
head_chunks = []
end = ("IEND", make_chunk("IEND", b""))
frame_chunks = []
frames = []
control = None
for type, data in chunks(file):
if type == "IHDR":
hdr = data
frame_chunks.append((type, data))
elif type == "acTL":
continue
elif type == "fcTL":
if any(type == "IDAT" for type, data in frame_chunks):
# IDAT inside chunk, go to next frame
frame_chunks.append(end)
frames.append((PNG.from_chunks(frame_chunks), control))
control = FrameControl.from_bytes(data[12:-4])
hdr = make_chunk("IHDR", struct.pack("!II", control.width, control.height) + hdr[16:-4])
frame_chunks = [("IHDR", hdr)]
else:
control = FrameControl.from_bytes(data[12:-4])
elif type == "IDAT":
frame_chunks.extend(head_chunks)
frame_chunks.append((type, data))
elif type == "fdAT":
# convert to IDAT
frame_chunks.extend(head_chunks)
frame_chunks.append(("IDAT", make_chunk("IDAT", data[12:-4])))
elif type == "IEND":
# end
frame_chunks.append(end)
frames.append((PNG.from_chunks(frame_chunks), control))
break
elif type in CHUNK_BEFORE_IDAT:
head_chunks.append((type, data))
else:
frame_chunks.append((type, data))
o = cls()
o.frames = frames
return o
def save(self, file):
"""Save entire image to a file.
:arg file: The destination.
:type file: path-like or file-like
"""
if isinstance(file, str) or hasattr(file, "__fspath__"):
with open(file, "wb") as f:
f.write(self.to_bytes())
else:
file.write(self.to_bytes())