Skip to content

Commit

Permalink
vg/vgpdf: migrate to jung-kurt/gofpdf
Browse files Browse the repository at this point in the history
This CL drops bitbucket/zombiezen/gopdf/pdf in favour of
github.com/jung-kurt/gofpdf.

Fixes gonum#389.
Fixes gonum#438.
Updates gonum#347.
  • Loading branch information
sbinet committed Apr 11, 2018
1 parent 088f079 commit 8470f1d
Showing 1 changed file with 180 additions and 55 deletions.
235 changes: 180 additions & 55 deletions vg/vgpdf/vgpdf.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ package vgpdf // import "gonum.org/v1/plot/vg/vgpdf"

import (
"bufio"
"bytes"
"fmt"
"image"
"image/color"
"image/png"
"io"
"log"
"math"

"bitbucket.org/zombiezen/gopdf/pdf"
pdf "github.com/jung-kurt/gofpdf"

"gonum.org/v1/plot/vg"
"gonum.org/v1/plot/vg/fonts"
)

// DPI is the nominal resolution of drawing in PDF.
Expand All @@ -25,123 +29,230 @@ const DPI = 72
// Canvas implements the vg.Canvas interface,
// drawing to a PDF.
type Canvas struct {
doc *pdf.Document
pdf *pdf.Fpdf
w, h vg.Length
page *pdf.Canvas
lineVisible bool

dpi int
nimgs int // images counter
ctx []context
fonts map[string]struct{}
}

type context struct {
fillc color.Color // fill color
drawc color.Color // draw color
linew vg.Length // line width
}

// New creates a new PDF Canvas.
func New(w, h vg.Length) *Canvas {
cfg := pdf.InitType{
UnitStr: "pt",
Size: pdf.SizeType{Wd: w.Points(), Ht: h.Points()},
}
c := &Canvas{
doc: pdf.New(),
pdf: pdf.NewCustom(&cfg),
w: w,
h: h,
lineVisible: true,
dpi: DPI,
ctx: make([]context, 1),
fonts: make(map[string]struct{}),
}
c.page = c.doc.NewPage(unit(w), unit(h))
vg.Initialize(c)
c.pdf.SetMargins(0, 0, 0)
c.pdf.AddPage()
c.Push()
c.Translate(vg.Point{0, h})
c.Scale(1, -1)

return c
}

func (c *Canvas) DPI() float64 {
return float64(c.dpi)
}

func (c *Canvas) cur() *context {
return &c.ctx[len(c.ctx)-1]
}

func (c *Canvas) Size() (w, h vg.Length) {
return c.w, c.h
}

func (c *Canvas) SetLineWidth(w vg.Length) {
c.page.SetLineWidth(unit(w))
c.cur().linew = w
lw := c.unit(w)
c.pdf.SetLineWidth(lw)
c.lineVisible = w > 0
}

func (c *Canvas) SetLineDash(dashes []vg.Length, offs vg.Length) {
ds := make([]pdf.Unit, len(dashes))
ds := make([]float64, len(dashes))
for i, d := range dashes {
ds[i] = unit(d)
ds[i] = c.unit(d)
}
c.page.SetLineDash(unit(offs), ds)
c.pdf.SetDashPattern(ds, c.unit(offs))
}

func (c *Canvas) SetColor(clr color.Color) {
c.page.SetStrokeColor(pdfColor(clr))
c.page.SetColor(pdfColor(clr))
if clr == nil {
clr = color.Black
}
c.cur().drawc = clr
c.cur().fillc = clr
c.pdf.SetFillColor(rgb(clr))
c.pdf.SetDrawColor(rgb(clr))
c.pdf.SetTextColor(rgb(clr))
}

func (c *Canvas) Rotate(r float64) {
c.page.Rotate(float32(r))
c.pdf.TransformRotate(-r*180/math.Pi, 0, 0)
}

func (c *Canvas) Translate(pt vg.Point) {
c.page.Translate(unit(pt.X), unit(pt.Y))
xp, yp := c.pdfPoint(pt)
c.pdf.TransformTranslate(xp, yp)
}

func (c *Canvas) Scale(x float64, y float64) {
c.page.Scale(float32(x), float32(y))
c.pdf.TransformScale(x*100, y*100, 0, 0)
}

func (c *Canvas) Push() {
c.page.Push()
c.ctx = append(c.ctx, *c.cur())
c.pdf.TransformBegin()
}

func (c *Canvas) Pop() {
c.page.Pop()
c.pdf.TransformEnd()
c.ctx = c.ctx[:len(c.ctx)-1]
}

func (c *Canvas) Stroke(p vg.Path) {
if c.lineVisible {
c.page.Stroke(pdfPath(c, p))
c.pdfPath(p, "D")
}
}

func (c *Canvas) Fill(p vg.Path) {
c.page.Fill(pdfPath(c, p))
c.pdfPath(p, "F")
}

func (c *Canvas) FillString(fnt vg.Font, pt vg.Point, str string) {
t := new(pdf.Text)
t.SetFont(fnt.Name(), unit(fnt.Size))
t.NextLineOffset(unit(pt.X), unit(pt.Y))
t.Text(str)
c.page.DrawText(t)
c.font(fnt, pt)
c.pdf.SetFont(fnt.Name(), "", c.unit(fnt.Size))

c.Push()
defer c.Pop()
c.Translate(pt)
// go-fpdf uses the top left corner as origin.
c.Scale(1, -1)
left, top, right, bottom := c.sbounds(fnt, str)
w := right - left
h := bottom - top
margin := c.pdf.GetCellMargin()

c.pdf.MoveTo(-left-margin, top)
c.pdf.CellFormat(w, h, str, "", 0, "BL", false, 0, "")
}

func (c *Canvas) sbounds(fnt vg.Font, txt string) (left, top, right, bottom float64) {
_, h := c.pdf.GetFontSize()
d := c.pdf.GetFontDesc("", "")
if d.Ascent == 0 {
// not defined (standard font?), use average of 81%
top = 0.81 * h
} else {
top = -float64(d.Ascent) * h / float64(d.Ascent-d.Descent)
}
return 0, top, c.pdf.GetStringWidth(txt), top + h
}

// whitespace determines if a rune is whitespace
func whitespace(r rune) bool {
return r == ' ' || r == '\n' || r == '\t'
}

// DrawImage implements the vg.Canvas.DrawImage method.
func (c *Canvas) DrawImage(rect vg.Rectangle, img image.Image) {
r := pdf.Rectangle{
Min: pdfPoint(rect.Min.X, rect.Min.Y),
Max: pdfPoint(rect.Max.X, rect.Max.Y),
opts := pdf.ImageOptions{ImageType: "png", ReadDpi: true}
name := c.imageName()

buf := new(bytes.Buffer)
err := png.Encode(buf, img)
if err != nil {
log.Panicf("error encoding image to PNG: %v", err)
}
c.pdf.RegisterImageOptionsReader(name, opts, buf)

xp, yp := c.pdfPoint(rect.Min)
wp, hp := c.pdfPoint(rect.Size())

c.pdf.ImageOptions(name, xp, yp, wp, hp, false, opts, 0, "")
}

// font registers a font and a size with the PDF canvas.
func (c *Canvas) font(fnt vg.Font, pt vg.Point) {
if _, ok := c.fonts[fnt.Name()]; ok {
return
}
if n, ok := vg.FontMap[fnt.Name()]; ok {
json, err := fonts.Asset(n + ".json")
if err != nil {
log.Panicf("vgpdf: could not load JSON description for TTF font %q: %v", n+".json", err)
}

zfile, err := fonts.Asset(n + ".z")
if err != nil {
log.Panicf("vgpdf: could not load zip file for TTF font: %q: %v", n+".z", err)
}

c.fonts[fnt.Name()] = struct{}{}
c.pdf.AddFontFromBytes(fnt.Name(), "", json, zfile)
return
}
c.page.DrawImage(img, r)
log.Panicf("vgpdf: not implemented")
}

// pdfPath returns a pdf.Path from a vg.Path.
func pdfPath(c *Canvas, path vg.Path) *pdf.Path {
p := new(pdf.Path)
// pdfPath processes a vg.Path and applies it to the canvas.
func (c *Canvas) pdfPath(path vg.Path, style string) {
var (
xp float64
yp float64
)
for _, comp := range path {
switch comp.Type {
case vg.MoveComp:
p.Move(pdfPoint(comp.Pos.X, comp.Pos.Y))
xp, yp = c.pdfPoint(comp.Pos)
c.pdf.MoveTo(xp, yp)
case vg.LineComp:
p.Line(pdfPoint(comp.Pos.X, comp.Pos.Y))
c.pdf.LineTo(c.pdfPoint(comp.Pos))
case vg.ArcComp:
arc(p, comp)
// FIXME(sbinet): use c.pdf.ArcTo
c.arc(comp, style)
case vg.CloseComp:
p.Close()
c.pdf.LineTo(xp, yp)
c.pdf.ClosePath()
default:
panic(fmt.Sprintf("Unknown path component type: %d\n", comp.Type))
}
}
return p
c.pdf.DrawPath(style)
return
}

// Approximate a circular arc using multiple
// cubic Bézier curves, one for each π/2 segment.
//
// This is from:
// http://hansmuller-flex.blogspot.com/2011/04/approximating-circular-arc-with-cubic.html
func arc(p *pdf.Path, comp vg.PathComp) {
func (c *Canvas) arc(comp vg.PathComp, style string) {
x0 := comp.Pos.X + comp.Radius*vg.Length(math.Cos(comp.Start))
y0 := comp.Pos.Y + comp.Radius*vg.Length(math.Sin(comp.Start))
p.Line(pdfPoint(x0, y0))
c.pdf.LineTo(c.pdfPointXY(x0, y0))

a1 := comp.Start
end := a1 + comp.Angle
Expand All @@ -158,15 +269,15 @@ func arc(p *pdf.Path, comp vg.PathComp) {

for left > epsilon {
a2 := a1 + sign*math.Min(math.Pi/2, left)
partialArc(p, comp.Pos.X, comp.Pos.Y, comp.Radius, a1, a2)
c.partialArc(comp.Pos.X, comp.Pos.Y, comp.Radius, a1, a2, style)
left -= math.Abs(a2 - a1)
a1 = a2
}
}

// Approximate a circular arc of fewer than π/2
// radians with cubic Bézier curve.
func partialArc(p *pdf.Path, x, y, r vg.Length, a1, a2 float64) {
func (c *Canvas) partialArc(x, y, r vg.Length, a1, a2 float64, style string) {
a := (a2 - a1) / 2
x4 := r * vg.Length(math.Cos(a))
y4 := r * vg.Length(math.Sin(a))
Expand All @@ -190,26 +301,26 @@ func partialArc(p *pdf.Path, x, y, r vg.Length, a1, a2 float64) {
y3r := x3*sinar + y3*cosar + y
x4 = r*vg.Length(math.Cos(a2)) + x
y4 = r*vg.Length(math.Sin(a2)) + y
p.Curve(pdfPoint(x2r, y2r), pdfPoint(x3r, y3r), pdfPoint(x4, y4))
c.pdf.Curve(c.unit(x2r), c.unit(y2r), c.unit(x3r), c.unit(y3r), c.unit(x4), c.unit(y4), style)
}

func pdfPoint(x, y vg.Length) pdf.Point {
return pdf.Point{X: unit(x), Y: unit(y)}
func (c *Canvas) pdfPointXY(x, y vg.Length) (float64, float64) {
return c.unit(x), c.unit(y)
}

func pdfColor(clr color.Color) (float32, float32, float32) {
if clr == nil {
clr = color.Black
}
r, g, b, _ := clr.RGBA()
return float32(r) / math.MaxUint16,
float32(g) / math.MaxUint16,
float32(b) / math.MaxUint16
func (c *Canvas) pdfPoint(pt vg.Point) (float64, float64) {
return c.unit(pt.X), c.unit(pt.Y)
}

// unit returns a pdf.Unit, converted from a vg.Length.
func unit(l vg.Length) pdf.Unit {
return pdf.Unit(l.Points()) * pdf.Pt
// unit returns a fpdf.Unit, converted from a vg.Length.
func (c *Canvas) unit(l vg.Length) float64 {
return l.Dots(c.DPI())
}

// imageName generates a unique image name for this PDF canvas
func (c *Canvas) imageName() string {
c.nimgs++
return fmt.Sprintf("image_%03d.png", c.nimgs)
}

// WriterCounter implements the io.Writer interface, and counts
Expand All @@ -229,12 +340,26 @@ func (w *writerCounter) Write(p []byte) (int, error) {
// After calling Write, the canvas is closed
// and may no longer be used for drawing.
func (c *Canvas) WriteTo(w io.Writer) (int64, error) {
c.page.Close()
c.Pop()
c.pdf.Close()
wc := writerCounter{Writer: w}
b := bufio.NewWriter(&wc)
if err := c.doc.Encode(b); err != nil {
if err := c.pdf.Output(b); err != nil {
return wc.n, err
}
err := b.Flush()
return wc.n, err
}

const (
c255 = 255.0 / 65535.0
)

// rgb converts a Go color into a gofpdf 3-tuple int
func rgb(c color.Color) (int, int, int) {
if c == nil {
c = color.Black
}
r, g, b, _ := c.RGBA()
return int(float64(r) * c255), int(float64(g) * c255), int(float64(b) * c255)
}

0 comments on commit 8470f1d

Please sign in to comment.