def tagblock(tag): """ Serialize block of tags `tag` should be a list of (*signature*, *element*) pairs, where *signature* (the key) is a length 4 string, and *element* is the content of the tag element (another string). The entire tag block (consisting of first a table and then the element data) is constructed and returned as a string. """ n = len(tag) tablelen = 12 * n # Build the tag table in two parts. A list of 12-byte tags, and a # string of element data. Offset is the offset from the start of # the profile to the start of the element data (so the offset for # the next element is this offset plus the length of the element # string so far). offset = 128 + tablelen + 4 # The table. As a string. table = png.strtobytes('') # The element data element = png.strtobytes('') for k, v in tag: table += struct.pack('>4s2L', png.strtobytes(k), offset + len(element), len(v)) element += v return struct.pack('>L', n) + table + element
def img(inp): """ Open the PDS IMG file `inp` and return (*pixels*, *info*). *pixels* is an iterator over the rows, *info* is the information dictionary. """ consumed = 1024 s = inp.read(consumed) record_type = pdskey(s, 'RECORD_TYPE') if record_type != png.strtobytes('FIXED_LENGTH'): raise png.FormatError( "Can only deal with FIXED_LENGTH record type (found %s)" % record_type) record_bytes = int(pdskey(s, 'RECORD_BYTES')) # file_records = int(pdskey(s, 'FILE_RECORDS')) label_records = int(pdskey(s, 'LABEL_RECORDS')) remaining = label_records * record_bytes - consumed s += inp.read(remaining) consumed += remaining image_pointer = int(pdskey(s, '^IMAGE')) # "^IMAGE" locates a record. Records are numbered starting from 1. image_index = image_pointer - 1 image_offset = image_index * record_bytes gap = image_offset - consumed assert gap >= 0 if gap: inp.read(gap) # This assumes there is only one OBJECT in the file, and it is the # IMAGE. height = int(pdskey(s, ' LINES')) width = int(pdskey(s, ' LINE_SAMPLES')) sample_type = pdskey(s, ' SAMPLE_TYPE') sample_bits = int(pdskey(s, ' SAMPLE_BITS')) # TODO: For Messenger MDIS, SAMPLE_BITS is reported as 16, but only values # from 0 ot 4095 are used. bitdepth = sample_bits if bitdepth == 16 and\ sample_type == png.strtobytes('MSB_UNSIGNED_INTEGER'): fmt = '>H' elif bitdepth == 8: fmt = '@B' else: raise png.FormatError('Unknown sample type: %s.' % sample_type) sample_bytes = (1, 2)[bitdepth > 8] row_bytes = sample_bytes * width fmt = fmt[:1] + str(width) + fmt[1:] def rowiter(): for y in range(height): yield struct.unpack(fmt, inp.read(row_bytes)) info = dict(greyscale=True, alpha=False, bitdepth=bitdepth, size=(width, height), gamma=1.0) return rowiter(), info
def desc(ascii): """ Return textDescription type [ICC 2001] 6.5.17. The ASCII part is filled in with the string `ascii`, the Unicode and ScriptCode parts are empty. """ ascii = png.strtobytes(ascii) + png.zerobyte l = len(ascii) return struct.pack('>L%ds2LHB67s' % l, l, ascii, 0, 0, 0, 0, png.strtobytes(''))
def testPNMsbit(self): """Test that PNM files can generates sBIT chunk.""" s = BytesIO() s.write(strtobytes('P6 8 1 1\n')) for pixel in range(8): s.write(struct.pack('<I', (0x4081 * pixel) & 0x10101)[:3]) s.flush() s.seek(0) o = BytesIO() _redirect_io(s, o, lambda: png.pnm2png.main(['testPNMsbit'])) r = png.Reader(bytes=o.getvalue()) sbit = r.chunk('sBIT')[1] self.assertEqual(sbit, strtobytes('\x01\x01\x01'))
def testPGMin(self): """Test that the command line tool can read PGM files.""" s = BytesIO() s.write(strtobytes('P5 2 2 3\n')) s.write(strtobytes('\x00\x01\x02\x03')) s.flush() s.seek(0) o = BytesIO() _redirect_io(s, o, lambda: png.pnm2png.main(['testPGMin'])) r = png.Reader(bytes=o.getvalue()) r.read() self.assertEqual(r.greyscale, True) self.assertEqual(r.bitdepth, 2)
def testPGMin(self): """Test that the command line tool can read PGM files.""" def do(): return png._main(['testPGMin']) s = BytesIO() s.write(strtobytes('P5 2 2 3\n')) s.write(strtobytes('\x00\x01\x02\x03')) s.flush() s.seek(0) o = BytesIO() _redirect_io(s, o, do) r = png.Reader(bytes=o.getvalue()) x,y,pixels,meta = r.read() self.assertTrue(r.greyscale) self.assertEqual(r.bitdepth, 2)
def RDvcgt(s): """Convert Apple CMVideoCardGammaType.""" # See # http://developer.apple.com/documentation/GraphicsImaging/Reference/ # ColorSync_Manager/Reference/reference.html#//apple_ref/c/ # tdef/CMVideoCardGammaType assert s[0:4] == png.strtobytes('vcgt') tagtype, = struct.unpack('>L', s[8:12]) if tagtype != 0: return s[8:] if tagtype == 0: # Table. _, count, size = struct.unpack('>3H', s[12:18]) if size == 1: fmt = 'B' elif size == 2: fmt = 'H' else: return s[8:] l = len(s[18:]) // size t = struct.unpack('>%d%s' % (l, fmt), s[18:]) t = group(t, count) return size, t return s[8:]
def seqtobytes(s): """ Convert a sequence of integers to a *bytes* instance. Good for plastering over Python 2 / Python 3 cracks. """ return strtobytes(''.join([chr(x) for x in s]))
def eachchunk(chunk): if chunk[0] != 'IDAT': return chunk data = zlib.decompress(chunk[1]) # Corrupt the first filter byte data = strtobytes('\x99') + data[1:] data = zlib.compress(data) return (chunk[0], data)
def eachchunk(chunk): if chunk[0] != 'IDAT': return chunk data = zlib.decompress(chunk[1]) data += strtobytes('\x00garbage') data = zlib.compress(data) chunk = (chunk[0], data) return chunk
def RDtext(s): """Convert ICC textType to Python string.""" # Note: type not specified or used in [ICC 2004], only in older # [ICC 2001]. # See [ICC 2001] 6.5.18 assert s[0:4] == png.strtobytes('text') return s[8:-1]
def _dehex(s): """Liberally convert from hex string to binary string.""" import re import binascii # Remove all non-hexadecimal digits s = re.sub(r'[^a-fA-F\d]', '', s) # binscii.unhexlify works in Python 2 and Python 3 (unlike # thing.decode('hex')). return BytesIO(binascii.unhexlify(strtobytes(s)))
def testICCPgrey(self): """Test constructing grey ICC Profile with iccp tool""" o = BytesIO() _redirect_io(None, o, lambda: png.iccp.main(['iccpgrey', '-mmkgrey', '-o-'])) o.seek(0) r = png.iccp.Profile() r.fromFile(o) self.assertEqual(r.d['colourspace'], png.strtobytes('GRAY'))
def testICCPread(self): """Test ICC Profile read from png and parsed with iccp tool""" pngsuite.png["ff99ff_iccp"].seek(0) o = BytesIO() _redirect_io(pngsuite.png["ff99ff_iccp"], o, lambda: png.iccp.main(['iccpexp', '-mview', '-o-'])) o.seek(0) s = o.read() res = (png.strtobytes("rTRC: ('curv', {'gamma': 1})") in s) self.assertEqual(res, True)
def testPPMASCIIin(self): """Test that the command line tool can read ASCII PPM files.""" s = os.path.join(os.path.dirname(__file__), 'testfiles', 'feep.ascii.ppm') o = BytesIO() _redirect_io(None, o, lambda: png.pnm2png.main(['testPPMASCIIin', s])) r = png.Reader(bytes=o.getvalue()) r.read() self.assertEqual(r.greyscale, False) # bitdepth 4 could be saved in RGB only as sBIT self.assertEqual(r.sbit, strtobytes('\x04\x04\x04'))
def fromString(self, profile, name='<unknown>'): self.d = dict() d = self.d if len(profile) < 128: raise png.FormatError("ICC Profile is too short.") d.update(dict( zip(['size', 'preferredCMM', 'version', 'profileclass', 'colourspace', 'pcs'], struct.unpack('>L4sL4s4s4s', profile[:24])))) if len(profile) < d['size']: warnings.warn( 'Profile size declared to be %d, but only got %d bytes' % (d['size'], len(profile))) d['version'] = '%08x' % d['version'] d['created'] = readICCdatetime(profile[24:36]) d.update(dict( zip(['acsp', 'platform', 'flag', 'manufacturer', 'model'], struct.unpack('>4s4s3L', profile[36:56])))) if d['acsp'] != png.strtobytes('acsp'): warnings.warn('acsp field not present (not an ICC Profile?).') d['deviceattributes'] = profile[56:64] d['intent'], = struct.unpack('>L', profile[64:68]) d['pcsilluminant'] = readICCXYZNumber(profile[68:80]) d['creator'] = profile[80:84] d['id'] = profile[84:100] ntags, = struct.unpack('>L', profile[128:132]) d['ntags'] = ntags fmt = '4s2L' * ntags # tag table tt = struct.unpack('>' + fmt, profile[132:132 + 12 * ntags]) tt = group(tt, 3) # Could (should) detect 2 or more tags having the same sig. But # we don't. Two or more tags with the same sig is illegal per # the ICC spec. # Convert (sig,offset,size) triples into (sig,value) pairs. rawtag = list(map(lambda x: (x[0], profile[x[1]:x[1] + x[2]]), tt)) self.rawtagtable = rawtag self.rawtagdict = dict(rawtag) tag = dict() # Interpret the tags whose types we know about for sig, v in rawtag: sig = png.bytestostr(sig) if sig in tag: warnings.warn("Duplicate tag %r found. Ignoring." % sig) continue v = ICCdecode(v) if v is not None: tag[sig] = v self.tag = tag self.name = name return self
def RDcurv(s): """Convert ICC curveType.""" # See [ICC 2001] 6.5.3 assert s[0:4] == png.strtobytes('curv') count, = struct.unpack('>L', s[8:12]) if count == 0: return dict(gamma=1) table = struct.unpack('>%dH' % count, s[12:]) if count == 1: return dict(gamma=table[0] * 2 ** -8) return table
def RDcurv(s): """Convert ICC curveType.""" # See [ICC 2001] 6.5.3 assert s[0:4] == png.strtobytes('curv') count, = struct.unpack('>L', s[8:12]) if count == 0: return dict(gamma=1) table = struct.unpack('>%dH' % count, s[12:]) if count == 1: return dict(gamma=table[0] * 2**-8) return table
def pdskey(s, k): """ Lookup key `k` in string `s`. Returns value (as a string), or raises exception if not found. """ assert re.match(r' *\^?[:\w]+$', k) safere = png.strtobytes('^' + re.escape(k) + r' *= *(\w+)') m = re.search(safere, s, re.MULTILINE) if not m: raise png.FormatError("Can't find %s." % k) return m.group(1)
def read_pnm_header(infile, supported=('P5', 'P6')): """ Read a PNM header, returning (format, width, height, depth, maxval). `width` and `height` are in pixels. `depth` is the number of channels in the image; for PBM and PGM it is synthesized as 1, for PPM as 3; for PAM images it is read from the header. `maxval` is synthesized (as 1) for PBM images. """ # Generally, see http://netpbm.sourceforge.net/doc/ppm.html # and http://netpbm.sourceforge.net/doc/pam.html supported = [png.strtobytes(x) for x in supported] # Technically 'P7' must be followed by a newline, so by using # rstrip() we are being liberal in what we accept. I think this # is acceptable. mode = infile.read(3).rstrip() if mode not in supported: raise NotImplementedError('file format %s not supported' % mode) if mode == png.strtobytes('P7'): # PAM header parsing is completely different. return read_pam_header(infile) # Expected number of tokens in header (3 for P4, 4 for P6) expected = 4 if mode in (png.strtobytes('P1'), png.strtobytes('P4')): expected = 3 header = [mode] header.extend(read_int_tokens(infile, expected - 1, False)) if len(header) == 3: # synthesize a MAXVAL header.append(1) depth = (1, 3)[mode in (png.strtobytes('P3'), png.strtobytes('P6'))] return header[0], header[1], header[2], depth, header[3]
def convert(f, output=None): """ Convert Plan 9 file to PNG format. Works with either uncompressed or compressed files. """ if output is None: output = sys.stdout r = f.read(11) if r == png.strtobytes('compressed\n'): aspng(output, *decompress(f)) else: aspng(output, *glue(f, r))
def read_pam_header(infile): """ Read (the rest of a) PAM header. `infile` should be positioned immediately after the initial 'P7' line (at the beginning of the second line). Returns are as for `read_pnm_header`. """ # Unlike PBM, PGM, and PPM, we can read the header a line at a time. header = dict() while True: l = infile.readline().strip() if l == png.strtobytes('ENDHDR'): break if not l: raise EOFError('PAM ended prematurely') if l[0] == png.strtobytes('#'): continue l = l.split(None, 1) if l[0] not in header: header[l[0]] = l[1] else: header[l[0]] += png.strtobytes(' ') + l[1] required = ['WIDTH', 'HEIGHT', 'DEPTH', 'MAXVAL'] required = [png.strtobytes(x) for x in required] WIDTH, HEIGHT, DEPTH, MAXVAL = required present = [x for x in required if x in header] if len(present) != len(required): raise png.Error('PAM file must specify ' 'WIDTH, HEIGHT, DEPTH, and MAXVAL') width = int(header[WIDTH]) height = int(header[HEIGHT]) depth = int(header[DEPTH]) maxval = int(header[MAXVAL]) if (width <= 0 or height <= 0 or depth <= 0 or maxval <= 0): raise png.Error( 'WIDTH, HEIGHT, DEPTH, MAXVAL must all be positive integers') return 'P7', width, height, depth, maxval
def encode(tsig, *l): """ Encode a Python value as an ICC type. `tsig` is the type signature to (the first 4 bytes of the encoded value, see [ICC 2004] section 10. """ if tsig not in encodefuncs: raise "No encoder for type %r." % tsig v = encodefuncs[tsig](*l) # Padd tsig out with spaces (and ensure it is bytes). tsig = (png.strtobytes(tsig + ' '))[:4] return tsig + png.zerobyte * 4 + v
def read_int_tokens(infile, n, allow_eof=False): """ Read ASCII integers separated with whitespaces as list of length `n` Skip comments started with '#' to the end of line If comment starts right after digit and newline starts with digit these digits form single number. """ result = [] EOF = [False] # Hack to allow modification in nested function # We may consume less or more than one line, so characters read one by one def getc(): c = infile.read(1) if not c: if not allow_eof or EOF[0]: raise png.Error('premature End of file') else: # small hack to simulate trailing whitespace at the end of file EOF[0] = True # but only once return ' ' return c token = bytes() while True: c = getc() if c.isspace(): if token: # post-token whitespace, save token result.append(int(token)) if len(result) == n: # we get here on last whitespace break # and clean for new token token = bytes() # Skip whitespace that precedes a token or between tokens. elif c == png.strtobytes('#'): # Skip comments to the end of line. infile.readline() # If there is no whitespaces after conventional newline # continue reading token elif c.isdigit(): token += c else: raise png.Error('unexpected character %s found ' % c) return result
def write_pnm(fileobj, width, height, pixels, meta): """Write a Netpbm PNM/PAM file.""" bitdepth = meta['bitdepth'] maxval = 2**bitdepth - 1 # Rudely, the number of image planes can be used to determine # whether we are L (PGM), LA (PAM), RGB (PPM), or RGBA (PAM). planes = meta['planes'] # Can be an assert as long as we assume that pixels and meta came # from a PNG file. assert planes in (1, 2, 3, 4) if planes in (1, 3): if 1 == planes: # PGM # Could generate PBM if maxval is 1, but we don't (for one # thing, we'd have to convert the data, not just blat it # out). fmt = 'P5' else: # PPM fmt = 'P6' header = '%s %d %d %d\n' % (fmt, width, height, maxval) if planes in (2, 4): # PAM # See http://netpbm.sourceforge.net/doc/pam.html if 2 == planes: tupltype = 'GRAYSCALE_ALPHA' else: tupltype = 'RGB_ALPHA' header = ('P7\nWIDTH %d\nHEIGHT %d\nDEPTH %d\nMAXVAL %d\n' 'TUPLTYPE %s\nENDHDR\n' % (width, height, planes, maxval, tupltype)) fileobj.write(png.strtobytes(header)) # Values per row vpr = planes * width # struct format fmt = '>%d' % vpr if maxval > 0xff: fmt = fmt + 'H' else: fmt = fmt + 'B' for row in pixels: fileobj.write(struct.pack(fmt, *row)) fileobj.flush()
def testPAMin(self): """Test that the command line tool can read PAM file.""" s = BytesIO() s.write(strtobytes('P7\nWIDTH 3\nHEIGHT 1\nDEPTH 4\nMAXVAL 255\n' 'TUPLTYPE RGB_ALPHA\nENDHDR\n')) # The pixels in flat row flat pixel format flat = [255, 0, 0, 255, 0, 255, 0, 120, 0, 0, 255, 30] asbytes = seqtobytes(flat) s.write(asbytes) s.flush() s.seek(0) o = BytesIO() _redirect_io(s, o, lambda: png.pnm2png.main(['testPAMin'])) r = png.Reader(bytes=o.getvalue()) pixels = r.read()[2] self.assertEqual(r.alpha, True) self.assertEqual(not r.greyscale, True) self.assertEqual(list(itertools.chain(*pixels)), flat)
def RDmluc(s): """ Convert ICC multiLocalizedUnicodeType. This types encodes several strings together with a language/country code for each string. A list of (*lc*, *string*) pairs is returned where *lc* is the 4 byte language/country code, and *string* is the string corresponding to that code. It seems unlikely that the same language/country code will appear more than once with different strings, but the ICC standard does not prohibit it. """ # See [ICC 2004] 10.13 assert s[0:4] == png.strtobytes('mluc') n, sz = struct.unpack('>2L', s[8:16]) assert sz == 12 record = [] for _ in range(n): lc, l, o = struct.unpack('4s2L', s[16 + 12 * n:28 + 12 * n]) record.append(lc, s[o:o + l]) # How are strings encoded? return record
def testText(self): """Test text information saving and retrieving""" # Use image as core for text pngsuite.png['basn2c16'].seek(0) r = png.Reader(file=pngsuite.png['basn2c16']) x, y, pixels, info = r.read() text = {'Software': 'PurePNG library', 'Source': 'PNGSuite', 'Comment': 'Text information test'} # Simple unicode test try: unic = unichr except NameError: unic = chr # Unicode only by type should be saved to tEXt text['Author'] = strtobytes('Pavel Zlatovratskii').decode('latin-1') # Non-latin unicode should go to iTXt # 'Be careful with unicode!' in russian. text['Warning'] = reduce(lambda x, y: x + unic(y), (1054, 1089, 1090, 1086, 1088, 1086, 1078, 1085, 1077, 1081, 32, 1089, 32, 1102, 1085, 1080, 1082, 1086, 1076, 1086, 1084, 33), '') # Embedded keyword test info_e = dict(info) # copy info_e.update(text) test_e = topngbytes('text_e.png', pixels, x, y, **info_e) x, y, pixels, info_r = png.Reader(bytes=test_e).read() self.assertEqual(text, info_r.get('text')) # Separate argument test info_a = dict(info) info_a['text'] = text # Here we can use any keyword, not only registered info_a['text']['Goddamn'] = 'I can do ANYTHING!' test_a = topngbytes('text_a.png', pixels, x, y, **info_a) x, y, pixels, info_r = png.Reader(bytes=test_a).read() self.assertEqual(text, info_r.get('text'))
def main(argv): """Run the PNG encoder with options from the command line.""" (options, infilename, infile, outfile) = parse_options(argv[1:]) if options.read_png: # Encode PNG to PPM pngObj = png.Reader(file=infile) width, height, pixels, meta = pngObj.asDirect() write_pnm(outfile, width, height, pixels, meta) else: # Encode PNM to PNG mode, width, height, depth, maxval = \ read_pnm_header(infile, ('P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7')) # When it comes to the variety of input formats, we do something # rather rude. Observe that L, LA, RGB, RGBA are the 4 colour # types supported by PNG and that they correspond to 1, 2, 3, 4 # channels respectively. So we use the number of channels in # the source image to determine which one we have. We do not # care about TUPLTYPE. greyscale = depth <= 2 pamalpha = depth in (2, 4) supported = [2 ** x - 1 for x in range(1, 17)] try: bitdepth = supported.index(maxval) + 1 except ValueError: raise NotImplementedError( 'your maxval (%s) not in supported list %s' % (maxval, str(supported))) writer = png.Writer(width, height, greyscale=greyscale, bitdepth=bitdepth, interlace=options.interlace, transparent=options.transparent, background=options.background, alpha=bool(pamalpha or options.alpha), gamma=options.gamma, compression=options.compression) if mode == png.strtobytes('P4'): rows = pbmb_scanlines(infile, width, height) elif mode in (png.strtobytes('P1'), png.strtobytes('P2'), png.strtobytes('P3')): rows = ascii_scanlines(infile, width, height, depth, bitdepth) else: rows = file_scanlines(infile, width, height, depth, bitdepth) if options.alpha: apgmfile = open(options.alpha, 'rb') _, awidth, aheight, adepth, amaxval = \ read_pnm_header(apgmfile, ('P5', )) if amaxval != maxval: raise NotImplementedError( 'maxval %s of alpha channel mismatch %s maxval %s' % (amaxval, infilename, maxval)) if adepth != 1: raise ValueError("alpha image should have 1 channel") if (awidth, aheight) != (width, height): raise ValueError("alpha channel image size mismatch" " (%s has %sx%s but %s has %sx%s)" % (infilename, width, height, options.alpha, awidth, aheight)) arows = file_scanlines(apgmfile, width, height, 1, bitdepth) merged = png.MergedPlanes(rows, depth, arows, 1, bitdepth) writer.write(outfile, merged) apgmfile.close() else: writer.write(outfile, rows) if infilename != '-': # if open - then close infile.close()
def eachchunk(cname, data): """Corrupt the first filter byte""" data = zlib.decompress(data) data = strtobytes('\x99') + data[1:] data = zlib.compress(data) return (cname, data)
def eachchunk(cname, data): """Adding garbage""" data = zlib.decompress(data) data += strtobytes('\x00garbage') data = zlib.compress(data) return (cname, data)