PDF-Dateien für das Internet komprimieren, inklusive Perl-Skript

Von Scribus ausgegebene PDFs sind für den Druck optimiert, und Sribus gibt sich die allergrößte Mühe, um sicherzustellen, daß die Ausgabe auf verschiedenen Druckmaschinen identisch aussieht. Der Preis für diese Verläßlichkeit sind Dateien, die um einiges größer sind als einfache PDFs. Dieser Tip führt einen Weg vor, die Dateigröße zu reduzieren, so daß sich mit Scribus erstellte PDFs über das Internet oder E.Mail verteilen können.

Meine Frau gibt einen Newsletter heraus, für den sie auch das Layout macht. Das Endprodukt wird überwiegend über das Internet und per E-Mail verbreitet und muß daher kleiner als 1 MB sein, am besten noch kleiner. Weil die Möglichkeiten von Scribus, die Ausgabegröße zu verkleinern, nicht ausreichten, habe ich einen Umweg über Postscript und zurück zu PDF gesucht, der eine bessere Komprimierung ermöglicht, und das im folgenden Beschriebene funktioniert erstaunlich gut.


 * 1) Exportieren Sie als PDF (1.3 oder 1.4), betten Sie alle Schriften vollständig ein und lassen Sie Bilder nicht neu berechnen  → newsletter_scribus.pdf [~2.8 MB]
 * 2) Wandeln Sie die Datei mit   in Postscript um → newsletter.ps [riesig]
 * 3) Konvertieren Sie die Datei mit ghostscript zurück ins PDF-Format (nur verwendte Zeichen einbetten, Bilder neu berechnen → newsletter_compact.pdf [~500 kB]. (Sie können ps2pdf13 [or ps2pdf14] für diesen Schritt verwenden; eine genauere Lösung liefert mein unten aufgeführtes Script.
 * 4) Wenn Sie möchten, können Sie die PDF-Datei noch "linearisieren", so daß der Adobe Reader schon Seiten anzeigen kann, während der Rest noch aus dem Netz geladen wird (ich habe dieses Feature nie ausprobiert).

=Bemerkungen=
 * Wie Sie sehen, ist die entstandene PDF-Datei mehr als fünfmal kleiner.
 * Für Schritt 2 wird die Möglichkeit des Adobe Readers, nach PS zu exportieren, nicht funktionieren – Sie benötigen Xpdf oder pdftops.
 * Für Schritt 3, brauchen Sie eine ghostscript-Version ab 8.0; ich habe es erfolgreich mit AFPL 8.15 und 8.53 sowie GNU 8.16, während ESP 7.07 nicht funktioniert.
 * Das Markieren von und das Suchen nach Text funktioniert einwandfrei mit der neuen Datei, während in der ursprünglichen PDF-Datei viele zu große Abstände zwischen Zeichen vorkamen (dies liegt an der Art und Weise, wie Scribus Text mit höchster Präzision setzt).
 * Außerdem habe ich herausgefunden, daß die komprimierte Datei im Adobe Reader wesentlich besser aussieht (nicht in Xpdf oder gv), aber das mag Geschmackssache sein:
 * TrueType-Fonts waren im Original nicht geglättet, aber in der komprimierten Version. Bei einfachen Testdateien funktionierte das Glätten jedoch in beiden Versionen, so daß ich nicht weiß, was das Problem ist.
 * Bilder waren im Original zu dunkel (offensichtlich ein Fehler bei der Darstellung von Transparenzen in Acroread 6 und 7), sahen jedoch in der komprimierten Version gut aus. Auch hier kann ich diesen Sachverhalt in einfachen Testdateien nicht reproduzieren.
 * Es gibt einen Nachteil: Durch die Transformation werden Nicht-ASCII-Buchstaben in Xpdf nicht angezeigt. Adobe Reader 7 oder gv haben dieses Problem nicht, so daß es sich um einen Bug in Xpdf/Poppler handeln könnte. Unter unseren Lesern liegt die Zahl der Xpdf-Benutzer ziemlich nahe an 1 (ich), so daß dies vermutlich kein Problem darstellt.
 * Weiterhin gehen durch die Transformation Metainformationen (Erstellungsdatum, Autor...), Lesezeichen, PDF-Anmerkungen, Hyperlinks etc. verloren. Das Skript ersetzt einige der Metainformationen. Ich habe auch versucht, Lesezeichen zu extrahieren und wiederherzustellen, aber die resultierenden PDFs verursachten Probleme mit dem Adobe Reader 7.

=Weitere Informationen=


 * Wer die entstandene PDF-Datei in einem Prüfprogramm testen möchte, kann den letzten Newsletter unter http://www.calgarymulticulturalcentre.com/CC_January_2006.pdf ausprobieren.

Das folgende Perl-Skript ruft nacheinander auf: Wenn Ihnen das erst- oder letztgenannte Programm fehlen sollten, müssen Sie die entsprechenden Zeilen auskommentieren.
 * 1) pdftk (um die Metainformationen zu extrahieren)
 * 2) pdftops
 * 3) gs
 * 4) pdfopt

Einige Hinweise zur Benutzung erhalten Sie, wenn Sie

ausführen. Bevor Sie es verwenden, sollten Sie Ihre Daten in die Zeilen, die mit `[Insert ... here]' gekennzeichnet sind, einsetzen.

Das Skript ist nicht sehr elegant, aber es funktionioert für meine Zwecke.

=compress-newsletter.pl= if [ -x /usr/local/bin/perl ]; then perl=/usr/local/bin/perl elif [ -x /usr/bin/perl ]; then perl=/usr/bin/perl else perl=`which perl| sed 's/.*aliased to *//'` fi
 * 1) !/bin/sh
 * 2)  -*-Perl-*-
 * 3) Run the right perl version:
 * 1) Run the right perl version:

exec $perl -x -S $0 "$@"    # -x: start from the following line
 * 1) ! /Good_Path/perl -w
 * 2) line 17
 * 1) line 17

use strict; use File::Temp qw/ :mktemp /;
 * 1) Name:   compress-newsletter
 * 2) Author: wd (Wolfgang.Dobler@ucalgary.ca)
 * 3) Date:   03-Oct-2005
 * 4) Description:
 * 5)   Use ghostscript's pdfwrite device (à la ps2pdf) to reduce the
 * 6)   Newsletter's PDF file size, and add meta information like author,
 * 7)   date, etc.
 * 8)   The preferred route is currently:
 * 9)                 [scribus>=1.2.3]
 * 10)                    file.pdf
 * 11)                 [pdftops>=3.00]
 * 12)                     file.ps
 * 13)            [pstopdf14 (gs-gnu-8.16 or higher)]
 * 14)                        V
 * 15)                    final.pdf
 * 16) Usage:
 * 17)   compress-newletter [-i col:gray:mono] Newsletter_big.pdf
 * 18) Options:
 * 19)   -i col:gray:mono
 * 20)   --imgres=col:gray:mono   Set resolution for downsampling color,
 * 21)                            grayscale and black-and-white images
 * 22)                            (default is 144:300:300)
 * 23)   --debug                  Be verbose and keep temporary files around
 * 1)   -i col:gray:mono
 * 2)   --imgres=col:gray:mono   Set resolution for downsampling color,
 * 3)                            grayscale and black-and-white images
 * 4)                            (default is 144:300:300)
 * 5)   --debug                  Be verbose and keep temporary files around

use Getopt::Long; Getopt::Long::config("bundling");
 * 1) Allow for `-Plp' as equivalent to `-P lp' etc:

my (%opts);			# Options hash for GetOptions my $doll='\$';			# Need this to trick CVS

GetOptions(\%opts,	  qw( -h   --help -i=s --imgres=s --debug -q  --quiet -v  --version ));
 * 1) Process command line

my $debug = ($opts{'debug'} ? 1 : 0 ); # undocumented debug option if ($debug) { printopts(\%opts); print "\@ARGV = `@ARGV'\n"; }

if ($opts{'h'} || $opts{'help'})   { die usage;   } if ($opts{'v'} || $opts{'version'}) { die version; }

my $quiet = ($opts{'q'} || $opts{'quiet'}  || ''           ); my $imgres = ($opts{'i'} || $opts{'imgres'} || '144:300:300');

my ($gs,     @gsargs     ) = ('gs'     ); my ($pdftops, @pdftopsargs) = ('pdftops'); my ($pdfopt, @pdfoptargs ) = ('pdfopt' );

my $infile = shift or die usage; (my $root=$infile) =~ s/\.(pdf|ps).*//; (my $outfile=$infile) =~ s/(.*)(\.(pdf|ps))/${1}_new${2}/; my $tmpfile = mktemp("${root}.tmp_XXXXXX");


 * 1) 0. Extract all sorts of information

print "Running pdftk ...\n"; print STDERR "pdftk $infile dump_data output\n" if ($debug); my $meta = `pdftk $infile dump_data output -`; my ($creator) = ( $meta =~		 m{InfoKey: Creator\s+InfoValue:\s*(.+)$}m		); $creator = 'Scribus 1.2.3' unless defined($creator); my $datestring = extract_CreationDate($meta); my @bookmarks = extract_bookmarks($meta);
 * 1) Extract Scribus version, creation date, bookmarks from original PDF:

my ($colres,$grayres,$monores) = ($imgres =~ /([0-9]+):([0-9]+):([0-9]+)/); die "Image resolution must be of form `col:gray:mono'\n" unless defined($monores);
 * 1) Extract desired image resolutions

push @pdftopsargs, "-level3"; my $psfile = mktemp("${root}.ps_XXXXXX"); push @pdftopsargs, $infile, $psfile; print "Running pdftops ...\n"; print STDERR "$pdftops @pdftopsargs\n" if ($debug); system($pdftops,@pdftopsargs);
 * 1) 1. Run pdftops

push @gsargs, qw{-q -dNOPAUSE -dBATCH}; push @gsargs, '-sDEVICE=pdfwrite'; push @gsargs, '-dCompatibilityLevel=1.3'; push @gsargs, '-dPDFSETTINGS=/screen'; push @gsargs, '-dEmbedAllFonts=true'; push @gsargs, '-dSubsetFonts=true'; push @gsargs, '-dColorImageDownsampleType=/Bicubic'; push @gsargs, "-dColorImageResolution=$colres"; push @gsargs, '-dGrayImageDownsampleType=/Bicubic'; push @gsargs, "-dGrayImageResolution=$grayres"; push @gsargs, '-dMonoImageDownsampleType=/Bicubic'; push @gsargs, "-dMonoImageResolution=$monores"; push @gsargs, "-sOutputFile=$tmpfile"; push @gsargs, "-c .setpdfwrite";
 * 1) 2. Run gs
 * 2) a) Prepare options
 * 1) One of /printer, /screen, /prepress, /ebook, /default; see Ps2pdf.htm:

my $metafile = "${root}.meta"; open(META, "> $metafile"); print META <<"DEAD_PARROT"; % Document information [% /CreationDate (D:$datestring) /ModDate (D:$datestring) /Creator ($creator) /Title ([Insert your document title here]) /Subject ([Insert the Subject here]) /Keywords ([Insert key words here]) /Author ([Insert author' nsme here]) /DOCINFO pdfmark
 * 1) b) Write meta information to temporary file
 * 2) my $metafile = mktemp("metainfo.tmp_XXXXXX");

% Initial view on opening the document [/View [/Fit] % Fit page in window /Page 1 % /PageMode /UseOutlines % /UseNone /UserOutlines /UseThumbs /FullScreen /DOCVIEW pdfmark

DEAD_PARROT


 * 1) Bookmarks. [Commented out for acroread 7.0 has problems] Currently at
 * 2) the mercy of the original bookmarks (and Scribus 1.2.2 does not allow
 * 3) to edit the bookmark names) and the encoding that pdftk understands
 * 4) (most quotation marks get mapped to `?').
 * 5) Ideally, one would write out the meta information file with
 * 6) `compress-newsletter -m CC.pdf' and use it then with
 * 7) `compress-newsletter CC.pdf'.
 * 8) % Bookmarks: @bookmarks

push @gsargs, '-f', $psfile, $metafile; print "Running gs ...\n"; print STDERR "$gs @gsargs\n" if ($debug); system($gs,@gsargs);

print "Running pdfopt ...\n"; print STDERR "$pdfopt @pdfoptargs $tmpfile $outfile\n" if ($debug); system($pdfopt,@pdfoptargs,$tmpfile,$outfile);
 * 1) 3. Run pdfopt

system('ls', '-l', $infile, $psfile, $tmpfile, $outfile);
 * 1) Some diagnostics:

END { # Clean up even in case of an error: unless ($debug) { foreach my $file ($psfile,$tmpfile) { unlink $file if (defined($file) && -f $file); }   } }

sub extract_CreationDate {

use POSIX qw(strftime);

my $meta = shift;

my ($cdate) = ( $meta =~		   m{InfoKey: CreationDate\s+InfoValue:\s*(.+)$}m		  ); # Time string: need to splice in "'" after hours and minutes of time zone # definition. To me this looks like the technical documentation was taken # too literally and now applications (and Acroread 7) insist on these # stupid markers. my $datestring; if ($cdate =~ /[0-9]{14}/) { # managed to extract CreationDate from $meta $datestring = "$cdate-06'00'"; } else {		        # Creation date unknown -- use current date my $tz = strftime "%z", localtime; $tz =~ s/([0-9][0-9])([0-9][0-9])/$1'$2'/; $datestring = strftime "%Y%m%d%H%M%S$tz", localtime; }

$datestring; } sub extract_bookmarks {

my $meta = shift;

my @bm;

while ($meta =~ /^BookmarkTitle:     \s* (.*) \n                      BookmarkLevel:      \s* (.*) \n                      BookmarkPageNumber: \s* (.*) /xmg) { my ($title,$level,$page) = ($1,$2,$3); push @bm, "[/Title ($title /Page $page /OUT pdfmark\n";   }

} sub printopts { my $optsref = shift; my %opts = %$optsref; foreach my $opt (keys(%opts)) { print STDERR "\$opts{$opt} = `$opts{$opt}'\n"; } } sub usage { my $thisfile = __FILE__; local $/ = '';             # Read paragraphs open(FILE, "<$thisfile") or die "Cannot open $thisfile\n"; while () { # Paragraph _must_ contain `Description:' or `Usage:' next unless /^\s*\#\s*(Description|Usage):/m; # Drop `Author:', etc. (anything before `Description:' or `Usage:') s/.*?\n(\s*\#\s*(Description|Usage):\s*\n.*)/$1/s; # Don't print comment sign: s/^\s*# ?//mg; last;                       # ignore body }   $_ or "\n"; } sub version { my $doll='\$';		# Need this to trick CVS my $cmdname = (split('/', $0))[-1]; my $rev = '$Revision: 1.8 $'; my $date = '$Date: 2006/02/02 09:38:52 $'; $rev =~ s/${doll}Revision:\s*(\S+).*/$1/; $date =~ s/${doll}Date:\s*(\S+).*/$1/; "$cmdname version $rev ($date)\n"; }
 * 1) Print command line options
 * 1) Extract description and usage information from this file's header.
 * 1) Return CVS data and version info.


 * 1) End of file compress-newsletter