Permalink
Browse files

Merge pull request #2 from fmw/master

support for generating documents with Apache FOP.

This is the first initial step showing how Apache FOP integration/abstraction could occur.
  • Loading branch information...
KushalP committed May 3, 2012
2 parents 406d031 + 249a943 commit e446b2648ecc58aaf4b936506f4e3b03ba40a0bc
Showing with 934 additions and 1 deletion.
  1. +3 −1 project.clj
  2. +104 −0 resources/fo/dummy-invoice.fo
  3. +208 −0 src/camelot/fo.clj
  4. +619 −0 test/camelot/test/fo.clj
View
@@ -4,5 +4,7 @@
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.4.0"]
- [org.apache.pdfbox/pdfbox "1.6.0"]]
+ [org.clojure/data.xml "0.0.3"]
+ [org.apache.pdfbox/pdfbox "1.6.0"]
+ [org.apache.xmlgraphics/fop "1.0"]]
:dev-dependencies [[lein-clojars "0.8.0"]])
@@ -0,0 +1,104 @@
+<?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="first" margin-right="1.5cm" margin-left="1.5cm" margin-bottom="2cm" margin-top="1cm" page-width="211mm" page-height="297mm">
+ <fo:region-body margin-top="0cm"/>
+ <fo:region-before extent="1cm"/>
+ <fo:region-after extent="1.5cm"/>
+ </fo:simple-page-master>
+ </fo:layout-master-set>
+ <fo:page-sequence master-reference="first">
+ <fo:static-content flow-name="xsl-region-before">
+ <fo:block font-size="10pt" line-height="14pt" text-align="end">Telephone: +31 (0)6 48012240</fo:block>
+ <fo:block font-size="10pt" line-height="14pt" text-align="end">Street: IJskelderstraat 30</fo:block>
+ <fo:block font-size="10pt" line-height="14pt" text-align="end">Postal code: 5046 NK</fo:block>
+ <fo:block font-size="10pt" line-height="14pt" text-align="end">City: Tilburg</fo:block>
+ <fo:block font-size="10pt" line-height="14pt" text-align="end">Chamber of Commerce: 18068751</fo:block>
+ <fo:block font-size="10pt" line-height="14pt" text-align="end">VAT: 1903.14.849.B01</fo:block>
+ <fo:block font-size="10pt" line-height="14pt" text-align="end">Bank: Rabobank 3285.04.165</fo:block>
+ </fo:static-content>
+ <fo:static-content flow-name="xsl-region-after">
+ <fo:block text-align="left" font-size="10pt" line-height="14pt"/>
+ </fo:static-content>
+ <fo:flow flow-name="xsl-region-body">
+ <fo:block text-align="left" line-height="14pt" font-size="35pt" space-after.optimum="15pt" space-before.optimum="0pt">Vixu.com</fo:block>
+ <fo:block text-align="left" font-size="10pt" line-height="14pt" space-after.optimum="20pt" space-before.optimum="100pt">
+ <fo:block text-align="left" font-size="10pt" line-height="14pt">BigCo</fo:block>
+ <fo:block text-align="left" font-size="10pt" line-height="14pt">Mr. John Doe</fo:block>
+ <fo:block text-align="left" font-size="10pt" line-height="14pt">Harteveltstraat 1</fo:block>
+ <fo:block text-align="left" font-size="10pt" line-height="14pt">2586 EL Den Haag</fo:block>
+ <fo:block text-align="left" font-size="10pt" line-height="14pt">The Netherlands</fo:block>
+ </fo:block>
+ <fo:block text-align="left" line-height="14pt" font-weight="bold" font-size="16" space-after.optimum="20pt">Invoice</fo:block>
+ <fo:table border-width="0.5pt" space-after.optimum="30pt">
+ <fo:table-column column-width="4cm"/>
+ <fo:table-column column-width="5cm"/>
+ <fo:table-body>
+ <fo:table-row border-width="0.5pt" keep-with-next="always">
+ <fo:table-cell>
+ <fo:block text-align="left" font-size="10pt" line-height="14pt">Date:</fo:block>
+ </fo:table-cell>
+ <fo:table-cell>
+ <fo:block text-align="left" font-size="10pt" line-height="14pt">05/02/2012</fo:block>
+ </fo:table-cell>
+ </fo:table-row>
+ <fo:table-row>
+ <fo:table-cell>
+ <fo:block text-align="left" font-size="10pt" line-height="14pt">Invoice number:</fo:block>
+ </fo:table-cell>
+ <fo:table-cell>
+ <fo:block text-align="left" font-size="10pt" line-height="14pt">BAZ01</fo:block>
+ </fo:table-cell>
+ </fo:table-row>
+ </fo:table-body>
+ </fo:table>
+ <fo:table border-width="0.5pt">
+ <fo:table-column column-width="14cm"/>
+ <fo:table-column column-width="3cm"/>
+ <fo:table-body>
+ <fo:table-row border-width="0.5pt" keep-with-next="always">
+ <fo:table-cell>
+ <fo:block text-align="left" font-size="10pt" line-height="14pt">Vixu.com basic subscription from March 1st 2012 to March 1st 2013:</fo:block>
+ </fo:table-cell>
+ <fo:table-cell>
+ <fo:block font-size="10pt" line-height="14pt" text-align="right">€ 1188.00</fo:block>
+ </fo:table-cell>
+ </fo:table-row>
+ <fo:table-row>
+ <fo:table-cell>
+ <fo:block text-align="left" font-size="10pt" line-height="14pt">Discount (10%):</fo:block>
+ </fo:table-cell>
+ <fo:table-cell>
+ <fo:block font-size="10pt" line-height="14pt" text-align="right">- € 118.80</fo:block>
+ </fo:table-cell>
+ </fo:table-row>
+ <fo:table-row>
+ <fo:table-cell>
+ <fo:block text-align="left" font-size="10pt" line-height="14pt">Subtotal:</fo:block>
+ </fo:table-cell>
+ <fo:table-cell>
+ <fo:block font-size="10pt" line-height="14pt" text-align="right">€ 1070.00</fo:block>
+ </fo:table-cell>
+ </fo:table-row>
+ <fo:table-row>
+ <fo:table-cell>
+ <fo:block text-align="left" font-size="10pt" line-height="14pt">Value Added Tax (19%):</fo:block>
+ </fo:table-cell>
+ <fo:table-cell>
+ <fo:block font-size="10pt" line-height="14pt" text-align="right">€ 203.30</fo:block>
+ </fo:table-cell>
+ </fo:table-row>
+ <fo:table-row font-weight="bold">
+ <fo:table-cell>
+ <fo:block text-align="left" font-size="10pt" line-height="14pt">Total:</fo:block>
+ </fo:table-cell>
+ <fo:table-cell>
+ <fo:block font-size="10pt" line-height="14pt" text-align="right">€ 1273.30</fo:block>
+ </fo:table-cell>
+ </fo:table-row>
+ </fo:table-body>
+ </fo:table>
+ <fo:block text-align="left" font-size="10pt" line-height="14pt" space-before.optimum="35pt">You are kindly requested to pay within 7 days. Please wire the amount due to Rabobank account number 3285.04.165.</fo:block>
+ </fo:flow>
+ </fo:page-sequence>
+</fo:root>
View
@@ -0,0 +1,208 @@
+;; src/camelot/fo.clj: Apache FOP support for Camelot
+;; Copyright 2012, F.M. (Filip) de Waard <fmw@vixu.com>.
+
+(ns camelot.fo
+ (:require [clojure.data.xml :as xml])
+ (:import [java.io File BufferedOutputStream FileOutputStream StringReader]
+ [org.apache.fop.apps FopFactory Fop MimeConstants]
+ [javax.xml.transform Transformer TransformerFactory]
+ [javax.xml.transform.stream StreamSource]
+ [javax.xml.transform.sax SAXResult]))
+
+(defn layout
+ "Defines a :fo:layout-master-set using the given attribute map
+ (i.e. :margin-right, :margin-left, :margin-bottom :margin-top,
+ :page-width, :page-height, region-before, region-body, region-after;
+ the last three being attribute maps for subnodes)."
+ [{:keys [margin-right
+ margin-left
+ margin-bottom
+ margin-top
+ page-width
+ page-height
+ region-body
+ region-before
+ region-after]}]
+ (xml/element
+ :fo:layout-master-set
+ {}
+ (xml/element
+ :fo:simple-page-master
+ {:master-name "first"
+ :margin-right (or margin-right "1.5cm")
+ :margin-left (or margin-left "1.5cm")
+ :margin-bottom (or margin-bottom "2cm")
+ :margin-top (or margin-top "1cm")
+ :page-width (or page-width "211mm")
+ :page-height (or page-height "297mm")}
+ (xml/element
+ :fo:region-body
+ (if (map? region-body)
+ region-body
+ {:margin-top "0cm"}))
+ (xml/element
+ :fo:region-before
+ (if (map? region-before)
+ region-before
+ {:extent "1cm"}))
+ (xml/element
+ :fo:region-after
+ (if (map? region-after)
+ region-after
+ {:extent "1.5cm"})))))
+
+(defn block?
+ "Returns true if the provided argument is a block"
+ [x]
+ (and (= (class x) clojure.data.xml.Element) (= (:tag x) :fo:block)))
+
+(defn block
+ "Defines a fo:block with the given attributes (optional) and
+ content (which is a string or a sequence of blocks)..
+
+ E.g. <fo:block font-size=\"10pt\"
+ line-height=\"14pt\"
+ text-align=\"end\">
+ Hic sunt dracones
+ </fo:block>"
+ ([content]
+ (block {} content))
+ ([attrs content]
+ (let [new-attrs (assoc attrs
+ :line-height (or (:line-height attrs) "14pt")
+ :font-size (or (:font-size attrs) "10pt")
+ :text-align (or (:text-align attrs) "left"))]
+ (cond
+ (string? content)
+ (xml/element :fo:block new-attrs content)
+ (every? block? content)
+ (apply (partial xml/element :fo:block new-attrs) content)))))
+
+(defn table-cell
+ "Defines a fo:table-cell with either a provided string
+ that returns a cell with default formatting or a (block)
+ with custom formatting."
+ [value]
+ (xml/element :fo:table-cell
+ {}
+ (cond
+ (string? value)
+ (block value)
+ (block? value)
+ value)))
+
+(defn table-row-has-attributes?
+ "Returns true if the provided sequence starts with a map
+ that isn't a block."
+ [row]
+ (and (map? (first row)) (not (block? (first row)))))
+
+(defn table-row
+ "Defines a table row with an optional first argument
+ containing an argument map, followed by the cell values."
+ [& row]
+ (let [has-attrs? (table-row-has-attributes? row)
+ attrs (if has-attrs?
+ (first row)
+ {})
+ values (if has-attrs?
+ (rest row)
+ row)]
+ (apply (partial xml/element :fo:table-row attrs)
+ (map table-cell values))))
+
+(defn table-column
+ "Defines a table column with the provided attributes (optional)
+ and width."
+ ([width]
+ (table-column {} width))
+ ([attrs width]
+ (xml/element :fo:table-column
+ (assoc attrs
+ :column-width width)
+ nil)))
+
+(defn table
+ "Defines a table with rows generated from a provided
+ sequence of vectors representing the individual rows.
+
+ If there is no attribute map provided for the first row
+ border-width=\"0.5pt\" and keep-with-next=\"always\"
+ are used by default for this row"
+ ([columns rows]
+ (table {} columns rows))
+ ([attrs columns rows]
+ (apply (partial xml/element
+ :fo:table
+ (if (not-empty attrs)
+ attrs
+ {:border-width "0.5pt"}))
+ (conj
+ (vec (map table-column columns))
+ (apply
+ (partial xml/element
+ :fo:table-body
+ {})
+ (map-indexed (fn [i row]
+ (apply
+ table-row
+ (if (and (= i 0)
+ (not (table-row-has-attributes? row)))
+ (cons {:border-width "0.5pt"
+ :keep-with-next "always"}
+ row)
+ row)))
+ rows))))))
+
+(defn region [type name & blocks]
+ "Defines a fo:static-content region with the given
+ type (:static-content or :flow) and name that contains
+ the given blocks.
+
+ E.g. <fo:static-content flow-name=\"xsl-region-before\">
+ <fo:block font-size=\"10pt\"
+ line-height=\"14pt\"
+ text-align=\"end\">Hic sunt dracones</fo:block>
+ </fo:static-content>"
+ (apply
+ (partial xml/element
+ (cond
+ (= type :static-content)
+ :fo:static-content
+ (= type :flow)
+ :fo:flow)
+ {:flow-name name})
+ blocks))
+
+(def header
+ (partial region :static-content "xsl-region-before"))
+
+(def footer
+ (partial region :static-content "xsl-region-after"))
+
+(def body
+ (partial region :flow "xsl-region-body"))
+
+(defn document
+ "Constructs a document with the :header-blocks and :footer-blocks
+ from the provided settings map and all provided blocks."
+ [settings & body-blocks]
+ (xml/element
+ :fo:root
+ {:xmlns:fo "http://www.w3.org/1999/XSL/Format"}
+ (layout (or (:layout settings) {}))
+ (xml/element
+ :fo:page-sequence {:master-reference "first"}
+ (apply header (or (:header-blocks settings) [(block "")]))
+ (apply footer (or (:footer-blocks settings) [(block "")]))
+ (apply body body-blocks))))
+
+(defn write-pdf!
+ "Writes the provided document to-file as a PDF document."
+ [document to-file]
+ (with-open [out (BufferedOutputStream. (FileOutputStream. to-file))]
+ (.transform (.newTransformer (TransformerFactory/newInstance))
+ (StreamSource. (StringReader. (xml/emit-str document)))
+ (SAXResult. (.getDefaultHandler
+ (.newFop (FopFactory/newInstance)
+ MimeConstants/MIME_PDF out))))))
Oops, something went wrong.

0 comments on commit e446b26

Please sign in to comment.