diff options
author | Kurt Jung <kurt.w.jung@code.google.com> | 2013-08-10 21:34:54 -0400 |
---|---|---|
committer | Kurt Jung <kurt.w.jung@code.google.com> | 2013-08-10 21:34:54 -0400 |
commit | 1e84d9be144408bfdbb95582191158acd1805c74 (patch) | |
tree | f3c39afe9b080972984eb1b065ac8b9adcbaf3d5 | |
parent | fc5c7916f175f0301b8e103a72825c7db1c79186 (diff) |
Added support for drawing curves and arcs with an example. This was adapted
from the geometric figures FPDF script by David Hernández Sanz, with a starter
function provided by Anthony Starks.
Anthony Starks also made a case for not closing the io.Writer after it is used
to produce a document. An additional method, OutputAndClose, was added so that
the library can close the writer if desired.
-rw-r--r-- | def.go | 6 | ||||
-rw-r--r-- | doc.go | 5 | ||||
-rw-r--r-- | fpdf.go | 193 | ||||
-rw-r--r-- | fpdf_test.go | 100 | ||||
-rw-r--r-- | ttfparser_test.go | 2 |
5 files changed, 271 insertions, 35 deletions
@@ -20,7 +20,9 @@ import ( "bytes" ) -const FPDF_VERSION = "1.7" +const ( + FPDF_VERSION = "1.7" +) type sizeType struct { wd, ht float64 @@ -118,6 +120,8 @@ type Fpdf struct { aliasNbPagesStr string // alias for total number of pages pdfVersion string // PDF version number fontDirStr string // location of font definition files + capStyle int // line cap style: butt 0, round 1, square 2 + joinStyle int // line segment join style: miter 0, round 1, bevel 2 err error // Set if error occurs during life cycle of instance } @@ -19,7 +19,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. The FPDF website is http://www.fpdf.org/. +directly from it. Drawing support is adapted from the FPDF geometric figures +script by David Hernández Sanz. The FPDF website is http://www.fpdf.org/. Features @@ -41,6 +42,8 @@ Features • Page compression +• Drawing support (lines, Bézier curves, arcs, ellipses) + 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 @@ -31,6 +31,7 @@ import ( "image/png" "io" "io/ioutil" + "math" "path" "strings" "time" @@ -396,9 +397,9 @@ func (f *Fpdf) open() { } // Terminates the PDF document. It is not necessary to call this method -// explicitly because Output() does it automatically. If the document contains -// no page, AddPage() is called to prevent the generation of an invalid -// document. +// explicitly because Output() and OutputAndClose() do it automatically. If the +// document contains no page, AddPage() is called to prevent the generation of +// an invalid document. func (f *Fpdf) Close() { if f.err != nil { return @@ -461,8 +462,9 @@ func (f *Fpdf) AddPageFormat(orientationStr string, size sizeType) { } // Start new page f.beginpage(orientationStr, size) - // Set line cap style to square - f.out("2 J") + // Set line cap style to current value + // f.out("2 J") + f.outf("%d J", f.capStyle) // Set line width f.lineWidth = lw f.outf("%.2f w", lw*f.k) @@ -615,26 +617,164 @@ func (f *Fpdf) SetLineWidth(width float64) { } } -// Draws a line between (x1, y1) and (x2, y2). +// Defines the line cap style. styleStr should be "butt", "round" or "square". +// A square style projects from the end of the line. The method can be called +// before the first page is created and the value is retained from page to +// page. +func (f *Fpdf) SetLineCapStyle(styleStr string) { + var capStyle int + switch styleStr { + case "round": + capStyle = 1 + case "square": + capStyle = 2 + default: + capStyle = 0 + } + if capStyle != f.capStyle { + f.capStyle = capStyle + if f.page > 0 { + f.outf("%d J", f.capStyle) + } + } +} + +// Draws a line between points (x1, y1) and (x2, y2). func (f *Fpdf) Line(x1, y1, x2, y2 float64) { f.outf("%.2f %.2f m %.2f %.2f l S", x1*f.k, (f.h-y1)*f.k, x2*f.k, (f.h-y2)*f.k) } +func fillDrawOp(styleStr string) (opStr string) { + switch strings.ToUpper(styleStr) { + case "F": + opStr = "f" + case "FD", "DF": + opStr = "B" + default: + opStr = "S" + } + return +} + // Outputs a rectangle. It can be drawn (border only), filled (with no border) -// or both. x and y specify the upper left corner of the rectangle. style can -// be "F" for filled, "D" for outlined only, or "DF" or "FD" for outlined and -// filled. -func (f *Fpdf) Rect(x, y, w, h float64, style string) { - var op string - if style == "F" { - op = "f" - } else if style == "FD" || style == "DF" { - op = "B" - } else { - op = "S" +// or both. x and y specify the upper left corner of the rectangle. styleStr +// can be "F" for filled, "D" for outlined only, or "DF" or "FD" for outlined +// and filled. An empty string will be replaced with "D". +func (f *Fpdf) Rect(x, y, w, h float64, styleStr string) { + f.outf("%.2f %.2f %.2f %.2f re %s", x*f.k, (f.h-y)*f.k, w*f.k, -h*f.k, fillDrawOp(styleStr)) +} + +// Draw a circle centered on point (x, y) with radius r. styleStr can be "F" +// for filled, "D" for outlined only, or "DF" or "FD" for outlined and filled. +// An empty string will be replaced with "D". +func (f *Fpdf) Circle(x, y, r float64, styleStr string) { + f.Ellipse(x, y, r, r, 0, styleStr) +} + +// Draw an ellipse centered at point (x, y). rx and ry specify its horizontal +// and vertical radii. degRotate specifies the counter-clockwise angle in +// degrees that the ellipse will be rotated. styleStr can be "F" for filled, +// "D" for outlined only, or "DF" or "FD" for outlined and filled. An empty +// string will be replaced with "D". +func (f *Fpdf) Ellipse(x, y, rx, ry, degRotate float64, styleStr string) { + f.Arc(x, y, rx, ry, degRotate, 0, 360, styleStr) +} + +// Outputs current point +func (f *Fpdf) point(x, y float64) { + f.outf("%.2f %.2f m", x*f.k, (f.h-y)*f.k) +} + +// Outputs quadratic curve from current point +func (f *Fpdf) curve(cx0, cy0, x1, y1, cx1, cy1 float64) { + f.outf("%.2f %.2f %.2f %.2f %.2f %.2f c", cx0*f.k, (f.h-cy0)*f.k, x1*f.k, + (f.h-y1)*f.k, cx1*f.k, (f.h-cy1)*f.k) +} + +// Draws a single-segment quadratic Bézier curve. The curve starts at the +// point (x0, y0) and ends at the point (x1, y1). The control point (cx, cy) +// specifies the curvature. At the start point, the curve is tangent to the +// straight line between the start point and the control point. At the end +// point, the curve is tangent to the straight line between the end point and +// the control point. styleStr can be "F" for filled, "D" for outlined only, or +// "DF" or "FD" for outlined and filled. An empty string will be replaced with +// "D". +func (f *Fpdf) Curve(x0, y0, cx, cy, x1, y1 float64, styleStr string) { + f.point(x0, y0) + f.outf("%.2f %.2f %.2f %.2f v %s", cx*f.k, (f.h-cy)*f.k, x1*f.k, (f.h-y1)*f.k, + fillDrawOp(styleStr)) +} + +// Draws a single-segment cubic Bézier curve. The curve starts at the point +// (x0, y0) and ends at the point (x1, y1). The control points (cx0, cy0) and +// (cx1, cy1) specify the curvature. At the start point, the curve is tangent +// to the straight line between the start point and the control point (cx0, +// cy0). At the end point, the curve is tangent to the straight line between +// the end point and the control point (cx1, cy1). styleStr can be "F" for +// filled, "D" for outlined only, or "DF" or "FD" for outlined and filled. An +// empty string will be replaced with "D". +func (f *Fpdf) CurveCubic(x0, y0, cx0, cy0, cx1, cy1, x1, y1 float64, styleStr string) { + f.point(x0, y0) + f.outf("%.2f %.2f %.2f %.2f %.2f %.2f c %s", cx0*f.k, (f.h-cy0)*f.k, + x1*f.k, (f.h-y1)*f.k, cx1*f.k, (f.h-cy1)*f.k, fillDrawOp(styleStr)) +} + +// Draw an elliptical arc centered at point (x, y). rx and ry specify its +// horizontal and vertical radii. degRotate specifies the angle that the arc +// will be rotated. degStart and degEnd specify the starting and ending angle +// of the arc. All angles are specified in degrees and measured +// counter-clockwise from the 3 o'clock position. styleStr can be "F" for +// filled, "D" for outlined only, or "DF" or "FD" for outlined and filled. An +// empty string will be replaced with "D". +func (f *Fpdf) Arc(x, y, rx, ry, degRotate, degStart, degEnd float64, styleStr string) { + x *= f.k + y = (f.h - y) * f.k + rx *= f.k + ry *= f.k + segments := int(degEnd-degStart) / 60 + if segments < 2 { + segments = 2 + } + angleStart := degStart * math.Pi / 180 + angleEnd := degEnd * math.Pi / 180 + angleTotal := angleEnd - angleStart + dt := angleTotal / float64(segments) + dtm := dt / 3 + if degRotate != 0 { + a := -degRotate * math.Pi / 180 + f.outf("q %.2f %.2f %.2f %.2f %.2f %.2f cm", math.Cos(a), -1*math.Sin(a), + math.Sin(a), math.Cos(a), x, y) + x = 0 + y = 0 + } + t := angleStart + a0 := x + rx*math.Cos(t) + b0 := y + ry*math.Sin(t) + c0 := -rx * math.Sin(t) + d0 := ry * math.Cos(t) + f.point(a0/f.k, f.h-(b0/f.k)) + for j := 1; j <= segments; j++ { + // Draw this bit of the total curve + t = (float64(j) * dt) + angleStart + a1 := x + rx*math.Cos(t) + b1 := y + ry*math.Sin(t) + c1 := -rx * math.Sin(t) + d1 := ry * math.Cos(t) + f.curve((a0+(c0*dtm))/f.k, + f.h-((b0+(d0*dtm))/f.k), + (a1-(c1*dtm))/f.k, + f.h-((b1-(d1*dtm))/f.k), + a1/f.k, + f.h-(b1/f.k)) + a0 = a1 + b0 = b1 + c0 = c1 + d0 = d1 + } + f.out(fillDrawOp(styleStr)) + if degRotate != 0 { + f.out("Q") } - // dbg("(Rect) x %.2f f.k %.2f", x, f.k) - f.outf("%.2f %.2f %.2f %.2f re %s", x*f.k, (f.h-y)*f.k, w*f.k, -h*f.k, op) } // Imports a TrueType, OpenType or Type1 font and makes it available. It is @@ -1442,9 +1582,18 @@ func (f *Fpdf) SetXY(x, y float64) { } // Send the PDF document to the writer specified by w. This method will close -// w, even if an error is detected and no document is produced. -func (f *Fpdf) Output(w io.WriteCloser) error { - defer w.Close() +// both f and w, even if an error is detected and no document is produced. +func (f *Fpdf) OutputAndClose(w io.WriteCloser) error { + f.Output(w) + w.Close() + return f.err +} + +// Send the PDF document to the writer specified by w. No output will take +// place if an error has occured in the document generation process. w remains +// open after this function returns. After returning, f is in a closed state +// and its methods should not be called. +func (f *Fpdf) Output(w io.Writer) error { if f.err != nil { return f.err } diff --git a/fpdf_test.go b/fpdf_test.go index 8f32da3..c8b1ba3 100644 --- a/fpdf_test.go +++ b/fpdf_test.go @@ -90,7 +90,7 @@ func ExampleFpdf_tutorial01() { pdf.AddPage() pdf.SetFont("Arial", "B", 16) pdf.Cell(40, 10, "Hello World!") - pdf.Output(docWriter(pdf, 1)) + pdf.OutputAndClose(docWriter(pdf, 1)) // Output: // Successfully generated pdf/tutorial01.pdf } @@ -116,7 +116,7 @@ func ExampleFpdf_tutorial02() { for j := 1; j <= 40; j++ { pdf.CellFormat(0, 10, fmt.Sprintf("Printing line number %d", j), "", 1, "", false, 0, "") } - pdf.Output(docWriter(pdf, 2)) + pdf.OutputAndClose(docWriter(pdf, 2)) // Output: // Successfully generated pdf/tutorial02.pdf } @@ -186,7 +186,7 @@ func ExampleFpdf_tutorial03() { } printChapter(1, "A RUNAWAY REEF", TEXT_DIR+"/20k_c1.txt") printChapter(2, "THE PROS AND CONS", TEXT_DIR+"/20k_c2.txt") - pdf.Output(docWriter(pdf, 3)) + pdf.OutputAndClose(docWriter(pdf, 3)) // Output: // Successfully generated pdf/tutorial03.pdf } @@ -286,7 +286,7 @@ func ExampleFpdf_tutorial04() { }) printChapter(1, "A RUNAWAY REEF", TEXT_DIR+"/20k_c1.txt") printChapter(2, "THE PROS AND CONS", TEXT_DIR+"/20k_c2.txt") - pdf.Output(docWriter(pdf, 4)) + pdf.OutputAndClose(docWriter(pdf, 4)) // Output: // Successfully generated pdf/tutorial04.pdf } @@ -405,7 +405,7 @@ func ExampleFpdf_tutorial05() { improvedTable() pdf.AddPage() fancyTable() - pdf.Output(docWriter(pdf, 5)) + pdf.OutputAndClose(docWriter(pdf, 5)) // Output: // Successfully generated pdf/tutorial05.pdf } @@ -499,7 +499,7 @@ func ExampleFpdf_tutorial06() { `You can also insert links on text, such as ` + `<a href="http://www.fpdf.org">www.fpdf.org</a>, or on an image: click on the logo.` writeHtml(htmlStr) - pdf.Output(docWriter(pdf, 6)) + pdf.OutputAndClose(docWriter(pdf, 6)) // Output: // Successfully generated pdf/tutorial06.pdf } @@ -510,7 +510,7 @@ func ExampleFpdf_tutorial07() { pdf.AddPage() pdf.SetFont("Calligrapher", "", 35) pdf.Cell(0, 10, "Enjoy new fonts with FPDF!") - pdf.Output(docWriter(pdf, 7)) + pdf.OutputAndClose(docWriter(pdf, 7)) // Output: // Successfully generated pdf/tutorial07.pdf } @@ -529,7 +529,7 @@ func ExampleFpdf_tutorial08() { pdf.Text(50, 110, "logo-rgb.png") pdf.Image(IMG_DIR+"/logo.jpg", 10, 130, 30, 0, false, "", 0, "") pdf.Text(50, 140, "logo.jpg") - pdf.Output(docWriter(pdf, 8)) + pdf.OutputAndClose(docWriter(pdf, 8)) // Output: // Successfully generated pdf/tutorial08.pdf } @@ -591,7 +591,7 @@ func ExampleFpdf_tutorial09() { pdf.MultiCell(colWd, 5, loremStr, "", "", false) pdf.Ln(-1) } - pdf.Output(docWriter(pdf, 9)) + pdf.OutputAndClose(docWriter(pdf, 9)) // Output: // Successfully generated pdf/tutorial09.pdf } @@ -610,7 +610,87 @@ func ExampleFpdf_tutorial10() { pdf.AddPage() pdf.SetFont("Calligrapher", "", 16) pdf.Writef(5, "\x95 %s \x95", pdf) - pdf.Output(docWriter(pdf, 10)) + pdf.OutputAndClose(docWriter(pdf, 10)) // Output: // Successfully generated pdf/tutorial10.pdf } + +// Geometric figures +func ExampleFpdf_tutorial11() { + const ( + thin = 0.2 + thick = 3.0 + ) + pdf := New("", "", "", FONT_DIR) + pdf.SetFont("Helvetica", "", 12) + pdf.SetFillColor(200, 200, 220) + pdf.AddPage() + + y := 15.0 + pdf.Text(10, y, "Circles") + pdf.SetFillColor(200, 200, 220) + pdf.SetLineWidth(thin) + pdf.Circle(20, y+15, 10, "D") + pdf.Circle(45, y+15, 10, "F") + pdf.Circle(70, y+15, 10, "FD") + pdf.SetLineWidth(thick) + pdf.Circle(95, y+15, 10, "FD") + pdf.SetLineWidth(thin) + + y += 40.0 + pdf.Text(10, y, "Ellipses") + pdf.SetFillColor(220, 200, 200) + pdf.Ellipse(30, y+15, 20, 10, 0, "D") + pdf.Ellipse(75, y+15, 20, 10, 0, "F") + pdf.Ellipse(120, y+15, 20, 10, 0, "FD") + pdf.SetLineWidth(thick) + pdf.Ellipse(165, y+15, 20, 10, 0, "FD") + pdf.SetLineWidth(thin) + + y += 40.0 + pdf.Text(10, y, "Curves (quadratic)") + pdf.SetFillColor(220, 220, 200) + pdf.Curve(10, y+30, 15, y-20, 40, y+30, "D") + pdf.Curve(45, y+30, 50, y-20, 75, y+30, "F") + pdf.Curve(80, y+30, 85, y-20, 110, y+30, "FD") + pdf.SetLineWidth(thick) + pdf.Curve(115, y+30, 120, y-20, 145, y+30, "FD") + pdf.SetLineCapStyle("round") + pdf.Curve(150, y+30, 155, y-20, 180, y+30, "FD") + pdf.SetLineWidth(thin) + pdf.SetLineCapStyle("butt") + + y += 40.0 + pdf.Text(10, y, "Curves (cubic)") + pdf.SetFillColor(220, 200, 220) + pdf.CurveCubic(10, y+30, 15, y-20, 40, y+30, 10, y+30, "D") + pdf.CurveCubic(45, y+30, 50, y-20, 75, y+30, 45, y+30, "F") + pdf.CurveCubic(80, y+30, 85, y-20, 110, y+30, 80, y+30, "FD") + pdf.SetLineWidth(thick) + pdf.CurveCubic(115, y+30, 120, y-20, 145, y+30, 115, y+30, "FD") + pdf.SetLineCapStyle("round") + pdf.CurveCubic(150, y+30, 155, y-20, 180, y+30, 150, y+30, "FD") + pdf.SetLineWidth(thin) + pdf.SetLineCapStyle("butt") + + y += 40.0 + pdf.Text(10, y, "Arcs") + pdf.SetFillColor(200, 220, 220) + pdf.SetLineWidth(thick) + pdf.Arc(45, y+35, 20, 10, 0, 0, 180, "FD") + pdf.SetLineWidth(thin) + pdf.Arc(45, y+35, 25, 15, 0, 90, 270, "D") + pdf.SetLineWidth(thick) + pdf.Arc(45, y+35, 30, 20, 0, 0, 360, "D") + pdf.SetLineCapStyle("round") + pdf.Arc(135, y+35, 20, 10, 135, 0, 180, "FD") + pdf.SetLineWidth(thin) + pdf.Arc(135, y+35, 25, 15, 135, 90, 270, "D") + pdf.SetLineWidth(thick) + pdf.Arc(135, y+35, 30, 20, 135, 0, 360, "D") + pdf.SetLineWidth(thin) + pdf.SetLineCapStyle("butt") + pdf.OutputAndClose(docWriter(pdf, 11)) + // Output: + // Successfully generated pdf/tutorial11.pdf +} diff --git a/ttfparser_test.go b/ttfparser_test.go index c856ec5..5e25eeb 100644 --- a/ttfparser_test.go +++ b/ttfparser_test.go @@ -51,7 +51,7 @@ func TestLoadMap(t *testing.T) { "168: 0x0E08 chochanthai", "169: 0x0E09 chochingthai", } - list, err := loadMap(GOFPDF_DIR + "/font/iso-8859-11.map") + list, err := loadMap(FONT_DIR + "/iso-8859-11.map") if err == nil { pos := 0 for j := 164; j < 170; j++ { |