diff options
author | Kurt Jung <kurt.w.jung@gmail.com> | 2015-08-12 16:16:26 -0400 |
---|---|---|
committer | Kurt Jung <kurt.w.jung@gmail.com> | 2015-08-12 16:16:26 -0400 |
commit | 64e9d36a887ca3f36dc5880153e4b062e0416ac5 (patch) | |
tree | b3e1222cdc4f7aca41b3ac39b4e2168ac77ee36e | |
parent | 798fdbd15806d1b27a36d47f394cb58b06a9af1e (diff) | |
parent | 0ab52d3f38a38a63fe1bba1a757fe4f28cd6a026 (diff) |
Merge branch 'marcusatbang-templating'
-rw-r--r-- | def.go | 2 | ||||
-rw-r--r-- | doc.go | 5 | ||||
-rw-r--r-- | fpdf.go | 9 | ||||
-rw-r--r-- | fpdf_test.go | 53 | ||||
-rw-r--r-- | template.go | 204 | ||||
-rw-r--r-- | template_impl.go | 122 | ||||
-rw-r--r-- | util.go | 34 |
7 files changed, 428 insertions, 1 deletions
@@ -160,6 +160,8 @@ type Fpdf struct { page int // current page number n int // current object number offsets []int // array of object offsets + templates map[int64]Template // templates used in this document + templateObjects map[int64]int //template object IDs within this document buffer fmtBuffer // buffer holding in-memory PDF pages []*bytes.Buffer // slice[page] of page content; 1-based state int // current document state @@ -48,6 +48,8 @@ Features • Layers +• Templates + 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 fonts. However, support is @@ -193,7 +195,8 @@ implementations for dashed line drawing and generalized font loading. Stani Michiels provided support for multi-segment path drawing with smooth line joins, line join styles, enhanced fill modes, and has helped greatly with package presentation and tests. Bruno Michel has provided valuable assistance -with the code. +with the code. Templating is adapted from the FPDF_Tpl library created by +Jan Slabon and Setasign. Roadmap @@ -72,6 +72,8 @@ func fpdfNew(orientationStr, unitStr, sizeStr, fontDirStr string, size SizeType) f.fonts = make(map[string]fontDefType) f.fontFiles = make(map[string]fontFileType) f.diffs = make([]string, 0, 8) + f.templates = make(map[int64]Template) + f.templateObjects = make(map[int64]int) f.images = make(map[string]*ImageInfoType) f.pageLinks = make([][]linkType, 0, 8) f.pageLinks = append(f.pageLinks, make([]linkType, 0, 0)) // pageLinks[0] is unused (1-based) @@ -3223,6 +3225,12 @@ func (f *Fpdf) putxobjectdict() { // foreach($this->images as $image) f.outf("/I%d %d 0 R", image.i, image.n) } + for _, tpl := range f.templates { + id := tpl.ID() + if objID, ok := f.templateObjects[id]; ok { + f.outf("/TPL%d %d 0 R", id, objID) + } + } } func (f *Fpdf) putresourcedict() { @@ -3305,6 +3313,7 @@ func (f *Fpdf) putresources() { return } f.putimages() + f.putTemplates() // Resource dictionary f.offsets[2] = f.buffer.Len() f.out("2 0 obj") diff --git a/fpdf_test.go b/fpdf_test.go index 57a2dd4..e4b8df4 100644 --- a/fpdf_test.go +++ b/fpdf_test.go @@ -1686,3 +1686,56 @@ func ExampleFpdf_DrawPath() { // Output: // Successfully generated pdf/Fpdf_DrawPath_fill.pdf } + +// This example demonstrates creating and using templates +func ExampleFpdf_CreateTemplate() { + pdf := gofpdf.New("P", "mm", "A4", "") + pdf.SetCompression(false) + // pdf.SetFont("Times", "", 12) + template := pdf.CreateTemplate(func(tpl *gofpdf.Tpl) { + tpl.Image(imageFile("logo.png"), 6, 6, 30, 0, false, "", 0, "") + tpl.SetFont("Arial", "B", 16) + tpl.Text(40, 20, "Template says hello") + tpl.SetDrawColor(0, 100, 200) + tpl.SetLineWidth(2.5) + tpl.Line(95, 12, 105, 22) + }) + _, tplSize := template.Size() + // fmt.Println("Size:", tplSize) + // fmt.Println("Scaled:", tplSize.ScaleBy(1.5)) + + template2 := pdf.CreateTemplate(func(tpl *gofpdf.Tpl) { + tpl.UseTemplate(template) + subtemplate := tpl.CreateTemplate(func(tpl2 *gofpdf.Tpl) { + tpl2.Image(imageFile("logo.png"), 6, 86, 30, 0, false, "", 0, "") + tpl2.SetFont("Arial", "B", 16) + tpl2.Text(40, 100, "Subtemplate says hello") + tpl2.SetDrawColor(0, 200, 100) + tpl2.SetLineWidth(2.5) + tpl2.Line(102, 92, 112, 102) + }) + tpl.UseTemplate(subtemplate) + }) + + pdf.SetDrawColor(200, 100, 0) + pdf.SetLineWidth(2.5) + pdf.SetFont("Arial", "B", 16) + + pdf.AddPage() + pdf.UseTemplate(template) + pdf.UseTemplateScaled(template, gofpdf.PointType{0, 30}, tplSize) + pdf.UseTemplateScaled(template, gofpdf.PointType{0, 60}, tplSize.ScaleBy(1.4)) + pdf.Line(40, 210, 60, 210) + pdf.Text(40, 200, "Template example page 1") + + pdf.AddPage() + pdf.UseTemplate(template2) + pdf.Line(60, 210, 80, 210) + pdf.Text(40, 200, "Template example page 2") + + fileStr := exampleFilename("Fpdf_CreateTemplate") + err := pdf.OutputFileAndClose(fileStr) + summary(err, fileStr) + // Output: + // Successfully generated pdf/Fpdf_CreateTemplate.pdf +} diff --git a/template.go b/template.go new file mode 100644 index 0000000..7561ae1 --- /dev/null +++ b/template.go @@ -0,0 +1,204 @@ +package gofpdf + +/* + * Copyright (c) 2015 Kurt Jung (Gmail: kurt.w.jung), + * Marcus Downing, Jan Slabon (Setasign) + * + * 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. + */ + +// CreateTemplate defines a new template using the current page size. +func (f *Fpdf) CreateTemplate(fn func(*Tpl)) Template { + return newTpl(PointType{0, 0}, f.curPageSize, f.unitStr, f.fontDirStr, fn, f) +} + +// CreateTemplateCustom starts a template, using the given bounds. +func (f *Fpdf) CreateTemplateCustom(corner PointType, size SizeType, fn func(*Tpl)) Template { + return newTpl(corner, size, f.unitStr, f.fontDirStr, fn, f) +} + +// CreateTemplate creates a template not attached to any document +func CreateTemplate(corner PointType, size SizeType, unitStr, fontDirStr string, fn func(*Tpl)) Template { + return newTpl(corner, size, unitStr, fontDirStr, fn, nil) +} + +// UseTemplate adds a template to the current page or another template, +// using the size and position at which it was originally written. +func (f *Fpdf) UseTemplate(t Template) { + if t == nil { + f.SetErrorf("Template is nil") + return + } + corner, size := t.Size() + f.UseTemplateScaled(t, corner, size) +} + +// UseTemplateScaled adds a template to the current page or another template, +// using the given page coordinates. +func (f *Fpdf) UseTemplateScaled(t Template, corner PointType, size SizeType) { + if t == nil { + f.SetErrorf("Template is nil") + return + } + + // You have to add at least a page first + if f.page <= 0 { + f.SetErrorf("Cannot use a template without first adding a page") + return + } + + // make a note of the fact that we actually use this template, as well as any other templates, + // images or fonts it uses + f.templates[t.ID()] = t + for _, tt := range t.Templates() { + f.templates[tt.ID()] = tt + } + for name, ti := range t.Images() { + name = sprintf("t%d-%s", t.ID(), name) + f.images[name] = ti + } + + // template data + _, templateSize := t.Size() + scaleX := size.Wd / templateSize.Wd + scaleY := size.Ht / templateSize.Ht + tx := corner.X * f.k + ty := (f.curPageSize.Ht - corner.Y - size.Ht) * f.k + + f.outf("q %.4F 0 0 %.4F %.4F %.4F cm", scaleX, scaleY, tx, ty) // Translate + f.outf("/TPL%d Do Q", t.ID()) +} + +var nextTemplateIDChannel = func() chan int64 { + ch := make(chan int64) + go func() { + var nextID int64 = 1 + for { + ch <- nextID + nextID++ + } + }() + return ch +}() + +// GenerateTemplateID gives the next template ID. These numbers are global so that they can never clash. +func GenerateTemplateID() int64 { + return <-nextTemplateIDChannel +} + +// Template is an object that can be written to, then used and re-used any number of times within a document. +type Template interface { + ID() int64 + Size() (PointType, SizeType) + Bytes() []byte + Images() map[string]*ImageInfoType + Templates() []Template +} + +// putTemplates writes the templates to the PDF +func (f *Fpdf) putTemplates() { + filter := "" + if f.compress { + filter = "/Filter /FlateDecode " + } + + templates := sortTemplates(f.templates) + var t Template + for _, t = range templates { + corner, size := t.Size() + + f.newobj() + f.templateObjects[t.ID()] = f.n + f.outf("<<%s/Type /XObject", filter) + f.out("/Subtype /Form") + f.out("/Formtype 1") + f.outf("/BBox [%.2F %.2F %.2F %.2F]", corner.X*f.k, corner.Y*f.k, (corner.X+size.Wd)*f.k, (corner.Y+size.Ht)*f.k) + if corner.X != 0 || corner.Y != 0 { + f.outf("/Matrix [1 0 0 1 %.5F %.5F]", -corner.X*f.k*2, corner.Y*f.k*2) + } + + // Template's resource dictionary + f.out("/Resources ") + f.out("<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]") + + tImages := t.Images() + tTemplates := t.Templates() + if len(tImages) > 0 || len(tTemplates) > 0 { + f.out("/XObject <<") + for _, ti := range tImages { + f.outf("/I%d %d 0 R", ti.i, ti.n) + } + for _, tt := range tTemplates { + id := tt.ID() + if objID, ok := f.templateObjects[id]; ok { + f.outf("/TPL%d %d 0 R", id, objID) + } + } + f.out(">>") + } + + f.out(">>") + + // Write the template's byte stream + buffer := t.Bytes() + // fmt.Println("Put template bytes", string(buffer[:])) + if f.compress { + buffer = sliceCompress(buffer) + } + f.outf("/Length %d >>", len(buffer)) + f.putstream(buffer) + f.out("endobj") + } +} + +// sortTemplates puts templates in a suitable order based on dependices +func sortTemplates(templates map[int64]Template) []Template { + chain := make([]Template, 0, len(templates)*2) + + // build a full set of dependency chains + for _, t := range templates { + tlist := templateChainDependencies(t) + for _, tt := range tlist { + if tt != nil { + chain = append(chain, tt) + } + } + } + + // reduce that to make a simple list + sorted := make([]Template, 0, len(templates)) +chain: + for _, t := range chain { + for _, already := range sorted { + if t == already { + continue chain + } + } + sorted = append(sorted, t) + } + + return sorted +} + +// templateChainDependencies is a recursive function for determining the full chain of template dependencies +func templateChainDependencies(template Template) []Template { + requires := template.Templates() + chain := make([]Template, len(requires)*2) + for _, req := range requires { + for _, sub := range templateChainDependencies(req) { + chain = append(chain, sub) + } + } + chain = append(chain, template) + return chain +} diff --git a/template_impl.go b/template_impl.go new file mode 100644 index 0000000..093e1fd --- /dev/null +++ b/template_impl.go @@ -0,0 +1,122 @@ +package gofpdf + +/* + * Copyright (c) 2015 Kurt Jung (Gmail: kurt.w.jung), + * Marcus Downing, Jan Slabon (Setasign) + * + * 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. + */ + +// newTpl creates a template, copying graphics settings from a template if one is given +func newTpl(corner PointType, size SizeType, unitStr, fontDirStr string, fn func(*Tpl), copyFrom *Fpdf) Template { + orientationStr := "p" + if size.Wd > size.Ht { + orientationStr = "l" + } + sizeStr := "" + + fpdf := fpdfNew(orientationStr, unitStr, sizeStr, fontDirStr, size) + tpl := Tpl{*fpdf} + if copyFrom != nil { + tpl.loadParamsFromFpdf(copyFrom) + } + tpl.Fpdf.SetAutoPageBreak(false, 0) + tpl.Fpdf.AddPage() + fn(&tpl) + bytes := tpl.Fpdf.pages[tpl.Fpdf.page].Bytes() + templates := make([]Template, 0, len(tpl.Fpdf.templates)) + for _, t := range tpl.Fpdf.templates { + templates = append(templates, t) + } + images := tpl.Fpdf.images + + id := GenerateTemplateID() + template := FpdfTpl{id, corner, size, bytes, images, templates} + return &template +} + +// FpdfTpl is a concrete implementation of the Template interface. +type FpdfTpl struct { + id int64 + corner PointType + size SizeType + bytes []byte + images map[string]*ImageInfoType + templates []Template +} + +// ID returns the global template identifier +func (t *FpdfTpl) ID() int64 { + return t.id +} + +// Size gives the bounding dimensions of this template +func (t *FpdfTpl) Size() (corner PointType, size SizeType) { + return t.corner, t.size +} + +// Bytes returns the actual template data, not including resources +func (t *FpdfTpl) Bytes() []byte { + return t.bytes +} + +// Images returns a list of the images used in this template +func (t *FpdfTpl) Images() map[string]*ImageInfoType { + return t.images +} + +// Templates returns a list of templates used in this template +func (t *FpdfTpl) Templates() []Template { + return t.templates +} + +// Tpl is an Fpdf used for writing a template. +// It has most of the facilities of an Fpdf,but cannot add more pages. +// Tpl is used directly only during the limited time a template is writable. +type Tpl struct { + Fpdf +} + +func (t *Tpl) loadParamsFromFpdf(f *Fpdf) { + t.Fpdf.compress = false + + t.Fpdf.k = f.k + t.Fpdf.x = f.x + t.Fpdf.y = f.y + t.Fpdf.lineWidth = f.lineWidth + t.Fpdf.capStyle = f.capStyle + t.Fpdf.joinStyle = f.joinStyle + + t.Fpdf.color.draw = f.color.draw + t.Fpdf.color.fill = f.color.fill + t.Fpdf.color.text = f.color.text + + t.Fpdf.currentFont = f.currentFont + t.Fpdf.fontFamily = f.fontFamily + t.Fpdf.fontSize = f.fontSize + t.Fpdf.fontSizePt = f.fontSizePt + t.Fpdf.fontStyle = f.fontStyle + t.Fpdf.ws = f.ws +} + +// AddPage does nothing because you cannot add pages to a template +func (t *Tpl) AddPage() { +} + +// AddPageFormat does nothign becasue you cannot add pages to a template +func (t *Tpl) AddPageFormat(orientationStr string, size SizeType) { +} + +// SetAutoPageBreak does nothing because you cannot add pages to a template +func (t *Tpl) SetAutoPageBreak(auto bool, margin float64) { +} @@ -278,3 +278,37 @@ func (f *Fpdf) UnicodeTranslatorFromDescriptor(cpStr string) (rep func(string) s } return } + +// Transform moves a point by given X, Y offset +func (p *PointType) Transform(x, y float64) PointType { + return PointType{p.X + x, p.Y + y} +} + +// Orientation returns the orientation of a given size: +// "P" for portrait, "L" for landscape +func (s *SizeType) Orientation() string { + if s == nil || s.Ht == s.Wd { + return "" + } + if s.Wd > s.Ht { + return "L" + } + return "P" +} + +// ScaleBy expands a size by a certain factor +func (s *SizeType) ScaleBy(factor float64) SizeType { + return SizeType{s.Wd * factor, s.Ht * factor} +} + +// ScaleToWidth adjusts the height of a size to match the given width +func (s *SizeType) ScaleToWidth(width float64) SizeType { + height := s.Ht * width / s.Wd + return SizeType{width, height} +} + +// ScaleToHeight adjsuts the width of a size to match the given height +func (s *SizeType) ScaleToHeight(height float64) SizeType { + width := s.Wd * height / s.Ht + return SizeType{width, height} +} |