我个人习惯的安装方式如下:
1.新建一个文件夹,并进入该文件夹中 2.调出命令行窗口 3.在命令行窗口中键入命令:go mod init url/groupName/projectName
在该文件夹中,新建一个 .go 文件。键入下列代码:
package main import ( "github.com/jung-kurt/gofpdf" ) func main() { pdf := gofpdf.New("P", "mm", "A4", "") pdf.AddPage() if err := pdf.OutputFileAndClose("1.pdf"); err != nil { panic(err.Error()) } }保存该文件。
在该文件夹中,继续调出命令行窗口,键入命令:go run fileName.go。 既能看到 go 系统会自动去 gofpdf 官方代码仓 拉取代码。 注意:请提前解决代理问题,否则几乎是不可能下载成功的。
该文件夹下出现了 “1.pdf” 这个文件,打开这个 pdf 文件查看下。能够正常打开,说明这个第三方库安装成功,并能正常使用了。
运行效果:
运行效果:
大意: ImageType:只支持:JPG、JPEG、PNG、GIF,这个四个格式。无所谓字符串大小写。如果为空,则根据文件扩展名自行推导。 ReadDpi:是否尝试自动从图像文件读取图像 dpi 信息。 通常,应将其设置为 true(请注意,并非所有图像都会提供此信息)。 但是,为了向后兼容该API的早期版本,它默认为 false。个人笔记注:并非所有图像都提供 dpi,就凭这一项,就应该设置为 false。
运行效果:
1.AddUTF8Font() 暂不支持 .ttc 格式的字体! 2.经实践:在 AddUTF8Font 和 SetFont() 中,设置 styleStr 属性,好像对中文字体无效。正在寻找其他解决方案。
打开 PDF 时,会弹出密码框,要求用户输入该文档的密码。
func main() { pdf := gofpdf.New("P", "mm", "A4", "") pdf.AddPage() pdf.AddUTF8Font("simfang", "", "simfang.ttf") pdf.SetFont("simfang", "", 20) pdf.Text(5, 10, "阿毛生信系统") //CnProtectPrint: 表示该文档允许被打印 pdf.SetProtection(gofpdf.CnProtectPrint, "abc123", "") if err := pdf.OutputFileAndClose("1.pdf"); err != nil { panic(err.Error()) } }运行效果:
运行效果:
GitHub 仓中的这个文件:fpdf_test.go,演示了该库的所有功能。
代码来源:fpdf_test.go#L310
func main() { pdf := gofpdf.New("P", "mm", "A4", "") pdf.SetTopMargin(30) pdf.AddUTF8Font("simfang", "", "simfang.ttf") pdf.SetFont("simfang", "", 12) //设置页眉 pdf.SetHeaderFuncMode(func() { pdf.Image("./images/amaosys-logo.png", 0, 0, 0, 0, false, "", 0, "") pdf.SetY(5) pdf.Ln(10) }, true) //设置页脚 pdf.SetFooterFunc(func() { pdf.SetY(-10) pdf.CellFormat( 0, 10, fmt.Sprintf("当前第 %d 页,共 {nb} 页", pdf.PageNo()), //字符串中的 {nb}。大括号是可以省的,但不建议这么做 "", 0, "C", false, 0, "", ) }) //给个空字符串就会去替换默认的 "{nb}"。 //如果这里指定了特别的字符串,那么SetFooterFunc() 中的 "nb" 也必须换成这个特别的字符串 pdf.AliasNbPages("") pdf.AddPage() for j := 0; j < 100; j++ { pdf.CellFormat( 0, 10, fmt.Sprintf("正在打印:%d", j), "", 1, "", false, 0, "", ) } if err := pdf.OutputFileAndClose("1.pdf"); err != nil { panic(err.Error()) } }运行效果:
代码来源:fpdf_test.go#L342
func main() { pdf := gofpdf.New("P", "mm", "A4", "") w, h := pdf.GetPageSize() fmt.Printf("pdf size, w:%.2f, h:%.2f", w, h) //pdf size, w:210.00, h:297.00 titleStr := "20000 Leagues Under the Seas" pdf.SetTitle(titleStr, false) pdf.SetAuthor("Jules Verne", false) pdf.SetHeaderFuncMode(func() { pdf.SetFont("Arial", "B", 15) wd := pdf.GetStringWidth(titleStr) + 6 pdf.SetY(0.6) //先要设置 Y,然后再设置 X。否则,会导致 X 失效 pdf.SetX((210 - wd) / 2) //水平居中的算法 pdf.SetDrawColor(0, 80, 180) //frame color pdf.SetFillColor(230, 230, 0) //background color pdf.SetTextColor(220, 50, 50) //text color pdf.SetLineWidth(1) pdf.CellFormat(wd, 10, titleStr, "1", 1, "CM", true, 0, "") //第 5 个参数,实际效果是:指定下一行的位置 pdf.Ln(5) }, false) pdf.SetFooterFunc(func() { pdf.SetY(-15) pdf.SetFont("Arial", "I", 8) pdf.SetTextColor(128, 128, 128) pdf.CellFormat( 0, 5, fmt.Sprintf("Page %d", pdf.PageNo()), "", 0, "C", false, 0, "", ) }) //标题 chapterTitle := func(chapNum int, titleStr string) { pdf.SetFont("Arial", "", 12) pdf.SetFillColor(200, 220, 255) //background color pdf.CellFormat( 0, 6, fmt.Sprintf("Chapter %d : %s", chapNum, titleStr), "", 1, "L", true, 0, "", ) pdf.Ln(2) } //主体 chapterBody := func(fileStr string) { textStr, err := ioutil.ReadFile(fileStr) if err != nil { pdf.SetError(err) } pdf.SetFont("Times", "", 12) //输出对齐文本 pdf.MultiCell(0, 5, string(textStr), "", "", false) pdf.Ln(-1) pdf.SetFont("", "I", 0) pdf.Cell(0, 5, "(end of excerpt)") } //印刷每一页 printChapter := func(chapNum int, titleStr, fileStr string) { pdf.AddPage() chapterTitle(chapNum, titleStr) chapterBody(fileStr) } printChapter(1, "A RUNAWAY REEF", "./text/20k_c1.txt") printChapter(2, "THE PROS AND CONS", "./text/20k_c2.txt") if err := pdf.OutputFileAndClose("1.pdf"); err != nil { panic(err.Error()) } }运行效果:
pdf.CellFormat() 中的第 5 个参数 ln,其数据类型是 int。
为了让后续的演示看起来更加清晰,这里将 3.2 的代码中,SetHeaderFuncMode() 函数片段中的最后一行 pdf.Ln(1) 注释了,也就是不采取创建空行。(其他代码可改可不改)
pdf.SetHeaderFuncMode(func() { pdf.SetFont("Arial", "B", 15) wd := pdf.GetStringWidth(titleStr) + 6 pdf.SetY(0.6) //先要设置 Y,然后再设置 X。否则,会导致 X 失效 pdf.SetX((210 - wd) / 2) //水平居中的算法 pdf.SetDrawColor(0, 80, 180) //frame color pdf.SetFillColor(230, 230, 0) //background color pdf.SetTextColor(220, 50, 50) //text color pdf.SetLineWidth(1) pdf.CellFormat(wd, 10, titleStr, "1", 2, "CM", true, 0, "") //pdf.Ln(1) //这行注释了 }, false)Ln(h float64) 表示:创建一个高度为 h 的空行。
Google 翻译:ln 指示呼叫后当前位置应该去哪里。 可能的值为 0(在右边),1(在下一行的开头)和 2(在下面)。放置 1 等同于放置 0,并在其后调用 Ln()。
0、1、2,这三个值的最终效果,差别很大。从最终效果来看,我认为官方文档的这句话:
Putting 1 is equivalent to putting 0 and calling Ln() just after.有错误,描述与实际效果不相同。
代码来源:fpdf_test.go#L420
import ( "fmt" "github.com/jung-kurt/gofpdf" "io/ioutil" ) func main() { var y0 float64 var crrntCol int pdf := gofpdf.New("P", "mm", "A4", "") pdf.SetDisplayMode("fullpage", "TwoColumnLeft") //添加“仿宋”字体 pdf.AddUTF8Font("simfang", "", "simfang.ttf") pdf.SetAuthor("毛莹", true) titleStr := "阿毛生信系统" pdf.SetTitle(titleStr, true) setCol := func(col int) { //根据给定的列,设置位置 crrntCol = col x := 10.0 + float64(col)*65.0 pdf.SetLeftMargin(x) pdf.SetX(x) } //标题 chapterTitle := func(chapNum int, titleStr string) { pdf.SetFont("Arial", "", 12) pdf.SetFillColor(200, 220, 255) //background color pdf.CellFormat( 0, 6, fmt.Sprintf("Chapter %d : %s", chapNum, titleStr), "", 1, "L", true, 0, "", ) pdf.Ln(2) y0 = pdf.GetY() } chapterBody := func(fileStr string) { txtBuf, err := ioutil.ReadFile(fileStr) if err != nil { panic(err) } pdf.SetFont("Times", "", 12) pdf.MultiCell(60, 5, string(txtBuf), "", "", false) pdf.SetFont("", "I", 0) pdf.Cell(0, 5, "(end of excerpt)") setCol(0) //返回第一列 } printChapter := func(num int, titleStr, fileStr string) { pdf.AddPage() chapterTitle(num, titleStr) chapterBody(fileStr) } //判定是否需要分页 pdf.SetAcceptPageBreakFunc(func() bool { if crrntCol < 2 { setCol(crrntCol + 1) pdf.SetY(y0) return false //继续保持在当前页 } setCol(0) return true //执行分页 }) pdf.SetHeaderFunc(func() { pdf.SetFont("simfang", "", 15) //设置“仿宋”字体 wd := pdf.GetStringWidth(titleStr) + 6 pdf.SetX((210 - wd) / 2) pdf.SetDrawColor(0, 80, 180) //frame color pdf.SetFillColor(230, 230, 0) //background color pdf.SetTextColor(220, 50, 50) //text color pdf.SetLineWidth(1) pdf.CellFormat(wd, 9, titleStr, "1", 1, "C", true, 0, "") pdf.Ln(5) y0 = pdf.GetY() //保存纵坐标 }) pdf.SetFooterFunc(func() { pdf.SetY(-15) pdf.SetFont("Arial", "I", 8) pdf.SetTextColor(128, 128, 128) pdf.CellFormat( 0, 10, fmt.Sprintf("Page %d", pdf.PageNo()), "", 0, "C", false, 0, "", ) }) printChapter(1, "A RUNAWAY REEF", "./text/20k_c1.txt") printChapter(2, "THE PROS AND CONS", "./text/20k_c2.txt") if err := pdf.OutputFileAndClose("1.pdf"); err != nil { panic(err.Error()) } }运行效果:
在每一段 Cell() 或者 Text() 文字之前,都要调用 pdf.SetFont() 设置好字体,不然就会沿用上一个已设置的字体。 如果文字内容没有任何复合字符,那么用默认的字体是没有问题。但如果出现了复合字符,那么就要提前设置好一个能够支持该复合字符的字体,否则就是乱码。 比如,我有一段文字内容,里面既有中文也有英文。此时,就必须要设置好一个能够识别中文字符的字体。 演示,将上例代码中的 chapterBody 函数修改成如下(其他代码可改可不改):
chapterBody := func(fileStr string) { txtBuf, err := ioutil.ReadFile(fileStr) if err != nil { panic(err) } //统计 unicode 字符的数量 runeLen := utf8.RuneCount(txtBuf) //把最终的 unicode 字符放入这个 []rune result := make([]rune, runeLen) //读取到的内容是 []byte 类型,但内容中有中文,需要用 utf8.DecodeRune() 转换为 unicode 字符 for i := 0; runeLen > 0; i++ { r, size := utf8.DecodeRune(txtBuf) result[i] = r txtBuf = txtBuf[size:] runeLen-- } //可以打印到控制台,看一下效果 fmt.Println(result) fmt.Println(string(result)) pdf.SetFont("simfang", "", 14) pdf.MultiCell(90, 5, string(result), "", "", false) pdf.Cell(0, 5, "(本文已结束)") setCol(0) //返回第一列 }运行效果: 备注:如果内容中有日文、德文等复合字符,就需要去找到对应的字体去支持它们。字体不支持这个复合字符,也会变成乱码。
有很多涉及到设置文档属性的方法,例如:
func (f *Fpdf) SetProducer(producerStr string, isUTF8 bool) func (f *Fpdf) SetTitle(titleStr string, isUTF8 bool) func (f *Fpdf) SetSubject(subjectStr string, isUTF8 bool) func (f *Fpdf) SetAuthor(authorStr string, isUTF8 bool) func (f *Fpdf) SetKeywords(keywordsStr string, isUTF8 bool) func (f *Fpdf) SetCreator(creatorStr string, isUTF8 bool) func (f *Fpdf) addFont(familyStr, styleStr, fileStr string, isUTF8 bool)这些方法中,都有一个 isUTF8 参数,其类型为 bool。该参数指定了是否以 UTF-8 编码来处理字符串。
设置为 true 时: 字符串将以 UTF-8 编码来处理。设置为 false 时: 字符串将以 ISO-8859-1 编码来处理。我的系统是 Windows,并且已安装了 Adobe Reader。使用 Adobe Reader 打开 PDF 文件,按 Ctrl + D 组合键,查看此 PDF 文件的属性。
设置为 true 时 能够正常显示中文字: 设置为 false 时 中文就变成了乱码:代码来源:fpdf_test.go#L525
func loremList() []string { return []string{ "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod " + "tempor incididunt ut labore et dolore magna aliqua.", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " + "aliquip ex ea commodo consequat.", "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " + "dolore eu fugiat nulla pariatur.", "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " + "officia deserunt mollit anim id est laborum.", } } func main() { const ( colCount = 3 colWd = 60.0 marginH = 15.0 lineHt = 5.5 cellGap = 2.0 ) type cellType struct { str string list [][]byte ht float64 } var ( cellList [colCount]cellType cell cellType ) pdf := gofpdf.New("P", "mm", "A4", "") // 210 x 297 header := [colCount]string{"Column A", "Column B", "Column C"} alignList := [colCount]string{"L", "C", "R"} strList := loremList() pdf.SetMargins(marginH, 15, marginH) pdf.SetFont("Arial", "", 14) pdf.AddPage() //设置表格第一行的样式(也就是在设置 <thead> 标签的样式) pdf.SetTextColor(224, 224, 224) pdf.SetFillColor(64, 64, 64) for colJ := 0; colJ < colCount; colJ++ { pdf.CellFormat(colWd, 10, header[colJ], "1", 0, "CM", true, 0, "") } pdf.Ln(-1) //设置每一行 pdf.SetTextColor(24, 24, 24) pdf.SetFillColor(255, 255, 255) y := pdf.GetY() count := 0 for rowJ := 0; rowJ < 2; rowJ++ { maxHt := lineHt //计算单元格的高度 for colJ := 0; colJ < colCount; colJ++ { count++ if count > len(strList) { count = 1 } cell.str = strings.Join(strList[:count], " ") cell.list = pdf.SplitLines([]byte(cell.str), colWd-cellGap*2) cell.ht = float64(len(cell.list)) * lineHt if cell.ht > maxHt { maxHt = cell.ht } cellList[colJ] = cell } //循环渲染每个单元格 x := marginH for colJ := 0; colJ < colCount; colJ++ { pdf.Rect(x, y, colWd, maxHt+cellGap*2, "D") cell = cellList[colJ] cellY := y + cellGap + (maxHt-cell.ht)/2 for splitJ := 0; splitJ < len(cell.list); splitJ++ { pdf.SetXY(x+cellGap, cellY) pdf.CellFormat(colWd-cellGap*2, lineHt, string(cell.list[splitJ]), "", 0, alignList[colJ], false, 0, "") cellY += lineHt } x += colWd } y += maxHt + cellGap*2 } if err := pdf.OutputFileAndClose("1.pdf"); err != nil { panic(err.Error()) } }运行效果:
代码来源:fpdf_test.go#L606
//strDelimit converts 'ABCDEFG' to, for example, 'A,BCD,EFG' func strDelimit(str string, sepstr string, sepcount int) string { pos := len(str) - sepcount for pos > 0 { str = str[:pos] + sepstr + str[pos:] pos = pos - sepcount } return str } func main() { pdf := gofpdf.New("P", "mm", "A4", "") // 210 x 297 type countryType struct { nameStr, capitalStr, areaStr, popStr string } countryList := make([]countryType, 0, 8) header := []string{"Country", "Capital", "Area (sq km)", "Pop. (thousands)"} loadData := func(fileStr string) { fl, err := os.Open(fileStr) if err != nil { panic(err) } defer fl.Close() scanner := bufio.NewScanner(fl) var c countryType for scanner.Scan() { //Austria;Vienna;83859;8075 lineStr := scanner.Text() list := strings.Split(lineStr, ";") if len(list) == 4 { c.nameStr = list[0] c.capitalStr = list[1] c.areaStr = list[2] c.popStr = list[3] countryList = append(countryList, c) } else { fmt.Printf("error tokenizing %s\n", lineStr) } } if len(countryList) == 0 { fmt.Printf("error loading data from %s\n", fileStr) } } //简单的表格 basicTable := func() { left := (210.0 - 4*40) / 2 pdf.SetX(left) for _, str := range header { pdf.CellFormat(40, 7, str, "1", 0, "", false, 0, "") } pdf.Ln(-1) for _, c := range countryList { pdf.SetX(left) pdf.CellFormat(40, 6, c.nameStr, "1", 0, "", false, 0, "") pdf.CellFormat(40, 6, c.capitalStr, "1", 0, "", false, 0, "") pdf.CellFormat(40, 6, c.areaStr, "1", 0, "", false, 0, "") pdf.CellFormat(40, 6, c.popStr, "1", 0, "", false, 0, "") pdf.Ln(-1) } } //Better table improvedTable := func() { w := []float64{40.0, 35.0, 40.0, 45.0} //列宽 wSum := 0.0 for _, v := range w { wSum += v } left := (210 - wSum) / 2 pdf.SetX(left) for j, str := range header { pdf.CellFormat(w[j], 7, str, "1", 0, "C", false, 0, "") } pdf.Ln(-1) for _, c := range countryList { pdf.SetX(left) pdf.CellFormat(w[0], 6, c.nameStr, "LR", 0, "", false, 0, "") pdf.CellFormat(w[1], 6, c.capitalStr, "LR", 0, "", false, 0, "") pdf.CellFormat(w[2], 6, strDelimit(c.areaStr, ",", 3), "LR", 0, "", false, 0, "") pdf.CellFormat(w[3], 6, strDelimit(c.popStr, ",", 3), "LR", 0, "", false, 0, "") pdf.Ln(-1) } pdf.SetX(left) pdf.CellFormat(wSum, 0, "", "T", 0, "", false, 0, "") } //Colored table fancyTable := func() { pdf.SetFillColor(255, 0, 0) pdf.SetTextColor(255, 255, 255) pdf.SetDrawColor(128, 0, 0) pdf.SetLineWidth(.3) pdf.SetFont("", "B", 0) w := []float64{40, 35, 40, 45} wSum := 0.0 for _, v := range w { wSum += v } left := (210 - wSum) / 2 pdf.SetX(left) for j, str := range header { pdf.CellFormat(w[j], 7, str, "1", 0, "C", true, 0, "") } pdf.Ln(-1) pdf.SetFillColor(224, 235, 255) pdf.SetTextColor(0, 0, 0) pdf.SetFont("", "", 0) fill := false for _, c := range countryList { pdf.SetX(left) pdf.CellFormat(w[0], 6, c.nameStr, "LR", 0, "", fill, 0, "") pdf.CellFormat(w[1], 6, c.capitalStr, "LR", 0, "", fill, 0, "") pdf.CellFormat(w[2], 6, strDelimit(c.areaStr, ",", 3), "LR", 0, "", fill, 0, "") pdf.CellFormat(w[3], 6, strDelimit(c.popStr, ",", 3), "LR", 0, "", fill, 0, "") pdf.Ln(-1) fill = !fill } pdf.SetX(left) pdf.CellFormat(wSum, 0, "", "T", 0, "", false, 0, "") } loadData("./text/countries.txt") pdf.SetFont("Arial", "", 14) pdf.AddPage() basicTable() pdf.AddPage() improvedTable() pdf.AddPage() fancyTable() if err := pdf.OutputFileAndClose("1.pdf"); err != nil { panic(err.Error()) } }运行效果:
代码来源:fpdf_test.go#L744
func main() { pdf := gofpdf.New("P", "mm", "A4", "") // 210 x 297 pdf.AddPage() //点击链接,跳转到当前文档中的位置 pdf.SetFont("Helvetica", "", 20) _, lineHt := pdf.GetFontSize() pdf.Write(lineHt, "To find out what's new in this tutorial, click ") pdf.SetFont("", "U", 0) link := pdf.AddLink() //添加链接 pdf.WriteLinkID(lineHt, "here", link) //注册这个链接 //添加图片链接、基础的 HTML <a> 标签链接 pdf.AddPage() pdf.SetLink(link, 0, -1) //使用刚才已注册好的链接 pdf.Image("./images/logo.png", 10, 12, 30, 0, false, "", 0, "http://www.fpdf.org") pdf.SetLeftMargin(45) pdf.SetFontSize(14) _, lineHt = pdf.GetFontSize() htmlStr := `You can now easily print text mixing different styles: <b>bold</b>, ` + `<i>italic</i>, <u>underlined</u>, or <b><i><u>all at once</u></i></b>!<br><br>` + `<center>You can also center text.</center>` + `<right>Or align it to the right.</right>` + `You can also insert links on text, such as ` + `<a href="https://github.com/jung-kurt/gofpdf/blob/master/fpdf_test.go">gofpdf github link.</a>, or on an image: click on the logo.` html := pdf.HTMLBasicNew() html.Write(lineHt, htmlStr) if err := pdf.OutputFileAndClose("1.pdf"); err != nil { panic(err.Error()) } }运行结果: 鼠标移动移动到 “here” 处,指针图标会变成手型图标,点击后会发生跳转。如下画面: 点击大象图案,或者 “gofpdf github link.”,都可以在浏览器中打开这个网站。
代码来源:fpdf_test.go#L778
func main() { pdf := gofpdf.New("P", "mm", "A4", "") // 210 x 297 pdf.AddFont("Calligrapher", "", "./font/calligra.json") pdf.AddPage() pdf.SetFont("Calligrapher", "", 35) pdf.Cell(0, 10, "Enjoy new fonts with FPDF!") if err := pdf.OutputFileAndClose("1.pdf"); err != nil { panic(err) } }运行效果:
这是我的目录结构: 官方的示例代码中,加载的是 calligra.json 文件,然后再去映射各个字体。 在这个 json 文件中,大概第 1250 个字符开始。有一个 File 字段,指定了另一个依赖文件的路径,其原本的值是 "calligra.z"。
需要将这个值改一下,改成:参照 main.go 文件的路径。最终结果如下:
calligra.json 文件存放在项目根目录的 font 文件夹中,main.go 文件启动整个程序。那么以 main.go 文件作为参照物来说:calligra.json 它存放的路径是在 main.go 目录下的 font 文件夹中。 main.go 文件的路径是 ./main.go,参照着这个路径来写 calligra.json 文件的路径,就是 ./font/calligra.z。(相对路径与绝对路径,均参照这个 main.go 文件)。 官方示例中,也是将这个 json 文件放置在项目根目录下的 font 文件夹中。但是,我在实践的时候,报了这个异常:panic: open calligra.z: The system cannot find the file specified.,无法找到这个文件。 后来点进 calligra.json 文件中,将 File 的值改成了上图中的路径值,就能成功了。改成绝对路径也可以:/font/calligra.z 。总之一句话:一切路径参照这个 main.go 文件的路径。
代码来源:fpdf_test.go#L792
func main() { pdf := gofpdf.New("P", "mm", "A4", "") // 210 x 297 pdf.SetLeftMargin(50.0) pdf.SetRightMargin(50.0) pdf.AddPage() pdf.SetFont("Helvetica", "", 12) pdf.WriteAligned(0, 35, "This text is the default alignment, Left", "") pdf.Ln(35) pdf.WriteAligned(0, 35, "This text is aligned Left", "L") pdf.Ln(35) pdf.WriteAligned(0, 35, "This text is aligned Center", "C") pdf.Ln(35) line := "This can by used to write justified text" leftMargin, _, rightMargin, _ := pdf.GetMargins() pageWidth, _ := pdf.GetPageSize() pageWidth -= leftMargin + rightMargin pdf.SetWordSpacing((pageWidth - pdf.GetStringWidth(line)) / float64(strings.Count(line, " "))) pdf.WriteAligned(pageWidth, 35, line, "L") if err := pdf.OutputFileAndClose("1.pdf"); err != nil { panic(err) } }运行效果:
代码来源:fpdf_test.go#L820
func main() { pdf := gofpdf.New("P", "mm", "A4", "") // 210 x 297 pdf.AddPage() pdf.SetFont("Arial", "", 11) pdf.Image("./images/logo.png", 10, 10, 30, 0, false, "", 0, "") pdf.Text(50, 20, "logo.png") pdf.Image("./images/logo.gif", 10, 40, 30, 0, false, "", 0, "") pdf.Text(50, 50, "logo.gif") pdf.Image("./images/logo-gray.png", 10, 70, 30, 0, false, "", 0, "") pdf.Text(50, 80, "logo-gray.png") pdf.Image("./images/logo-rgb.png", 10, 100, 30, 0, false, "", 0, "") pdf.Text(50, 110, "logo-rgb.png") pdf.Image("./images/logo.jpg", 10, 130, 30, 0, false, "", 0, "") pdf.Text(50, 140, "logo.jpg") if err := pdf.OutputFileAndClose("1.pdf"); err != nil { panic(err) } }运行效果:
pdf 的内容都是印刷的方式写入到文件中,所以,所有的图片都是以 dpi 为计量单位。为的就是方便后续渲染、打印。 dpi 的描述可以参考这篇文章:link
1.图片的原始 dpi 数值。 2.Image() 函数中 w 和 h 设置的 dpi 数值。 3.pdf 设置的页面单位(mm / cm)。
最终的 dpi 渲染值,是综合这 3 个因素而得出。
如果 w 和 h 均为 0,则图像以 96 dpi 渲染。 如果 w 或 h 为零,则将从另一个维度计算得出,以便保持宽高比。 如果 w 和/或 h 为 -1,则将从 ImageInfoType 对象读取该尺寸的 dpi。PNG文件可以包含 dpi 信息(如果存在的话),该信息将填充在 ImageInfoType 对象中,并用于“宽度”,“高度”和“范围”计算中。否则,可以使用 SetDpi() 函数将 dpi 从默认值 72 更改。 如果 w 和 h 为其他任何负值,则它们的绝对值表示其 dpi 范围。
图片渲染方式是由最终的 dpi 数值所决定。 首先查看一下一张图片的属性: pdf 页面设置单位为 mm,将 Image() 中的 w 参数设置为 100 后,得到下图效果: 使用 Adobe Reader 打开这个 pdf 文件,选中一张图片,复制它。 打开 Photoshop,然后 “新建” 一个新的 PS 文件。PS 它会读取剪切板中的内容,如果剪切板中的内容是图片,那么新建一个新的 PS 文件时,它会自动将像素调整为剪切板中图片的像素大小。 可以看到,尽管在 pdf 中,这张图片看起来很大,但图片像素根本没有发生变化: 因为在 Image() 函数中,设置了 w 的值为 100。所以,渲染 dpi 的值发生了改变。最终印刷 pdf 的时候,图片就根据了这个变大的 dpi 值进行渲染。
代码来源:fpdf_test.go#L843
func main() { var opt gofpdf.ImageOptions pdf := gofpdf.New("P", "mm", "A4", "") // 210 x 297 pdf.AddPage() pdf.SetFont("Arial", "", 11) opt.ImageType = "png" pdf.ImageOptions("./images/logo.png", 10, 10, 30, 0, false, opt, 0, "") opt.AllowNegativePosition = true pdf.ImageOptions("./images/logo.png", -10, 50, 30, 0, false, opt, 0, "") if err := pdf.OutputFileAndClose("1.pdf"); err != nil { panic(err) } }运行效果:
AllowNegativePosition 这个属性隶属于 gofpdf.ImageOptions 结构体中。 官方文档对这个属性的描述:
AllowNegativePosition can be set to true in order to prevent the default coercion of negative x values to the current x position.Google 翻译后: 可以将 AllowNegativePosition 设置为 true,以防止将负 x 值默认强制转换为当前 x 位置。 实际作用: 是否允许负 x 值生效。指定这个属性为 true 后,如果图片的横坐标设置了负数,那么这张就可以溢出到横坐标(如 3.10 代码的运行结果,第二张大象图片,就是溢出了横坐标)。
将图片印刷进 pdf 文件的是:pdf.ImageOptions() 方法。设置图片选项的是:gofpdf.ImageOptions 属性。
pdf.ImageOptions() 方法是将图片印刷进 pdf 文件中,起到的是渲染的作用,真正实现让人在 pdf 中能够看到这张图片。 gofpdf.ImageOptions 属性是设置图片的一些选项。它有 3 个属性:图片格式、DPI 选项、是否允许负 x 值生效。
方法的本质就是:一个函数关联给了一个字面量。ImageOptions() 方法是 gofpdf.Fpdf 这个结构体的关联函数。
ImageOptions 属性是:gofpdf 包下的 ImageOptions 结构体,其中包含了如下 3 个字段:
type ImageOptions struct { ImageType string ReadDpi bool AllowNegativePosition bool }1.都是在 gofpdf 包下。 2.都是结构体类型。
将 AllowNegativePosition 属性设置为 falase 后,就会强制将负 x 的值,转为正 x 值。也就是负 x 值不生效了。 运行效果:
代码来源:fpdf_test.go#L863
func main() { var ( opt gofpdf.ImageOptions fl *os.File err error ) pdf := gofpdf.New("P", "mm", "A4", "") pdf.AddPage() pdf.SetFont("Arial", "", 11) fl, err = os.Open("./images/logo.png") if err != nil { panic(err) return } opt.ImageType = "png" opt.AllowNegativePosition = true //从 fl 中加载文件,并注册图像。注册名为 logo(类似于注册一个事件,名为 logo) _ = pdf.RegisterImageOptionsReader("logo", opt, fl) fl.Close() //注册完,就可以关闭这个文件了 for x := -20.0; x <= 40.0; x += 5 { //使用这个名为 logo 的注册图像(类似调用 logo 的事件) pdf.ImageOptions("logo", x, x+20, 0, 0, false, opt, 0, "") } if err := pdf.OutputFileAndClose("1.pdf"); err != nil { panic(err) } }运行效果:
pdf.RegisterImageOptionsReader() 最大的优势在于:一次注册,到处使用。 调用 pdf.RegisterImageOptionsReader() 方法进行注册时,不会立即使用这张图片(不会立即渲染这张图片),需要在 pdf.Image() 系列方法中使用这个注册名称时,才会将图片渲染到 pdf 中。 如果某张图片会重复多次使用,那就把这张图片通过 pdf.RegisterImageOptionsReader() 方法,注册成图像信息。之后就可以反复使用这个图像信息的名称来渲染这张图片,再也不用每用一次都去重新读取、重新设置了,提高效率。
把 gofpdf.New() 方法中的 orientationStr 这个字符串参数,设置成字符串值 L。即可让 pdf 变成相框样式。 代码来源:fpdf_test.go#L892
func loremList() []string { return []string{ "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod " + "tempor incididunt ut labore et dolore magna aliqua.", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " + "aliquip ex ea commodo consequat.", "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " + "dolore eu fugiat nulla pariatur.", "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " + "officia deserunt mollit anim id est laborum.", } } func lorem() string { return strings.Join(loremList(), " ") } func main() { var ( y0 float64 crrntCol int ) const ( pageWd = 297.0 //A4 210.0 x 297.0 margin = 10.0 gutter = 4 colNum = 3 colWd = (pageWd - 2*margin - (colNum-1)*gutter) / colNum ) //把 orientationStr 这个字符串参数,设置成 "L"。即可让 pdf 变成相框样式 pdf := gofpdf.New("L", "mm", "A4", "") loremStr := lorem() setCol := func(col int) { crrntCol = col x := margin + float64(col)*(colWd+gutter) pdf.SetLeftMargin(x) pdf.SetX(x) } pdf.SetHeaderFunc(func() { titleStr := "gofpdf" pdf.SetFont("Helvetica", "B", 48) wd := pdf.GetStringWidth(titleStr) + 6 pdf.SetX((pageWd - wd) / 2) pdf.SetTextColor(128, 128, 160) pdf.Write(12, titleStr[:2]) pdf.SetTextColor(128, 128, 128) pdf.Write(12, titleStr[2:]) pdf.Ln(20) y0 = pdf.GetY() }) pdf.SetAcceptPageBreakFunc(func() bool { if crrntCol < colNum-1 { setCol(crrntCol + 1) pdf.SetY(y0) return false //开启新的一列,而不是新的一页 } setCol(0) return true }) pdf.AddPage() pdf.SetFont("Times", "", 12) for j := 0; j < 20; j++ { if j == 1 { pdf.Image("./images/fpdf.png", -1, 0, colWd, 0, true, "", 0, "") } else if j == 5 { pdf.Image("./images/golang-gopher.png", -1, 0, colWd, 0, true, "", 0, "") } pdf.MultiCell(colWd, 5, loremStr, "", "", false) pdf.Ln(-1) } if err := pdf.OutputFileAndClose("1.pdf"); err != nil { panic(err) } }运行结果:
涉及路径时,依然注意:要参照着 main.go 文件的所在目录来写 font 路径。 代码来源:fpdf_test.go#L952
func main() { var err error err = gofpdf.MakeFont("./font/CalligrapherRegular.pfb", "./font/cp1252.map", "font", nil, true) if err != nil { panic(err) return } pdf := gofpdf.New("", "", "", "") pdf.SetFontLocation("font") pdf.SetTitle("世界", true) pdf.SetAuthor("世界", true) pdf.SetSubject("世界", true) pdf.SetCreator("世界", true) pdf.SetKeywords("世界", true) pdf.AddFont("Calligrapher", "", "CalligrapherRegular.json") pdf.AddPage() pdf.SetFont("Calligrapher", "", 16) pdf.Writef(5, "\x95 %s \x95", pdf) if err := pdf.OutputFileAndClose("1.pdf"); err != nil { panic(err) } }运行结果:
代码来源:fpdf_test.go#L1060
func main() { const ( gapX = 10.0 gapY = 9.0 rectW = 40.0 rectH = 58.0 pageW = 210 ) //设置透明度的方法:pdf.SetAlpha()。其中 blendModeStr 参数的值,只能是这些 modeList := []string{ "Normal", "Multiply", "Screen", "Overlay", "Darken", "Lighten", "ColorDodge", "ColorBurn", "HardLight", "SoftLight", "Difference", "Exclusion", "Hue", "Saturation", "Color", "Luminosity", } pdf := gofpdf.New("", "", "", "") pdf.SetLineWidth(2) pdf.SetAutoPageBreak(false, 0) pdf.AddPage() pdf.SetFont("Helvetica", "", 18) pdf.SetXY(0, gapY) pdf.SetTextColor(0, 0, 0) pdf.CellFormat(float64(pageW), gapY, "Alpha Blending Modes", "", 0, "C", false, 0, "") j := 0 y := 3 * gapY for col := 0; col < 4; col++ { x := gapX for row := 0; row < 4; row++ { //画一个黑色的外框框 pdf.Rect(x, y, rectW, rectH, "D") //框框的底部,画一个黑色的长方形。并在长方形中,写上白色的文字内容 pdf.SetFont("Helvetica", "B", 12) pdf.SetFillColor(0, 0, 0) pdf.SetTextColor(250, 250, 230) pdf.SetXY(x, y+rectH-4) pdf.CellFormat(rectW, 5, modeList[j], "", 0, "C", true, 0, "") //外框框中,写上一个大写的斜体 A pdf.SetFont("Helvetica", "I", 150) pdf.SetTextColor(80, 80, 120) pdf.SetXY(x, y+2) pdf.CellFormat(rectW, rectH, "A", "", 0, "C", false, 0, "") //设置的 golang-gopher.png 透明度。注意:融合模式 blendModeStr 的值需要一个字符串 pdf.SetAlpha(0.5, modeList[j]) pdf.Image("./images/golang-gopher.png", x-gapX, y, rectW+2*gapX, 0, false, "", 0, "") //一个设置完了,把透明度重置为正常模式,方便进行下一个案例演示 pdf.SetAlpha(1.0, "Normal") x += rectW + gapX j++ } y += rectH + gapY } if err := pdf.OutputFileAndClose("1.pdf"); err != nil { panic(err) } }运行结果:
代码来源:fpdf_test.go#L1248
func main() { pdf := gofpdf.New("P", "mm", "A4", "") //添加 "仿宋" 字体 pdf.AddUTF8Font("fangsong", "", "simfang.ttf") pdf.AddPage() //使用字体 pdf.SetFont("fangsong", "", 15) pdf.Bookmark("阿毛生信系统简介", 0, 0) //顶级书签,并且不显示该书签在 pdf 中 pdf.Bookmark("发展历程", 1, -1) //二级书签,名称叫“发展历程”,显示在 pdf 中 pdf.Cell(0, 6, "发展历程") //点击“发展历程”这个书签,即会跳转到 pdf 中“发展历程”的所在位置 pdf.Ln(100) //空出几行,为了演示跳转效果 pdf.Bookmark("荣誉资质", 1, -1) //二级书签,名称叫“荣誉资质”,显示在 pdf 中 pdf.Cell(0, 6, "荣誉资质") //点击“荣誉资质”这个书签,即会跳转到 pdf 中“荣誉资质”的所在位置 pdf.AddPage() pdf.Bookmark("我们的产品", 0, 0) pdf.Bookmark("核心产品简介", 1, -1) pdf.Cell(0, 6, "核心产品简介") if err := pdf.OutputFileAndClose("1.pdf"); err != nil { panic(err) } }运行结果: 点击书签,即会跳转到各自书签的所在位置。
该方法的完整参数列表:func (f *Fpdf) Bookmark(txtStr string, level int, y float64) 官方文档对于这个方法的描述:
Bookmark sets a bookmark that will be displayed in a sidebar outline. txtStr is the title of the bookmark. level specifies the level of the bookmark in the outline; 0 is the top level, 1 is just below, and so on. y specifies the vertical position of the bookmark destination in the current page; -1 indicates the current position.Google 翻译+自己的理解: 设置一个书签,该书签将显示在侧边栏轮廓中。txtStr 是书签的标题。level 指定大纲中书签的级别:0 是最高级别,1 处于 0 之下,依此类推。 y 指定书签目标在当前页面中的垂直位置。-1 表示当前位置,0 表示不显示到 pdf 中。
读取一个已存在的 pdf 中的内容,添加到新的 pdf。 感谢网友 @yellowriver007 提出了这个需求,使我有机会接触到了这个知识点。
1.一个有内容的 pdf 文件。 2.下载好以下 2 个第三方 Go 依赖库:
"github.com/jung-kurt/gofpdf" "github.com/jung-kurt/gofpdf/contrib/gofpdi"编译、运行,打开这个 test.pdf 文件,可以看到最终效果如下: 找不到其他有内容的 pdf 文件,拿了单位的 OA 文件作为源文件,部分敏感内容已涂抹,请见谅。已确认源 pdf 文件中的所有内容,都已写入了这个新的 test.pdf 文件。
直接上代码:
package main import ( "github.com/jung-kurt/gofpdf" ) func main() { //设置页面参数 pdf := gofpdf.New("P", "mm", "A4", "") //添加一页 pdf.AddPage() pdf.TransformBegin() // begin transform pdf.TransformRotate(45, 70, -5) // 逆时针旋转 45 度 //将图片放入到 pdf 文档中 //ImageOptions(src, x, y, width, height, flow, options, link, linkStr) pdf.ImageOptions( "1.jpg", 0, 0, 0, 0, false, gofpdf.ImageOptions{ImageType: "jpg", ReadDpi: false}, 0, "", ) pdf.TransformEnd() // end transform // 新添加一页 pdf.AddPage() pdf.TransformBegin() pdf.TransformRotate(-45, 0, 60) // 顺时针旋转 45 度 pdf.ImageOptions( "1.jpg", 0, 0, 0, 0, false, gofpdf.ImageOptions{ImageType: "jpg", ReadDpi: false}, 0, "", ) pdf.TransformEnd() if err := pdf.OutputFileAndClose("1.pdf"); err != nil { panic(err.Error()) } }运行代码,打开生成的 pdf,可以看到效果如下:
作者对于这个方法的描述如下:
TransformRotate rotates the following text, drawings and images around the center point (x, y). angle is specified in degrees and measured counter-clockwise from the 3 o'clock position. The TransformBegin() example demonstrates this method.Google 翻译大意:“变换旋转”使以下文本,图形和图像围绕中心点(x,y)旋转。 角度以度为单位指定,并从3点钟位置逆时针测量。 没看懂这个描述,因此图片旋转后,我一直调不好图片的位置,恳请各位看官指点。
在学习 3.3.1 案例的时候,使用了记事本去写一些中文+英文。好了,一运行,PDF 的第一个字符一直都是一个空心的方块。想了半天,才记起来,Windows 的记事本会出现 “boom 头”,这个讨厌的东西。“boom 头” 是 3 个无法显示的空字符,这个是 Windows 特有的问题。Unix 以及 Linux 系统,不会有这个问题。后来在云服务器上用 vim 写了内容,再下载到本地。再次运行代码,就没有任何问题了。
1.极简入门 2.link1 3.link2 4.gofpdf 官方文档