outguess-png-imager.pl 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. #!/usr/bin/perl
  2. # Author: Trizen
  3. # Date: 07 February 2022
  4. # https://github.com/trizen
  5. # Hide arbitrary data into the pixels of a PNG image, storing 3 bits in each pixel color.
  6. # Concept inspired by outguess:
  7. # https://github.com/resurrecting-open-source-projects/outguess
  8. # https://uncovering-cicada.fandom.com/wiki/OutGuess
  9. # Q: How does it work?
  10. # A: The script uses the Imager library to read the RGB color values of each pixel.
  11. # Then it changes the last bit of each value to one bit from the data to be encoded.
  12. # Q: How does the decoding work?
  13. # A: The first 32 bits from the first 32 pixels of the image, form the length of the encoded data.
  14. # Then the remaining bits (3 bits from each pixel) are collected to form the encoded data.
  15. # The script also does transparent Deflate compression and decompression of the encoded data.
  16. use 5.020;
  17. use strict;
  18. use warnings;
  19. no warnings 'once';
  20. use Imager;
  21. use Getopt::Long qw(GetOptions);
  22. use experimental qw(signatures);
  23. binmode(STDIN, ':raw');
  24. binmode(STDOUT, ':raw');
  25. sub encode_data ($data, $img_file) {
  26. my $image = Imager->new(file => $img_file)
  27. or die Imager->errstr();
  28. require IO::Compress::RawDeflate;
  29. IO::Compress::RawDeflate::rawdeflate(\$data, \my $compressed_data)
  30. or die "rawdeflate failed: $IO::Compress::RawDeflate::RawDeflateError\n";
  31. $data = $compressed_data;
  32. my $bin = unpack("B*", $data);
  33. my $width = $image->getwidth();
  34. my $height = $image->getheight();
  35. my $maximum_data_size = 3 * (($width * $height - 32) >> 3);
  36. my $data_size = length($bin) >> 3;
  37. if ($data_size == 0) {
  38. die sprintf("No data was given!\n");
  39. }
  40. if ($data_size > $maximum_data_size) {
  41. die sprintf(
  42. "Data is too large (%s bytes) for this image (exceeded by %.2f%%).\n"
  43. . "Maximum data size for this image is %s bytes.\n",
  44. $data_size, 100 - ($maximum_data_size / $data_size * 100),
  45. $maximum_data_size
  46. );
  47. }
  48. warn sprintf("Compressed data size: %s bytes (%.2f%% out of max %s bytes)\n",
  49. $data_size, $data_size / $maximum_data_size * 100,
  50. $maximum_data_size);
  51. my $length_bin = unpack("B*", pack("N*", $data_size));
  52. $bin = reverse($length_bin . $bin);
  53. my $size = length($bin);
  54. OUTER: foreach my $y (0 .. $height - 1) {
  55. my $x = 0;
  56. foreach my $color ($image->getscanline(x => 0, y => $y, width => $width)) {
  57. if ($size > 0) {
  58. my ($red, $green, $blue, $alpha) = $color->rgba;
  59. $color->set((map { (($_ >> 1) << 1) | (chop($bin) || 0) } ($red, $green, $blue)), $alpha);
  60. $size -= 3;
  61. }
  62. else {
  63. last OUTER;
  64. }
  65. $image->setpixel(x => $x++, y => $y, color => $color);
  66. }
  67. }
  68. return $image;
  69. }
  70. sub decode_data ($img_file) {
  71. my $image = Imager->new(file => $img_file)
  72. or die Imager->errstr();
  73. my $width = $image->getwidth;
  74. my $height = $image->getheight;
  75. my $bin = '';
  76. my $size = 0;
  77. my $length = $width * $height;
  78. my $find_length = 1;
  79. my $max_data_size = 3 * ($length - 4);
  80. OUTER: foreach my $y (0 .. $height - 1) {
  81. foreach my $color ($image->getscanline(x => 0, y => $y, width => $width)) {
  82. if ($size < $length) {
  83. my ($red, $green, $blue) = $color->rgba;
  84. $bin .= join('', map { $_ & 1 } ($red, $green, $blue));
  85. $size += 3;
  86. if ($find_length and $size >= 32) {
  87. $length = unpack("N*", pack("B*", substr($bin, 0, 32)));
  88. $find_length = 0;
  89. $size = length($bin) - 32;
  90. $bin = substr($bin, 32);
  91. if ($length > $max_data_size or $length == 0) {
  92. die "No hidden data was found in this image!\n";
  93. }
  94. warn sprintf("Compressed data size: %s bytes\n", $length);
  95. $length <<= 3;
  96. }
  97. }
  98. else {
  99. last OUTER;
  100. }
  101. }
  102. }
  103. my $data = pack("B*", substr($bin, 0, $length));
  104. require IO::Uncompress::RawInflate;
  105. IO::Uncompress::RawInflate::rawinflate(\$data, \my $uncompressed)
  106. or die "rawinflate failed: $IO::Uncompress::RawInflate::RawInflateError\n";
  107. warn sprintf("Uncompressed data size: %s bytes\n", length($uncompressed));
  108. return $uncompressed;
  109. }
  110. sub help ($exit_code = 0) {
  111. print <<"EOT";
  112. usage: $0 [options] [input] [output]
  113. options:
  114. -z [file] : encode a given data file
  115. example:
  116. # Encode
  117. perl $0 -z=data.txt input.jpg encoded.png
  118. # Decode
  119. perl $0 encoded.png decoded-data.txt
  120. EOT
  121. exit($exit_code);
  122. }
  123. my $data_file;
  124. GetOptions("z|f|encode=s" => \$data_file,
  125. "h|help" => sub { help(0) },)
  126. or die("Error in command line arguments\n");
  127. if (defined($data_file)) {
  128. my $input_image = shift(@ARGV) // help(2);
  129. my $output_image = shift(@ARGV);
  130. open my $fh, '<:raw', $data_file
  131. or die "Can't open file <<$data_file>> for reading: $!";
  132. my $data = do {
  133. local $/;
  134. <$fh>;
  135. };
  136. close $fh;
  137. my $img = encode_data($data, $input_image);
  138. if (defined($output_image)) {
  139. if ($output_image !~ /\.png\z/i) {
  140. die "The output image must have the '.png' extension!\n";
  141. }
  142. $img->write(file => $output_image)
  143. or die $img->errstr;
  144. }
  145. else {
  146. $img->write(fh => \*STDOUT, type => 'png')
  147. or die $img->errstr;
  148. }
  149. }
  150. else {
  151. my $input_image = shift(@ARGV) // help(2);
  152. my $output_file = shift(@ARGV);
  153. my $data = decode_data($input_image);
  154. if (defined($output_file)) {
  155. open my $fh, '>:raw', $output_file
  156. or die "Can't open file <<$output_file>> for writing: $!";
  157. print $fh $data;
  158. close $fh;
  159. }
  160. else {
  161. print $data;
  162. }
  163. }