1
# ----------------------------------------------------------------------------
3
# Copyright (c) 2006-2008 Alex Holkner
6
# Redistribution and use in source and binary forms, with or without
7
# modification, are permitted provided that the following conditions
10
# * Redistributions of source code must retain the above copyright
11
# notice, this list of conditions and the following disclaimer.
12
# * Redistributions in binary form must reproduce the above copyright
13
# notice, this list of conditions and the following disclaimer in
14
# the documentation and/or other materials provided with the
16
# * Neither the name of pyglet nor the names of its
17
# contributors may be used to endorse or promote products
18
# derived from this software without specific prior written
21
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
# POSSIBILITY OF SUCH DAMAGE.
33
# ----------------------------------------------------------------------------
34
# png.py - PNG encoder in pure Python
35
# Copyright (C) 2006 Johann C. Rocholl <johann@browsershots.org>
36
# <ah> Modifications for pyglet by Alex Holkner <alex.holkner@gmail.com>
38
# Permission is hereby granted, free of charge, to any person
39
# obtaining a copy of this software and associated documentation files
40
# (the "Software"), to deal in the Software without restriction,
41
# including without limitation the rights to use, copy, modify, merge,
42
# publish, distribute, sublicense, and/or sell copies of the Software,
43
# and to permit persons to whom the Software is furnished to do so,
44
# subject to the following conditions:
46
# The above copyright notice and this permission notice shall be
47
# included in all copies or substantial portions of the Software.
49
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
50
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
51
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
52
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
53
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
54
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
55
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
58
# Contributors (alphabetical):
59
# Nicko van Someren <nicko@nicko.org>
61
# Changelog (recent first):
62
# 2006-06-17 Nicko: Reworked into a class, faster interlacing.
63
# 2006-06-17 Johann: Very simple prototype PNG decoder.
64
# 2006-06-17 Nicko: Test suite with various image generators.
65
# 2006-06-17 Nicko: Alpha-channel, grey-scale, 16-bit/plane support.
66
# 2006-06-15 Johann: Scanline iterator interface for large input files.
67
# 2006-06-09 Johann: Very simple prototype PNG encoder.
71
Pure Python PNG Reader/Writer
73
This is an implementation of a subset of the PNG specification at
74
http://www.w3.org/TR/2003/REC-PNG-20031110 in pure Python. It reads
75
and writes PNG files with 8/16/24/32/48/64 bits per pixel (greyscale,
76
RGB, RGBA, with 8 or 16 bits per layer), with a number of options. For
77
help, type "import png; help(png)" in your python interpreter.
79
This file can also be used as a command-line utility to convert PNM
80
files to PNG. The interface is similar to that of the pnmtopng program
81
from the netpbm package. Type "python png.py --help" at the shell
82
prompt for usage and a list of options.
86
__revision__ = '$Rev$'
88
__author__ = '$Author$'
95
from array import array
98
_adam7 = ((0, 0, 8, 8),
107
def interleave_planes(ipixels, apixels, ipsize, apsize):
109
Interleave color planes, e.g. RGB + A = RGBA.
111
Return an array of pixels consisting of the ipsize bytes of data
112
from each pixel in ipixels followed by the apsize bytes of data
113
from each pixel in apixels, for an image of size width x height.
115
itotal = len(ipixels)
116
atotal = len(apixels)
117
newtotal = itotal + atotal
118
newpsize = ipsize + apsize
119
# Set up the output buffer
121
# It's annoying that there is no cheap way to set the array size :-(
124
# Interleave in the pixel data
125
for i in range(ipsize):
126
out[i:newtotal:newpsize] = ipixels[i:itotal:ipsize]
127
for i in range(apsize):
128
out[i+ipsize:newtotal:newpsize] = apixels[i:atotal:apsize]
131
class Error(Exception):
136
PNG encoder in pure Python.
139
def __init__(self, width, height,
150
Create a PNG encoder object.
153
width, height - size of the image in pixels
154
transparent - create a tRNS chunk
155
background - create a bKGD chunk
156
gamma - create a gAMA chunk
157
greyscale - input data is greyscale, not RGB
158
has_alpha - input data has alpha channel (RGBA)
159
bytes_per_sample - 8-bit or 16-bit input data
160
compression - zlib compression level (1-9)
161
chunk_limit - write multiple IDAT chunks to save memory
163
If specified, the transparent and background parameters must
164
be a tuple with three integer values for red, green, blue, or
165
a simple integer (or singleton tuple) for a greyscale image.
167
If specified, the gamma parameter must be a float value.
170
if width <= 0 or height <= 0:
171
raise ValueError("width and height must be greater than zero")
173
if has_alpha and transparent is not None:
175
"transparent color not allowed with alpha channel")
177
if bytes_per_sample < 1 or bytes_per_sample > 2:
178
raise ValueError("bytes per sample must be 1 or 2")
180
if transparent is not None:
182
if type(transparent) is not int:
184
"transparent color for greyscale must be integer")
186
if not (len(transparent) == 3 and
187
type(transparent[0]) is int and
188
type(transparent[1]) is int and
189
type(transparent[2]) is int):
191
"transparent color must be a triple of integers")
193
if background is not None:
195
if type(background) is not int:
197
"background color for greyscale must be integer")
199
if not (len(background) == 3 and
200
type(background[0]) is int and
201
type(background[1]) is int and
202
type(background[2]) is int):
204
"background color must be a triple of integers")
208
self.transparent = transparent
209
self.background = background
211
self.greyscale = greyscale
212
self.has_alpha = has_alpha
213
self.bytes_per_sample = bytes_per_sample
214
self.compression = compression
215
self.chunk_limit = chunk_limit
216
self.interlaced = interlaced
222
self.psize = self.bytes_per_sample * 2
225
self.psize = self.bytes_per_sample
230
self.psize = self.bytes_per_sample * 4
233
self.psize = self.bytes_per_sample * 3
235
def write_chunk(self, outfile, tag, data):
237
Write a PNG chunk to the output file, including length and checksum.
239
# http://www.w3.org/TR/PNG/#5Chunk-layout
240
outfile.write(struct.pack("!I", len(data)))
243
checksum = zlib.crc32(tag)
244
checksum = zlib.crc32(data, checksum)
245
# <ah> Avoid DeprecationWarning: struct integer overflow masking
246
# with Python2.5/Windows.
247
checksum = checksum & 0xffffffff
248
outfile.write(struct.pack("!I", checksum))
250
def write(self, outfile, scanlines):
252
Write a PNG image to the output file.
254
# http://www.w3.org/TR/PNG/#5PNG-file-signature
255
outfile.write(struct.pack("8B", 137, 80, 78, 71, 13, 10, 26, 10))
257
# http://www.w3.org/TR/PNG/#11IHDR
262
self.write_chunk(outfile, 'IHDR',
263
struct.pack("!2I5B", self.width, self.height,
264
self.bytes_per_sample * 8,
265
self.color_type, 0, 0, interlaced))
267
# http://www.w3.org/TR/PNG/#11tRNS
268
if self.transparent is not None:
270
self.write_chunk(outfile, 'tRNS',
271
struct.pack("!1H", *self.transparent))
273
self.write_chunk(outfile, 'tRNS',
274
struct.pack("!3H", *self.transparent))
276
# http://www.w3.org/TR/PNG/#11bKGD
277
if self.background is not None:
279
self.write_chunk(outfile, 'bKGD',
280
struct.pack("!1H", *self.background))
282
self.write_chunk(outfile, 'bKGD',
283
struct.pack("!3H", *self.background))
285
# http://www.w3.org/TR/PNG/#11gAMA
286
if self.gamma is not None:
287
self.write_chunk(outfile, 'gAMA',
288
struct.pack("!L", int(self.gamma * 100000)))
290
# http://www.w3.org/TR/PNG/#11IDAT
291
if self.compression is not None:
292
compressor = zlib.compressobj(self.compression)
294
compressor = zlib.compressobj()
297
for scanline in scanlines:
299
data.extend(scanline)
300
if len(data) > self.chunk_limit:
301
compressed = compressor.compress(data.tostring())
303
# print >> sys.stderr, len(data), len(compressed)
304
self.write_chunk(outfile, 'IDAT', compressed)
307
compressed = compressor.compress(data.tostring())
310
flushed = compressor.flush()
311
if len(compressed) or len(flushed):
312
# print >> sys.stderr, len(data), len(compressed), len(flushed)
313
self.write_chunk(outfile, 'IDAT', compressed + flushed)
315
# http://www.w3.org/TR/PNG/#11IEND
316
self.write_chunk(outfile, 'IEND', '')
318
def write_array(self, outfile, pixels):
320
Encode a pixel array to PNG and write output file.
323
self.write(outfile, self.array_scanlines_interlace(pixels))
325
self.write(outfile, self.array_scanlines(pixels))
327
def convert_ppm(self, ppmfile, outfile):
329
Convert a PPM file containing raw pixel data into a PNG file
330
with the parameters set in the writer object.
334
pixels.fromfile(ppmfile,
335
self.bytes_per_sample * self.color_depth *
336
self.width * self.height)
337
self.write(outfile, self.array_scanlines_interlace(pixels))
339
self.write(outfile, self.file_scanlines(ppmfile))
341
def convert_ppm_and_pgm(self, ppmfile, pgmfile, outfile):
343
Convert a PPM and PGM file containing raw pixel data into a
344
PNG outfile with the parameters set in the writer object.
347
pixels.fromfile(ppmfile,
348
self.bytes_per_sample * self.color_depth *
349
self.width * self.height)
351
apixels.fromfile(pgmfile,
352
self.bytes_per_sample *
353
self.width * self.height)
354
pixels = interleave_planes(pixels, apixels,
355
self.bytes_per_sample * self.color_depth,
356
self.bytes_per_sample)
358
self.write(outfile, self.array_scanlines_interlace(pixels))
360
self.write(outfile, self.array_scanlines(pixels))
362
def file_scanlines(self, infile):
364
Generator for scanlines from an input file.
366
row_bytes = self.psize * self.width
367
for y in range(self.height):
368
scanline = array('B')
369
scanline.fromfile(infile, row_bytes)
372
def array_scanlines(self, pixels):
374
Generator for scanlines from an array.
376
row_bytes = self.width * self.psize
378
for y in range(self.height):
380
stop = start + row_bytes
381
yield pixels[start:stop]
383
def old_array_scanlines_interlace(self, pixels):
385
Generator for interlaced scanlines from an array.
386
http://www.w3.org/TR/PNG/#8InterlaceMethods
388
row_bytes = self.psize * self.width
389
for xstart, ystart, xstep, ystep in _adam7:
390
for y in range(ystart, self.height, ystep):
391
if xstart < self.width:
394
yield pixels[offset:offset+row_bytes]
397
offset = y*row_bytes + xstart* self.psize
398
skip = self.psize * xstep
399
for x in range(xstart, self.width, xstep):
400
row.extend(pixels[offset:offset + self.psize])
404
def array_scanlines_interlace(self, pixels):
406
Generator for interlaced scanlines from an array.
407
http://www.w3.org/TR/PNG/#8InterlaceMethods
409
row_bytes = self.psize * self.width
410
for xstart, ystart, xstep, ystep in _adam7:
411
for y in range(ystart, self.height, ystep):
412
if xstart >= self.width:
415
offset = y * row_bytes
416
yield pixels[offset:offset+row_bytes]
419
# Note we want the ceiling of (self.width - xstart) / xtep
420
row_len = self.psize * (
421
(self.width - xstart + xstep - 1) / xstep)
422
# There's no easier way to set the length of an array
423
row.extend(pixels[0:row_len])
424
offset = y * row_bytes + xstart * self.psize
425
end_offset = (y+1) * row_bytes
426
skip = self.psize * xstep
427
for i in range(self.psize):
428
row[i:row_len:self.psize] = \
429
pixels[offset+i:end_offset:skip]
434
A simple file-like interface for strings and arrays.
437
def __init__(self, buf):
442
r = buf[offset:offset+n]
443
if isinstance(r, array):
450
PNG decoder in pure Python.
453
def __init__(self, _guess=None, **kw):
455
Create a PNG decoder object.
457
The constructor expects exactly one keyword argument. If you
458
supply a positional argument instead, it will guess the input
459
type. You can choose among the following arguments:
460
filename - name of PNG input file
461
file - object with a read() method
462
pixels - array or string with PNG data
465
if ((_guess is not None and len(kw) != 0) or
466
(_guess is None and len(kw) != 1)):
467
raise TypeError("Reader() takes exactly 1 argument")
469
if _guess is not None:
470
if isinstance(_guess, array):
471
kw["pixels"] = _guess
472
elif isinstance(_guess, str):
473
kw["filename"] = _guess
474
elif isinstance(_guess, file):
478
self.file = file(kw["filename"])
480
self.file = kw["file"]
482
self.file = _readable(kw["pixels"])
484
raise TypeError("expecting filename, file or pixels array")
486
def read_chunk(self):
488
Read a PNG chunk from the input file, return tag name and data.
490
# http://www.w3.org/TR/PNG/#5Chunk-layout
492
data_bytes, tag = struct.unpack('!I4s', self.file.read(8))
494
raise ValueError('Chunk too short for header')
495
data = self.file.read(data_bytes)
496
if len(data) != data_bytes:
497
raise ValueError('Chunk %s too short for required %i data octets'
499
checksum = self.file.read(4)
500
if len(checksum) != 4:
501
raise ValueError('Chunk %s too short for checksum', tag)
502
verify = zlib.crc32(tag)
503
verify = zlib.crc32(data, verify)
504
verify = struct.pack('!i', verify)
505
if checksum != verify:
506
# print repr(checksum)
507
(a,) = struct.unpack('!I', checksum)
508
(b,) = struct.unpack('!I', verify)
509
raise ValueError("Checksum error in %s chunk: 0x%X != 0x%X"
513
def _reconstruct_sub(self, offset, xstep, ystep):
519
offset += self.psize * xstep
521
for index in range(self.psize, self.row_bytes):
524
pixels[offset] = (x + a) & 0xff
528
byte_step = self.psize * xstep
529
for index in range(byte_step, self.row_bytes, byte_step):
530
for i in range(self.psize):
531
x = pixels[offset + i]
532
a = pixels[a_offset + i]
533
pixels[offset + i] = (x + a) & 0xff
534
offset += self.psize * xstep
535
a_offset += self.psize * xstep
537
def _reconstruct_up(self, offset, xstep, ystep):
542
b_offset = offset - (self.row_bytes * ystep)
544
for index in range(self.row_bytes):
547
pixels[offset] = (x + b) & 0xff
551
for index in range(0, self.row_bytes, xstep * self.psize):
552
for i in range(self.psize):
553
x = pixels[offset + i]
554
b = pixels[b_offset + i]
555
pixels[offset + i] = (x + b) & 0xff
556
offset += self.psize * xstep
557
b_offset += self.psize * xstep
559
def _reconstruct_average(self, offset, xstep, ystep):
561
Reverse average filter.
564
a_offset = offset - (self.psize * xstep)
565
b_offset = offset - (self.row_bytes * ystep)
567
for index in range(self.row_bytes):
569
if index < self.psize:
577
pixels[offset] = (x + ((a + b) >> 1)) & 0xff
582
for index in range(0, self.row_bytes, self.psize * xstep):
583
for i in range(self.psize):
585
if index < self.psize:
588
a = pixels[a_offset + i]
592
b = pixels[b_offset + i]
593
pixels[offset + i] = (x + ((a + b) >> 1)) & 0xff
594
offset += self.psize * xstep
595
a_offset += self.psize * xstep
596
b_offset += self.psize * xstep
598
def _reconstruct_paeth(self, offset, xstep, ystep):
600
Reverse Paeth filter.
603
a_offset = offset - (self.psize * xstep)
604
b_offset = offset - (self.row_bytes * ystep)
605
c_offset = b_offset - (self.psize * xstep)
606
# There's enough inside this loop that it's probably not worth
607
# optimising for xstep == 1
608
for index in range(0, self.row_bytes, self.psize * xstep):
609
for i in range(self.psize):
611
if index < self.psize:
613
b = pixels[b_offset+i]
615
a = pixels[a_offset+i]
616
b = pixels[b_offset+i]
617
c = pixels[c_offset+i]
622
if pa <= pb and pa <= pc:
628
pixels[offset+i] = (x + pr) & 0xff
629
offset += self.psize * xstep
630
a_offset += self.psize * xstep
631
b_offset += self.psize * xstep
632
c_offset += self.psize * xstep
634
# N.B. PNG files with 'up', 'average' or 'paeth' filters on the
635
# first line of a pass are legal. The code above for 'average'
636
# deals with this case explicitly. For up we map to the null
637
# filter and for paeth we map to the sub filter.
639
def reconstruct_line(self, filter_type, first_line, offset, xstep, ystep):
640
# print >> sys.stderr, "Filter type %s, first_line=%s" % (
641
# filter_type, first_line)
642
filter_type += (first_line << 8)
643
if filter_type == 1 or filter_type == 0x101 or filter_type == 0x104:
644
self._reconstruct_sub(offset, xstep, ystep)
645
elif filter_type == 2:
646
self._reconstruct_up(offset, xstep, ystep)
647
elif filter_type == 3 or filter_type == 0x103:
648
self._reconstruct_average(offset, xstep, ystep)
649
elif filter_type == 4:
650
self._reconstruct_paeth(offset, xstep, ystep)
653
def deinterlace(self, scanlines):
654
# print >> sys.stderr, ("Reading interlaced, w=%s, r=%s, planes=%s," +
655
# " bpp=%s") % (self.width, self.height, self.planes, self.bps)
658
# Make the array big enough
659
temp = scanlines[0:self.width*self.height*self.psize]
662
for xstart, ystart, xstep, ystep in _adam7:
663
# print >> sys.stderr, "Adam7: start=%s,%s step=%s,%s" % (
664
# xstart, ystart, xstep, ystep)
665
filter_first_line = 1
666
for y in range(ystart, self.height, ystep):
667
if xstart >= self.width:
669
filter_type = scanlines[source_offset]
672
offset = y * self.row_bytes
673
a[offset:offset+self.row_bytes] = \
674
scanlines[source_offset:source_offset + self.row_bytes]
675
source_offset += self.row_bytes
677
# Note we want the ceiling of (width - xstart) / xtep
678
row_len = self.psize * (
679
(self.width - xstart + xstep - 1) / xstep)
680
offset = y * self.row_bytes + xstart * self.psize
681
end_offset = (y+1) * self.row_bytes
682
skip = self.psize * xstep
683
for i in range(self.psize):
684
a[offset+i:end_offset:skip] = \
685
scanlines[source_offset + i:
686
source_offset + row_len:
688
source_offset += row_len
690
self.reconstruct_line(filter_type, filter_first_line,
691
offset, xstep, ystep)
692
filter_first_line = 0
695
def read_flat(self, scanlines):
700
filter_first_line = 1
701
for y in range(self.height):
702
filter_type = scanlines[source_offset]
704
a.extend(scanlines[source_offset: source_offset + self.row_bytes])
706
self.reconstruct_line(filter_type, filter_first_line,
708
filter_first_line = 0
709
offset += self.row_bytes
710
source_offset += self.row_bytes
715
Read a simple PNG file, return width, height, pixels and image metadata
717
This function is a very early prototype with limited flexibility
718
and excessive use of memory.
720
signature = self.file.read(8)
721
if (signature != struct.pack("8B", 137, 80, 78, 71, 13, 10, 26, 10)):
722
raise Error("PNG file has invalid header")
727
tag, data = self.read_chunk()
728
except ValueError, e:
729
raise Error('Chunk error: ' + e.args[0])
731
# print >> sys.stderr, tag, len(data)
732
if tag == 'IHDR': # http://www.w3.org/TR/PNG/#11IHDR
733
(width, height, bits_per_sample, color_type,
734
compression_method, filter_method,
735
interlaced) = struct.unpack("!2I5B", data)
736
bps = bits_per_sample / 8
738
raise Error("unsupported pixel depth")
739
if bps > 2 or bits_per_sample != (bps * 8):
740
raise Error("invalid pixel depth")
745
elif color_type == 2:
749
elif color_type == 4:
753
elif color_type == 6:
758
raise Error("unknown PNG colour type %s" % color_type)
759
if compression_method != 0:
760
raise Error("unknown compression method")
761
if filter_method != 0:
762
raise Error("unknown filter method")
765
self.psize = bps * planes
768
self.row_bytes = width * self.psize
769
elif tag == 'IDAT': # http://www.w3.org/TR/PNG/#11IDAT
770
compressed.append(data)
773
image_metadata["background"] = struct.unpack("!1H", data)
775
image_metadata["background"] = struct.unpack("!3H", data)
778
image_metadata["transparent"] = struct.unpack("!1H", data)
780
image_metadata["transparent"] = struct.unpack("!3H", data)
782
image_metadata["gamma"] = (
783
struct.unpack("!L", data)[0]) / 100000.0
784
elif tag == 'IEND': # http://www.w3.org/TR/PNG/#11IEND
786
scanlines = array('B', zlib.decompress(''.join(compressed)))
788
pixels = self.deinterlace(scanlines)
790
pixels = self.read_flat(scanlines)
791
image_metadata["greyscale"] = greyscale
792
image_metadata["has_alpha"] = has_alpha
793
image_metadata["bytes_per_sample"] = bps
794
image_metadata["interlaced"] = interlaced
795
return width, height, pixels, image_metadata
798
def test_suite(options):
800
Run regression test and write PNG file to stdout.
803
# Below is a big stack of test image generators
805
def test_gradient_horizontal_lr(x, y):
808
def test_gradient_horizontal_rl(x, y):
811
def test_gradient_vertical_tb(x, y):
814
def test_gradient_vertical_bt(x, y):
817
def test_radial_tl(x, y):
818
return max(1-math.sqrt(x*x+y*y), 0.0)
820
def test_radial_center(x, y):
821
return test_radial_tl(x-0.5, y-0.5)
823
def test_radial_tr(x, y):
824
return test_radial_tl(1-x, y)
826
def test_radial_bl(x, y):
827
return test_radial_tl(x, 1-y)
829
def test_radial_br(x, y):
830
return test_radial_tl(1-x, 1-y)
832
def test_stripe(x, n):
833
return 1.0*(int(x*n) & 1)
835
def test_stripe_h_2(x, y):
836
return test_stripe(x, 2)
838
def test_stripe_h_4(x, y):
839
return test_stripe(x, 4)
841
def test_stripe_h_10(x, y):
842
return test_stripe(x, 10)
844
def test_stripe_v_2(x, y):
845
return test_stripe(y, 2)
847
def test_stripe_v_4(x, y):
848
return test_stripe(y, 4)
850
def test_stripe_v_10(x, y):
851
return test_stripe(y, 10)
853
def test_stripe_lr_10(x, y):
854
return test_stripe(x+y, 10)
856
def test_stripe_rl_10(x, y):
857
return test_stripe(x-y, 10)
859
def test_checker(x, y, n):
860
return 1.0*((int(x*n) & 1) ^ (int(y*n) & 1))
862
def test_checker_8(x, y):
863
return test_checker(x, y, 8)
865
def test_checker_15(x, y):
866
return test_checker(x, y, 15)
875
"GLR": test_gradient_horizontal_lr,
876
"GRL": test_gradient_horizontal_rl,
877
"GTB": test_gradient_vertical_tb,
878
"GBT": test_gradient_vertical_bt,
879
"RTL": test_radial_tl,
880
"RTR": test_radial_tr,
881
"RBL": test_radial_bl,
882
"RBR": test_radial_br,
883
"RCTR": test_radial_center,
884
"HS2": test_stripe_h_2,
885
"HS4": test_stripe_h_4,
886
"HS10": test_stripe_h_10,
887
"VS2": test_stripe_v_2,
888
"VS4": test_stripe_v_4,
889
"VS10": test_stripe_v_10,
890
"LRS": test_stripe_lr_10,
891
"RLS": test_stripe_rl_10,
892
"CK8": test_checker_8,
893
"CK15": test_checker_15,
898
def test_pattern(width, height, depth, pattern):
902
pfun = test_patterns[pattern]
904
for y in range(height):
905
for x in range(width):
906
a.append(int(pfun(float(x)/fw, float(y)/fh) * 255))
908
for y in range(height):
909
for x in range(width):
910
v = int(pfun(float(x)/fw, float(y)/fh) * 65535)
915
def test_rgba(size=256, depth=1,
916
red="GTB", green="GLR", blue="RTL", alpha=None):
917
r = test_pattern(size, size, depth, red)
918
g = test_pattern(size, size, depth, green)
919
b = test_pattern(size, size, depth, blue)
921
a = test_pattern(size, size, depth, alpha)
922
i = interleave_planes(r, g, depth, depth)
923
i = interleave_planes(i, b, 2 * depth, depth)
925
i = interleave_planes(i, a, 3 * depth, depth)
928
# The body of test_suite()
930
if options.test_size:
931
size = options.test_size
933
if options.test_deep:
938
kwargs["red"] = options.test_red
939
if options.test_green:
940
kwargs["green"] = options.test_green
941
if options.test_blue:
942
kwargs["blue"] = options.test_blue
943
if options.test_alpha:
944
kwargs["alpha"] = options.test_alpha
945
pixels = test_rgba(size, depth, **kwargs)
947
writer = Writer(size, size,
948
bytes_per_sample=depth,
949
transparent=options.transparent,
950
background=options.background,
952
has_alpha=options.test_alpha,
953
compression=options.compression,
954
interlaced=options.interlace)
955
writer.write_array(sys.stdout, pixels)
958
def read_pnm_header(infile, supported='P6'):
960
Read a PNM header, return width and height of the image in pixels.
963
while len(header) < 4:
964
line = infile.readline()
965
sharp = line.find('#')
968
header.extend(line.split())
969
if len(header) == 3 and header[0] == 'P4':
970
break # PBM doesn't have maxval
971
if header[0] not in supported:
972
raise NotImplementedError('file format %s not supported' % header[0])
973
if header[0] != 'P4' and header[3] != '255':
974
raise NotImplementedError('maxval %s not supported' % header[3])
975
return int(header[1]), int(header[2])
978
def color_triple(color):
980
Convert a command line color value to a RGB triple of integers.
981
FIXME: Somewhere we need support for greyscale backgrounds etc.
983
if color.startswith('#') and len(color) == 4:
984
return (int(color[1], 16),
987
if color.startswith('#') and len(color) == 7:
988
return (int(color[1:3], 16),
991
elif color.startswith('#') and len(color) == 13:
992
return (int(color[1:5], 16),
994
int(color[9:13], 16))
999
Run the PNG encoder with options from the command line.
1001
# Parse command line arguments
1002
from optparse import OptionParser
1003
version = '%prog ' + __revision__.strip('$').replace('Rev: ', 'r')
1004
parser = OptionParser(version=version)
1005
parser.set_usage("%prog [options] [pnmfile]")
1006
parser.add_option("-i", "--interlace",
1007
default=False, action="store_true",
1008
help="create an interlaced PNG file (Adam7)")
1009
parser.add_option("-t", "--transparent",
1010
action="store", type="string", metavar="color",
1011
help="mark the specified color as transparent")
1012
parser.add_option("-b", "--background",
1013
action="store", type="string", metavar="color",
1014
help="save the specified background color")
1015
parser.add_option("-a", "--alpha",
1016
action="store", type="string", metavar="pgmfile",
1017
help="alpha channel transparency (RGBA)")
1018
parser.add_option("-g", "--gamma",
1019
action="store", type="float", metavar="value",
1020
help="save the specified gamma value")
1021
parser.add_option("-c", "--compression",
1022
action="store", type="int", metavar="level",
1023
help="zlib compression level (0-9)")
1024
parser.add_option("-T", "--test",
1025
default=False, action="store_true",
1026
help="create a test image")
1027
parser.add_option("-R", "--test-red",
1028
action="store", type="string", metavar="pattern",
1029
help="test pattern for the red image layer")
1030
parser.add_option("-G", "--test-green",
1031
action="store", type="string", metavar="pattern",
1032
help="test pattern for the green image layer")
1033
parser.add_option("-B", "--test-blue",
1034
action="store", type="string", metavar="pattern",
1035
help="test pattern for the blue image layer")
1036
parser.add_option("-A", "--test-alpha",
1037
action="store", type="string", metavar="pattern",
1038
help="test pattern for the alpha image layer")
1039
parser.add_option("-D", "--test-deep",
1040
default=False, action="store_true",
1041
help="use test patterns with 16 bits per layer")
1042
parser.add_option("-S", "--test-size",
1043
action="store", type="int", metavar="size",
1044
help="width and height of the test image")
1045
(options, args) = parser.parse_args()
1048
if options.transparent is not None:
1049
options.transparent = color_triple(options.transparent)
1050
if options.background is not None:
1051
options.background = color_triple(options.background)
1053
# Run regression tests
1055
return test_suite(options)
1057
# Prepare input and output files
1061
elif len(args) == 1:
1062
ppmfilename = args[0]
1063
ppmfile = open(ppmfilename, 'rb')
1065
parser.error("more than one input file")
1066
outfile = sys.stdout
1069
width, height = read_pnm_header(ppmfile)
1070
writer = Writer(width, height,
1071
transparent=options.transparent,
1072
background=options.background,
1073
has_alpha=options.alpha is not None,
1074
gamma=options.gamma,
1075
compression=options.compression)
1076
if options.alpha is not None:
1077
pgmfile = open(options.alpha, 'rb')
1078
awidth, aheight = read_pnm_header(pgmfile, 'P5')
1079
if (awidth, aheight) != (width, height):
1080
raise ValueError("alpha channel image size mismatch" +
1081
" (%s has %sx%s but %s has %sx%s)"
1082
% (ppmfilename, width, height,
1083
options.alpha, awidth, aheight))
1084
writer.convert_ppm_and_pgm(ppmfile, pgmfile, outfile,
1085
interlace=options.interlace)
1087
writer.convert_ppm(ppmfile, outfile,
1088
interlace=options.interlace)
1091
if __name__ == '__main__':