User Tools

Site Tools


misc:xslt

XSL

XSL FO, FO

Beispiel XML

Hier sehen wir ein Beispieldokument (ein gekürztes Profil), welches wir später in HTML und XSL-FO bzw. PDF umwandeln wollen. Am Ende des Dokuments befinden sich Links, unter denen alle ausführlichen Dokumente heruntergeladen werden können.

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="profil-default.xsl" ?>
<profil>
    <stand>15.01.2011</stand>
    <person>
        <name>Göbl</name>
        <vorname>Heinrich</vorname>
        <strasse>Ludwig-Thoma-Str. 27</strasse>
        <ort>83026 Rosenheim</ort>
        <geburtsdatum>1969</geburtsdatum>
        <telefon>+49 171 310 45 41</telefon>
        <email>sysprog@goebl.com</email>
        <web>http://www.goebl.com/</web>
        <web>http://www.gulp.de/Profil/hgoebl.html</web>
        <it_erfahrung_seit>1988</it_erfahrung_seit>
        <foto_hires>hgoebl_354x472.jpg</foto_hires>
        <foto_lowres>hgoebl_177x236.jpeg</foto_lowres>
        <qualifikationen>
            <ausbildung>1995: Diplom Informatiker (FH)</ausbildung>
            <ausbildung>2000: Oracle Certified Professional (SQL, PL/SQL)</ausbildung>
            <ausbildung>2001: SCPJ</ausbildung>
            <ausbildung>2004: IBM Certified Enterprise Developer</ausbildung>
        </qualifikationen>
        <fremdsprachen>
            <fremdsprache>Englisch</fremdsprache>
            <fremdsprache>Spanisch</fremdsprache>
        </fremdsprachen>
    </person>
    <erfahrungen>
        <erfahrung kategorie="Sprachen">
            Java, SQL, JavaScript, XML/XSLT, C/C++, Perl, PHP
        </erfahrung>
        <erfahrung kategorie="Datenbanken">
            ORACLE, MS SQLServer, DB2, MySQL
        </erfahrung>
    </erfahrungen>
    <projekte>
        <projekt>
            <zeitraum>02/2010 - 01/2011</zeitraum>
            <firma>Automobilbranche</firma>
            <zusatz>München, freie Mitarbeit</zusatz>
            <taetigkeit>
                Teleservice Switchboard (Telediagnose, Teleprogrammierung)
            </taetigkeit>
            <taetigkeit>Migration WebLogic 8 auf 10, EJB 2.1 auf 3.0</taetigkeit>
            <software>
                Java 5, Oracle 11g, WebLogic 10g, WebSphere MQ, EJB 3, JPA, JSF, MyFaces,
                ajax4jsf, jQuery, JAX-WS, JAXB 2, JMS, XSLT, ant, Mockito, PL/SQL, soapUI
            </software>
        </projekt>
        <projekt>
            <zeitraum>12/2009 - 01/2010</zeitraum>
            <firma>Fortbildung</firma>
            <zusatz>(autodidakt)</zusatz>
            <software>
                JPA 2.0, EJB 3.1, JSF 2.0, jQuery, RESTful HTTP, Glassfish 3, Netbeans,
                HTML 5, CSS 3, YAML, Android, Mockito, DBUnit
            </software>
        </projekt>
    </projekte>
</profil>

Umwandlung in HTML

Das Ergebnis sieht ungefähr so aus (aus Platzgründen nebeneinander):

Ergebnis

Root-Element und Styles

Die Umwandlung in HTML erfolgt über ein XSLT Stylesheet mit Ausgabeformat “html”.
Das erste Template matched auf die Root-Node “profil”.
Nachdem der der Rahmen (head, body, h1) existiert, werden über <xsl:apply-templates> die 3 größeren Blöcke person, erfahrungen und projekte generiert.

Für das Aussehen sorgen wir bei HTML natürlich mit CSS, indem im <head> in Link auf ein Stylesheet erzeugt wird: <link rel=“stylesheet” type=“text/css” href=“profil-default.css”/>.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output method="html"/>

    <xsl:template match="/profil">
        <html>
            <head>
                <title>Profil <xsl:value-of
                    select="concat(person/vorname, ' ', person/name)"/></title>
                <link rel="stylesheet" type="text/css" href="profil-default.css"/>
            </head>
            <body>
                <h1>Profil <xsl:value-of
                    select="concat(person/vorname, ' ', person/name)"/></h1>
                <p>Stand: <xsl:value-of select="stand"/></p>
                <xsl:apply-templates select="person"/>
                <xsl:apply-templates select="erfahrungen"/>
                <xsl:apply-templates select="projekte"/>
            </body>
        </html>
    </xsl:template>

    <!-- ... -->

</xsl:stylesheet>

Element Person

Die interessanteste Stelle hierbei ist die Umsetzung von fremdsprachen.
Dabei werden die Inhalte aller fremdsprache Elemente mit Komma getrennt dargestellt. Mit <xsl:if test=“position() != 1”> findet eine Prüfung statt, die verhindert, dass das erste Element schon ein Komma vorangestellt bekommt.

    <xsl:template match="person">
        <h2>Person/Überblick</h2>
        <table class="person box">
            <tr>
                <td class="label">Name:</td>
                <td class="value"><xsl:value-of
                    select="concat(vorname, ' ', name)"/></td>
                <td rowspan="7"><img class="foto" src="{foto_lowres}"
                    alt="{concat('Foto von ', vorname, ' ', name)}"/></td>
            </tr>
            <tr>
                <td class="label">Adresse:</td>
                <td class="value">
                    <xsl:value-of select="strasse"/><br/><xsl:value-of select="ort"/>
                </td>
            </tr>
            <tr>
                <td class="label">Telefon:</td>
                <td class="value"><xsl:value-of select="telefon"/></td>
            </tr>
            <tr>
                <td class="label">E-Mail:</td>
                <td class="value">
                    <a href="mailto:{email}"><xsl:value-of select="email"/></a>
                </td>
            </tr>
            <tr>
                <td class="label">Internet:</td>
                <td class="value">
                <xsl:for-each select="web">
                    <a href="{.}"><xsl:value-of select="."/></a><br/>
                </xsl:for-each>
                </td>
            </tr>
            <tr>
                <td class="label">Qualifikationen, Zertifikate:</td>
                <td class="value" colspan="2">
                    <ul class="qualification">
                    <xsl:for-each select="qualifikationen/ausbildung">
                        <li><xsl:value-of select="text()"/></li>
                    </xsl:for-each>
                    </ul>
                </td>
            </tr>
            <tr>
                <td class="label">Fremdsprachen:</td>
                <td class="value" colspan="2">
                <xsl:for-each select="fremdsprachen/fremdsprache">
                    <xsl:if test="position() != 1"><xsl:text>, </xsl:text></xsl:if>
                    <xsl:value-of select="text()"/>
                </xsl:for-each>
                </td>
            </tr>
        </table>
    </xsl:template>

Element Erfahrungen

Hier kann man sehen, wie auf Attribute zugegriffen wird (@kategorie):

    <xsl:template match="erfahrungen">
        <h2>EDV-Erfahrung</h2>
        <table class="box">
            <xsl:for-each select="erfahrung">
            <tr>
                <td class="label"><xsl:value-of select="@kategorie"/>:</td>
                <td class="value"><xsl:value-of select="text()"/></td>
            </tr>
            </xsl:for-each>
        </table>
    </xsl:template>

Element Projekte

Die Besonderheit hier ist, dass hier im Gegensatz zum Rest des Stylesheets, der Umwandlung in Form von <xsl:apply-templates /> “freien Lauf” gelassen wird. Sinn dabei ist, ggf. Unterstrukturen ebenfalls in die Zielstruktur zu übernehmen. Dies passiert in diesem Fall nur über die default-templates. Man könnte aber ganz leicht neue <xsl:template> Elemente hinzufügen, welche die default-templates überdecken.

    <xsl:template match="projekte">
        <h2>Projekte</h2>
        <xsl:for-each select="projekt">
        <table class="box">
            <tr>
                <td class="label"><xsl:value-of select="zeitraum"/>:</td>
                <td class="value">
                    <span class="firma"><xsl:value-of select="firma"/></span>
                    <xsl:text>, </xsl:text>
                    <span class="zusatz"><xsl:value-of select="zusatz"/></span>
                    <xsl:for-each select="taetigkeit">
                        <div class="taetigkeit"><xsl:apply-templates /></div>
                    </xsl:for-each>
                </td>
            </tr>
            <tr>
                <td class="label">Software:</td>
                <td class="value">
                    <xsl:for-each select="software">
                        <div class="software"><xsl:apply-templates /></div>
                    </xsl:for-each>
                </td>
            </tr>
        </table>
        <br/>
        </xsl:for-each>
    </xsl:template>

Umsetzung in XSL-FO

Bei der Umwandlung von FO nach PDF erhält man dieses Ergebnis:

Root-Element und Styles

Bei der Umsetzung nach XSL-FO ist der Rahmen wesentlich umfangreicher als bei HTML. Zudem gibt es keine klare Trennung von Struktur und Inhalt, wie etwa bei HTML und CSS oder OpenDocument Format mit content.xml und styles.xml. Trotzdem hat man sehr gute Möglichkeiten, bestimmte Formatiermerkmale (in FO-Sprache “Traits”) von der Struktur zu trennen. Dazu gibt es das XLST Element <xsl:attribute-set>, welches sich auch hervorragend zur Vererbung von Merkmalen eignet.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:fo="http://www.w3.org/1999/XSL/Format" version="1.0">
    <xsl:output method="xml"/>

    <xsl:attribute-set name="font">
        <xsl:attribute name="font-family">Arial, Helvetica, sans-serif</xsl:attribute>
        <xsl:attribute name="font-size">10pt</xsl:attribute>
    </xsl:attribute-set>
    <xsl:attribute-set name="link">
        <xsl:attribute name="color">#0000CC</xsl:attribute>
        <xsl:attribute name="text-decoration">underline</xsl:attribute>
    </xsl:attribute-set>
    <xsl:attribute-set name="h1" use-attribute-sets="font">
        <xsl:attribute name="font-size">16pt</xsl:attribute>
        <xsl:attribute name="font-weight">bold</xsl:attribute>
        <xsl:attribute name="keep-with-next">10</xsl:attribute>
        <xsl:attribute name="space-before">10mm</xsl:attribute>
        <xsl:attribute name="space-after">4mm</xsl:attribute>
    </xsl:attribute-set>
    <xsl:attribute-set name="h2" use-attribute-sets="h1">
        <xsl:attribute name="font-size">14pt</xsl:attribute>
        <xsl:attribute name="space-before">5mm</xsl:attribute>
        <xsl:attribute name="space-after">3mm</xsl:attribute>
    </xsl:attribute-set>
    <xsl:attribute-set name="box">
        <xsl:attribute name="font-family">Arial, Helvetica, sans-serif</xsl:attribute>
        <xsl:attribute name="space-before">2mm</xsl:attribute>
        <xsl:attribute name="space-after">2mm</xsl:attribute>
        <xsl:attribute name="border-style">solid</xsl:attribute>
        <xsl:attribute name="border-color">#CCCCCC</xsl:attribute>
        <xsl:attribute name="border-width">0.3mm</xsl:attribute>
        <xsl:attribute name="keep-together.within-page">always</xsl:attribute>
    </xsl:attribute-set>
    <xsl:attribute-set name="cell">
        <xsl:attribute name="padding">1.5mm</xsl:attribute>
    </xsl:attribute-set>

    <!-- ... -->

</xsl:stylesheet>

Was bei HTML das <html> Tag ist, rendern wir bei FO als <fo:root>.
Darunter befinden sich:

  • <fo:layout-master-set> stellt u.a. die Seitenränder ein.
  • <fo:declarations> nimmt Metadaten auf, die z.B. im PDF unter Eigenschaften zu sehen sind. Dieses Element ist optional.
  • <fo:page-sequence> ist so etwas ähnliches wie <body> in HTML.
    • <fo:static-content flow-name=“xsl-region-before”> bildet die Kopfzeile
    • <fo:static-content flow-name=“xsl-region-after”> wird die Fußzeile
    • <fo:flow flow-name=“xsl-region-body”> ist sozusagen das “Fleisch” der Seite.

    <xsl:template match="/profil">
        <fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
            <fo:layout-master-set>
                <fo:simple-page-master
                    master-name   = "A4Master"
                    page-height   = "29.7cm"
                    page-width    = "21cm"
                    margin-top    = "10mm"
                    margin-bottom = "10mm"
                    margin-left   = "10mm"
                    margin-right  = "10mm">
                    <fo:region-body
                        margin-top    = "10mm"
                        margin-bottom = "16mm"
                        margin-left   = "5mm"
                        margin-right  = "5mm"/>
                    <fo:region-before
                        extent = "16mm"/>
                    <fo:region-after
                        extent = "10mm"/>
                </fo:simple-page-master>
            </fo:layout-master-set>
            <fo:declarations>
                <x:xmpmeta xmlns:x="adobe:ns:meta/">
                    <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
                        <rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">
                            <!-- Dublin Core properties go here -->
                            <dc:title>
                                <xsl:value-of select="concat('Profil ', person/vorname, ' ', person/name)"/>
                            </dc:title>
                            <dc:creator>
                                <xsl:value-of select="concat(person/vorname, ' ', person/name)"/>
                            </dc:creator>
                        </rdf:Description>
                        <rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/">
                            <!-- XMP properties go here -->
                            <xmp:CreatorTool>Apache FOP, NetBeans IDE, Linux Mint</xmp:CreatorTool>
                        </rdf:Description>
                    </rdf:RDF>
                </x:xmpmeta>
            </fo:declarations>
            <fo:page-sequence master-reference="A4Master">
                <fo:static-content flow-name="xsl-region-before">
                    <xsl:call-template name="single-row-table-left-right">
                        <xsl:with-param name="right">Profil
                            <xsl:value-of select="concat(person/vorname, ' ', person/name)"/>
                        </xsl:with-param>
                        <xsl:with-param name="left">
                        </xsl:with-param>
                    </xsl:call-template>
                </fo:static-content>
                <fo:static-content flow-name="xsl-region-after">
                    <xsl:call-template name="single-row-table-left-right">
                        <xsl:with-param name="left">Stand:
                            <xsl:value-of select="stand"/>
                        </xsl:with-param>
                        <xsl:with-param name="right">
                            Seite <fo:page-number/> von <fo:page-number-citation-last ref-id="last-page"/>
                        </xsl:with-param>
                    </xsl:call-template>
                </fo:static-content>
                <fo:flow flow-name="xsl-region-body">
                    <xsl:call-template name="h1">
                        <xsl:with-param name="value">Profil
                            <xsl:value-of select="concat(person/vorname, ' ', person/name)"/>
                        </xsl:with-param>
                    </xsl:call-template>
                    <xsl:apply-templates select="person"/>
                    <xsl:apply-templates select="erfahrungen"/>
                    <xsl:apply-templates select="projekte"/>
                    <fo:block id="last-page"/>
                </fo:flow>
            </fo:page-sequence>
        </fo:root>
    </xsl:template>

Oft gebraucht in Kopf- oder Fußzeile ist die Seitennummer und die Anzahl der Seiten insgesamt.
Dazu werden in diesem Beispiel in der Fußzeile (<fo:static-content flow-name=“xsl-region-after”>) zwei FO-Elemente eingefügt: Seite <fo:page-number/> von <fo:page-number-citation-last ref-id=“last-page”/>.
Damit der Prozessor weiß, was die höchste Seitennummer ist, wird als allerletztes Element im Body ein leerer Block mit der entsprechenden ID angelegt: <fo:block id=“last-page”/>.
Anmerkung: Apache FOP 1.0 setzt das bei der Umwandlung in PDF korrekt um, die Umwandlung in RTF liefert bei dieser Art von Vorwärts-Referenzen Fehler mit dem Effekt, dass man die Gesamtanzahl der Seiten nicht im Dokument hat.

Grundlegende Templates

Im folgenden Codeausschnitt finden sich Templates mit Parametern. Damit soll erreicht werden, dass wiederkehrende Strukturen immer auf dieselbe Art erzeugt werden und bei Änderungsbedarf nur an einer Stelle geändert werden muss (DRY-Prinzip, Antipattern Copy&Paste, “Copy is my hobby”).
Am Beispiel single-row-table-left-right kann man gut erkennen, wie in XSL-FO eine einfache Tabelle aufgebaut ist. Tatsächlich gibt es gerade bei Tabellen aber sehr viel mehr Möglichkeiten.
Im Template row-2-cols ist ein Parameter mit Default-Wert: <xsl:param name=“colspan” select=“'1'”/>. Hier auch gut zu sehen ist die Umsetzung des HTML-Attributs colspan. Dies wird zu number-columns-spanned.

    <xsl:template name="row-2-cols">
        <xsl:param name="label"/>
        <xsl:param name="value"/>
        <xsl:param name="colspan" select="'1'"/>
        <fo:table-row>
            <fo:table-cell xsl:use-attribute-sets="cell">
                <fo:block font-weight="bold"><xsl:copy-of select="$label"/></fo:block>
            </fo:table-cell>
            <fo:table-cell xsl:use-attribute-sets="cell" number-columns-spanned="{$colspan}">
                <fo:block><xsl:copy-of select="$value"/></fo:block>
            </fo:table-cell>
        </fo:table-row>
    </xsl:template>

    <xsl:template name="single-row-table-left-right">
        <xsl:param name="left"/>
        <xsl:param name="right"/>
        <fo:table table-layout="fixed" width="100%">
            <fo:table-column column-width="proportional-column-width(1)"/>
            <fo:table-column column-width="proportional-column-width(1)"/>
            <fo:table-body>
                <fo:table-row>
                    <fo:table-cell>
                        <fo:block text-align="start"><xsl:copy-of select="$left"/></fo:block>
                    </fo:table-cell>
                    <fo:table-cell>
                        <fo:block text-align="end"><xsl:copy-of select="$right"/></fo:block>
                    </fo:table-cell>
                </fo:table-row>
            </fo:table-body>
        </fo:table>
    </xsl:template>

    <xsl:template match="a">
        <fo:basic-link external-destination="{.}" xsl:use-attribute-sets="link">
            <xsl:value-of select="."/>
        </fo:basic-link>
    </xsl:template>

Element Person

In diesem (gekürzten) Codeausschnitt sind folgende interessante Aspekte enthalten:

  • HTML <td rowspan=“…”> wird in FO mit <fo:table-cell number-rows-spanned=“…”> umgesetzt.
  • Apache FOP kann wohl derzeit nur mit Tabellen umgehen, die table-layout=“fixed” haben. Die Spaltenbreiten werden mit <fo:table-column column-width=“…”/> angegeben, wobei der Wert proportional-column-width(1) scheinbar ähnliche Wirkung hat wie *.
  • HTML <img src=“…”> findet in <fo:external-graphic src=“…”> seine Entsprechung.
  • Ein einfacher Zeilenvorschub im Sinne von HTML <br/> existiert in XSL-FO offensichtlich nicht. Mit einem leeren Block <fo:block/> erreicht man aber Ähnliches.
  • HTML <a href=“…”> werden in FO als <fo:basic-link external-destination=“…”> ausgegeben.

    <xsl:template match="person">
        <xsl:call-template name="h2">
            <xsl:with-param name="value">Person/Überblick</xsl:with-param>
        </xsl:call-template>
        <fo:table table-layout="fixed" width="100%" xsl:use-attribute-sets="box">
            <fo:table-column column-width="40mm"/>
            <fo:table-column column-width="75mm"/>
            <fo:table-column column-width="proportional-column-width(1)"/>
            <fo:table-body>
                <fo:table-row>
                    <fo:table-cell xsl:use-attribute-sets="cell">
                        <fo:block font-weight="bold">Name:</fo:block>
                    </fo:table-cell>
                    <fo:table-cell xsl:use-attribute-sets="cell">
                        <fo:block><xsl:value-of select="concat(vorname, ' ', name)"/></fo:block>
                    </fo:table-cell>
                    <fo:table-cell number-rows-spanned="7" xsl:use-attribute-sets="cell">
                        <fo:block text-align="end">
                            <fo:external-graphic src="{foto_hires}" content-height="57mm" content-width="42mm"/>
                        </fo:block>
                    </fo:table-cell>
                </fo:table-row>
                <xsl:call-template name="row-2-cols">
                    <xsl:with-param name="label">Adresse:</xsl:with-param>
                    <xsl:with-param name="value">
                        <xsl:value-of select="strasse"/><fo:block/><xsl:value-of select="ort"/>
                    </xsl:with-param>
                </xsl:call-template>
                <xsl:call-template name="row-2-cols">
                    <xsl:with-param name="label">E-Mail:</xsl:with-param>
                    <xsl:with-param name="value">
                        <fo:basic-link external-destination="mailto:{email}" xsl:use-attribute-sets="link">
                            <xsl:value-of select="email"/>
                        </fo:basic-link>
                    </xsl:with-param>
                </xsl:call-template>
                <xsl:call-template name="row-2-cols">
                    <xsl:with-param name="label">Internet:</xsl:with-param>
                    <xsl:with-param name="value">
                        <xsl:for-each select="web">
                            <fo:basic-link external-destination="{.}" xsl:use-attribute-sets="link">
                                <xsl:value-of select="."/>
                            </fo:basic-link>
                            <fo:block/>
                        </xsl:for-each>
                    </xsl:with-param>
                </xsl:call-template>
                <xsl:call-template name="row-2-cols">
                    <xsl:with-param name="label">Fremdsprachen:</xsl:with-param>
                    <xsl:with-param name="colspan">2</xsl:with-param>
                    <xsl:with-param name="value">
                        <xsl:for-each select="fremdsprachen/fremdsprache">
                            <xsl:if test="position() != 1"><xsl:text>, </xsl:text></xsl:if>
                            <xsl:value-of select="text()"/>
                        </xsl:for-each>
                    </xsl:with-param>
                </xsl:call-template>
            </fo:table-body>
        </fo:table>
    </xsl:template>

Umwandlung

Dieses Skript (runfop.sh) wandelt das Profil in verschiedene Formate um:

#!/bin/bash
if [ "x$FOP" == "x" ]; then
    FOP=/opt/fop-1.0/fop
fi

$FOP -xml hgoebl.xml -xsl profil-fop.xsl -pdf hgoebl.pdf
$FOP -xml hgoebl.xml -xsl profil-default.xsl -foout hgoebl.html
$FOP -xml hgoebl.xml -xsl profil-fop.xsl -foout hgoebl.fo
$FOP hgoebl.fo -rtf hgoebl.rtf

Und damit das PDF Dokument nicht im Browser geöffnet wird, sondern heruntergeladen wird, gibt es noch dieses PHP-Script (downloadhgoeblpdf.php):

<?php
header('Content-type: application/pdf');
header('Content-Disposition: attachment; filename="hgoebl.pdf"');
readfile('hgoebl.pdf');
?>

Download Dokumente

Mittlerweile haben sich die Skripte verändert. Unter anderem werden nun noch mehr Formate unterstützt und ein Profil kann in mehreren Sprachen verfasst und generiert werden. Der Quelltext befindet sich nun unter https://github.com/hgoebl/it-profile-generator/

misc/xslt.txt · Last modified: 2012/01/08 15:31 by hgoebl