一些基本操作
第一章的內容是介紹了一些基礎的內容,本章介紹的內容則是一些更底層的東西。到後面的章節會涉及操作現有pdf的內容,希望大家耐心等待。
當我們談論iText文檔裏面底層(low-level)的內容的時候,我們會參考被寫入PDF官方文檔裏面的那些PDF語法。PDF定義的一系列的操作在iText中都有對應,例如m
操作對應moveTo()
方法,l
操作對應lineTo()
方法,S
操作對應stroke()
方法等等。通過這些方法,我們可以畫出路徑和形狀。
我們來看下面這個簡單的例子:
-406 0 m
406 0 l
S
這段在pdf中的語法的含義就是:
- 移動到座標(-406,0)
- 然後從(-406,0)畫一條線到(406,0),構成一條路徑
- 最後畫出這一條線,stroke()對應的就是畫出路徑
這段對應iText裏面的操作就是這樣的:
canvas.moveTo(-406, 0)
.lineTo(406, 0)
.stroke();
乍一看很簡單,但是canvas
這個對象是什麼呢?讓我們通過幾個例子來找尋答案。
在pdf畫座標系
在畫布(canvas)上畫線
假如我們要創建如下圖1的pdf:
這個例子在pdf上面畫出了X和Y座標系。讓我們一步一步地解釋這其中的過程:
PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
PageSize ps = PageSize.A4.rotate();
PdfPage page = pdf.addNewPage(ps);
PdfCanvas canvas = new PdfCanvas(page);
// Draw the axes畫出座標系
pdf.close();
- 我們不再使用
Document
這個對象 - 就像之前第一章所講得一樣,,我們創建了
PdfWriter
和PdfDocument
對象。 - 我們沒有像之前一樣創建頁面默認大小的
Document
對象,而是創建了特定PageSize
的PdfPage
- 頁面是A4大小,旋轉過後變成了橫向
- 創建完
PdfPage
以後,我們使用它創建了PdfCanvas
- 在
PdfCanvas
進行了一系列的操作,完成我們的座標系的繪畫 - 最後我們,我們想要在頁面(page)中添加我們所繪畫的內容,只需關閉
PdfDocument
對象即可
在之前的章節中,我們使用
document.close()
關閉Document
對象,這個操作其實暗地裏也關閉了PdfDocument
對象。現在這裏不再有Document
對象,所以我們必須手動關閉PdfDocument
在PDF中,所有測量都以用戶單位(user unit)完成。 默認情況下,一個用戶單位對應一個點。 這意味着在一英寸(one inch)內有72個用戶單位。 在PDF中,X軸指向右側,Y軸指向上。 如果您使用PageSize
對象創建頁面大小,則座標系的原點位於頁面的左下角。 我們用作操作符(如m或l操作)的操作數的所有座標都使用此座標系。 我們可以通過改變當前的變換矩陣來改變座標系。
座標系統和變化矩陣
如果你上過幾何學的相關課程,那你應該就會知道我們可以通過一個變化矩陣來作用於對象,可以使之進行平移、旋轉、縮放等操作。 假設我們要移動座標系,使座標系的原點位於頁面正中間。在這種情況下,我們需要使用concatMatrix()
方法的參數是:
canvas.concatMatrix(1, 0, 0, 1, ps.getWidth() / 2, ps.getHeight() / 2);
concatMatrix()
方法的參數是一個3*3的變換矩陣:
a b 0
c d 0
e f 1
在這裏,這個矩陣的第三行的是固定值:(0,0,1),之所以是這樣是因爲我們是在二維裏面操作。而a
,b
,c
和d
的值在這裏可以用的值爲來旋轉、縮放和平移座標系。當然我們沒必要規定x軸必須水平,y軸垂直垂直,不過我們爲了簡單起見,我們就這麼規定,所以規定a
,b
,c
和d
的值爲1
,0
,0
和1
。e
和f
定義平移(translation)距離,在這裏我們平移大小分筆試ps
高度和寬度的1/2
圖像狀態(The graphics state)
上一節提到變化矩陣是Page
的圖像狀態之一,還有諸如線條寬度、畫線顏色、填充顏色等狀態。在接下來的章節中,我們會更加深入探討其他一些圖像狀態。在這裏我們只需知道:默認的線條寬度是1個用戶單位並且默認的線條顏色是黑色。下面代碼是畫上圖的過程:
//1.Draw X axis
canvas.moveTo(-(ps.getWidth() / 2 - 15), 0)
.lineTo(ps.getWidth() / 2 - 15, 0)
.stroke();
//2.Draw X axis arrow
canvas.setLineJoinStyle(PdfCanvasConstants.LineJoinStyle.ROUND)
.moveTo(ps.getWidth() / 2 - 25, -10)
.lineTo(ps.getWidth() / 2 - 15, 0)
.lineTo(ps.getWidth() / 2 - 25, 10).stroke()
.setLineJoinStyle(PdfCanvasConstants.LineJoinStyle.MITER);
//3.Draw Y axis
canvas.moveTo(0, -(ps.getHeight() / 2 - 15))
.lineTo(0, ps.getHeight() / 2 - 15)
.stroke();
//4.Draw Y axis arrow
canvas.saveState()
.setLineJoinStyle(PdfCanvasConstants.LineJoinStyle.ROUND)
.moveTo(-10, ps.getHeight() / 2 - 25)
.lineTo(0, ps.getHeight() / 2 - 15)
.lineTo(10, ps.getHeight() / 2 - 25).stroke()
.restoreState();
//5.Draw X serif
for (int i = -((int) ps.getWidth() / 2 - 61);
i < ((int) ps.getWidth() / 2 - 60); i += 40) {
canvas.moveTo(i, 5).lineTo(i, -5);
}
//6.Draw Y serif
for (int j = -((int) ps.getHeight() / 2 - 57);
j < ((int) ps.getHeight() / 2 - 56); j += 40) {
canvas.moveTo(5, j).lineTo(-5, j);
}
canvas.stroke();
這段代碼可以劃分如下幾部分:
- 1和3段代碼大家應該很熟悉啦,就是畫出x和y軸
- 第2段代碼是畫了箭頭,也就是兩條線連接在一起。交匯點的樣式有很多:1)斜接,兩線交接於一點 2)斜切,角是斜切的 3)圓形,角是圓的 我們通過調用一次
moveTo
和兩次lineTo
函數來構建路徑,最後我們把交匯點的樣式重置爲默認值,這樣不會影響之後的一些操作了,但是這並不是還原之前圖像狀態的最好的方法 - 第4段代碼向我們展示瞭如果我們要改變圖像狀態更好的操作方式:1)我們用
saveState()
方法來保存當前的圖像狀態 2)然後改變圖像狀態,畫線或者其他任意形狀 3)最後我們使用restoreState()
方法來還原原始的圖像狀態,所有在saveState()
之後的改變圖像狀態的操作都會撤銷,當你進行很多改變圖像狀態的操作時,這將會很有效 - 第5、6段代碼我們每個40個用戶單元就劃一個分隔符,值得注意的是,我們並沒有立馬就調用
stroke()
函數,而是等所有所有路徑都畫好以後,我們再調用函數來畫。
當然不止這麼一種方式在畫布上畫線和形狀,考慮到pdf生成的速度、生成文件的大小和在視圖裏面渲染的速度,我們很難討論上述這種方法是好還是壞的,這在我們以後章節會討論。
在這裏注意
saveState()
和restoreState()
必須成對出現,而且restoreState()
不能先於savaState()
添加網格線
現在我們在之前的圖片的基礎上,更改線的寬度,增加一個虛線,添加不同顏色的網格線,形成如下圖2所示:
在這個例子中,我們首先定義一系列的Color
對象:
Color grayColor = new DeviceCmyk(0.f, 0.f, 0.f, 0.875f);
Color greenColor = new DeviceCmyk(1.f, 0.f, 1.f, 0.176f);
Color blueColor = new DeviceCmyk(1.f, 0.156f, 0.f, 0.118f);
在PDF官方文檔(ISO-32000) 中,定義了很多的顏色空間,不同的顏色空間在iText中對應着不同的class
,最常用的顏色空間是DeviceGray
(灰度空間,只需一個亮度參數),DeviceRgb
(RGB空間,有紅色、綠色和藍色決定)和DeviceCmyk
(印刷四色空間,由青色、品紅、黃色和黑色),在這個例子中,我們使用的是DeviceCmyk
空間。
注意,我們使用的不是來自
java.awt.Color
定義的顏色,而是來自iText的Color
的類,可以在com.itextpdf.kernel.color
包中找到。
如果我們想要畫藍色的網格線,可以使用如下代碼:
canvas.setLineWidth(0.5f).setStrokeColor(blueColor);
for (int i = -((int) ps.getHeight() / 2 - 57);
i < ((int) ps.getHeight() / 2 - 56); i += 40) {
canvas.moveTo(-(ps.getWidth() / 2 - 15), i)
.lineTo(ps.getWidth() / 2 - 15, i);
}
for (int j = -((int) ps.getWidth() / 2 - 61);
j < ((int) ps.getWidth() / 2 - 60); j += 40) {
canvas.moveTo(j, -(ps.getHeight() / 2 - 15))
.lineTo(j, ps.getHeight() / 2 - 15);
}
canvas.stroke();
在一開始,我們設置線條的寬度爲0.5用戶單位,接下來就是畫路線,最後調用stroke()
函數。
畫座標的話,我們只用之前的方法來畫,不過在畫之前,我們改變線條寬度和畫筆顏色。
canvas.setLineWidth(3).setStrokeColor(grayColor);
畫完座標系後,我們用2個用戶寬度來畫虛線:
canvas.setLineWidth(2).setStrokeColor(greenColor)
.setLineDash(10, 10, 8)
.moveTo(-(ps.getWidth() / 2 - 15), -(ps.getHeight() / 2 - 15))
.lineTo(ps.getWidth() / 2 - 15, ps.getHeight() / 2 - 15).stroke();
可以有很多變量來定義一個虛線(line dash),但是在本例中,我們只需三個變量,實線(dash)的長度爲10個用戶單位,實線之間的間隙爲10個用戶單位,相位(phase)爲8個用戶單位(相位是在實線模塊中定義實線的開始?這段翻譯不是特別清楚,原文:the phase is 8 user units —the phase defines the distance in the dash pattern to start the dash.)
我們可以試試在
PdfCanvas
中的其他方法,例如curveTo()
函數是用來畫曲線的,rectangle()
方法是用來畫矩形的,以及還有一些其他的方法。除了用stroke
方法來畫線以後,我們還可以用fill()
方法填充路徑。PdfCanvas
類中提供的方法遠遠多於java版本的PDF操作器,並且也提供了在PDF中沒有的構造路徑的方法,例如橢圓和圓形
下一節我們會討論一下圖像狀態中可以改變文字的絕對位置的那一部分
文字狀態
下圖3展示了星球大戰5帝國反擊戰的開頭的文字:
如果想要創建這樣一個pdf,最好的方式是創建不同對齊方式的Paragraph
對象,主題居中,內容左對齊,然後把Paragraph
對象添加到Document
中。在高級的api(high-level approach)中會把連續的文字分成很多段,當文字長度超過頁面寬度會引入換行符,當文字內容超出頁面高度時會換頁。
當然我們有更簡單方式,使用低級的方式(low-level approach),我們只要把文字分解成幾塊就行了:
List<String> text = new ArrayList();
text.add(" Episode V ");
text.add(" THE EMPIRE STRIKES BACK ");
text.add("It is a dark time for the");
text.add("Rebellion. Although the Death");
text.add("Star has been destroyed,");
text.add("Imperial troops have driven the");
text.add("Rebel forces from their hidden");
text.add("base and pursued them across");
text.add("the galaxy.");
text.add("Evading the dreaded Imperial");
text.add("Starfleet, a group of freedom");
text.add("fighters led by Luke Skywalker");
text.add("has established a new secret");
text.add("base on the remote ice world");
text.add("of Hoth...");
出於簡便的原因,我們把原來在左下角的座標系放到左上角來,然後我們使用beginText()
方法來創建文本對象,並改變文本的狀態:
canvas.concatMatrix(1, 0, 0, 1, 0, ps.getHeight());
canvas.beginText()
.setFontAndSize(PdfFontFactory.createFont(FontConstants.COURIER_BOLD), 14)
.setLeading(14 * 1.2f)
.moveText(70, -40);
- 我們改變文本狀態爲等寬粗體和改變字體大小爲14,這樣以後的字體都爲這個格式
- 間距爲字體大小的1.2倍
- 最後,我們向右移動70個用戶單位,向下移動40個用戶單位,也就是(70,-40)開始顯示文字
緊接着,我們一個一個遍歷text數組裏面的string,每個string另一起行,保持上述的間距,最後使用endText()
方法結束。
for (String s : text) {
//Add text and move to the next line
canvas.newlineShowText(s);
}
canvas.endText();
注意:我們使用
newlineShowText()
必須在beginText()
和endText()
之間,並且beginText()
—》endText()
之間順序不能打亂
炫酷的文字
先看看如下圖4的效果:
是不是很酷炫?其實很簡單的,我們一步一步來走:
canvas.rectangle(0, 0, ps.getWidth(), ps.getHeight())
.setColor(Color.BLACK, true)
.fill();
我們首先創建一個矩形,矩形的左下角的座標爲(0,0),寬度和高度爲頁面的寬度和高度,然後我們設置填充顏色爲黑色,當然我們可以使用setFillColor(Color.BLACK)
來設置填充顏色,但這裏我們使用更通用的setColor()
方法。setColor()
的第二個參數爲是否改變填充顏色,最後我們填充這個矩形就OK 了,然後就是文字部分了:
canvas.concatMatrix(1, 0, 0, 1, 0, ps.getHeight());
Color yellowColor = new DeviceCmyk(0.f, 0.0537f, 0.769f, 0.051f);
float lineHeight = 5;
float yOffset = -40;
canvas.beginText()
.setFontAndSize(PdfFontFactory.createFont(FontConstants.COURIER_BOLD), 1)
.setColor(yellowColor, true);
for (int j = 0; j < text.size(); j++) {
String line = text.get(j);
float xOffset = ps.getWidth() / 2 - 45 - 8 * j;
float fontSizeCoeff = 6 + j;
float lineSpacing = (lineHeight + j) * j / 1.5f;
int stringWidth = line.length();
for (int i = 0; i < stringWidth; i++) {
float angle = (maxStringWidth / 2 - i) / 2f;
float charXOffset = (4 + (float) j / 2) * i;
canvas.setTextMatrix(fontSizeCoeff, 0,
angle, fontSizeCoeff / 1.5f,
xOffset + charXOffset, yOffset - lineSpacing)
.showText(String.valueOf(line.charAt(i)));
}
}
canvas.endText();
像之前一樣,我們再次把座標系放在左上角,並使用Cmyk空間定義了黃色,定義了行距和y軸一開始的位置,然後開始添加文字,我們使用等寬粗體並定義了字體的大小爲1用戶單位,雖然只有1用戶單位,但是我們會在後面通過文本矩陣的方式來放大文本,在裏面我們不會用setLeading()
來設置行距,因爲我們沒有使用newlineShowText()
方法。我們設置好文字顏色,然後一個一個文字顯示,而不是之前整行畫。
每個字符的圖像在字體裏面定義爲是畫好的路徑,默認情況下,這些字符的路徑都是被填充的,這就是爲什麼我們設置fill的color能改變字體顏色的原因
我們開始循環文本,將每行讀入一個String, 我們需要一系列數學變量來定義將用於定位每個字形的文本矩陣的不同元素: 我們爲每一行定義一個xOffset
變量來決定當前行的文字的起始位置,字體大小被定義爲1個用戶單位,但是我們將它與一個fontSizeCoeff
相乘,這取決於文本數組中行的索引。同事我們還將定義yOffset來決定每一行的起始位置。
計算每行中的字符數,然後循環所有字符, 我們根據字符在行中的位置定義一個angle變量,charOffset變量取決於行的索引和字符的位置。
最後,設置文本的變換矩陣,a
和d
定義了縮放比例,c
參數定義了傾斜程度,然後計算字符的座標來定義e
和f
參數,每個字符的位置確定以後,使用showText()
函數來顯示字符。這個方法不會另起一行來顯示字符,我們通過循環的方式來另起一行,最後使用endext()
來關閉text對象
這個例子很複雜,但是通過這個例子我們可以看出,我們可以創建任意內容,只要在PDF可以做到,那麼iText也可以做到。但請放心,以後的例子將更容易理解
總結
在本章,我們一直在嘗試着在PDF裏面的各種操作,並在iText裏面的進行相應的操作。我們已經學到了一種稱爲圖形狀態的概念,它擁有當前的變換矩陣,線寬,顏色等屬性。文本狀態是涵蓋與文本相關的所有屬性的圖形狀態的子集,例如文本矩陣,文本的字體和大小以及我們尚未討論的許多其他屬性。 我們將在另一個教程中詳細介紹。你可能會想知道爲什麼開發人員需要訪問低級API,而不是使用iText的很多高級功能, 這個問題將在下一章回答。