Skip to content

Commit

Permalink
:graphics can .setFont with system fonts. With tests and updated docu…
Browse files Browse the repository at this point in the history
…mentation. (#140)

* Slow but working font-aware g2d.

* Improved speed with a cache.  Intialized same as FontFactory.

* g2d-register-fonts now does recursive directory descent if requested.

* updated readme and example

* Starting on creating test file for setFont

* Tests broken down by OS: Ubuntu, MacOS, Windows

* Fixed g2d font subdirectory registration. Tweaked README.md comment for readability.

* Tweaked test for cross-platform check.

* Simplified graphics-2d test. Uses included font.

* Cross-platform graphics-2d test work MacOS Ubuntu

* Updated README.md to match output of example.clj.

* Made g2d-register-fonts accept optional font directory, per yogthos' recommend.
  • Loading branch information
benzenwen authored and yogthos committed Jul 13, 2017
1 parent e5fbfb2 commit 8c51b2d
Show file tree
Hide file tree
Showing 8 changed files with 400 additions and 200 deletions.
420 changes: 223 additions & 197 deletions README.md

Large diffs are not rendered by default.

Binary file modified example.pdf
Binary file not shown.
2 changes: 1 addition & 1 deletion project.clj
@@ -1,4 +1,4 @@
(defproject clj-pdf "2.2.27"
(defproject clj-pdf "2.2.28-SNAPSHOT"
:description "PDF generation library"
:url "https://github.com/yogthos/clj-pdf"

Expand Down
1 change: 1 addition & 0 deletions src/clj/clj_pdf/core.clj
Expand Up @@ -1164,6 +1164,7 @@
(nil? @fonts-registered?))
; register fonts in usual directories
(FontFactory/registerDirectories)
(g2d/g2d-register-fonts)
(reset! fonts-registered? true)))

(defn- parse-meta [doc-meta]
Expand Down
64 changes: 63 additions & 1 deletion src/clj/clj_pdf/graphics_2d.clj
@@ -1,11 +1,17 @@
(ns clj-pdf.graphics-2d
(:require [clojure.java.io :as io]
[clojure.string :as str])
(:import
[java.awt Graphics2D]
[cljpdf.text.pdf DefaultFontMapper PdfWriter]
cljpdf.text.Rectangle))

(declare g2d-register-fonts)
(def g2d-fonts-registered? (atom nil))
(def default-font-mapper (DefaultFontMapper.))

(defn with-graphics [{:keys [^PdfWriter pdf-writer page-width page-height font-mapper under translate rotate scale] :as meta} f]
(let [font-mapper (or font-mapper (DefaultFontMapper.))
(let [font-mapper (or font-mapper default-font-mapper)
template (if under
(.getDirectContentUnder pdf-writer)
(.getDirectContent pdf-writer))
Expand All @@ -26,3 +32,59 @@
(Rectangle. 0.0 0.0) ; choose a better placeholder?
(finally
(.dispose g2d)))))

(def common-font-dirs
[["/Library/Fonts", true]
["/System/Library/Fonts", false]
["c:/windows/fonts", false]
["c:/winnt/fonts", false]
["d:/windows/fonts", false]
["d:/winnt/fonts", false]
["/usr/share/X11/fonts", true]
["/usr/X/lib/X11/fonts", true]
["/usr/openwin/lib/X11/fonts", true]
["/usr/share/fonts", true]
["/usr/X11R6/lib/X11/fonts", true]])

(defn- full-path [parent filename]
(str/join [parent java.io.File/separator filename]))

(defn g2d-register-fonts-helper [dir recursive]
(.insertDirectory default-font-mapper dir)
(if recursive
(let [fd (io/file dir)
files (.list fd)
paths (map #(full-path dir %) files)]
(doseq [path paths]
(if (.isDirectory (io/file path))
(g2d-register-fonts-helper
path recursive))))))

(defn g2d-register-fonts
"Walks common font directories and registers them for use. Optionally
accepts a coll of absolute directories to register each with a
subdirectory walk directive, in the form of 'common-font-dirs'. Eval
with custom fonts solely to override system fonts. Eval empty first
then again with custom fonts to augment system fonts."
[& [font-dirs]]
;; This is guarded by a stateful atom in addition to core/fonts-registered?
;; because get-font-maps can also trigger a g2d-register-fonts. For a big
;; font library, registration can be slow.
(when (or (nil? @g2d-fonts-registered?)
font-dirs)
(doseq [[dir recursive] (or font-dirs common-font-dirs)]
(g2d-register-fonts-helper dir recursive))
(reset! g2d-fonts-registered? true)))

;;; Utility functions

(defn get-font-maps
"Returns a map with :mapper and :aliases keys, each with a Java HashMap as a val.
:mapper connects available AWT font names each to a PDF font object.
:aliases connects alternate names each to a AWT font name.
Names (strings) in either HashMap can be used in a :graphics element's .setFont directive.
Will register common system font directories if not already registered."
[]
(g2d-register-fonts)
{:mapper (.getMapper default-font-mapper)
:aliases (.getAliases default-font-mapper)})
16 changes: 15 additions & 1 deletion test/clj_pdf/test/example.clj
Expand Up @@ -117,11 +117,25 @@
:target "http://www.curiousattemptbunny.com/2009/01/simple-clojure-graphics-api.html"}
"http://www.curiousattemptbunny.com/2009/01/simple-clojure-graphics-api.html"]]

[:graphics {:under false :translate [53 120]}
(fn [g2d]
(doto g2d
(.setColor Color/BLACK)
(.setFont (java.awt.Font. "SansSerif" java.awt.Font/BOLD 20))
(.drawString ":graphics Drawing" (float 0) (float 0))))]

[:graphics {:translate [150 300] :rotate (radians -90)}
(fn [g2d]
(.setColor g2d Color/GREEN)
(draw-tree g2d 50 10))]

[:graphics {:under false :translate [70 270] :rotate (radians -35)}
(fn [g2d]
(doto g2d
(.setColor (java.awt.Color. 96 96 96))
(.setFont (java.awt.Font. "Serif" java.awt.Font/PLAIN 14))
(.drawString "drawString with setFont and rotate" (float 0) (float 0))))]

[:chart {:type :pie-chart
:title "Vector Pie"
:vector true
Expand Down Expand Up @@ -149,7 +163,7 @@
:target "https://en.wikipedia.org/wiki/File:Example.svg"}
"https://en.wikipedia.org/wiki/File:Example.svg"]]

[:svg {:under true :translate [0 270] :scale 0.95}
[:svg {:under true :translate [0 200] :scale 0.95}
(clojure.java.io/file "test/Example.svg")]

[:pagebreak]
Expand Down
97 changes: 97 additions & 0 deletions test/clj_pdf/test/graphics_2d.clj
@@ -0,0 +1,97 @@
(ns clj-pdf.test.graphics-2d
(:use clj-pdf.core clj-pdf.graphics-2d clojure.test clojure.java.io)
(:require [clojure.string :as re])
(:import [java.awt Font Graphics2D]
[cljpdf.text.pdf DefaultFontMapper]
[cljpdf.text FontFactory]))

;; Utilities for the test. Is there a better place to put these shared defns?

(defn add-test-path-prefix [filename]
(str "test" java.io.File/separator filename))

(defn fix-pdf [^bytes pdf-bytes]
(-> (String. pdf-bytes)
; obviously these will get changed on each run, so strip the creation/modification date/times
(re/replace #"ModDate\((.*?)\)" "")
(re/replace #"CreationDate\((.*?)\)" "")
; not sure what this is for ... ?
(re/replace #"\[(.*?)\]" "")
; these are kind of hacky, but it seems that the prefix characters before the font name "Carlito"
; will get randomly generated on each run ... ?
(re/replace #"Font\/([A-Z]+\+Carlito)" "Font/SLDHIE+Carlito")
(re/replace #"FontBBox\/([A-Z]+\+Carlito)" "FontBBox/SLDHIE+Carlito")
(re/replace #"FontName\/([A-Z]+\+Carlito)" "FontName/SLDHIE+Carlito")
(re/replace #"BaseFont\/([A-Z]+\+Carlito)" "BaseFont/SLDHIE+Carlito")))

(defn read-file ^bytes [file-path]
(with-open [reader (input-stream file-path)]
(let [length (.length (file file-path))
buffer (byte-array length)]
(.read reader buffer 0 length)
buffer)))

(defn pdf->bytes ^bytes [doc]
(let [out (new java.io.ByteArrayOutputStream)]
(pdf doc out)
(.toByteArray out)))

;; MODIFIED from clj_pdf/test/core.clj. Pulled out font-filename from let binding to use later.
(def font-filename (add-test-path-prefix "Carlito-Regular.ttf"))
(defn inject-test-font [doc]
(let [font-props {:encoding :unicode
:ttf-name font-filename}]
(update-in (vec doc) [0 :font] merge font-props)))

(defn generate-pdf [doc output-filename]
(let [doc1 (inject-test-font doc)]
(println "regenerating pdf" output-filename)
(pdf doc1 (add-test-path-prefix output-filename))
true))

(defn eq? [doc1 filename]
(let [filename (add-test-path-prefix filename)
doc1 (inject-test-font doc1)
doc1-bytes (pdf->bytes doc1)
doc2-bytes (read-file filename)]
(is (= (fix-pdf doc1-bytes)
(fix-pdf doc2-bytes)))))

(defn regenerate-test-pdfs []
(with-redefs [eq? generate-pdf]
(run-tests)))

; run this to regenerate all test pdfs
#_(regenerate-test-pdfs)

#_(run-tests)

(def test-directory (.getAbsolutePath (file "test")))

(g2d-register-fonts [[test-directory true]])

(def font-file (clojure.java.io/file font-filename))
(def AWT-CARLITO-BASE (java.awt.Font/createFont java.awt.Font/TRUETYPE_FONT font-file))
(def AWT-CARLITO (.deriveFont AWT-CARLITO-BASE (float 18)))

(deftest setFont
(eq?
[{:title "Graphics setFont test doc"
:left-margin 10
:right-margin 50
:top-margin 20
:bottom-margin 25
:font {:size 12}
:size :a4
:orientation "portrait"
:register-system-fonts? true}
[:chapter "Graphics2D tests"]
[:heading "setFont"]
[:paragraph "This test substitutes a single font, Carlito, for all system fonts. Carlito is an open source font. Carlito-Regular.ttf is included in this repo. Usage note: evaluate (clj-pdf.graphics2d/get-font-maps) to see available system fonts and their exact names. In a pinch, the Java default font names are: Serif, SansSerif, Monospaced, Dialog, and DialogInput."]
[:paragraph "The font system for Graphics2D, invoked with the :graphics tag, is different than that used in the rest of clj-pdf. Enabling ':register-system-fonts? true' in the document metadata will also register system fonts for use with Graphics2D's .setFont."]
[:paragraph "The \"a\" below is drawn using a :graphics tag and (.drawString...). As such, it can be placed arbitrarily (and could also be rotated). It should be set in Carlito, and have an 18 point font size. Unfortunately, there are visually undetectable variations in output between systems that prevent more text from being rendered while allowing for automated testing. Hence the short string."]
[:graphics {:under false :translate [70 350]}
(fn [g2d]
(.setFont g2d AWT-CARLITO)
(.drawString g2d "a" (float 0) (float 0)))]]
"graphics.pdf"))
Binary file added test/graphics.pdf
Binary file not shown.

0 comments on commit 8c51b2d

Please sign in to comment.