Acme-Steganography-Image-Png
view release on metacpan or search on metacpan
use vars qw($VERSION @ISA);
use Imager;
require Class::Accessor;
use Carp;
@ISA = qw(Class::Accessor);
$VERSION = '0.06';
my @keys = qw(offset data section x y datum_length done filename_generator
suffix);
# What arguments can we accept to the constructor.
# Am I reinventing the wheel here?
my %keys;
@keys{@keys} = ();
sub _keys {
return \%keys;
}
Acme::Steganography::Image::Png->mk_accessors(@keys);
# This will get refactored out at some point to support other formats.
sub generate_header {
my ($self) = shift;
my $section = $self->section;
my $header = pack 'w', $section;
if (!$section) {
$header .= pack 'w', length ${$self->data};
}
$header;
}
sub default_filename_generator {
my $state = shift;
$state ||= 0;
my $new_state = $state+1;
# really unimaginative filenames by default
($state, $new_state);
# Raw data as a greyscale PNG
sub make_image {
my $self = shift;
my $img = new Imager;
$img->read(data=>$_[0], type => 'raw', xsize => $self->x,
ysize => $self->y, datachannels=>1, storechannels=>1, bits=>8);
$img;
}
sub calculate_datum_length {
my $self = shift;
$self->x * $self->y;
}
sub extract_payload {
my ($class, $img) = @_;
my $datum;
$img->write(data=> \$datum, type => 'raw');
$datum;
}
@ISA = 'Acme::Steganography::Image::Png::RGB';
# Raw data in the low bits of a colour image
Acme::Steganography::Image::Png->mk_accessors('raw');
sub extract_payload {
my ($class, $img) = @_;
my ($raw, $data);
$img->write(data=> \$raw, type => 'raw');
my $end = length ($raw)/3;
for (my $offset = 0; $offset < $end; ++$offset) {
my ($red, $green, $blue) = unpack 'x' . ($offset * 3) . 'C3', $raw;
my $datum = (($red & 0x1F) << 11) | (($green & 0x1F) << 6) | ($blue & 0x3F);
$data .= pack 'n', $datum;
}
$data;
}
sub make_image {
my $self = shift;
# We get a copy to play with
my $raw = $self->raw;
my $offset = length ($raw)/3;
my $img = new Imager;
while ($offset--) {
my $datum = unpack 'x' . ($offset * 2) . 'n', $_[0];
my $rgb = substr ($raw, $offset * 3, 3);
# Pack 16 bits into the low bits of R G and B
$rgb &= "\xE0\xE0\xC0";
$rgb |= pack 'C3', $datum >> 11, ($datum >> 6) & 0x1F, $datum & 0x3F;
substr($raw, $offset * 3, 3, $rgb);
}
$img->read(data=>$raw, type => 'raw', xsize => $self->x,
ysize => $self->y, datachannels => 3,interleave => 0);
$img;
}
sub calculate_datum_length {
my $self = shift;
$self->x * $self->y * 2;
}
package Acme::Steganography::Image::Png::RGB::556FS;
use vars '@ISA';
@ISA = 'Acme::Steganography::Image::Png::RGB::556';
# Raw data in the low bits of a colour image, with Floyd-Steinberg dithering
@ISA = 'Acme::Steganography::Image::Png::RGB';
# Raw data in the low bits of a colour image
Acme::Steganography::Image::Png->mk_accessors('raw');
sub extract_payload {
my ($class, $img) = @_;
my ($raw, $data);
$img->write(data=> \$raw, type => 'raw');
my $end = length ($raw)/3;
for (my $offset = 0; $offset < $end; ++$offset) {
my ($red, $green, $blue) = unpack 'x' . ($offset * 3) . 'C3', $raw;
my $datum = (($red & 0x7) << 5) | (($green & 0x3) << 3) | ($blue & 0x7);
$data .= chr $datum;
}
$data;
}
sub make_image {
my $self = shift;
# We get a copy to play with
my $raw = $self->raw;
my $offset = length ($raw)/3;
my $img = new Imager;
while ($offset--) {
my $datum = unpack "x$offset C", $_[0];
my $rgb = substr ($raw, $offset * 3, 3);
# Pack 8 bits into the low bits of R G and B
$rgb &= "\xF8\xFC\xF8";
$rgb |= ("\x07\x03\x07" & pack 'C3', $datum >> 5, $datum >> 3, $datum);
substr($raw, $offset * 3, 3, $rgb);
}
$img->read(data=>$raw, type => 'raw', xsize => $self->x,
ysize => $self->y, datachannels => 3,interleave => 0);
$img;
}
sub calculate_datum_length {
my $self = shift;
$self->x * $self->y;
}
package Acme::Steganography::Image::Png::RGB;
use vars '@ISA';
@ISA = 'Acme::Steganography::Image::Png';
# Raw data in the low bits of a colour image
$self->raw($raw);
$self->SUPER::write_images;
}
package Acme::Steganography::Image::Png;
sub generate_next_image {
my ($self) = shift;
my $datum = $self->generate_header;
my $offset = $self->offset;
my $datum_length = $self->datum_length;
# Fill our blob of data to the correct length
my $grab = $datum_length - length $datum;
$datum .= substr ${$self->data()}, $offset, $grab;
$self->offset($offset + $grab);
if (length $datum < $datum_length) {
# Need to pad it. NUL is so uninspiring.
$datum .= "N" x ($datum_length - length $datum);
$self->done(1);
} elsif (length ${$self->data()} == $self->offset) {
warn length $datum;
}
$self->section($self->section + 1);
$self->make_image($datum);
}
sub new {
my $class = shift;
croak "Use a classname, not a reference for " . __PACKAGE__ . "::new"
if ref $class;
}
sub type {
'png';
}
sub write_images {
my $self = shift;
$self->section(0);
$self->offset(0);
$self->datum_length($self->calculate_datum_length());
my $type = $self->type;
my $filename_generator
= $self->filename_generator || \&default_filename_generator;
my @filenames;
my ($filename, $state);
while (!$self->done()) {
my $image = $self->generate_next_image;
($filename, $state) = &$filename_generator($state);
push @filenames, $filename;
}
@filenames;
}
# package method
sub read_files {
my $class = shift;
# This is intentionally a "sparse" array to avoid some "interesting" DOS
# possibilities.
my $length;
my %got;
foreach my $file (@_) {
my $img = new Imager;
$img->open(file => $file) or carp "Can't read '$file': " . $img->errstr;
my $payload = $class->extract_payload($img);
my $datum;
my $section;
($section, $datum) = unpack "wa*", $payload;
if ($section == 0) {
# Oops. Strip off the length.
($length, $datum) = unpack "wa*", $datum;
}
$got{$section} = $datum;
}
carp "Did not find first section in files @_" unless defined $length;
my $data = join '', map {$got{$_}} sort {$a <=> $b} keys %got;
substr ($data, $length) = '';
$data;
}
1;
__END__
=head1 NAME
Acme::Steganography::Image::Png - hide data (badly) in Png images
t/Tester.pm view on Meta::CPAN
my $data;
{
local $/;
open FH, $^X or die "Can't open $^X: $!";
binmode FH;
$data = <FH>;
}
is (length $data, -s $^X, "Read in $^X for testing");
sub test_package {
my $package = shift;
my $writer = $package->new();
ok($writer, "Testing $package");
$writer->data(\$data);
my @filenames = $writer->write_images(@_);
cmp_ok (@filenames, '>', 0, "Generated some images");
foreach (@filenames) {
ok (-e $_, "$_ exists");
}
my $reread = $package->read_files(reverse @filenames);
is (length $reread, length $data, "Same length");
# No. I don't want not equal diagnsotics sent to stderr
ok ( $reread eq $data, "Same contents");
unlink @filenames unless $::DEBUG;
}
1;
( run in 0.684 second using v1.01-cache-2.11-cpan-65fba6d93b7 )