#!/usr/bin/perl =pod =head1 NAME psbind - Transform PostScript files to save trees and reduce guilt =head1 SYNOPSIS B<psbind> [I<option>]... [I<input-file-name> [I<output-file-name>]] =head1 DESCRIPTION C<psbind> examines the margins in a PostScript document and rearranges the pages to fit them onto paper efficiently. It outputs a transformed PostScript document. Because C<psbind> detects the margins in its input automatically, it is particularly useful on documents with large or unbalanced margins. For example, many PostScript documents are laid out for paper sizes smaller than A4 or Letter. C<psbind> can place two such pages onto one output page, often without shrinking the text. It is also useful for printing documents formatted for A4 paper on Letter stock, or vice versa. The simplest way to invoke C<psbind> is without any arguments, as in: =over 4 psbind =back By default, C<psbind> reads a PostScript document from standard input and reformats it 2-up (i.e., placing two input pages on each output page). It leaves approximately 1/4 inch (6 mm) margins, and writes the result to standard output. By adding command line options, you can: =over 4 =item * Place a different number of input pages onto each output page (e.g., C<psbind -4>). =item * Recenter the text on each page without combining pages (C<psbind -C>). =item * Simply trim off the margins, for further processing by another program (C<psbind -T>). =item * Send the output to a printer (C<psbind -PI<printer-name>>) or a file. =item * Fine-tune output formatting (e.g., C<psbind --margin=2cm>), margin detection (e.g., C<psbind --sample=2-10>), and other options. =back The rest of this document describes how. =head1 OPTIONS In addition to specifying the options described below to C<psbind> on the command line, you can put them in an environment variable named C<PSBIND>. As one might expect, options specified in this environment variable can be overridden on the command line. =head2 Input and output locations You can invoke C<psbind> with zero, one, or two file names on the command line. =over 4 =item * If you invoke C<psbind> with no file name on the command line, it acts as a filter. In other words, it reads a PostScript document from standard input and writes a transformed document to standard output. =over 4 psbind =back =item * If you invoke C<psbind> with one file name on the command line, it reads from the specified file and writes to standard output. =over 4 psbind I<input-file-name> =back =item * If you invoke C<psbind> with two file names on the command line, it reads from the first file and writes to the second file. If the second file already exists, it will be overwritten. =over 4 psbind I<input-file-name output-file-name> =back =back You can also send the output directly to a printer. =over 4 =item B<-P/--printer=>I<printer-name> Send output to printer. Send the transformed PostScript document to the specified printer by invoking C<lpr>. This option cannot be specified in conjunction with an output file name. =back =head2 Output formatting options You can tell C<psbind> to do one of three things: combine pages, recenter pages, or trim pages. The default is to combine pages (2-up). =over 4 =item B<-N/--nup> Combine pages. Place multiple input pages (2 by default) on each output page. Resize the material and optimize the layout to minimize wasted space. To change the number of input pages combined into each output page, use the C<-n/--nup-n> option. =item B<-C/--center> Recenter pages. Shift each page by an offset so that margins are balanced between top and bottom and between left and right. Do not resize or combine pages. =item B<-T/--trim> Trim pages. Shift each page by an offset, and change the paper size information in the document, so that there is no margin whatsoever in the output document. This option is useful only if the output of C<psbind> is fed into another program for post-processing. =back The following option is relevant only when combining pages (C<-N/--nup>) or recentering pages (C<-C/--center>): =over 4 =item B<-p/--paper=>I<a4/letter/other-paper-size> Set output paper size. By default, C<psbind> produces output for the default paper size returned by C<paperconf> (if C<libpaperg> is installed), or Letter paper (otherwise). You can use this option to switch to a different paper size: any size known to C<libpaperg> (if it is installed), or A4 or Letter (otherwise). =back The following options are relevant only when combining pages (C<-N/--nup>). =over 4 =item B<-n/--nup-n=>I<n> Set number of input pages per output page. By default, C<psbind> puts 2 input pages on each output page; this option adjusts the number. To zoom each input page to fit one output page, say C<-n1>. =item B<-1>, B<-2>, ... Abbreviation for C<-n1>, C<-n2>, .... =item B<--margin/--nup-margin=>I<dimension> Set output margin. This option specifies how much margin to leave around each output page. The default is 1/8 inch (3 mm). You can override this setting in centimeters (C<0.3cm>), millimeters (C<3mm>), inches (C<0.125in>), or PostScript points (C<9pt>). If you do not specify a unit, PostScript points are assumed. =item B<--border/--nup-border=>I<dimension> Set output border. This option specifies how much space to leave around each I<input> page in the combined output. The default is 1/8 inch (3 mm). =item B<--magic> Try very hard to keep the size of the original image. Documents generally look worse when rescaled. This is especially true of documents that contain bitmaps, be them bitmap fonts or bitmap images. The C<--magic> option suppresses the default rescaling behavior of C<psbind> whenever possible. By "whenever possible" we mean whenever the unscaled originals will fit in the output, without taking C<--margin> or C<--border> into account. This option is particularly useful for documents that C<psbind> would normally decide to I<expand> after trimming off margins. =back The following options are effective at all times, but probably useful only when combining pages (C<-N/--nup>). =over 4 =item B<--scoot> Prepend blank page to input. Many documents show page numbers to the right on odd-numbered pages and to the left on even-numbered pages. When printing such documents 2-up, the page numbers on adjacent input pages end up next to each other and look funny. To solve this problem, this option prepends a blank page to the input, so that odd-numbered pages appear to the right, and even-numbered pages to the left. =item B<--tumble> Rotate every other output page. Some people like to flip through their duplex documents along the short edge of the paper rather than the long edge. This option rotates every other page in the output document by 180 degrees to achieve the effect. =back =head2 Margin detection options C<psbind> detects margins in the input document as follows: =over 4 =item 1. Run Ghostscript to compute the extent of each input page. By default, only sample the first 10 pages. =item 2. Compute the left and right margins to be the maximum amounts that would not run into any sampled extent. By default, compute separate left and right margins for odd-numbered and even-numbered pages. =item 3. Compute the top and bottom margins to be the maximum amounts that would not run into any sampled extent. By default, compute a single set of top and bottom margins for all pages. =item 4. Check if the computed margins seem strange. ("Strange" is defined as: The computed extent of odd-numbered pages, even-numbered pages, or both exceed 1200 PostScript points or fall under 100 PostScript points in either or both dimension.) If they seem strange, run Ghostscript to rewrite the input document, then try detecting the margins again. =back The following options allow you to fine-tune this margin detection process. =over 4 =item B<-s/--sample=>I<pages> Choose input pages to sample. By default, the first 10 pages are sampled from the input document to detect its margins. This options allows you to specify a different set of pages to sample. The list of C<pages> should be a comma-separated list of page ranges, each of which may be a page number, or a page range of the form C<I<first>-I<last>>. If C<first> is omitted, the first page is assumed, and if C<last> is omitted, the last page is assumed. Negative numbers in C<last> indicate pages relative to the end of the document, counting backwards. For example, to skip page 5 (perhaps because it sticks out a little bit), say C<--sample=1-4,6-10>. =item B<--xmodulus=>I<n> Specify page modulus for detecting horizontal extent. By default, left and right margins are determined separately for odd-numbered pages and even-numbered pages. This behavior corresponds to a default setting of C<--xmodulus=2>. To determine a single horizontal extent for all pages taken together, say C<--xmodulus=1>. You can also change this setting to integers greater than 2, but it probably does not make any sense. =item B<--ymodulus=>I<n> Specify page modulus for detecting vertical extent. By default, a single set of top and bottom margins is determined for all pages taken together. This behavior corresponds to a default setting of C<--ymodulus=1>. To determine vertical extents separately for odd-numbered pages and even-numbered pages, say C<--ymodulus=2>. You can also change this setting to integers greater than 2, but it probably does not make any sense. =item B<--fix=>I<auto/no/yes/force> Invoke or suppress C<fixps>. By default, C<psbind> first tries to determine margins using the original input document; if this first attempt fails, it then invokes C<fixps --force> to rewrite the document for a second try. To disable C<fixps> invocation altogether, say C<--fix=no>. To invoke C<fixps --force> right away, say C<--fix=force>. To invoke C<fixps> right away (without C<--force>), say C<--fix=yes>. =back =head2 Miscellaneous options =over 4 =item B<-q/--quiet> Suppress status messages. Usually, C<psbind> prints external commands as it executes them. It also produces messages summarizing the margin detection process. This option suppresses these messages. =item B<--ghostscript=>I<program-name> =item B<--psnup=>I<program-name> =item B<--pstops=>I<program-name> =item B<--psselect=>I<program-name> =item B<--fixps=>I<program-name> =item B<--lpr=>I<program-name> =item B<--paperconf=>I<program-name> Specify locations of external programs. To do its job, C<psbind> invokes the external programs listed above. By default, C<psbind> searches for these programs under their standard names on the executable path. These options override how C<psbind> invokes external programs. For example, to invoke C<lp> instead of C<lpr>, say C<psbind --lpr=lp>. =item B<--help> Display usage information. This option makes C<psbind> display usage information and do nothing else. =item B<--manual> Display complete documentation. This option makes C<psbind> display complete documentation and do nothing else. =back =head1 PREREQUISITES C<psbind> is a Perl program; to run it, your system needs to have Perl 5 installed (see L<perl(1)>). C<psbind> also requires Ghostscript with C<bbox> device support (see L<gs(1)>), as well as C<psutils> (see L<psnup(1)>, L<pstops(1)>, L<psselect(1)>, and L<fixps(1)>). =head1 COREQUISITES For sending its output to a printer, C<psbind> relies on C<lpr> (see L<lpr(1)>). For paper size information, C<psbind> relies on C<libpaperg> if it is available (see L<paperconf(1)>). =head1 VERSION This version of C<psbind> is dated 2005-02-20. =head1 AUTHOR AND COPYRIGHT Copyright (c) 2001-2005, Chung-chieh Shan. This program 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 2 of the License, or (at your option) any later version. This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place Suite 330, Boston, MA 02111-1307, USA. You may contact Chung-chieh Shan at: Department of Computer Science Rutgers, the State University of New Jersey 110 Frelinghuysen Road Piscataway, NJ 08854, USA ccshan@post.harvard.edu Latest contact information may be found on the World Wide Web at http://www.cs.rutgers.edu/~ccshan/ =head1 THANKS Thanks to Dylan Thurston, Danny Calegari, Nathan Dunfield, Norman Ramsey, and Kaihsu Tai for helpful suggestions and encouragement. =head1 README C<psbind> examines the margins in a PostScript document and rearranges the pages to fit them onto paper efficiently. =head1 SCRIPT CATEGORIES CPAN/Administrative =cut use strict; use Getopt::Long; use Pod::Usage; my $VERSION = 2005_02_20; ## Parse command line options if (length $ENV{PSBIND}) { require Text::ParseWords; unshift @ARGV, Text::ParseWords::shellwords($ENV{PSBIND}); } local $SIG{__WARN__} = sub { print STDERR $_[0] unless $_[0] =~ /^Ignoring '!' modifier for short option/s; }; Getopt::Long::Configure(qw(no_auto_abbrev no_getopt_compat no_require_order permute bundling)); my $MESSAGE = "\npsbind - Transform PostScript files to save trees and reduce guilt\n"; my %options = ( help => 0, # Display help message manual => 0, # Display manual page printer => undef, # Output printer name nup => undef, # Action choice 1: invoke psnup (default) center => undef, # Action choice 3: center on page trim => undef, # Action choice 2: trim to bounding box paper => '', # Output paper size (letter or a4) n => undef, # Number of pages per sheet, for --nup ndigits => '', # Number of pages per sheet, from -0 .. -9 margin => 9, # Margin around each sheet, for --nup border => 9, # Border around each page, for --nup magic => undef, # Try to keep original image size, for --nup scoot => undef, # Whether to prepend a blank virtual page tumble => undef, # Whether to rotate every other (n-up'd) page sample => '-10', # Input pages to sample xmodulus => 2, # Modulus with which to detect input width ymodulus => 1, # Modulus with which to detect input height fix => 'auto', # Fixps invocation control (auto/no/yes/force) quiet => 0, # Whether to hide status messages ghostscript => 'gs', # Command for invoking Ghostscript psnup => 'psnup', # Command for invoking psnup pstops => 'pstops', # Command for invoking pstops psselect => 'psselect', # Command for invoking psselect fixps => 'fixps', # Command for invoking fixps lpr => 'lpr', # Command for invoking lpr paperconf => 'paperconf', # Command for invoking paperconf ); GetOptions ( \%options, 'manual|help|h!', 'printer|P=s', 'nup|N!', 'center|C!', 'trim|T!', 'paper|p=s', 'n|nup-n=i', 'margin|nup-margin|m=s', 'border|nup-border|b=s', 'magic!', 'scoot!', 'tumble!', 'sample|s=s', 'xmodulus|x-modulus|xmod|x-mod|xm|x=i', 'ymodulus|y-modulus|ymod|y-mod|ym|y=i', 'fix=s', 'quiet|q!', 'ghostscript=s', 'psnup=s', 'pstops=s', 'psselect=s', 'fixps=s', 'lpr=s', 'paperconf=s', map(("$_", sub { $options{ndigits} .= $_[0] }), 0..9) ) or pod2usage(-message => $MESSAGE, -exitstatus => 3); pod2usage(-message => $MESSAGE, -exitstatus => 0, -verbose => 2) if $options{manual}; pod2usage(-message => $MESSAGE, -exitstatus => 0, -verbose => 1) if $options{help}; if (length $options{ndigits}) { die "Usage error: The -n option cannot be specified if -<digit> is.\n" if defined $options{n}; $options{n} = $options{ndigits}; } $options{n} = 2 unless defined $options{n}; die "Usage error: The -n option must be set to a positive integer.\n" unless $options{n} > 0; $options{nup} = 1 if not defined $options{nup} and not $options{trim} and not $options{center}; die "Usage error: Only one of --nup, --trim and --center may be specified.\n" if $options{nup} + $options{trim} + $options{center} != 1; my %fix = qw(auto auto a auto automatic auto default auto no no n no false no off no yes yes y yes true yes on yes force force f force full force rewrite force); die "Usage error: The value of --fix must be one of: auto, no, yes, force.\n" if not defined ($options{fix} = $fix{lc($options{fix})}); my $sample = parse_sample($options{sample}) or die qq|Usage error: "$options{sample}" is not a valid list of pages.\n|; my ($paper_cx, $paper_cy) = parse_paper($options{paper}) or die qq|Usage error: Unknown paper size "$options{paper}".\n|; die "Usage error: Number of input pages per output page must be positive.\n" if $options{n} <= 0; die "Usage error: --magic only works with 1 or 2 input pages per output page.\n" if $options{magic} and $options{n} > 2; unshift @ARGV, "-" if @ARGV == 0; push @ARGV, undef if defined $options{printer}; push @ARGV, "-" if @ARGV < 2; @ARGV == 2 or die "Usage error: Too many file names specified.\n"; sub parse_sample ($) { my $sample = $_[0]; $sample =~ s/^,+//s; my @sample = split /,+/, $sample; @sample or return undef; my @ret; foreach my $sample (@sample) { my ($beg, $end) = ($sample =~ /^\s*([+]?\d*)\s*(?:-\s*([-+]?\d*)\s*)?$/s) or return undef; $beg ||= 1; defined $end and $end ||= -1 or $end = $beg; print STDERR "$beg -- $end\n"; push @ret, [$beg, $end]; } return \@ret; } sub parse_paper ($) { my $paper = $_[0]; my $cmd = "$options{paperconf} -s"; $cmd .= length($paper) ? " -p \Q$paper\E" : ""; if ($options{quiet}) { $cmd .= " 2>/dev/null" } else { print STDERR "$cmd\n" } return ($1, $2) if `$cmd` =~ /^\s*(\d+)\s+(\d+)\s*$/si; return (612, 792) if $paper =~ /^(?:us|letter)?$/si; return (596, 842) if $paper =~ /^a4$/si; return (); } ## Choose and keep track of temporary file names BEGIN { $SIG{INT} = $SIG{QUIT} = sub { exit 1 } } my @temporary; sub temporary () { require File::Temp; require Fcntl; my ($fh, $fn) = File::Temp::tempfile(UNLINK => 1); push @temporary, $fh; print STDERR qq|Creating temporary file "$fn".\n| unless $options{quiet}; fcntl $fh, &Fcntl::F_SETFD, 0 or die qq|Cannot turn off close-on-exec for temporary file: $!\n|; return ($fh, "/dev/fd/" . fileno($fh)); } ## Put standard input in a temporary file, if necessary if ($ARGV[0] eq "-") { require File::Copy; my ($fh, $fn) = temporary; File::Copy::copy(\*STDIN, $fh) or die qq|Could not copy standard input to temporary file: $!.\n|; $ARGV[0] = $fn; } ## Use Ghostscript to find bounding boxes of pages my ($xmin, $xmax, $cx, $ymin, $ymax, $cy); my @bbox_trial = (); $options{fix} =~ /^auto|no$/s and push @bbox_trial, [$ARGV[0], 'file']; $options{fix} =~ /^yes$/s and push @bbox_trial, ["$options{fixps} \Q$ARGV[0]\E", 'pipe']; $options{fix} =~ /^auto|force$/s and push @bbox_trial, ["$options{fixps} --force \Q$ARGV[0]\E", 'pipe']; while (@bbox_trial) { my ($input, $input_type) = @{shift @bbox_trial}; if ($input_type eq 'pipe') { my ($fh, $fn) = temporary; $input = "$input | tee \Q$fn\E"; $ARGV[0] = $fn; } if (($xmin, $xmax, $cx, $ymin, $ymax, $cy) = find_bbox($input, $input_type)) { last if $options{fix} ne 'auto'; last if $cx > 100 and $cx < 1200 and $cy > 100 and $cy < 1200; } } die "Could not determine page layout from sampled pages.\n" if grep { not defined } $xmin, $xmax, $cx, $ymin, $ymax, $cy; print STDERR "x: (@$xmin)--(@$xmax) = $cx\n" unless $options{quiet}; print STDERR "y: (@$ymin)--(@$ymax) = $cy\n" unless $options{quiet}; sub find_bbox { my ($x1, $y1, $x2, $y2) = gs_bbox(@_); my $n = scalar(@$x1) or return; my $relevant = relevant($n); my ($xmin, $xmax, $cx) = detect($x1, $x2, $relevant, $options{xmodulus}, $options{ymodulus}) or return; my ($ymin, $ymax, $cy) = detect($y1, $y2, $relevant, $options{ymodulus}, $options{xmodulus}) or return; return ($xmin, $xmax, $cx, $ymin, $ymax, $cy); } # Invoke Ghostscript and read its output sub gs_bbox { my ($input, $input_type) = @_; # Read input from file or pipe my @x1; my @y1; my @x2; my @y2; # Bounding boxes by page, in points # Compose command to invoke ghostscript with the bbox device driver my $cmd = "$options{ghostscript} -dNOPLATFONTS -dNOPAUSE " . "-dQUIET -dSAFER -sDEVICE=bbox -dWhiteIsOpaque=true " . "-dBATCH -sOutputFile=/dev/null"; if ($input_type eq 'file') { $cmd = "$cmd \Q$input\E 2>&1" } elsif ($input_type eq 'pipe') { $cmd = "$input | $cmd - 2>&1" } else { die } print STDERR "$cmd\n" unless $options{quiet}; # Be ready to cut off Ghostscript if we don't need further information my $max_sample = 0; { my @sample = map @$_, @$sample; if (not grep $_ < 0, @sample) { $max_sample >= $_ or $max_sample = $_ foreach @sample; } } # Invoke Ghostscript and read bounding box information from it open GS, "$cmd|" or die "Could not invoke Ghostscript: $!.\n"; while (defined($_ = <GS>) and not ($max_sample > 0 and @x1 >= $max_sample)) { if (my ($x1, $y1, $x2, $y2) = /^%%\s*BoundingBox\s*:\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s*$/is) { push @x1, $x1; push @y1, $y1; push @x2, $x2; push @y2, $y2; printf STDERR "%4d: %s", scalar(@x1), $_ unless $options{quiet}; } } close GS; # Done return (\@x1, \@y1, \@x2, \@y2); } # Decide which pages' bounding boxes we actually want to consider sub relevant ($) { my ($n) = @_; my @relevant; foreach my $s (@$sample) { my ($beg, $end) = @$s; $relevant[$_] = 1 foreach ($beg > 0 ? $beg - 1 : $beg + $n) .. ($end > 0 ? $end - 1 : $end + $n); } return \@relevant; } # Detect input page size sub detect ($$$$$) { my ($list1, $list2, $relevant, $modulus, $other_modulus) = @_; my @min = (undef) x $modulus; my @max = (undef) x $modulus; for (my $i = 0; $i < @$relevant; ++$i) { if ($relevant->[$i] and $list1->[$i] < $list2->[$i]) { my $offset = $i % $modulus; defined $min[$offset] and $min[$offset] <= $list1->[$i] or $min[$offset] = $list1->[$i]; defined $max[$offset] and $max[$offset] >= $list2->[$i] or $max[$offset] = $list2->[$i]; } } return () if grep !defined, @min or grep !defined, @max; my $sizes = zip(sub { $_[1] - $_[0] }, \@min, \@max); my $size = undef; defined $size and $size >= $_ or $size = $_ foreach @$sizes; @min = (@min) x $other_modulus; @max = (@max) x $other_modulus; return (\@min, \@max, $size); } ## Construct command line for performing desired action my ($ps2ps_cx, $ps2ps_cy); # Output bounding box from pstops my ($final_cx, $final_cy); # Final %%BoundingBox and %%PageBoundingBox if ($options{nup}) { ($ps2ps_cx, $ps2ps_cy) = ($cx, $cy); if ($options{magic}) { my ($magic_x, $magic_y) = ($paper_cx, $paper_cy); ($magic_x, $magic_y) = ($magic_y, $magic_x) if $magic_x > $magic_y; $magic_y /= $options{n}; ($magic_x, $magic_y) = ($magic_y, $magic_x) if $magic_x > $magic_y; if ($cx <= $magic_x and $cy <= $magic_y) { $options{margin} = $options{border} = 0; ($ps2ps_cx, $ps2ps_cy) = ($magic_x, $magic_y); $options{quiet} or print STDERR "Magic effective in portrait mode -- look ma, no shrinking!\n"; } elsif ($cy <= $magic_x and $cx <= $magic_y) { $options{margin} = $options{border} = 0; ($ps2ps_cx, $ps2ps_cy) = ($magic_y, $magic_x); $options{quiet} or print STDERR "Magic effective in landscape mode -- look ma, no shrinking!\n"; } else { $options{quiet} or print STDERR "Magic not effective; oh well.\n"; } } ($final_cx, $final_cy) = ($paper_cx, $paper_cy); } elsif ($options{center}) { ($ps2ps_cx, $ps2ps_cy) = ($paper_cx, $paper_cy); ($final_cx, $final_cy) = ($paper_cx, $paper_cy); } else # $options{trim} { ($ps2ps_cx, $ps2ps_cy) = ($cx, $cy); ($final_cx, $final_cy) = ($cx, $cy); } my $quiet = $options{"quiet"} ? " -q" : ""; my $cmd = "$options{pstops}$quiet"; $cmd .= " '" . scalar(@$xmin) . ":" . join(",", map sprintf("%d(%g,%g)", $_, ($ps2ps_cx - $cx) / 2 - $xmin->[$_], ($ps2ps_cy - $cy) / 2 - $ymin->[$_]), 0..$#$xmin); $cmd .= "' \Q$ARGV[0]\E"; if ($options{scoot}) { $cmd .= " | $options{psselect}$quiet -p_,-"; } if ($options{nup}) { $cmd .= " | $options{psnup}$quiet"; $cmd .= " -w$paper_cx -h$paper_cy -W$ps2ps_cx -H$ps2ps_cy"; $cmd .= " -m\Q$options{margin}\E -b\Q$options{border}\E -\Q$options{n}\E"; } if ($options{tumble}) { $cmd .= " | $options{pstops}$quiet '2:0,1U($paper_cx,$paper_cy)'"; } ## Do it; filter output to fix DSC comments my $out; if ($ARGV[1] eq "-") { $out = \*STDOUT; } elsif (defined $options{printer}) { require IO::Pipe; $out = IO::Pipe->new; $out->writer("$options{lpr} -P\Q$options{printer}\E"); } else { require IO::File; $out = IO::File->new($ARGV[1], ">") or die qq|Could not open "$ARGV[1]" for writing: $!.\n|; } print STDERR "$cmd\n" unless $options{quiet}; open PS, "$cmd|" or die "Could not invoke psutils: $!.\n"; while (<PS>) { if (/^\s*%%\s*(BoundingBox|PageBoundingBox)\s*:/is) { print $out "%%$1: 0 0 $final_cx $final_cy\n"; } elsif (/^\s*%%\s*(DocumentMedia\s*:\s*\S+\s+)\d+\s+\d+(\s+.*)$/is) { print $out "%%$1$final_cx $final_cy$2"; } elsif (/^\s*%%\s*(DocumentPaperSizes|PaperSize)\s*:/is) { # Do nothing } else { print $out $_; } } close PS; close $out; # The bane of functional programming sub zip { my $code = shift; my @ret; for (my $i = 0; grep $i < @$_, @_; ++$i) { push @ret, $code->(map $_->[$i], @_); } return \@ret; }