Alte Skripte laufen wieder

Generell ist es kein großes Problem, Skripte für vorhergehende Versionen in InDesign zum laufen zu kriegen. Auch wenn sich gerne einige Details im Document Object Model verändern, bringt jede neue InDesign Version einen abwärtskompatiblen Interpreter mit.
Entweder man stellt die Eigenschaft app.scriptPreferences.version = "#.0" am Anfang des Skripte auf die passende Version, oder verwendet eine target-Direktive #target "InDesign-#.0". Wer das ESTK nicht öffnen will verschiebt das Skript einfach in einen Ordner mit der Namenskonvention Version #.0 Scripts.

Bei den Versionen kann man nur noch über die nicht direkt intuitiven Versionsnummern stolpern – die sich aus dem Versionssprung InDesign 2.0 auf InDesign CS erklären.

Skript für … Ordner app.scriptPreferences.version #target
InDesign 2.0 Version 2.0 Scripts 2.0 „InDesign-2.0“
InDesign CS Version 3.0 Scripts 3.0 „InDesign 3.0“
InDesign CS2 Version 4.0 Scripts 4.0 „InDesign-4.0“
InDesign CS3 Version 5.0 Scripts 5.0 „InDesign 5.0“
InDesign CS4 Version 6.0 Scripts 6.0 „InDesign-6.0
InDesign CS5 Version 7.0 Scripts 7.0 „InDesign 7.0“
InDesign CS5.5 Version 7.5 Scripts 7.5 „InDesign-7.5

Wer lieber eine Benutzeroberfläche haben möchte, kann sich auch das Skript Make it Run von Martinho da Gloria herunterladen. Ich denke das folgende Bild erklärt es ganz gut.

Ich hoffe damit hat sich die ein oder andere Support-Anfrage erledigt :-/

Das große Was ist was des EPUB- und XHTML-Exports

Beim EPUB- bzw. HTML-Export (Seit CS5.5 Datei → Export) stellt sich oft die Frage, welche Seitenobjekte und welche Formateinstellungen wie exportiert werden. Die folgende Tabelle gibt eine Übersicht über die Umsetzung in HTML-Elemente bzw. CSS-Regeln. Auffallend ist, dass der HTML und EPUB-Export manchmal zu verschiedenen Ergebnissen führen. Ich vermute, dass es noch weitere Unstimmigkeiten gibt – prinzipiell sollte meiner Ansicht das gleiche Ergebnis generiert werden. Einen entsprechenden Bug-Report habe ich bei Adobe eingereicht.

In den grau hinterlegten Zeilen finden Sie Seitenobjekte/Absätze bzw. Tabellen und deren xhtml-Umsetzung. InDesign schreibt übrigens xhtml mit ein paar HTML5 Einsprengseln – aber eigentlich kein HTML wie man anhand dem Namen der Funktion vermuten würde. In den weißen Zellen folgen dann immer die CSS-Eigenschaften für das vorhergehende Element.

Alternativ kann man sich auch die inhaltlich gleiche PDF Aufbereitung herunterladen und ausdrucken.

Ergänzungen und Korrekturen sind willkommen! Hinterlassen Sie einen Kommentar oder schicken Sie mir eine Mail!

Objekt/
InDesign-Eigenschaft
Element/
CSS-Deklarationen
Anmerkung
Dokument <body>
<div id="DokumentName.html">
</div>
</body>
Ggf. Sprachattribut
xml:lang="en-US"
Ränder HTML:
body { margin: 0.5em; }
EPUB:
@page { margin : 0.5em; }
Einstellung im Export-Dialog
Gruppen <div class="ObjectStyle"></div> Bei Gruppen ohne Objektformat class="group"
Rahmen von Bildern <div class="ObjectStyle"></div>
Bilder <img class="ObjectStyle"
width="#" height="#"
alt="file.jpg"
src="images/file.jpeg"/>
Format wie in Objekt­exportoptionen oder Export-Dialog JPG, GIF oder PNGBei Bildrahmen ohne Objektformat class="image"
Bildausrichtung und -abstände display: block;
margin: 1em 0 2em auto;
Bei Objektexportoptionen
Klasse img.media-#
Verankerte Bilder
mit Konturenführung
<img class="leftFloat".../>
<img class="rightFloat".../>
Feste CSS-Regel:.leftFloat {
float : left;
}
Links/Rechts Aufteilung: Position von der Zeilenmitte.
Textabschnitte/Textrahmen <div class="ObjectStyle"></div> Objektformat des ersten Textrahmens, bei Textrahmen ohne Objektformat class="story"
Absätze <p class="ParagraphStyle"></p>Lokale Abweichungen mit
class="para-style-override-#"
Zuweisung von <p>, <h1>,…<h6>
und/oder Klasse möglich.
Ggf. Sprachattribut xml:lang="en-US"
Ausrichtung text-align: left;
Einzug erste Zeile text-indent: 1px;
Abstände margin: 4px 5px 6px 7x; Kein Abstand: margin: 0;
Verschachteltes Zeichenformat CSS-Pseudoelement p.ParagraphStyle:first-line {} Wird nicht von allen Readern dargestellt
Weitere Formatierungen bei den Inline-Formaten
Nummerierte Liste <ol>
<li class="Absatzformat"></li>
</ol>
Statische Listen mit
<li value"#">
Ggf. Sprachattribut xml:lang="en-US"
Einzug Links
<ol class="List-#">
Aufzählungsliste <ul>  <li class="Absatzformat"></li>
</ul>
Ggf. Sprachattribut xml:lang="en-US"
Einzug Links
<ul class="List-#">
Abstand links <ol>/<ul> margin-left: 9px;
Abstand oben <li> margin-top: 0;
Abstand unten <li> margin-bottom: 10px;
Abstand rechts <li> margin-right: 10px;
Weitere Formatierungen wie bei Absatz oder Inline-Formaten
Inline-Formatierung <span class="CharacterStyle">
</span>
Lokale Abweichungen mit
class="char-style-override-#"
Zuweisung von <em>, <strong>, <span> und/oder Klasse möglich.
Ggf. Sprachattribut xml:lang="en-US"
Schriftart font-family: "Schriftname"; EPUB CSS: Schriftdeklaration über
@font-face {...}
Schriftgröße font-size: 0.83em;
Fett font-weight: bold;
Kursiv font-style: italic;
Kapitälchen font-variant: small-caps;
Farbe color:#000000;
Hoch/Tiefstellung vertical-align: super;
Unterstrichen text-decoration: underline;
Durchgestrichen text-decoration: line-through;
Kapitälchen font-variant: small-caps;
Versalien text-transform: uppercase;
Harter Zeilenumbruch <br/>
Hyperlinkziele/Textanker <a id="Name des Ankers"/>  Einige Reader erwarten <a id=”Name des Ankers”></a>
Hyperlink <a href="#Name des Ankers"></a>
Tabellen <table id="table-#" class="Tabellenformat"> Feste CSS-Regel:table.Tabellenformat {
border-collapse: collapse;
border-color: #000000;
border-style: solid;
border-width: 1px;
margin-bottom: -4px;
margin-top: 4px;
}
Kopf-,
Körper-,
Fußbereich
<thead></thead>,
<tbody></tbody>,
<tfoot></tfoot>
Feste CSS-Regel:tbody, thead, tfoot, tr, td, th {
border-color: inherit;
border-style: inherit;
border-width: inherit;
}
Tabellenzeilen <tr></tr> Bei angewählter Option
Abwechselndes Muster Zeile
<tr class="Row-Column-#">
Flächenfarbe background-color: #D90000;
Tabellenspalten <col class="Row-Column-#" /> Erstellung nur bei angewählter Option abwechselndes Muster Spalte
Flächenfarbe background-color: #D90000;
Tabellenzellen <td>
Lokale Abweichungen mit
class="cell-style-override-#"
Feste CSS-Regel:td {
height: 5px;
width: 10px;
}
Flächenfarbe background-color: #D90000;
Zellenversatz padding-bottom : 9px;padding-left : 6px;padding-right : 9px;padding-top : 9px; Unklare/fehlerhafte Umsetzung
Video <div class="image">  <video id="FileName.mp4" width="#" height="#" tabindex="0">    <source type="video/mp4" src="File.mp4"> </source>  </video></div>
Audio <div class="image">
<audio id="FileName.mp3" height="#" width="#" controls="controls" tabindex="0">
<source type="audio/mpeg" src="FileName.mp3"> </source>
</audio>
</div>

Skripte mit Hilfe von HTTP und ZIP updaten

Die Bibliothek Extendables von Stijn Debrouwere ist inzwischen schon fast ein Jahr im Orbit. Die Idee ist ExtendScript, also die JavaScript Implementierung fast aller Adobe Anwendungen, mit nützlichen Funktionen zu erweitern. Die Klassiker sind die Erweiterungen der Klassen String und Array, wie z.B. die Methoden "Zeichenkette".trim() oder "Zeichenkette„.contains().

Neben vielen Erweiterungen und Gimmicks fällt vor allem das HTTP-Modul auf. Die bekannten Verfahren über das Socket-Objekt sind eher aufwändig und man braucht ein gesundes Verständnis des Protokolls HTTP. Mit Extendables kann man sich die Programmierung auf dieser Ebene sparen und direkt mit Webdiensten kommunizieren. Das Minimalbeispiel aus der Dokumentation zeigt das Vorgehen recht gut:

#include "extendables/extendables.jsx";
var http = require("http");
var response = http.get("http://www.w3c.org");
if (response.status == 200) {
    $.writeln(response.body);
} else {
    $.writeln("Connection failed");
}

Mit Hilfe der #include Direktive wird die Bibliothek extendables.jsx eingebunden. Das http-Package wird über die Funktion require() aktiviert und kann dann über die Variable http angesprochen werden. Mit get() kann eine Webadresse abgerufen werden, das Ergebnis der Abfrage wird hier in der Variable response gespeichert, sie enthält in der Eigenschaft body die Website. In der hier nicht verwendeten Eigenschaft headers sind weitere Statusinformationen enthalten. Weitere Infos zum Thema HTTP spukt die gute alte Wikipedia recht übersichtlich aus.

Ich möchte hier ein Praxisbeispiel vorstellen. Gerade komplexe Skripte laufen manchmal nicht ganz fehlerfrei und brauchen ab und zu ein Update. Wenn diese Skripte dann noch auf verschiedenen Rechnern verteilt sind, wäre es praktisch, diese über ein Webupdate zu aktualisieren – ganz so wie das inzwischen jede moderne Software macht.

Für das Skript braucht man natürlich einen Webserver. Das Beispiel arbeitet mit einem Skript, das die Uhrzeit bei Erstellung anzeigt. Es wird minütlich auf meinem Server per Cronjob aktualisiert und bietet somit ein ideales Beispiel für das Update-Skript.

Das eigentliche Skript ist in der Funktion checkForUpdates() enthalten, die mit den Parametern _updateServerURL und _updateFile gesteuert werden kann.

checkForUpdates ("http://www.publishingx.de/software/test/", "package.zip");

Der aktuelle Pfad wird ebenfalls mit Hilfe der Extendables Erweiterung component()ermittelt.

var _path = Folder(File($.fileName).component('path'));

Die Versionsprüfung wird anhand des Inhalts der Textdatei etag.txt vorgenommen. Im sogenannten ETag speichert der Webserver die Versionsinformation einer Datei, dies wird z.B. beim Caching von Webbrowsern eingesetzt. Später beim Update speichert das Skript die Information, hier wird sie zunächst ausgelesen, um sie später mit der Version auf dem Server zu vergleichen. Das ist nicht der eigentliche Umgang mit dem HTTP Etag (304 Modified). Laut HTTP Standard sollte der Client anfragen, ob es überhaupt notwendig ist die neue Datei zu laden. Da dies in Extendables aber nicht implementiert ist, werde ich diesen Vergleich lokal vornehmen (und muss deswegen in jedem Fall den Header herunterladen).

var _etagFile =  File (_path + "/etag.txt");
if (_etagFile.open("r")) {
	var _localEtag = _etagFile.read();
	_etagFile.close();
}

Zunächst wird geprüft, ob eine Internet-Verbindung besteht:

if( http.has_internet_access()) { /* ... */ }

Da eine ZIP-Datei heruntergeladen werden soll, muss die Anfrage im Binary-Mode vorgenommen werden. Deswegen muss der HTTPRequest selber gebaut werden, was aber kein Problem ist. Wichtig ist, das encoding des Requests auf BINARY zu stellen.

var _updateURL = _updateServerURL + _updateFile;
var _req = new http.HTTPRequest("GET", _updateURL);
_req.encoding("BINARY");
var _resp = _req.do();

In _resp ist nun die Antwort enthalten. Zunächst wird anhand der Eigenschaft status geprüft, ob die Daten vorhanden sind.

if( _resp.status == 200 ) { /* ... */ }

Wenn wir Daten erhalten haben, wird das ETag aus dem Header geladen und mit dem lokalen ETag verglichen.

var _remoteETag = _resp.headers["ETag"]; // == _resp.headers.ETag;
if (_remoteETag != _localEtag) { { /* ... */ }

Sollte eine veränderte Version vorliegen, muss diese gespeichert und entpackt werden. Dazu wird zunächst die ZIP-Datei in den TEMP-Folder geschrieben und dann mit app.unpackageUCF() entpackt. Beim Schreiben muss man natürlich wieder das richtige encoding einstellen.

var _zipFile = new File (Folder.temp + "/temp.zip");
_zipFile.encoding = "BINARY";
if (_zipFile.open("w")) {
	_zipFile.write(_resp.body);
	_zipFile.close();
}
// ...
app.unpackageUCF(_zipFile, _path);

Im Zip-Archiv befindet sich die Datei scriptToUpdate.jsx die dann automatisch auf dem lokalen Rechner überschrieben wird. Achten Sie darauf, dass InDesign unter Windows7 im Anwedungsordner normalerweise keine Schreibrechte hat, das Skript empfiehlt sich also für den Benutzer-Ordner.

Das Update Skript inklusive Extendables kann natürlich auch vollständig heruntergeladen werden: checkForUpdates.zip Leider ist in der aktuellen Version von Extendables ein bei er Verarbeitung von Binary-Daten enthalten. Stijn hat zugesagt diesen bald zu beheben, solange muss man leider noch die im Download-Archiv enthaltene und von mir gepatchte Version verwenden!

markupFORUM

Heute war das markupFORUM in Stuttgart. Es gab viele interessanten Beiträge von XSLT bis EPUB. Es hat richtig Spaß gemacht Fachpublikum zu treffen und über die Zukunft von XML zu diskutieren.

Meine Vortragsunterlagen zum Thema »InDesign und XML – wie geht’s weiter« können Sie hier herunterladen.

XML-Workflows in InDesign

Gerrit Imsieke von le-tex hat unter XML-first Workflows in InDesign: Ropes of Sand? eine lesenswerte Zusammenfassung über mögliche Herangehensweisen von strukturierten Daten und InDesign geschrieben.

Er favorisiert letztlich die in meinem tekom-Vortrag unter Datenextraktion vorgestellte Lösung:

Er legt sich allerdings auf eine IDML-Lösung fest, während ich momentan oftmals eine Skripting-Lösung bevorzuge. Ein Vorteil beim Skripting ist, dass man die Qualitätssicherung innerhalb von InDesign interaktiv mit den Anwendern durchführen kann. Ein Nachteil ist sicherlich die Geschwindigkeit – vor allem wen man versucht pseudo intelligent verschiedene Situationen fürs XML zu »erraten«.

Unabhängig von Skripting oder IDML: Dieser Ansatz hat immer den Nachteil, dass der Anwender zunächst in seiner klassischen Arbeitsweise beliebig sinnfreie InDesign-Dokumente erstellen kann. Diese sehen vielleicht im Druck noch gut aus, sind aber für strukturierte Daten ungeeignet. Eine Datenextraktion erfordert immer die Arbeit nach festgelegten Konventionen und Vorgehensweisen, so dass die automatisierte Auswertung gelingen kann.

Auch wenn ich kein Freund von XML-First-Workflows bin, hat man hier die Möglichkeit über die Einführung einer neuen Technologie neue Arbeitsweisen und Konventionen einzuführen. Beim »weiter so« ist dies nur schwer zu vermitteln. Zusätzlich bietet sich die Möglichkeit, die Produkte zu standardisieren und sich von rätselhaft begründeten Ausnahmen zu verabschieden.

Whitespace Search&Replace

Die Anforderung, mehrere Leerzeichen zusammenzufassen, taucht immer wieder auf. Bei Suchen/Ersetzungen mit Leerräumen bzw. Whitespace ist allerdings besondere Vorsicht geboten.
Zwischen Leerräumen befinden sich oft Marker, die bei der Suche ignoriert werde. Diese werden dann bei der Ersetzung unbeabsichtigt gelöscht. Das Grundsätzliche Problem habe ich bereits in diesem Post zusammengefasst.

Bei der Suche mit GREP gibt es die Möglichkeit mit \s alle Leerräume inklusive der Umbruchzeichen zu finden. Ab InDesign CS4 empfiehlt es sich mit der Unicode-Zeichenklasse \p{Z*} nur noch die Leerräume zu suchen.

Wenn ausschließlich nach Leeräumen gesucht wird, werden die folgenden Zeichen gefunden (whitespace.indd):

Wenn jetzt mehrere dieser Zeichen hintereinander auftreten, und durch einen einzigen Leerraum ersetzt werden sollen. Entstehen meiner Ansicht nach zwei grundsätzliche Probleme:

  1. Ignorierte Zeichen wie z.B. Indexmarken oder XML-Tags, die innerhalb des Suchergebnisses stehen, werden ungefragt verworfen.
  2. Die verschiedenen Leerräume haben eine unterschiedliche Wertigkeit. Wenn z.B. ein geschütztes Leerzeichen im Suchtreffer enthalten ist, sollte dieses bei der Ersetzung nicht durch ein einfaches Leerzeichen ausgetauscht werden.

Der erste Punkt ist unstrittig, hier muss um einen InDesign-Bug herumprogrammiert werden. Der zweite Punkt wird vermutlich je nach Ansicht des Anwenders bzw. Produkts unterschiedlich beantwortet werden. Deswegen muss man eventuell die Priorität der Zeichen, die erhalten bleiben sollen, individuell anpassen.

Wenn mit der Zeichenklasse \s arbeitet muss man sich zusätzlich noch überlegen, was bei der Zusammenführung mit den Formatierungseinstellungen der Absätze geschehen soll.
InDesign überträgt bei der Ersetzung die Einstellungen des vorletzten Absatzes auf den letzten Absatz. Ich bevorzuge aber das gegensätzliche Verhalten: Die Formateinstellungen des letzten Absatzes sollen erhalten bleiben. Mir reicht es zunächst, wenn das Absatzformat gerettet wird. Einzelne Einstellungen können aber nach dem gleichen Muster übernommen werden.

Um diese Probleme in den Griff zu kriegen habe ich ein Skript entwickelt. Es basiert auf dem findAndDo-Skript. Das gesamte Skript whitespacer.jsx steht zum Download bereit. Die interessantesten Teile stelle ich im folgenden vor:

Mit GREP werden alle Textstellen mit mindestens zwei aufeinanderfolgenden Leerzeichen gesucht:

app.findGrepPreferences.findWhat = "\\s{2,}";

Das Dokument muss rückwärts durchsucht werden, da Textänderungen den Index verschieben.

var _results = app.activeDocument.findGrep (true);   

Innerhalb einer for-Schleife werden die Suchergebnisse analysiert. Dazu wird mit indexOf geprüft, ob das Zeichen im Ergebnis vorhanden ist. Je weiter unten das Zeichen in Liste steht, desto höher ist die Priorität :

 // Normal Space
 if (_results[i].contents.indexOf ("\u0020") > -1) 
 _mostImportant = _results[i].contents.indexOf ("\u0020"); 
 // Nonbreaking Space
 if (_results[i].contents.indexOf ("\u00A0") > -1) 
 _mostImportant = _results[i].contents.indexOf ("\u00A0");
 // ..

Falls mehrere Absätze konsolidiert werden, merke ich mir das Absatzformat des letzten Absatzes. Hier müssen wiederum zwei Fälle unterschieden werden: Wenn der letzte Absatz mit Leerraum beginnt, gehört er zum Suchergebnis. Wenn nicht muss mit nextItem der nächste Absatz adressiert werden.

 var  _savedPStyle  = false;
 var _REbreak = RegExp("\\r","g");
 var _REStartsWhite = RegExp("^\\s");
 var _res = _results[i].contents.match (_REbreak);
 if (_res != null && _res.length > 1) {
   _lastPar = _results[i].paragraphs[-1];        
   if (_REwhite.test (_lastPar.contents) )
   _savedPStyle =_lastPar.appliedParagraphStyle;
   else 
   _savedPStyle =_results[i].paragraphs.nextItem (_lastPar ).appliedParagraphStyle; 
 }

Bei der eigentlichen Ersetzung werden alle Zeichen entfernt. Das zu erhaltende Zeichen, Marken, und bei der Suche ignorierte Zeichen werden geschützt:

 var _resArr = _results[i].characters.everyItem().getElements();
 for (var k = _resArr.length -1; k >= 0; k--) {
   var _char  = _resArr[k];
   if ( _char.contents != "\uFEFF" &&
        _char.contents  != SpecialCharacters.END_NESTED_STYLE &&
        _char.contents  != SpecialCharacters.INDENT_HERE_TAB &&
        _char.contents  != SpecialCharacters.DISCRETIONARY_HYPHEN &&
        _char.contents  != SpecialCharacters.DISCRETIONARY_LINE_BREAK &&
        _char.contents  != SpecialCharacters.ZERO_WIDTH_NONJOINER &&
        _char.contents  != SpecialCharacters.ZERO_WIDTH_JOINER &&
        k != _mostImportant)  _char.remove ();
   }
 }

Zu guter letzt wird das ehemalige Absatzformat erneut zugewiesen

 if (_savedPStyle) 
 _char.parentStory.characters[(_char.paragraphs[0].characters[-1].index + 1)].
paragraphs[0].appliedParagraphStyle;       
}

Das Skript ist noch nicht der Weisheit letzter Schluss. Insbesondere über die Performance müsste man noch nachdenken. Soweit ich das testen konnte läuft es sehr stabil.