From 5dd5cb88814c86919e18527229269e5d1bb82dbc Mon Sep 17 00:00:00 2001 From: Kurt Jung Date: Tue, 28 Jan 2014 15:35:50 -0500 Subject: Support for unstyled, path-only SVG images of the type generated by the jSignature web control. --- doc.go | 44 ++++++----- fpdf.go | 2 +- fpdf_test.go | 132 ++++++++++++++++++++++++++++----- image/signature.svg | 43 +++++++++++ svgbasic.go | 210 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 393 insertions(+), 38 deletions(-) create mode 100644 image/signature.svg create mode 100644 svgbasic.go diff --git a/doc.go b/doc.go index ff372f8..1dad3f2 100644 --- a/doc.go +++ b/doc.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013 Kurt Jung (Gmail: kurt.w.jung) + * Copyright (c) 2013-2014 Kurt Jung (Gmail: kurt.w.jung) * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -15,21 +15,8 @@ */ /* -Package gofpdf implements a PDF document generator. - -This package's code and documentation are closely derived from the FPDF library -created by Olivier Plathey, and a number of font and image resources are copied -directly from it. Drawing support is adapted from the FPDF geometric figures -script by David Hernández Sanz. Transparency support is adapted from the FPDF -transparency script by Martin Hall-May. Support for gradients and clipping is -adapted from FPDF scripts by Andreas Würmser. Support for outline bookmarks is -adapted from Olivier Plathey by Manuel Cornes. Support for transformations is -adapted from the FPDF transformation script by Moritz Wagner and Andreas -Würmser. Lawrence Kesteloot provided code to allow an image's extent to be -determined prior to placement. Bruno Michel has provided valuable assistance -with the code. - -The FPDF website is http://www.fpdf.org/. +Package gofpdf implements a PDF document generator with high level support for +text, drawing and images. Features @@ -57,10 +44,31 @@ Features • Clipping +• Basic path-only SVG images + gofpdf has no dependencies other than the Go standard library. All tests pass on Linux, Mac and Windows platforms. Like FPDF version 1.7, from which gofpdf -is derived, this package does not yet support UTF-8 source text. gofpdf is -copyrighted by Kurt Jung and is released under the MIT License. +is derived, this package does not yet support UTF-8 source text. + +Acknowledgments + +This package's code and documentation are closely derived from the FPDF library +created by Olivier Plathey, and a number of font and image resources are copied +directly from it. Drawing support is adapted from the FPDF geometric figures +script by David Hernández Sanz. Transparency support is adapted from the FPDF +transparency script by Martin Hall-May. Support for gradients and clipping is +adapted from FPDF scripts by Andreas Würmser. Support for outline bookmarks is +adapted from Olivier Plathey by Manuel Cornes. Support for transformations is +adapted from the FPDF transformation script by Moritz Wagner and Andreas +Würmser. Lawrence Kesteloot provided code to allow an image's extent to be +determined prior to placement. Bruno Michel has provided valuable assistance +with the code. + +The FPDF website is http://www.fpdf.org/. + +License + +gofpdf is copyrighted by Kurt Jung and is released under the MIT License. Installation diff --git a/fpdf.go b/fpdf.go index 30cf006..c2625d0 100644 --- a/fpdf.go +++ b/fpdf.go @@ -826,7 +826,7 @@ func (f *Fpdf) Curve(x0, y0, cx, cy, x1, y1 float64, styleStr string) { // the current draw color, line width, and cap style centered on the curve's // path. Filling uses the current fill color. // -// See tutorial 11 for an example of this function. +// See tutorials 11 and 20 for examples of this function. func (f *Fpdf) CurveCubic(x0, y0, cx0, cy0, x1, y1, cx1, cy1 float64, styleStr string) { f.point(x0, y0) f.outf("%.5f %.5f %.5f %.5f %.5f %.5f c %s", cx0*f.k, (f.h-cy0)*f.k, diff --git a/fpdf_test.go b/fpdf_test.go index d70875a..c8bf2e3 100644 --- a/fpdf_test.go +++ b/fpdf_test.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013 Kurt Jung (Gmail: kurt.w.jung) + * Copyright (c) 2013-2014 Kurt Jung (Gmail: kurt.w.jung) * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -88,6 +88,18 @@ func docWriter(pdf *gofpdf.Fpdf, idx int) *pdfWriter { return pw } +func imageFile(fileStr string) string { + return filepath.Join(cnImgDir, fileStr) +} + +func fontFile(fileStr string) string { + return filepath.Join(cnFontDir, fileStr) +} + +func textFile(fileStr string) string { + return filepath.Join(cnTextDir, fileStr) +} + // Convert 'ABCDEFG' to, for example, 'A,BCD,EFG' func strDelimit(str string, sepstr string, sepcount int) string { pos := len(str) - sepcount @@ -187,7 +199,7 @@ func ExampleFpdf_tutorial01() { func ExampleFpdf_tutorial02() { pdf := gofpdf.New("P", "mm", "A4", cnFontDir) pdf.SetHeaderFunc(func() { - pdf.Image(cnImgDir+"/logo.png", 10, 6, 30, 0, false, "", 0, "") + pdf.Image(imageFile("logo.png"), 10, 6, 30, 0, false, "", 0, "") pdf.SetY(5) pdf.SetFont("Arial", "B", 15) pdf.Cell(80, 0, "") @@ -274,8 +286,8 @@ func ExampleFpdf_tutorial03() { chapterTitle(chapNum, titleStr) chapterBody(fileStr) } - printChapter(1, "A RUNAWAY REEF", cnTextDir+"/20k_c1.txt") - printChapter(2, "THE PROS AND CONS", cnTextDir+"/20k_c2.txt") + printChapter(1, "A RUNAWAY REEF", textFile("20k_c1.txt")) + printChapter(2, "THE PROS AND CONS", textFile("20k_c2.txt")) pdf.OutputAndClose(docWriter(pdf, 3)) // Output: // Successfully generated pdf/tutorial03.pdf @@ -374,8 +386,8 @@ func ExampleFpdf_tutorial04() { // Page number pdf.CellFormat(0, 10, fmt.Sprintf("Page %d", pdf.PageNo()), "", 0, "C", false, 0, "") }) - printChapter(1, "A RUNAWAY REEF", cnTextDir+"/20k_c1.txt") - printChapter(2, "THE PROS AND CONS", cnTextDir+"/20k_c2.txt") + printChapter(1, "A RUNAWAY REEF", textFile("20k_c1.txt")) + printChapter(2, "THE PROS AND CONS", textFile("20k_c2.txt")) pdf.OutputAndClose(docWriter(pdf, 4)) // Output: // Successfully generated pdf/tutorial04.pdf @@ -488,7 +500,7 @@ func ExampleFpdf_tutorial05() { } pdf.CellFormat(wSum, 0, "", "T", 0, "", false, 0, "") } - loadData(cnTextDir + "/countries.txt") + loadData(textFile("countries.txt")) pdf.SetFont("Arial", "", 14) pdf.AddPage() basicTable() @@ -583,7 +595,7 @@ func ExampleFpdf_tutorial06() { // Second page pdf.AddPage() pdf.SetLink(link, 0, -1) - pdf.Image(cnImgDir+"/logo.png", 10, 12, 30, 0, false, "", 0, "http://www.fpdf.org") + pdf.Image(imageFile("logo.png"), 10, 12, 30, 0, false, "", 0, "http://www.fpdf.org") pdf.SetLeftMargin(45) pdf.SetFontSize(14) htmlStr := `You can now easily print text mixing different styles: bold, ` + @@ -613,15 +625,15 @@ func ExampleFpdf_tutorial08() { pdf := gofpdf.New("P", "mm", "A4", cnFontDir) pdf.AddPage() pdf.SetFont("Arial", "", 11) - pdf.Image(cnImgDir+"/logo.png", 10, 10, 30, 0, false, "", 0, "") + pdf.Image(imageFile("logo.png"), 10, 10, 30, 0, false, "", 0, "") pdf.Text(50, 20, "logo.png") - pdf.Image(cnImgDir+"/logo.gif", 10, 40, 30, 0, false, "", 0, "") + pdf.Image(imageFile("logo.gif"), 10, 40, 30, 0, false, "", 0, "") pdf.Text(50, 50, "logo.gif") - pdf.Image(cnImgDir+"/logo-gray.png", 10, 70, 30, 0, false, "", 0, "") + pdf.Image(imageFile("logo-gray.png"), 10, 70, 30, 0, false, "", 0, "") pdf.Text(50, 80, "logo-gray.png") - pdf.Image(cnImgDir+"/logo-rgb.png", 10, 100, 30, 0, false, "", 0, "") + pdf.Image(imageFile("logo-rgb.png"), 10, 100, 30, 0, false, "", 0, "") pdf.Text(50, 110, "logo-rgb.png") - pdf.Image(cnImgDir+"/logo.jpg", 10, 130, 30, 0, false, "", 0, "") + pdf.Image(imageFile("logo.jpg"), 10, 130, 30, 0, false, "", 0, "") pdf.Text(50, 140, "logo.jpg") pdf.OutputAndClose(docWriter(pdf, 8)) // Output: @@ -673,9 +685,9 @@ func ExampleFpdf_tutorial09() { pdf.SetFont("Times", "", 12) for j := 0; j < 20; j++ { if j == 1 { - pdf.Image(cnImgDir+"/fpdf.png", -1, 0, colWd, 0, true, "", 0, "") + pdf.Image(imageFile("fpdf.png"), -1, 0, colWd, 0, true, "", 0, "") } else if j == 5 { - pdf.Image(cnImgDir+"/golang-gopher.png", -1, 0, colWd, 0, true, "", 0, "") + pdf.Image(imageFile("golang-gopher.png"), -1, 0, colWd, 0, true, "", 0, "") } pdf.MultiCell(colWd, 5, loremStr, "", "", false) pdf.Ln(-1) @@ -687,7 +699,7 @@ func ExampleFpdf_tutorial09() { // Test the corner cases as reported by the gocov tool func ExampleFpdf_tutorial10() { - gofpdf.MakeFont(cnFontDir+"/calligra.ttf", cnFontDir+"/cp1252.map", cnFontDir, nil, true) + gofpdf.MakeFont(fontFile("calligra.ttf"), fontFile("cp1252.map"), cnFontDir, nil, true) pdf := gofpdf.New("", "", "", "") pdf.SetFontLocation(cnFontDir) pdf.SetTitle("世界", true) @@ -822,7 +834,7 @@ func ExampleFpdf_tutorial12() { pdf.SetXY(x, y+2) pdf.CellFormat(rectW, rectH, "A", "", 0, "C", false, 0, "") pdf.SetAlpha(0.5, modeList[j]) - pdf.Image(cnImgDir+"/golang-gopher.png", x-gapX, y, rectW+2*gapX, 0, false, "", 0, "") + pdf.Image(imageFile("golang-gopher.png"), x-gapX, y, rectW+2*gapX, 0, false, "", 0, "") pdf.SetAlpha(1.0, "Normal") x += rectW + gapX j++ @@ -887,7 +899,7 @@ func ExampleFpdf_tutorial14() { y += 28 pdf.ClipEllipse(26, y+10, 16, 10, true) - pdf.Image(cnImgDir+"/logo.jpg", 10, y, 32, 0, false, "JPG", 0, "") + pdf.Image(imageFile("logo.jpg"), 10, y, 32, 0, false, "JPG", 0, "") pdf.ClipEnd() pdf.ClipCircle(60, y+10, 10, true) @@ -1105,7 +1117,7 @@ func ExampleFpdf_tutorial18() { pdf.SetMargins(10, 10, 10) pdf.SetFont("Helvetica", "", 15) for j, str := range fileList { - fileStr = filepath.Join(cnImgDir, str) + fileStr = imageFile(str) infoPtr = pdf.RegisterImage(fileStr, "") imgWd, imgHt = infoPtr.Extent() switch j { @@ -1158,3 +1170,85 @@ func ExampleFpdf_tutorial19() { // Output: // Successfully generated pdf/tutorial19.pdf } + +// This example demonstrates how to render a simple path-only SVG image of the +// type generated by the jSignature web control. +func ExampleFpdf_tutorial20() { + const ( + fontPtSize = 18.0 + lineHt = fontPtSize * 25.4 / 72.0 + wd = 100.0 + sigFileStr = "signature.svg" + ) + var ( + sig gofpdf.SvgBasicType + err error + ) + pdf := gofpdf.New("P", "mm", "A4", cnFontDir) // A4 210.0 x 297.0 + link := func(showStr, urlStr string) { + pdf.SetFont("", "U", 0) + pdf.SetTextColor(0, 0, 128) + pdf.WriteLinkString(lineHt, showStr, urlStr) + pdf.SetTextColor(0, 0, 0) + pdf.SetFont("", "", 0) + } + pdf.SetFont("Times", "", fontPtSize) + pdf.AddPage() + pdf.SetMargins(10, 10, 10) + pdf.Write(lineHt, "This example renders a simple ") + link("SVG", "http://www.w3.org/TR/SVG/") + pdf.Write(lineHt, " (scalable vector graphics) image that contains only "+ + "basic path commands without any styling, color fill, reflection or "+ + "endpoint closures. In particular, the type of vector graphic returned from a ") + link("jSignature", "http://willowsystems.github.io/jSignature/#/demo/") + pdf.Write(lineHt, " web control is supported and is used in this example.") + pdf.Ln(3 * lineHt) + sig, err = gofpdf.SvgBasicFileParse(imageFile(sigFileStr)) + if err == nil { + scale := 150 / sig.Wd + scaleY := 50 / sig.Ht + if scale > scaleY { + scale = scaleY + } + originX := (210.0 - scale*sig.Wd) / 2.0 + originY := pdf.GetY() + 10 + var x, y, newX, newY float64 + var cx0, cy0, cx1, cy1 float64 + var path []gofpdf.SvgBasicSegmentType + var seg gofpdf.SvgBasicSegmentType + val := func(arg int) (float64, float64) { + return originX + scale*seg.Arg[arg], originY + scale*seg.Arg[arg+1] + } + pdf.SetLineCapStyle("round") + pdf.SetLineWidth(0.25) + pdf.SetDrawColor(0, 0, 128) + for j := 0; j < len(sig.Segments) && pdf.Ok(); j++ { + path = sig.Segments[j] + for k := 0; k < len(path) && pdf.Ok(); k++ { + seg = path[k] + switch seg.Cmd { + case 'M': + x, y = val(0) + pdf.SetXY(x, y) + case 'L': + newX, newY = val(0) + pdf.Line(x, y, newX, newY) + x, y = newX, newY + case 'C': + cx0, cy0 = val(0) + cx1, cy1 = val(2) + newX, newY = val(4) + pdf.CurveCubic(x, y, cx0, cy0, newX, newY, cx1, cy1, "D") + x, y = newX, newY + default: + pdf.SetErrorf("Unexpected path command '%c'", seg.Cmd) + } + } + } + } else { + pdf.SetError(err) + } + pdf.OutputAndClose(docWriter(pdf, 20)) + // Output: + // Successfully generated pdf/tutorial20.pdf +} diff --git a/image/signature.svg b/image/signature.svg new file mode 100644 index 0000000..cdbb4af --- /dev/null +++ b/image/signature.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/svgbasic.go b/svgbasic.go new file mode 100644 index 0000000..4c58f95 --- /dev/null +++ b/svgbasic.go @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2014 Kurt Jung (Gmail: kurt.w.jung) + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package gofpdf + +import ( + "encoding/xml" + "fmt" + "io/ioutil" + "strconv" + "strings" +) + +var pathCmdSub *strings.Replacer + +func init() { + // Handle permitted constructions like "100L200,230" + pathCmdSub = strings.NewReplacer(",", " ", + "L", " L ", "l", " l ", + "C", " C ", "c", " c ", + "M", " M ", "m", " m ") +} + +// SvgBasicSegmentType describes a single curve or position segment +type SvgBasicSegmentType struct { + Cmd byte // See http://www.w3.org/TR/SVG/paths.html for path command structure + Arg [6]float64 +} + +func absolutizePath(segs []SvgBasicSegmentType) { + var x, y float64 + var segPtr *SvgBasicSegmentType + adjust := func(pos int, adjX, adjY float64) { + segPtr.Arg[pos] += adjX + segPtr.Arg[pos+1] += adjY + } + for j, seg := range segs { + segPtr = &segs[j] + if j == 0 && seg.Cmd == 'm' { + segPtr.Cmd = 'M' + } + switch segPtr.Cmd { + case 'M': + x = seg.Arg[0] + y = seg.Arg[1] + case 'm': + adjust(0, x, y) + segPtr.Cmd = 'M' + x = segPtr.Arg[0] + y = segPtr.Arg[1] + case 'L': + x = seg.Arg[0] + y = seg.Arg[1] + case 'l': + adjust(0, x, y) + segPtr.Cmd = 'L' + x = segPtr.Arg[0] + y = segPtr.Arg[1] + case 'C': + x = seg.Arg[4] + y = seg.Arg[5] + case 'c': + adjust(0, x, y) + adjust(2, x, y) + adjust(4, x, y) + segPtr.Cmd = 'C' + x = segPtr.Arg[4] + y = segPtr.Arg[5] + } + } +} + +func pathParse(pathStr string) (segs []SvgBasicSegmentType, err error) { + var seg SvgBasicSegmentType + var j, argJ, argCount, prevArgCount int + setup := func(n int) { + // It is not strictly necessary to clear arguments, but result may be clearer + // to caller + for j := 0; j < len(seg.Arg); j++ { + seg.Arg[j] = 0.0 + } + argJ = 0 + argCount = n + prevArgCount = n + } + var str string + var c byte + pathStr = pathCmdSub.Replace(pathStr) + strList := strings.Fields(pathStr) + count := len(strList) + for j = 0; j < count && err == nil; j++ { + str = strList[j] + if argCount == 0 { // Look for path command or argument continuation + c = str[0] + if c == '-' || (c >= '0' && c <= '9') { // More arguments + if j > 0 { + setup(prevArgCount) + // Repeat previous action + if seg.Cmd == 'M' { + seg.Cmd = 'L' + } else if seg.Cmd == 'm' { + seg.Cmd = 'l' + } + } else { + err = fmt.Errorf("expecting SVG path command at first position, got %s", str) + } + } + } + if err == nil { + if argCount == 0 { + seg.Cmd = str[0] + switch seg.Cmd { + case 'M', 'm': // Absolute/relative moveto: x, y + setup(2) + case 'C', 'c': // Absolute/relative Bézier curve: cx0, cy0, cx1, cy1, x1, y1 + setup(6) + case 'L', 'l': // Absolute/relative lineto: x, y + setup(2) + default: + err = fmt.Errorf("expecting SVG path command at position %d, got %s", j, str) + } + } else { + seg.Arg[argJ], err = strconv.ParseFloat(str, 64) + if err == nil { + argJ++ + argCount-- + if argCount == 0 { + segs = append(segs, seg) + } + } + } + } + } + if err == nil { + if argCount == 0 { + absolutizePath(segs) + } else { + err = fmt.Errorf("Expecting additional (%d) numeric arguments", argCount) + } + } + return +} + +// SvgBasicType aggregates the information needed to describe a multi-segment +// basic vector image +type SvgBasicType struct { + Wd, Ht float64 + Segments [][]SvgBasicSegmentType +} + +// SvgBasicParse parses a simple scalable vector graphics (SVG) buffer into a +// descriptor. Only a small subset of the SVG standard, in particular the path +// information generated by jSignature, is supported. The returned path data +// includes only the commands 'M' (absolute moveto: x, y), 'L' (absolute +// lineto: x, y), and 'C' (absolute cubic Bézier curve: cx0, cy0, cx1, cy1, +// x1,y1). +func SvgBasicParse(buf []byte) (sig SvgBasicType, err error) { + type pathType struct { + D string `xml:"d,attr"` + } + type srcType struct { + Wd float64 `xml:"width,attr"` + Ht float64 `xml:"height,attr"` + Paths []pathType `xml:"path"` + } + var src srcType + err = xml.Unmarshal(buf, &src) + if err == nil { + if src.Wd > 0 && src.Ht > 0 { + sig.Wd, sig.Ht = src.Wd, src.Ht + var segs []SvgBasicSegmentType + for _, path := range src.Paths { + if err == nil { + segs, err = pathParse(path.D) + if err == nil { + sig.Segments = append(sig.Segments, segs) + } + } + } + } else { + err = fmt.Errorf("Unacceptable values for basic SVG extent: %.2f x %.2f", + sig.Wd, sig.Ht) + } + } + return +} + +// SvgBasicParse parses a simple scalable vector graphics (SVG) file into a +// basic descriptor. See SvgBasicParse for additional comments. +func SvgBasicFileParse(svgFileStr string) (sig SvgBasicType, err error) { + var buf []byte + buf, err = ioutil.ReadFile(svgFileStr) + if err == nil { + sig, err = SvgBasicParse(buf) + } + return +} -- cgit v1.2.1-24-ge1ad