Beim halbautomatischen Umwandeln von gedruckten Zeitschriftenartikeln ins PDF-Format hilft das heute vorgestellte Perl-Skript. Den Ablauf triggert ein Taster am Scanner.
Langjährige Leser wissen, dass meine Perl-Snapshots seit bald 14 Jahren erscheinen. Daher stapeln sich inzwischen rund 150 Ausgaben des Linux-Magazins in meiner Wohnung und belegen etwa zweieinhalb Regalmeter. Weil sich die Anmietung zusätzlichen Stauraums mit einem Blick auf den Mietspiegel von San Francisco verbietet, sollten die historischen Magazine nun in der Altpapiertonne landen. Der Nostalgie wegen wollte ich aber vorher die Seiten der Perl-Kolumne mit einem Scanner und einem Skript [2] ins PDF-Format retten.
Wider das Ermatten
Scanprogramme wie »xsane« oder das Ubuntu neuerdings beiliegende »simple-scan« erledigen Einzelscans ohne viel Aufwand. Beim Einlesen mehrerer Zeitschriftenseiten, deren Jpeg-Bilder danach in ein mehrseitiges PDF-Dokument einfließen sollen, ermattet ohne bessere Automatisierung aber auch der fleißigste Scanner-Operator schnell.
Das Perl-Skript »artscan« führt deshalb per Menü durch den Scanvorgang und zeigt in einer Listbox in Echtzeit die gerade ablaufenden Schritte an (Abbildung 1). Als zusätzliche Erleichterung muss ich lediglich den grünen Knopf am Scanner drücken, sobald die aktuelle Artikelseite ordnungsgemäß positioniert ist (Abbildung 2). Um eine Reihe bereits eingescannter Einzelbilder zu verwerfen, tippe ich stattdessen [N] (für new), worauf das Skript die bis dahin bereits zwischengespeicherten Bilder vergisst.
Nach dem Scannen der letzten Seite eines Artikels betätige ich die Taste [F] (für finish). Das Skript überführt daraufhin die zwischengespeicherten Einzelseiten mit Hilfe des Programms »convert« aus dem Fundus der Imagemagick-Programmsammlung vom PNM-Format in Jpeg-Bilder.

Abbildung 2: Auf Knopfdruck läuft der Scanner an und liest das Titelbild der Oktober-Ausgabe des Linux-Magazins aus dem Jahr 1996 ein. Der Bediener muss nicht die PC-Tatstatur benutzen.
Schrumpfen mit Jpeg
Die damit verbundene Kompression reduziert den Speicherbedarf um bis zu 90 Prozent. Ein weiterer Aufruf von »convert« bündelt die Jpeg-Sammlung dann zu einem mehrseitigen PDF-Dokument und legt es in einem voreingestellten Ausgabeverzeichnis ab. Die Fußzeile im Terminal zeigt den Pfad des Ergebnisdokuments an. Den Scanvorgang löst der User entweder durch Drücken der Taste [S] aus oder durch den am Scanner befindlichen grünen Knopf.
Während der Operator am Scanner hantiert und krampfhaft versucht die Vorlage trotz des Falzes möglichst bündig einzulegen, wäre es äußerst umständlich, auch noch das im Terminal laufende Skript zu bedienen, um den Scanvorgang zu starten. Scanner wie ein von mir benutztes Modell von Epson verfügen deshalb über einen Knopf, der über das USB-Interface dem Rechner ein Signal gibt, das er beliebig interpretieren kann.
Das unter Ubuntu erhältliche Paket »scanbuttond« [3] enthält einen Daemon, der eventuell angeschlossene Scanner überwacht und jedes Mal, wenn der Scannerknopf aktiviert wurde, das voreingestellte Skript »/etc/scanbuttond/buttonpressed.sh« laufen lässt. Füge ich in dieses Skript eine Zeile wie
kill -USR1 `cat /tmp/pdfs/pid`
ein, sendet es bei jedem Knopfdruck das Signal »USR1« an den Prozess, der seine PID in der Datei »/tmp/pdfs/pid« abgelegt hat. Das Skript »artscan« schreibt sofort nach dem Hochfahren die in Perl als »$$« vorliegende PID des aktuellen Prozesses samt einem abschließenden Zeilenumbruch in diese PID-Datei. Dafür greift es auf die Funktion »blurt()« aus dem Fundus des Moduls Sysadm::Install zurück.
Ringelreihe mit POE
Die Terminal-Oberfläche von »artscan« (Listing 2) benutzt das schon früher öfter verwendete POE-Framework vom CPAN. Das Modul Curses::UI::POE stellt die Verbindung der POE-Eventschleife mit der Curses-Library her, die Ascii-basierte Grafikelemente auf die Terminaloberfläche zeichnet und auf Tastendrücke des Users reagiert.
|
Listing 2: |
|---|
001 #!/usr/local/bin/perl -w
002 use strict;
003 use local::lib;
004 use POE;
005 use POE::Wheel::Run;
006 use Curses::UI::POE;
007 use Sysadm::Install qw(:all);
008 use File::Temp qw(tempfile);
009 use File::Basename;
010
011 my $PDF_DIR = "/tmp/artscan";
012 mkd $PDF_DIR unless -d $PDF_DIR;
013
014 my $pidfile = "$PDF_DIR/pid";
015 blurt "$$n", $pidfile;
016
017 my @LBOX_LINES = ();
018 my $BUSY = 0;
019 my $LAST_PDF;
020 my @IMAGES = ();
021 my $HEAP;
022
023 my $CUI = Curses::UI::POE->new(
024 -color_support => 1,
025 inline_states => {
026 _start => sub {
027 $HEAP = $_[HEAP];
028 $_[KERNEL]->sig( "USR1",
029 "article_scan" );
030 },
031 scan_finished => &scan_finished,
032 article_scan => &article_scan,
033 });
034
035 my $WIN = $CUI->add("win_id", "Window");
036
037 my $TOP = $WIN->add( qw( top Label
038 -y 0 -width -1 -paddingspaces 1
039 -fg white -bg blue
040 ), -text => "artscan v1.0" );
041
042 my $LBOX = $WIN->add(qw( lb Listbox
043 -padtop 1 -padbottom 1 -border 1),
044 );
045
046 my $FOOT = $WIN->add(qw( bottom Label
047 -y -1 -paddingspaces 1
048 -fg white -bg blue));
049
050 footer_update();
051
052 $CUI->set_binding(sub { exit 0; }, "q");
053 $CUI->set_binding( &article_new, "n");
054 $CUI->set_binding( &article_scan, "s" );
055 $CUI->set_binding( &article_finish, "f" );
056
057 $CUI->mainloop;
058
059 ###########################################
060 sub article_new {
061 ###########################################
062 return if $BUSY;
063 @IMAGES = ();
064 footer_update();
065 }
066
067 ###########################################
068 sub article_finish {
069 ###########################################
070 return if $BUSY;
071 $BUSY = 1;
072
073 $FOOT->text("Converting ...");
074 $FOOT->draw();
075
076 my @jpg_files = ();
077
078 for my $image ( @IMAGES ) {
079 my $jpg_file =
080 "$PDF_DIR/" . basename( $image );
081 $jpg_file =~ s/.pnm$/.jpg/;
082 push @jpg_files, $jpg_file;
083 task("convert", $image, $jpg_file);
084 }
085
086 my $pdf_file = next_pdf_file();
087
088 $FOOT->text("Writing PDF ...");
089 $FOOT->draw();
090
091 task("convert", @jpg_files, $pdf_file);
092 unlink @jpg_files;
093
094 $LAST_PDF = $pdf_file;
095 @IMAGES = ();
096
097 lbox_add("PDF $LAST_PDF ready.");
098 footer_update();
099 $BUSY = 0;
100 }
101
102 ###########################################
103 sub next_pdf_file {
104 ###########################################
105 my $idx = 0;
106
107 my @pdf_files = sort <$PDF_DIR/*.pdf>;
108
109 if( scalar @pdf_files > 0 ) {
110 ($idx) = ($pdf_files[-1] =~ /(d+)/);
111 }
112
113 return "$PDF_DIR/" .
114 sprintf("%04d", $idx + 1) . ".pdf";
115 }
116
117 ###########################################
118 sub task {
119 ###########################################
120 my($command, @args) = @_;
121
122 lbox_add("Running $command" . " @args");
123 tap($command, @args);
124 }
125
126 ###########################################
127 sub article_scan {
128 ###########################################
129 return if $BUSY;
130 $BUSY = 1;
131
132 my($fh, $tempfile) = tempfile(
133 DIR => $PDF_DIR,
134 SUFFIX => ".pnm", UNLINK => 1);
135
136 lbox_add("Scanning $tempfile");
137
138 my $wheel =
139 POE::Wheel::Run->new(
140 Program => "./scan.sh",
141 ProgramArgs => [$tempfile],
142 StderrEvent => 'ignore',
143 CloseEvent => "scan_finished",
144 );
145
146 $HEAP->{scanner} = {
147 wheel => $wheel, file => $tempfile };
148
149 $FOOT->text("Scanning ... ");
150 $FOOT->draw();
151 }
152
153 ###########################################
154 sub scan_finished {
155 ###########################################
156 my($heap) = @_[HEAP, KERNEL];
157
158 push @IMAGES, $heap->{scanner}->{file};
159 delete $heap->{scanner};
160 footer_update();
161 $BUSY = 0;
162 }
163
164 ###########################################
165 sub footer_update {
166 ###########################################
167 my $text = "[n]ew [s]can [f]inish [q]" .
168 "uit (" . scalar @IMAGES . " pending)";
169
170 if( defined $LAST_PDF ) {
171 $text .= " [$LAST_PDF]";
172 }
173 $FOOT->text($text);
174 $FOOT->draw();
175 }
176
177 ###########################################
178 sub lbox_add {
179 ###########################################
180 my($line) = @_;
181
182 if( scalar @LBOX_LINES >=
183 $LBOX->height() - 4) {
184 shift @LBOX_LINES;
185 }
186 push @LBOX_LINES, $line;
187
188 $LBOX->{-values} = [@LBOX_LINES];
189 $LBOX->{-labels} = { map { $_ => $_ }
190 @LBOX_LINES };
191 $LBOX->draw();
192 }
|
Die Implementierung folgt aus Platzgründen ausnahmsweise nicht den strengen Regeln des Cooperative-Multitasking-Framework, nach denen eine Task niemals eine andere blockieren darf. Da aber der User während eines laufenden Scanvorgangs eh nicht viel mehr unternehmen kann, als abzuwarten, bis dieser beendet ist, nimmt es das Skript nicht so genau und friert währenddessen einfach das User-Interface ein.
Der in Zeile 26 von Listing 2 definierte Start-Handler »_start« speichert den POE-Session-Heap in der globalen Variablen »$HEAP«, damit auch die per »set_binding()« definierten Tastendruck-Handler ab Zeile 52 auf die Daten der UI-POE-Session zugreifen können.
Damit das Programm auf das Unix-Signal »USR1« hin den Handler »article_scan« anspringt, ruft Zeile 28 die Methode »sig()« des POE-Kernels auf und weist dem Signal den POE-Zustand »article_scan« zu. Dieser setzt in Zeile 32 die ab Zeile 127 definierte Funktion »article_scan()« als Ansprungadresse. Den dritten POE-Zustand, »scan_finished«, schließlich springt der Kernel an, falls ein asynchron abgesetzter Scanvorgang später abgeschlossen ist.
Die grafische Oberfläche baut auf einem in Zeile 35 definierten Window-Element auf und besteht aus einer Top-Leiste »$TOP«, einer Listbox »$LBOX« und einer Fußzeile »$FOOT«. Mit »add()« fügt das Skript die Widgets von oben nach unten jeweils ins Hauptfenster ein. Die Fußzeile liegt wegen des Parameterpaars »y -1« ganz unten im Fenster, die Breiteneinstellung »-width -1« der Top-Leiste bewirkt, dass sich die Leiste über die gesamte Breite des offenen Terminalfensters erstreckt.
Nach einem Druck auf die Taste [N] ruft POE wegen des Binding in Zeile 53 die ab Zeile 60 definierte Funktion »article_new()« auf, die alle eventuell vorhandenen Elemente des globalen Image-Array »@IMAGES« löscht. Allerdings nur, falls die globale Variable »$BUSY« nicht gesetzt ist, was verschiedene Stellen des Programms tun, um sicherzustellen, dass der User keine Aktionen durch Tastendrücke auslöst.
Gerade laufende Aktivitäten meldet das Skript entweder in der Fußzeile oder mit Hilfe der Funktion »lbox_add()«, die einen Eintrag in die mittige Listbox einfügt und überschüssige Elemente am oberen Rand abschneidet, sodass sich die Illusion einer scrollenden Datei ergibt. Aufgaben wie das Konvertieren von Scanner-Rohdaten im PNM-Format nach Jpeg erledigt die ab Zeile 118 definierte Funktion »task()«. Sie reicht die ihr übergebenen Argumente mittels »tap()« aus dem CPAN-Modul Sysadm::Install an die Shell weiter.
Das Skript nummeriert die erzeugten PDF-Dateien. Es findet den nächsten Wert, indem es das PDF-Verzeichnis nach allen bisher angelegten PDF-Dateien durchsucht und die Nummer der letzten um 1 erhöht.
Arbeitspferd »scanimage«
Den Scanvorgang könnte nun das CPAN-Modul Sane [4] steuern, doch dann müsste sich das Skript um allerlei Krimskrams kümmern, etwa das Freigeben der Sane-Schnittstelle beim Programmabbruch, weil sonst künftige Scanversuche blockieren würden [5]. Stattdessen wählt es den einfachen Weg über das dem Sane-Paket beiliegende Programm »scanimage«, das es über das Shellskript »scan.sh« aufruft.
Wie Listing 1 zeigt, setzt es die Auflösung auf 300 dpi, was für normale Zeitschriften ausreicht. Der Parameter »–mode« bestimmt mit dem Wert »Color« einen Farbscan, der Default-Modus war bei meinem Scanner Schwarz-Weiß. Die von »scanimage« auf Stdout ausgegebene Bilddatei im Rohdatenformat PNM leitet das Shellskript in eine Datei um, deren Name ihm das Perl-Skript überreicht hat.
|
Listing 1: |
|---|
1 scanimage -x 1000 -y 1000 --resolution=300 --mode Color >$1 |
Mein Scanner belichtet ohne die Parameter »x« und »y« allerdings nur einen kleinen Ausschnitt der Seite. Die im Skript verwendeten Werte von jeweils 1000 für »-x« und »-y« reduziert das Sane-Backend auf die maximal verfügbare Fläche, was ziemlich genau der Größe einer Zeitschriftenseite entspricht. Für andere Scannertypen oder Printerzeugnisse sind die verwendeten Parameter bei Bedarf anzupassen.
Zum Einsammeln der Scanner-Rohdaten legt Listing 2 mit der Funktion »tempfile()« des CPAN-Moduls File::Temp in Zeile 132 temporäre Dateien an, die wegen der Option »UNLINK« nach dem Freigeben der letzten auf sie verweisenden Referenz automatisch verschwinden.
Den Aufruf des Scan-Skripts »scan.sh« im gleichen Verzeichnis übernimmt das POE-Rädchen POE::Wheel::Run, das einen Parallelprozess startet, dort das Kommando mit der temporären Ausgabedatei aufruft und wegen des Parameters »CloseEvent« nach getaner Arbeit den POE-Zustand »scan_finished« anspringt. Dies geschieht asynchron, sodass »new()« in Zeile 139 sofort wieder zurückkehrt.
Damit das Rädchen auch nach dem Verlassen der Funktion »article_scan()« ununterbrochen weiterläuft, speichert Zeile 146 die Wheel-Daten im Session-Heap. Zeile 149 schreibt dann noch schnell »Scanning …« in die Fußzeile, bevor die Funktion »article_scan()« endet und die Kontrolle zurück an den POE-Kernel geht, der weitere Events abarbeitet.
Schließt endlich der Scanner den Einlesevorgang ab, aktiviert das Wheel die Funktion »scan_finished()« ab Zeile 154, die die Wheel-Daten aus dem Heap löscht und den Namen der temporären Datei mit den eingefangenen Rohdaten ans Ende des globalen Array »@IMAGES« anfügt.
Installation
Die Ubuntu-Pakete »imagemagick«, »libfile-temp-perl«, »libpoe-perl«, »libcurses-ui-perl« und »libsysadm-install-perl« installieren das nötige Rüstzeug, um das Skript zum Laufen zu bringen. Das Mini-Shellskript »scan.sh« landet ausführbar im gleichen Verzeichnis wie das Hauptskript »artscan«.
Bietet die Linux-Distribution kein Paket für Curses::UI::POE an, ist es manuell mit einer CPAN-Shell zu installieren. Geschieht das mit »local::lib«, sollte das Skript dies wie in Zeile 3 von Listing 2 ebenfalls angeben, andernfalls ist es nicht notwendig. Wer direkt mit dem Sane-Backend seines Scanners herumspielen möchte, dem sei für diesen Zweck das CPAN-Modul Sane empfohlen, das bei Ubuntu als »libsane-perl« bereits fertig im Repository vorliegt.
Verbesserungen
Der Scanvorgang lässt sich mit einem Scanner mit Einzelblatteinzug noch effizienter gestalten. Ist der Archivar willens, das Heft mit einer dicken Schere oder Schneidemaschine am Falz aufzutrennen, kann der Scanner die Seiten automatisch eine nach der anderen einziehen. Die Rückseiten folgen in einem zweiten Durchgang und das Skript kann die Seiten wieder in die richtige Reihenfolge bringen (Abbildung 3). (jcb)
|
Infos |
|---|
|
[1] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2011/03/Perl] [2] Michael Schilli: Papiercontainer: [https://www.linux-magazin.de/Heft-Abo/Ausgaben/2005/05/Papiercontainer] [3] Scanbuttond: [http://scanbuttond.sourceforge.net] [4] Perl-Modul Sane: [http://search.cpan.org/~ratcliffe/Sane-0.03/lib/Sane.pm] [5] Sane – Scanner Access Now Easy: [http://www.sane-project.org/html] |








