# Code in the PDF::API2::Basic::PDF namespace was originally copied from the
# Text::PDF distribution.
#
# Copyright Martin Hosken <Martin_Hosken@sil.org>
#
# Martin Hosken's code may be used under the terms of the MIT license.
# Subsequent versions of the code have the same license as PDF::API2.
package PDF::API2::Basic::PDF::Dict;
use base 'PDF::API2::Basic::PDF::Objind';
use strict;
no warnings qw[ deprecated recursion uninitialized ];
our $VERSION = '2.043'; # VERSION
our $mincache = 16 * 1024 * 1024;
use File::Temp;
use PDF::API2::Basic::PDF::Array;
use PDF::API2::Basic::PDF::Filter;
use PDF::API2::Basic::PDF::Name;
=head1 NAME
PDF::API2::Basic::PDF::Dict - Low-level dictionary and stream objects
=head1 INSTANCE VARIABLES
There are various special instance variables which are used to look after,
particularly, streams. Each begins with a space:
=over
=item stream
Holds the stream contents for output
=item streamfile
Holds the stream contents in an external file rather than in memory. This is
not the same as a PDF file stream. The data is stored in its unfiltered form.
=item streamloc
If both ' stream' and ' streamfile' are empty, this indicates where in the
source PDF the stream starts.
=back
=head1 METHODS
=cut
sub new {
my $class = shift();
$class = ref($class) if ref($class);
my $self = $class->SUPER::new(@_);
$self->{' realised'} = 1;
return $self;
}
=head2 $type = $d->type($type)
Get/Set the standard Type key. It can be passed, and will return, a text value rather than a Name object.
=cut
sub type {
my $self = shift();
if (scalar @_) {
my $type = shift();
$self->{'Type'} = ref($type) ? $type : PDF::API2::Basic::PDF::Name->new($type);
}
return unless exists $self->{'Type'};
return $self->{'Type'}->val();
}
=head2 @filters = $d->filter(@filters)
Get/Set one or more filters being used by the optional stream attached to the dictionary.
=cut
sub filter {
my ($self, @filters) = @_;
# Developer's Note: the PDF specification allows Filter to be
# either a name or an array, but other parts of this codebase
# expect an array. If these are updated uncomment the
# commented-out lines in order to accept both types.
# if (scalar @filters == 1) {
# $self->{'Filter'} = ref($filters[0]) ? $filters[0] : PDF::API2::Basic::PDF::Name->new($filters[0]);
# }
# elsif (scalar @filters) {
@filters = map { ref($_) ? $_ : PDF::API2::Basic::PDF::Name->new($_) } @filters;
$self->{'Filter'} = PDF::API2::Basic::PDF::Array->new(@filters);
# }
}
# Undocumented alias, which may be removed in a future release
sub filters { return filter(@_); }
=head2 $d->outobjdeep($fh)
Outputs the contents of the dictionary to a PDF file. This is a recursive call.
It also outputs a stream if the dictionary has a stream element. If this occurs
then this method will calculate the length of the stream and insert it into the
stream's dictionary.
=cut
sub outobjdeep {
my ($self, $fh, $pdf) = @_;
if (defined $self->{' stream'} or defined $self->{' streamfile'} or defined $self->{' streamloc'}) {
if ($self->{'Filter'} and $self->{' nofilt'}) {
$self->{'Length'} ||= PDF::API2::Basic::PDF::Number->new(length($self->{' stream'}));
}
elsif ($self->{'Filter'} or not defined $self->{' stream'}) {
$self->{'Length'} = PDF::API2::Basic::PDF::Number->new(0) unless defined $self->{'Length'};
$pdf->new_obj($self->{'Length'}) unless $self->{'Length'}->is_obj($pdf);
}
else {
$self->{'Length'} = PDF::API2::Basic::PDF::Number->new(length($self->{' stream'}));
}
}
$fh->print('<< ');
foreach my $key (sort {
$a eq 'Type' ? -1 : $b eq 'Type' ? 1 :
$a eq 'Subtype' ? -1 : $b eq 'Subtype' ? 1 : $a cmp $b
} keys %$self) {
next if $key =~ m/^[\s\-]/o;
next unless $self->{$key};
$fh->print('/' . PDF::API2::Basic::PDF::Name::string_to_name($key, $pdf) . ' ');
$self->{$key}->outobj($fh, $pdf);
$fh->print(' ');
}
$fh->print('>>');
# Now handle the stream (if any)
my (@filters, $loc);
if (defined $self->{' streamloc'} and not defined $self->{' stream'}) {
# read a stream if in file
$loc = $fh->tell();
$self->read_stream();
$fh->seek($loc, 0);
}
if (not $self->{' nofilt'} and defined $self->{'Filter'} and (defined $self->{' stream'} or defined $self->{' streamfile'})) {
my $hasflate = -1;
for my $i (0 .. scalar(@{$self->{'Filter'}{' val'}}) - 1) {
my $filter = $self->{'Filter'}{' val'}[$i]->val();
# hack to get around LZW patent
if ($filter eq 'LZWDecode') {
if ($hasflate < -1) {
$hasflate = $i;
next;
}
$filter = 'FlateDecode';
$self->{'Filter'}{' val'}[$i]{'val'} = $filter; # !!!
}
elsif ($filter eq 'FlateDecode') {
$hasflate = -2;
}
my $filter_class = "PDF::API2::Basic::PDF::Filter::$filter";
push (@filters, $filter_class->new());
}
splice(@{$self->{'Filter'}{' val'}}, $hasflate, 1) if $hasflate > -1;
}
if (defined $self->{' stream'}) {
$fh->print(" stream\n");
$loc = $fh->tell();
my $stream = $self->{' stream'};
unless ($self->{' nofilt'}) {
foreach my $filter (reverse @filters) {
$stream = $filter->outfilt($stream, 1);
}
}
$fh->print($stream);
## $fh->print("\n"); # newline goes into endstream
}
elsif (defined $self->{' streamfile'}) {
open(my $dictfh, "<", $self->{' streamfile'}) || die "Unable to open $self->{' streamfile'}";
binmode($dictfh, ':raw');
$fh->print(" stream\n");
$loc = $fh->tell();
my $stream;
while (read($dictfh, $stream, 4096)) {
unless ($self->{' nofilt'}) {
foreach my $filter (reverse @filters) {
$stream = $filter->outfilt($stream, 0);
}
}
$fh->print($stream);
}
close $dictfh;
unless ($self->{' nofilt'}) {
$stream = '';
foreach my $filter (reverse @filters) {
$stream = $filter->outfilt($stream, 1);
}
$fh->print($stream);
}
## $fh->print("\n"); # newline goes into endstream
}
if (defined $self->{' stream'} or defined $self->{' streamfile'}) {
my $length = $fh->tell() - $loc;
unless ($self->{'Length'}{'val'} == $length) {
$self->{'Length'}{'val'} = $length;
$pdf->out_obj($self->{'Length'}) if $self->{'Length'}->is_obj($pdf);
}
$fh->print("\nendstream"); # next is endobj which has the final cr
}
}
=head2 $d->read_stream($force_memory)
Reads in a stream from a PDF file. If the stream is greater than
C<PDF::Dict::mincache> (defaults to 32768) bytes to be stored, then
the default action is to create a file for it somewhere and to use that
file as a data cache. If $force_memory is set, this caching will not
occur and the data will all be stored in the $self->{' stream'}
variable.
=cut
sub read_stream {
my ($self, $force_memory) = @_;
my $fh = $self->{' streamsrc'};
my $len = $self->{'Length'}->val();
$self->{' stream'} = '';
my @filters;
if (defined $self->{'Filter'}) {
my $i = 0;
foreach my $filter ($self->{'Filter'}->elements()) {
my $filter_class = "PDF::API2::Basic::PDF::Filter::" . $filter->val();
unless ($self->{'DecodeParms'}) {
push(@filters, $filter_class->new());
}
elsif ($self->{'Filter'}->isa('PDF::API2::Basic::PDF::Name') and $self->{'DecodeParms'}->isa('PDF::API2::Basic::PDF::Dict')) {
push(@filters, $filter_class->new($self->{'DecodeParms'}));
}
elsif ($self->{'DecodeParms'}->isa('PDF::API2::Basic::PDF::Array')) {
my $parms = $self->{'DecodeParms'}->val->[$i];
push(@filters, $filter_class->new($parms));
}
else {
push(@filters, $filter_class->new());
}
$i++;
}
}
my $last = 0;
if (defined $self->{' streamfile'}) {
unlink ($self->{' streamfile'});
$self->{' streamfile'} = undef;
}
seek $fh, $self->{' streamloc'}, 0;
my $dictfh;
my $readlen = 4096;
for (my $i = 0; $i < $len; $i += $readlen) {
my $data;
unless ($i + $readlen > $len) {
read $fh, $data, $readlen;
}
else {
$last = 1;
read $fh, $data, $len - $i;
}
foreach my $filter (@filters) {
$data = $filter->infilt($data, $last);
}
# Start using a temporary file if the stream gets too big
if (not $force_memory and not defined $self->{' streamfile'} and (length($self->{' stream'}) + length($data)) > $mincache) {
$dictfh = File::Temp->new(TEMPLATE => 'pdfXXXXX', SUFFIX => 'dat', TMPDIR => 1);
$self->{' streamfile'} = $dictfh->filename();
print $dictfh $self->{' stream'};
undef $self->{' stream'};
}
if (defined $self->{' streamfile'}) {
print $dictfh $data;
}
else {
$self->{' stream'} .= $data;
}
}
close $dictfh if defined $self->{' streamfile'};
$self->{' nofilt'} = 0;
return $self;
}
=head2 $d->val
Returns the dictionary, which is itself.
=cut
sub val {
return $_[0];
}
1;