Blog: Filtering embedded timestamps from PNGs: png_chunk_filter.py

File png_chunk_filter.py, 2.8 KB (added by retracile, 2 years ago)

png_chunk_filter.py

Line 
1#!/usr/bin/env python3
2"""Code for iterating over and filtering PNG file chunks.
3
4See https://en.wikipedia.org/wiki/Portable_Network_Graphics for a description
5of the PNG file format.
6"""
7
8import sys
9import argparse
10import struct
11
12
13class InvalidPngError(Exception):
14    """PNG parsing error exception.
15    """
16
17PNG_HEADER = bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
18
19class PngIterator:
20    """Iterates over the chunks in a PNG file/stream.
21    """
22    def __init__(self, stream):
23        self.stream = stream
24        header = stream.read(len(PNG_HEADER))
25        if PNG_HEADER != header:
26            raise InvalidPngError(f"Not a PNG; found {header}")
27
28    def __iter__(self):
29        return self
30
31    def __next__(self):
32        raw_length = self.stream.read(4)
33        if not raw_length: # Nothing more to read
34            raise StopIteration
35        length = struct.unpack('>L', raw_length)[0]
36        chunk_type = self.stream.read(4)
37        chunk_data = self.stream.read(length)
38        chunk_crc = self.stream.read(4)
39        return (raw_length, chunk_type, chunk_data, chunk_crc)
40
41
42def main(argv=None):
43    """Filter chunks from a PNG file.
44    """
45    if argv is None:
46        argv = sys.argv
47
48    parser = argparse.ArgumentParser(description=main.__doc__)
49    parser.add_argument('--exclude', action='append', default=[],
50                        help="chunk types to remove from the PNG image.")
51    parser.add_argument('--verbose', action='store_true',
52                        help="list chunks encountered and exclusions")
53    parser.add_argument('filename')
54    parser.add_argument('target')
55    args = parser.parse_args(argv[1:])
56
57    if args.verbose:
58        verbose = sys.stderr.write
59    else:
60        def verbose(_message):
61            pass
62
63    if args.filename == '-':
64        source_file = sys.stdin.buffer # binary data
65    else:
66        source_file = open(args.filename, 'rb')
67
68    # Ensure the input file is a valid PNG before we create the target file.
69    try:
70        png_chunks = PngIterator(source_file)
71    except InvalidPngError as error:
72        parser.error(f"Bad input: {error}")
73
74    if args.target == '-':
75        target_file = sys.stdout.buffer # binary data
76    else:
77        target_file = open(args.target, 'wb')
78
79    verbose(f"Excluding {', '.join(sorted(args.exclude))} chunks\n")
80    excludes = set(bytes(x, 'utf8') for x in args.exclude)
81
82    target_file.write(PNG_HEADER)
83    for raw_length, chunk_type, chunk_data, chunk_crc in png_chunks:
84        verbose(f"Found {chunk_type.decode('utf8')} chunk\n")
85        if chunk_type in excludes:
86            verbose(f"Excluding {chunk_type.decode('utf8')} chunk\n")
87        else:
88            target_file.write(raw_length + chunk_type + chunk_data + chunk_crc)
89
90    return 0
91
92
93if __name__ == '__main__':
94    sys.exit(main())