png.pm 4.08 KB
Newer Older
Andrew Ayer's avatar
Andrew Ayer committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
#
# Copyright 2015 Chris Lamb <lamby@debian.org>
# Copyright 2015 Andrew Ayer <agwa@andrewayer.name>
#
# This file is part of strip-nondeterminism.
#
# strip-nondeterminism is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# strip-nondeterminism is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with strip-nondeterminism.  If not, see <http://www.gnu.org/licenses/>.
#
package File::StripNondeterminism::handlers::png;

use strict;
use warnings;

25
use File::StripNondeterminism::Common qw(copy_data);
Andrew Ayer's avatar
Andrew Ayer committed
26 27
use File::Basename qw/dirname/;
use POSIX qw/strftime/;
28
use List::Util qw/min/;
Andrew Ayer's avatar
Andrew Ayer committed
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

sub crc {
	my ($data) = @_;
	return Archive::Zip::computeCRC32($data);
}

sub chunk {
	my ($type, $data) = @_;
	return pack('Na4a*N', length($data), $type, $data, crc($type . $data));
}

sub time_chunk {
	my ($seconds) = @_;
	my ($sec, $min, $hour, $mday, $mon, $year) = gmtime($seconds);
	return chunk('tIME', pack('nCCCCC', 1900+$year, $mon+1, $mday, $hour, $min, $sec));
}

sub text_chunk {
	my ($keyword, $data) = @_;
	return chunk('tEXt', pack('Z*a*', $keyword, $data));
}

sub normalize {
	my ($filename) = @_;

	my $tempfile = File::Temp->new(DIR => dirname($filename));

56 57 58
	open(my $fh, '+<', $filename) or die "$filename: open: $!";

	if (_normalize($filename, $fh, $tempfile)) {
59 60 61
		$tempfile->close;
		copy_data($tempfile->filename, $filename)
			or die "$filename: unable to overwrite: copy_data: $!";
62 63 64
	}

	close $fh;
65 66

	return 1;
67 68 69 70 71 72 73
}

sub _normalize {
	my ($filename, $fh, $tempfile) = @_;

	my $canonical_time = $File::StripNondeterminism::canonical_time;

74
	my $buf;
75
	my $modified = 0;
76 77
	my $bytes_read;

Andrew Ayer's avatar
Andrew Ayer committed
78 79
	read($fh, my $magic, 8); $magic eq "\x89PNG\r\n\x1a\n"
		or die "$filename: does not appear to be a PNG";
80

Andrew Ayer's avatar
Andrew Ayer committed
81 82 83 84 85
	print $tempfile $magic;

	while (read($fh, my $header, 8) == 8) {
		my ($len, $type) = unpack('Na4', $header);

86 87
		# Include the trailing CRC when reading
		$len += 4;
88 89 90 91

		# We cannot trust the value of $len so we cannot simply read
		# that many bytes in memory. Therefore rely on a sane value
		# for a "header" and hope that matches everything.
92
		if ($len < 4096) {
93 94 95
			my $bytes_read = read($fh, my $data, $len);

			if ($bytes_read != $len) {
96
				warn "$filename: invalid length in '$type' header";
97 98
				return 0;
			}
99 100 101

			if ($type eq "tIME") {
				print $tempfile time_chunk($canonical_time) if defined($canonical_time);
102
				$modified = 1;
103 104 105 106
				next;
			} elsif (($type =~ /[tiz]EXt/) && ($data =~ /^(date:[^\0]+|Creation Time)\0/)) {
				print $tempfile text_chunk($1, strftime("%Y-%m-%dT%H:%M:%S-00:00",
								gmtime($canonical_time))) if defined($canonical_time);
107
				$modified = 1;
108 109
				next;
			}
110 111

			print $tempfile $header . $data;
112
		} else {
Chris Lamb's avatar
Chris Lamb committed
113
			print $tempfile $header;
Andrew Ayer's avatar
Andrew Ayer committed
114

Chris Lamb's avatar
Chris Lamb committed
115 116 117
			while ($len > 0) {
				# Can't trust $len so read data part in chunks
				$bytes_read = read($fh, $buf, min($len, 4096));
118

Chris Lamb's avatar
Chris Lamb committed
119 120 121 122
				if ($bytes_read == 0) {
					warn "$filename: invalid length in '$type' header";
					return 0;
				}
123

Chris Lamb's avatar
Chris Lamb committed
124 125
				print $tempfile $buf;
				$len -= $bytes_read;
126
			}
Chris Lamb's avatar
Chris Lamb committed
127
			defined($bytes_read) or die "$filename: read failed: $!";
128
		}
129 130 131 132

		# Stop processing immediately in case there's garbage after the
		# PNG datastream. (https://bugs.debian.org/802057)
		last if $type eq 'IEND';
Andrew Ayer's avatar
Andrew Ayer committed
133 134
	}

135 136 137
	# Copy through trailing garbage.  Conformant PNG files don't have trailing
	# garbage (see http://www.w3.org/TR/PNG/#15FileConformance item c), however
	# in the interest of strip-nondeterminism being as transparent as possible,
138 139
	# we preserve the garbage.(#802057)
	my $garbage = 0;
140 141
	while ($bytes_read = read($fh, $buf, 4096)) {
		print $tempfile $buf;
142
		$garbage += $bytes_read;
143 144
	}
	defined($bytes_read) or die "$filename: read failed: $!";
145 146
	warn "$filename: $garbage bytes of garbage after IEND chunk"
		if $garbage > 0;
147

148
	return $modified;
Andrew Ayer's avatar
Andrew Ayer committed
149 150 151
}

1;