Skip to content

XSL-FO — XML Formatting Objects for PDF Publishing, Layout, Tables, Page Masters

DodaTech Updated 2026-06-20 8 min read

XML documents need to be rendered as PDFs, print-ready documents, or high-quality formatted output. XSL-FO (XML Formatting Objects) is the standard for XML-to-PDF publishing. This guide teaches XSL-FO page layout, block/inline formatting, tables, lists, and PDF generation with Apache FOP.

What You'll Learn

You will design XSL-FO page masters, format text with block and inline elements, create tables with headers and spanning, build lists, generate PDFs from XML using Apache FOP, and apply XSL-FO in real-world document publishing. DodaZIP uses XSL-FO for generating formatted documentation and reports from XML configuration data.

Learning Path

flowchart LR
  A[XSLT] --> B[XML Signatures]
  B --> C[XSL-FO
You are here] C --> D[XML Publishing Pipeline] style C fill:#f90,color:#fff

XSL-FO Document Structure

An XSL-FO document defines pages, flows, and formatting:

<?xml version="1.0" encoding="UTF-8"?>
<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
    <fo:layout-master-set>
        <fo:simple-page-master master-name="page"
            page-width="8.5in" page-height="11in"
            margin-top="0.75in" margin-bottom="0.75in"
            margin-left="1in" margin-right="1in">
            <fo:region-body margin-top="0.5in"/>
            <fo:region-before extent="0.5in"/>
            <fo:region-after extent="0.5in"/>
        </fo:simple-page-master>
    </fo:layout-master-set>

    <fo:page-sequence master-reference="page">
        <fo:static-content flow-name="xsl-region-before">
            <fo:block text-align="center"
                font-size="10pt" font-style="italic">
                DodaTech Documentation
            </fo:block>
        </fo:static-content>
        <fo:static-content flow-name="xsl-region-after">
            <fo:block text-align="center" font-size="9pt">
                Page <fo:page-number/>
            </fo:block>
        </fo:static-content>

        <fo:flow flow-name="xsl-region-body">
            <fo:block font-size="24pt" font-weight="bold"
                space-after="12pt">
                XML Publishing Guide
            </fo:block>
            <fo:block font-size="12pt" line-height="1.4"
                space-after="6pt">
                XSL-FO transforms XML into formatted pages
                suitable for PDF generation.
            </fo:block>
        </fo:flow>
    </fo:page-sequence>
</fo:root>

Page Masters

Multiple pages with different layouts:

<fo:layout-master-set>
    <!-- Cover page: no margins -->
    <fo:simple-page-master master-name="cover"
        page-width="8.5in" page-height="11in"
        margin="0in">
        <fo:region-body margin="0in"/>
    </fo:simple-page-master>

    <!-- Normal page: with margins -->
    <fo:simple-page-master master-name="normal"
        page-width="8.5in" page-height="11in"
        margin-top="0.75in" margin-bottom="0.75in"
        margin-left="1in" margin-right="1in">
        <fo:region-body margin-top="0.5in"/>
        <fo:region-before extent="0.5in"/>
        <fo:region-after extent="0.5in"/>
    </fo:simple-page-master>

    <!-- Landscape page for tables -->
    <fo:simple-page-master master-name="landscape"
        page-width="11in" page-height="8.5in"
        margin="0.75in">
        <fo:region-body/>
    </fo:simple-page-master>

    <!-- Page sequence master for alternating -->
    <fo:page-sequence-master master-name="document">
        <fo:repeatable-page-master-alternatives>
            <fo:conditional-page-master-reference
                master-reference="cover"
                page-position="first"/>
            <fo:conditional-page-master-reference
                master-reference="normal"
                page-position="rest"/>
            <fo:conditional-page-master-reference
                master-reference="landscape"
                odd-or-even="even"/>
        </fo:repeatable-page-master-alternatives>
    </fo:page-sequence-master>
</fo:layout-master-set>

Block Elements

<!-- Paragraph with indentation -->
<fo:block text-indent="0.25in"
    space-before="6pt" space-after="6pt"
    text-align="justify">
    This paragraph has first-line indentation and
    justified alignment with spacing before and after.
</fo:block>

<!-- Headings with varying sizes -->
<fo:block font-size="18pt" font-weight="bold"
    font-family="Helvetica" space-before="12pt"
    space-after="6pt" keep-with-next="always">
    Section Heading
</fo:block>

<fo:block font-size="14pt" font-weight="bold"
    space-before="8pt" space-after="4pt">
    Subsection Heading
</fo:block>

<!-- Code block with monospace font -->
<fo:block font-family="Courier" font-size="9pt"
    background-color="#f0f0f0" padding="6pt"
    border="1pt solid #ccc" white-space="pre"
    linefeed-treatment="preserve">
    &lt;?xml version="1.0"?&gt;
    &lt;root&gt;
        &lt;element/&gt;
    &lt;/root&gt;
</fo:block>

Inline Elements

<fo:block>
    Text with
    <fo:inline font-weight="bold">bold</fo:inline>,
    <fo:inline font-style="italic">italic</fo:inline>,
    <fo:inline font-size="8pt">small</fo:inline>,
    <fo:inline color="red">colored</fo:inline>,
    <fo:inline text-decoration="underline">underlined</fo:inline>,
    and
    <fo:inline background-color="yellow">highlighted</fo:inline>
    formatting.
</fo:block>

Tables

<fo:table table-layout="fixed" width="100%"
    border-collapse="collapse">
    <!-- Column widths -->
    <fo:table-column column-width="30%"/>
    <fo:table-column column-width="50%"/>
    <fo:table-column column-width="20%"/>

    <!-- Table header -->
    <fo:table-header font-weight="bold"
        background-color="#e0e0e0">
        <fo:table-row>
            <fo:table-cell padding="4pt"
                border="0.5pt solid #999">
                <fo:block>Feature</fo:block>
            </fo:table-cell>
            <fo:table-cell padding="4pt"
                border="0.5pt solid #999">
                <fo:block>Description</fo:block>
            </fo:table-cell>
            <fo:table-cell padding="4pt"
                border="0.5pt solid #999"
                text-align="center">
                <fo:block>Version</fo:block>
            </fo:table-cell>
        </fo:table-row>
    </fo:table-header>

    <!-- Table body -->
    <fo:table-body>
        <fo:table-row>
            <fo:table-cell padding="4pt"
                border="0.5pt solid #999">
                <fo:block>XML Signing</fo:block>
            </fo:table-cell>
            <fo:table-cell padding="4pt"
                border="0.5pt solid #999">
                <fo:block>Digital signature support</fo:block>
            </fo:table-cell>
            <fo:table-cell padding="4pt"
                border="0.5pt solid #999"
                text-align="center">
                <fo:block>2.0+</fo:block>
            </fo:table-cell>
        </fo:table-row>
        <fo:table-row>
            <fo:table-cell padding="4pt"
                border="0.5pt solid #999">
                <fo:block>XSL-FO</fo:block>
            </fo:table-cell>
            <fo:table-cell padding="4pt"
                border="0.5pt solid #999">
                <fo:block>PDF rendering engine</fo:block>
            </fo:table-cell>
            <fo:table-cell padding="4pt"
                border="0.5pt solid #999"
                text-align="center">
                <fo:block>1.1</fo:block>
            </fo:table-cell>
        </fo:table-row>
    </fo:table-body>
</fo:table>

<!-- Cell spanning multiple columns -->
<fo:table-cell number-columns-spanned="3"
    padding="4pt" border="0.5pt solid #999"
    background-color="#fffde7">
    <fo:block font-weight="bold" text-align="center">
        This cell spans all three columns
    </fo:block>
</fo:table-cell>

<!-- Cell spanning multiple rows -->
<fo:table-cell number-rows-spanned="2">
    <fo:block>Vertical merge</fo:block>
</fo:table-cell>

Lists

<fo:list-block space-before="6pt" space-after="6pt"
    provisional-distance-between-starts="0.3in"
    provisional-label-separation="0.1in">

    <!-- Bullet list item -->
    <fo:list-item>
        <fo:list-item-label end-indent="label-end()">
            <fo:block>&#x2022;</fo:block>
        </fo:list-item-label>
        <fo:list-item-body start-indent="body-start()">
            <fo:block>First item in the bullet list</fo:block>
        </fo:list-item-body>
    </fo:list-item>

    <!-- Numbered list item -->
    <fo:list-item>
        <fo:list-item-label end-indent="label-end()">
            <fo:block>1.</fo:block>
        </fo:list-item-label>
        <fo:list-item-body start-indent="body-start()">
            <fo:block>Second item with numbering</fo:block>
        </fo:list-item-body>
    </fo:list-item>

    <fo:list-item>
        <fo:list-item-label end-indent="label-end()">
            <fo:block>2.</fo:block>
        </fo:list-item-label>
        <fo:list-item-body start-indent="body-start()">
            <fo:block>Third item in this list</fo:block>
        </fo:list-item-body>
    </fo:list-item>
</fo:list-block>

Generating PDF with Apache FOP

import org.apache.fop.apps.*;
import javax.xml.transform.*;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamSource;
import java.io.*;

public class FopPdfGenerator {
    public static void generatePdf(File foFile, File pdfFile)
            throws Exception {

        // Configure FOP
        FopFactory fopFactory = FopFactory.newInstance(
            new File(".").toURI());
        FOUserAgent foUserAgent = fopFactory.newFOUserAgent();

        // Create output stream
        try (OutputStream out = new FileOutputStream(pdfFile)) {
            // Construct FOP with PDF output
            Fop fop = fopFactory.newFop(
                MimeConstants.MIME_PDF, foUserAgent, out);

            // Transform FO file to PDF
            TransformerFactory factory =
                TransformerFactory.newInstance();
            Transformer transformer = factory.newTransformer();

            Source src = new StreamSource(foFile);
            Result res = new SAXResult(fop.getDefaultHandler());
            transformer.transform(src, res);

            System.out.println("PDF generated: " + pdfFile);
        }
    }

    public static void main(String[] args) throws Exception {
        if (args.length < 2) {
            System.out.println("Usage: java FopPdfGenerator in.fo out.pdf");
            return;
        }
        generatePdf(new File(args[0]), new File(args[1]));
    }
}

XML to PDF Pipeline

Combine XSLT and FOP to transform raw XML into PDF:

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

    <xsl:template match="/">
        <fo:root>
            <fo:layout-master-set>
                <fo:simple-page-master master-name="page"
                    page-width="8.5in" page-height="11in"
                    margin="1in">
                    <fo:region-body/>
                    <fo:region-before extent="0.5in"/>
                    <fo:region-after extent="0.5in"/>
                </fo:simple-page-master>
            </fo:layout-master-set>

            <fo:page-sequence master-reference="page">
                <fo:static-content flow-name="xsl-region-before">
                    <fo:block text-align="right"
                        font-size="9pt" color="#666">
                        <xsl:value-of select="document/title"/>
                    </fo:block>
                </fo:static-content>

                <fo:flow flow-name="xsl-region-body">
                    <fo:block font-size="20pt" font-weight="bold"
                        space-after="12pt">
                        <xsl:value-of select="document/title"/>
                    </fo:block>

                    <xsl:apply-templates select="document/section"/>
                </fo:flow>
            </fo:page-sequence>
        </fo:root>
    </xsl:template>

    <xsl:template match="section">
        <fo:block font-size="14pt" font-weight="bold"
            space-before="12pt" space-after="6pt">
            <xsl:value-of select="@title"/>
        </fo:block>
        <xsl:apply-templates select="paragraph"/>
    </xsl:template>

    <xsl:template match="paragraph">
        <fo:block space-after="6pt" text-align="justify">
            <xsl:value-of select="."/>
        </fo:block>
    </xsl:template>
</xsl:stylesheet>

Run the pipeline:

# Step 1: Transform XML to XSL-FO
java -cp saxon9he.jar net.sf.saxon.Transform \
    -s:input.xml -xsl:xml-to-fo.xsl -o:output.fo

# Step 2: Generate PDF from XSL-FO
java -cp fop.jar:lib/* org.apache.fop.cli.Main \
    -fo output.fo -pdf output.pdf

# Combined one-liner
java -cp saxon9he.jar net.sf.saxon.Transform \
    -s:input.xml -xsl:xml-to-fo.xsl \
    | java -cp fop.jar:lib/* org.apache.fop.cli.Main \
    -fo - -pdf output.pdf

Common XSL-FO Mistakes

1. Not Setting white-space for Code Blocks

Without white-space="pre" and linefeed-treatment="preserve", code blocks collapse whitespace and lose indentation and line breaks.

2. Missing Column Definitions

Tables without <fo:table-column> widths default to equal-width columns, which rarely produces the desired layout. Always specify column widths.

3. Overlapping Regions

If <fo:region-body> margin-top overlaps with <fo:region-before> extent, content collides. Ensure region extents are within the body margin.

4. Forgetting keep-with-next on Headings

Headings that appear alone at the bottom of a page confuse readers. Add keep-with-next="always" to headings to keep them with their following paragraph.

5. Incorrect Namespace URI

The XSL-FO namespace is http://www.w3.org/1999/XSL/Format. The xsl prefix is for XSLT (http://www.w3.org/1999/XSL/Transform). Confusing them causes rendering failures.

6. Using Unsupportable CSS Properties

XSL-FO is not CSS. Properties like display, margin, or padding don't work. Use XSL-FO equivalents: space-before, space-after, padding, border.

7. Page Number References Across Sequences

Using <fo:page-number-citation> to reference page numbers in different page sequences requires careful setup. Ensure the referenced ID is unique and accessible.

Practice Questions

1. What namespace does XSL-FO use? http://www.w3.org/1999/XSL/Format. The prefix fo: is conventional.

2. How do you create a multi-column layout in XSL-FO? Use <fo:region-body column-count="2" column-gap="0.25in"/> on the page master, or use <fo:block span="all"> for full-width elements.

3. What is the difference between fo:region-body, fo:region-before, fo:region-after? region-body is the main content area. region-before is the top/header area. region-after is the bottom/footer area. Each has its own flow name.

4. How do you handle page breaks in XSL-FO? Use page-break-before="always", page-break-after="always", or page-break-inside="avoid" on blocks to control pagination.

5. Challenge: Design an XSL-FO template for a product catalog with a cover page, table of contents, product listings with images and prices, and page numbers. Answer: Use a page sequence master with cover (first), table of contents (roman numerals), and body (arabic numerals) page masters. Apply XSLT templates for each section. Use keep-with-next for headings and avoid orphans/widows with widows="2" orphans="2".

Mini Project: XML Invoice to PDF

<?xml version="1.0" encoding="UTF-8"?>
<invoice number="INV-2026-001" date="2026-06-20">
    <bill-to>
        <name>Acme Corp</name>
        <address>123 Main St, City</address>
    </bill-to>
    <items>
        <item>
            <description>DodaZIP Enterprise License</description>
            <quantity>5</quantity>
            <unit-price>299.00</unit-price>
        </item>
        <item>
            <description>Durga Antivirus Pro - 1yr</description>
            <quantity>10</quantity>
            <unit-price>49.99</unit-price>
        </item>
    </items>
</invoice>

XSL-FO output:

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

    <xsl:template match="/">
        <fo:root>
            <fo:layout-master-set>
                <fo:simple-page-master master-name="invoice"
                    page-width="8.5in" page-height="11in"
                    margin="0.75in">
                    <fo:region-body/>
                </fo:simple-page-master>
            </fo:layout-master-set>

            <fo:page-sequence master-reference="invoice">
                <fo:flow flow-name="xsl-region-body">
                    <!-- Header -->
                    <fo:block font-size="24pt" font-weight="bold"
                        color="#1a237e" space-after="4pt">
                        INVOICE
                    </fo:block>
                    <fo:block font-size="10pt" color="#666"
                        space-after="12pt">
                        Number: <xsl:value-of select="invoice/@number"/>
                        | Date: <xsl:value-of select="invoice/@date"/>
                    </fo:block>

                    <!-- Bill To -->
                    <fo:block font-weight="bold"
                        space-before="12pt" space-after="4pt">
                        Bill To:
                    </fo:block>
                    <fo:block>
                        <xsl:value-of select="invoice/bill-to/name"/>
                    </fo:block>
                    <fo:block space-after="12pt">
                        <xsl:value-of select="invoice/bill-to/address"/>
                    </fo:block>

                    <!-- Items Table -->
                    <fo:table width="100%" border-collapse="collapse">
                        <fo:table-column column-width="60%"/>
                        <fo:table-column column-width="15%"
                            text-align="center"/>
                        <fo:table-column column-width="25%"
                            text-align="right"/>

                        <fo:table-header font-weight="bold"
                            background-color="#1a237e" color="white">
                            <fo:table-row>
                                <fo:table-cell padding="6pt"
                                    border="0.5pt solid #1a237e">
                                    <fo:block>Description</fo:block>
                                </fo:table-cell>
                                <fo:table-cell padding="6pt"
                                    border="0.5pt solid #1a237e">
                                    <fo:block>Qty</fo:block>
                                </fo:table-cell>
                                <fo:table-cell padding="6pt"
                                    border="0.5pt solid #1a237e">
                                    <fo:block>Amount</fo:block>
                                </fo:table-cell>
                            </fo:table-row>
                        </fo:table-header>

                        <fo:table-body>
                            <xsl:for-each select="invoice/items/item">
                                <fo:table-row>
                                    <fo:table-cell padding="4pt"
                                        border="0.5pt solid #ccc">
                                        <fo:block>
                                            <xsl:value-of
                                                select="description"/>
                                        </fo:block>
                                    </fo:table-cell>
                                    <fo:table-cell padding="4pt"
                                        border="0.5pt solid #ccc"
                                        text-align="center">
                                        <fo:block>
                                            <xsl:value-of
                                                select="quantity"/>
                                        </fo:block>
                                    </fo:table-cell>
                                    <fo:table-cell padding="4pt"
                                        border="0.5pt solid #ccc"
                                        text-align="right">
                                        <fo:block>
                                            $<xsl:value-of
                                                select="format-number(
                                                    quantity * unit-price,
                                                    '#,##0.00')"/>
                                        </fo:block>
                                    </fo:table-cell>
                                </fo:table-row>
                            </xsl:for-each>

                            <!-- Total row -->
                            <fo:table-row font-weight="bold">
                                <fo:table-cell padding="4pt"
                                    border="0.5pt solid #ccc">
                                    <fo:block>Total</fo:block>
                                </fo:table-cell>
                                <fo:table-cell padding="4pt"
                                    border="0.5pt solid #ccc"/>
                                <fo:table-cell padding="4pt"
                                    border="0.5pt solid #ccc"
                                    text-align="right">
                                    <fo:block font-size="14pt">
                                        $<xsl:value-of
                                            select="format-number(
                                                sum(invoice/items/item/
                                                    quantity *
                                                    unit-price),
                                                '#,##0.00')"/>
                                    </fo:block>
                                </fo:table-cell>
                            </fo:table-row>
                        </fo:table-body>
                    </fo:table>

                    <!-- Footer -->
                    <fo:block text-align="center"
                        space-before="24pt" font-size="8pt"
                        color="#999">
                        Generated by DodaTech Publishing System
                    </fo:block>
                </fo:flow>
            </fo:page-sequence>
        </fo:root>
    </xsl:template>
</xsl:stylesheet>

FAQ

What's the difference between XSL-FO and CSS for print?

XSL-FO is purpose-built for paginated media (PDF, print). CSS Paged Media is newer and simpler but less powerful. XSL-FO handles complex layouts (running headers/footers, page-number citations, table spanning better than CSS).

What tools support XSL-FO?

Apache FOP (free), Antenna House Formatter, RenderX XEP, PrinceXML (CSS-based but handles FO). Apache FOP is the most widely used open-source option.

Can I convert HTML to XSL-FO?

Yes — tools like html2fo exist, but HTML-to-FO conversion is lossy due to fundamental differences between flowing HTML and paginated FO. It's easier to start from XML with XSLT.

Is XSL-FO still relevant in 2026?

Yes — for enterprise document generation (invoices, reports, statements, catalogs) where paginated PDF output is required and HTML-to-PDF rendering is insufficient.

How do I handle images in XSL-FO?

Use <fo:external-graphic src="url('image.png')" content-width="6in"/>. For SVG, use <fo:instream-foreign-object> with the SVG namespace.

Can I embed fonts in the PDF?

Yes — use Apache FOP's font configuration. Register TTF or OTF fonts in a fop.xconf file and reference them by family name in the FO.

What's Next

XML Digital Signatures
Advanced XSLT
XML Namespaces

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-20.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro