From 8f46928acb094c8838f75d663b2f560dbf934773 Mon Sep 17 00:00:00 2001 From: Marcus Downing Date: Tue, 4 Aug 2015 08:20:25 +0100 Subject: Templating --- fpdf_test.go | 55 ++++++++++++++- template.go | 209 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ template_impl.go | 131 ++++++++++++++++++++++++++++++++++ 3 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 template.go create mode 100644 template_impl.go diff --git a/fpdf_test.go b/fpdf_test.go index 57a2dd4..c21b1ad 100644 --- a/fpdf_test.go +++ b/fpdf_test.go @@ -29,7 +29,7 @@ import ( "strconv" "strings" - "github.com/jung-kurt/gofpdf" + "github.com/marcusatbang/gofpdf" ) // Absolute path needed for gocov tool; relative OK for test @@ -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("template") + err := pdf.OutputFileAndClose(fileStr) + summary(err, fileStr) + // Output: + // Successfully generated pdf/template.pdf +} diff --git a/template.go b/template.go new file mode 100644 index 0000000..17cea11 --- /dev/null +++ b/template.go @@ -0,0 +1,209 @@ +package gofpdf + +// +// GoFPDI +// +// Copyright 2015 Marcus Downing +// +// FPDI +// +// Copyright 2004-2014 Setasign - Jan Slabon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// 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("< 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..9879699 --- /dev/null +++ b/template_impl.go @@ -0,0 +1,131 @@ +package gofpdf + +// +// GoFPDI +// +// Copyright 2015 Marcus Downing +// +// FPDI +// +// Copyright 2004-2014 Setasign - Jan Slabon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import ( +// "bytes" +) + +// 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) { +} -- cgit v1.2.1-24-ge1ad From 480d3959083ea26fed145ecf7e75361510c4e998 Mon Sep 17 00:00:00 2001 From: Marcus Downing Date: Tue, 4 Aug 2015 08:29:46 +0100 Subject: Templating fixes --- def.go | 2 ++ fpdf.go | 9 +++++++++ fpdf_test.go | 6 +++--- util.go | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/def.go b/def.go index 437449c..f1c8d7b 100644 --- a/def.go +++ b/def.go @@ -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 diff --git a/fpdf.go b/fpdf.go index 22de11d..dfd721f 100644 --- a/fpdf.go +++ b/fpdf.go @@ -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 c21b1ad..e4b8df4 100644 --- a/fpdf_test.go +++ b/fpdf_test.go @@ -29,7 +29,7 @@ import ( "strconv" "strings" - "github.com/marcusatbang/gofpdf" + "github.com/jung-kurt/gofpdf" ) // Absolute path needed for gocov tool; relative OK for test @@ -1733,9 +1733,9 @@ func ExampleFpdf_CreateTemplate() { pdf.Line(60, 210, 80, 210) pdf.Text(40, 200, "Template example page 2") - fileStr := exampleFilename("template") + fileStr := exampleFilename("Fpdf_CreateTemplate") err := pdf.OutputFileAndClose(fileStr) summary(err, fileStr) // Output: - // Successfully generated pdf/template.pdf + // Successfully generated pdf/Fpdf_CreateTemplate.pdf } diff --git a/util.go b/util.go index ce391f5..11cb016 100644 --- a/util.go +++ b/util.go @@ -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} +} -- cgit v1.2.1-24-ge1ad From 3c7e84a50bb3a1b617b6820d23ba2edcadf4fd6c Mon Sep 17 00:00:00 2001 From: Marcus Downing Date: Wed, 12 Aug 2015 20:27:09 +0100 Subject: License and attribution --- doc.go | 5 ++++- template.go | 37 ++++++++++++++++--------------------- template_impl.go | 41 ++++++++++++++++------------------------- 3 files changed, 36 insertions(+), 47 deletions(-) diff --git a/doc.go b/doc.go index 398ab9b..941f399 100644 --- a/doc.go +++ b/doc.go @@ -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 diff --git a/template.go b/template.go index 17cea11..7561ae1 100644 --- a/template.go +++ b/template.go @@ -1,26 +1,21 @@ package gofpdf -// -// GoFPDI -// -// Copyright 2015 Marcus Downing -// -// FPDI -// -// Copyright 2004-2014 Setasign - Jan Slabon -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// +/* + * 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 { diff --git a/template_impl.go b/template_impl.go index 9879699..093e1fd 100644 --- a/template_impl.go +++ b/template_impl.go @@ -1,30 +1,21 @@ package gofpdf -// -// GoFPDI -// -// Copyright 2015 Marcus Downing -// -// FPDI -// -// Copyright 2004-2014 Setasign - Jan Slabon -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import ( -// "bytes" -) +/* + * 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 { -- cgit v1.2.1-24-ge1ad