commit
5afea81825
156 changed files with 30287 additions and 0 deletions
@ -0,0 +1,21 @@ |
|||
MIT License |
|||
|
|||
Copyright (c) 2017 Kurt Jung and contributors acknowledged in the documentation |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all |
|||
copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
@ -0,0 +1,29 @@ |
|||
all : documentation |
|||
|
|||
documentation : doc/index.html doc.go README.md |
|||
|
|||
cov : all |
|||
go test -v -coverprofile=coverage && go tool cover -html=coverage -o=coverage.html |
|||
|
|||
check : |
|||
golint . |
|||
go vet -all . |
|||
gofmt -s -l . |
|||
goreportcard-cli -v | grep -v cyclomatic |
|||
|
|||
README.md : doc/document.md |
|||
pandoc --read=markdown --write=gfm < $< > $@ |
|||
|
|||
doc/index.html : doc/document.md doc/html.txt |
|||
pandoc --read=markdown --write=html --template=doc/html.txt \
|
|||
--metadata pagetitle="GoFPDF Document Generator" < $< > $@ |
|||
|
|||
doc.go : doc/document.md doc/go.awk |
|||
pandoc --read=markdown --write=plain $< | awk --assign=package_name=gofpdf --file=doc/go.awk > $@ |
|||
gofmt -s -w $@ |
|||
|
|||
build : |
|||
go build -v |
|||
|
|||
clean : |
|||
rm -f coverage.html coverage doc/index.html doc.go README.md |
@ -0,0 +1,282 @@ |
|||
# GoFPDF document generator |
|||
|
|||
[](http://unmaintained.tech/) |
|||
[](https://raw.githubusercontent.com/jung-kurt/gofpdf/master/LICENSE) |
|||
[](https://goreportcard.com/report/github.com/jung-kurt/gofpdf) |
|||
[](https://godoc.org/github.com/jung-kurt/gofpdf) |
|||
|
|||
 |
|||
|
|||
Package gofpdf implements a PDF document generator with high level |
|||
support for text, drawing and images. |
|||
|
|||
## Features |
|||
|
|||
- UTF-8 support |
|||
- Choice of measurement unit, page format and margins |
|||
- Page header and footer management |
|||
- Automatic page breaks, line breaks, and text justification |
|||
- Inclusion of JPEG, PNG, GIF, TIFF and basic path-only SVG images |
|||
- Colors, gradients and alpha channel transparency |
|||
- Outline bookmarks |
|||
- Internal and external links |
|||
- TrueType, Type1 and encoding support |
|||
- Page compression |
|||
- Lines, Bรฉzier curves, arcs, and ellipses |
|||
- Rotation, scaling, skewing, translation, and mirroring |
|||
- Clipping |
|||
- Document protection |
|||
- Layers |
|||
- Templates |
|||
- Barcodes |
|||
- Charting facility |
|||
- Import PDFs as templates |
|||
|
|||
gofpdf has no dependencies other than the Go standard library. All tests |
|||
pass on Linux, Mac and Windows platforms. |
|||
|
|||
gofpdf supports UTF-8 TrueType fonts and โright-to-leftโ languages. Note |
|||
that Chinese, Japanese, and Korean characters may not be included in |
|||
many general purpose fonts. For these languages, a specialized font (for |
|||
example, |
|||
[NotoSansSC](https://github.com/jsntn/webfonts/blob/master/NotoSansSC-Regular.ttf) |
|||
for simplified Chinese) can be used. |
|||
|
|||
Also, support is provided to automatically translate UTF-8 runes to code |
|||
page encodings for languages that have fewer than 256 glyphs. |
|||
|
|||
## We Are Closed |
|||
|
|||
This repository will not be maintained, at least for some unknown |
|||
duration. But it is hoped that gofpdf has a bright future in the open |
|||
source world. Due to Goโs promise of compatibility, gofpdf should |
|||
continue to function without modification for a longer time than would |
|||
be the case with many other languages. |
|||
|
|||
Forks should be based on the [last viable |
|||
commit](https://github.com/jung-kurt/gofpdf/commit/603f56990463f011cb1dbb64ef7f872c1adc009a). |
|||
Tools such as |
|||
[active-forks](https://techgaun.github.io/active-forks/index.html#jung-kurt/gofpdf) |
|||
can be used to select a fork that looks promising for your needs. If a |
|||
particular fork looks like it has taken the lead in attracting |
|||
followers, this README will be updated to point people in that |
|||
direction. |
|||
|
|||
The efforts of all contributors to this project have been deeply |
|||
appreciated. Best wishes to all of you. |
|||
|
|||
## Installation |
|||
|
|||
To install the package on your system, run |
|||
|
|||
``` shell |
|||
go get github.com/jung-kurt/gofpdf |
|||
``` |
|||
|
|||
Later, to receive updates, run |
|||
|
|||
``` shell |
|||
go get -u -v github.com/jung-kurt/gofpdf/... |
|||
``` |
|||
|
|||
## Quick Start |
|||
|
|||
The following Go code generates a simple PDF file. |
|||
|
|||
``` go |
|||
pdf := gofpdf.New("P", "mm", "A4", "") |
|||
pdf.AddPage() |
|||
pdf.SetFont("Arial", "B", 16) |
|||
pdf.Cell(40, 10, "Hello, world") |
|||
err := pdf.OutputFileAndClose("hello.pdf") |
|||
``` |
|||
|
|||
See the functions in the |
|||
[fpdf\_test.go](https://github.com/jung-kurt/gofpdf/blob/master/fpdf_test.go) |
|||
file (shown as examples in this documentation) for more advanced PDF |
|||
examples. |
|||
|
|||
## Errors |
|||
|
|||
If an error occurs in an Fpdf method, an internal error field is set. |
|||
After this occurs, Fpdf method calls typically return without performing |
|||
any operations and the error state is retained. This error management |
|||
scheme facilitates PDF generation since individual method calls do not |
|||
need to be examined for failure; it is generally sufficient to wait |
|||
until after `Output()` is called. For the same reason, if an error |
|||
occurs in the calling application during PDF generation, it may be |
|||
desirable for the application to transfer the error to the Fpdf instance |
|||
by calling the `SetError()` method or the `SetErrorf()` method. At any |
|||
time during the life cycle of the Fpdf instance, the error state can be |
|||
determined with a call to `Ok()` or `Err()`. The error itself can be |
|||
retrieved with a call to `Error()`. |
|||
|
|||
## Conversion Notes |
|||
|
|||
This package is a relatively straightforward translation from the |
|||
original [FPDF](http://www.fpdf.org/) library written in PHP (despite |
|||
the caveat in the introduction to [Effective |
|||
Go](https://golang.org/doc/effective_go.html)). The API names have been |
|||
retained even though the Go idiom would suggest otherwise (for example, |
|||
`pdf.GetX()` is used rather than simply `pdf.X()`). The similarity of |
|||
the two libraries makes the original FPDF website a good source of |
|||
information. It includes a forum and FAQ. |
|||
|
|||
However, some internal changes have been made. Page content is built up |
|||
using buffers (of type bytes.Buffer) rather than repeated string |
|||
concatenation. Errors are handled as explained above rather than |
|||
panicking. Output is generated through an interface of type io.Writer or |
|||
io.WriteCloser. A number of the original PHP methods behave differently |
|||
based on the type of the arguments that are passed to them; in these |
|||
cases additional methods have been exported to provide similar |
|||
functionality. Font definition files are produced in JSON rather than |
|||
PHP. |
|||
|
|||
## Example PDFs |
|||
|
|||
A side effect of running `go test ./...` is the production of a number |
|||
of example PDFs. These can be found in the gofpdf/pdf directory after |
|||
the tests complete. |
|||
|
|||
Please note that these examples run in the context of a test. In order |
|||
run an example as a standalone application, youโll need to examine |
|||
[fpdf\_test.go](https://github.com/jung-kurt/gofpdf/blob/master/fpdf_test.go) |
|||
for some helper routines, for example `exampleFilename()` and |
|||
`summary()`. |
|||
|
|||
Example PDFs can be compared with reference copies in order to verify |
|||
that they have been generated as expected. This comparison will be |
|||
performed if a PDF with the same name as the example PDF is placed in |
|||
the gofpdf/pdf/reference directory and if the third argument to |
|||
`ComparePDFFiles()` in internal/example/example.go is true. (By default |
|||
it is false.) The routine that summarizes an example will look for this |
|||
file and, if found, will call `ComparePDFFiles()` to check the example |
|||
PDF for equality with its reference PDF. If differences exist between |
|||
the two files they will be printed to standard output and the test will |
|||
fail. If the reference file is missing, the comparison is considered to |
|||
succeed. In order to successfully compare two PDFs, the placement of |
|||
internal resources must be consistent and the internal creation |
|||
timestamps must be the same. To do this, the methods `SetCatalogSort()` |
|||
and `SetCreationDate()` need to be called for both files. This is done |
|||
automatically for all examples. |
|||
|
|||
## Nonstandard Fonts |
|||
|
|||
Nothing special is required to use the standard PDF fonts (courier, |
|||
helvetica, times, zapfdingbats) in your documents other than calling |
|||
`SetFont()`. |
|||
|
|||
You should use `AddUTF8Font()` or `AddUTF8FontFromBytes()` to add a |
|||
TrueType UTF-8 encoded font. Use `RTL()` and `LTR()` methods switch |
|||
between โright-to-leftโ and โleft-to-rightโ mode. |
|||
|
|||
In order to use a different non-UTF-8 TrueType or Type1 font, you will |
|||
need to generate a font definition file and, if the font will be |
|||
embedded into PDFs, a compressed version of the font file. This is done |
|||
by calling the MakeFont function or using the included makefont command |
|||
line utility. To create the utility, cd into the makefont subdirectory |
|||
and run โgo buildโ. This will produce a standalone executable named |
|||
makefont. Select the appropriate encoding file from the font |
|||
subdirectory and run the command as in the following example. |
|||
|
|||
``` shell |
|||
./makefont --embed --enc=../font/cp1252.map --dst=../font ../font/calligra.ttf |
|||
``` |
|||
|
|||
In your PDF generation code, call `AddFont()` to load the font and, as |
|||
with the standard fonts, SetFont() to begin using it. Most examples, |
|||
including the package example, demonstrate this method. Good sources of |
|||
free, open-source fonts include [Google |
|||
Fonts](https://fonts.google.com/) and [DejaVu |
|||
Fonts](http://dejavu-fonts.org/). |
|||
|
|||
## Related Packages |
|||
|
|||
The [draw2d](https://github.com/llgcode/draw2d) package is a two |
|||
dimensional vector graphics library that can generate output in |
|||
different forms. It uses gofpdf for its document production mode. |
|||
|
|||
## Contributing Changes |
|||
|
|||
gofpdf is a global community effort and you are invited to make it even |
|||
better. If you have implemented a new feature or corrected a problem, |
|||
please consider contributing your change to the project. A contribution |
|||
that does not directly pertain to the core functionality of gofpdf |
|||
should be placed in its own directory directly beneath the `contrib` |
|||
directory. |
|||
|
|||
Here are guidelines for making submissions. Your change should |
|||
|
|||
- be compatible with the MIT License |
|||
- be properly documented |
|||
- be formatted with `go fmt` |
|||
- include an example in |
|||
[fpdf\_test.go](https://github.com/jung-kurt/gofpdf/blob/master/fpdf_test.go) |
|||
if appropriate |
|||
- conform to the standards of [golint](https://github.com/golang/lint) |
|||
and [go vet](https://golang.org/cmd/vet/), that is, `golint .` and |
|||
`go vet .` should not generate any warnings |
|||
- not diminish [test coverage](https://blog.golang.org/cover) |
|||
|
|||
[Pull requests](https://help.github.com/articles/using-pull-requests/) |
|||
are the preferred means of accepting your changes. |
|||
|
|||
## License |
|||
|
|||
gofpdf is released under the MIT License. It is copyrighted by Kurt Jung |
|||
and the contributors acknowledged below. |
|||
|
|||
## Acknowledgments |
|||
|
|||
This packageโs code and documentation are closely derived from the |
|||
[FPDF](http://www.fpdf.org/) library created by Olivier Plathey, and a |
|||
number of font and image resources are copied directly from it. Bruno |
|||
Michel has provided valuable assistance with the code. 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. Layer support is adapted |
|||
from Olivier Plathey. Support for transformations is adapted from the |
|||
FPDF transformation script by Moritz Wagner and Andreas Wรผrmser. PDF |
|||
protection is adapted from the work of Klemen Vodopivec for the FPDF |
|||
product. Lawrence Kesteloot provided code to allow an imageโs extent to |
|||
be determined prior to placement. Support for vertical alignment within |
|||
a cell was provided by Stefan Schroeder. Ivan Daniluk generalized the |
|||
font and image loading code to use the Reader interface while |
|||
maintaining backward compatibility. Anthony Starks provided code for the |
|||
Polygon function. Robert Lillack provided the Beziergon function and |
|||
corrected some naming issues with the internal curve function. Claudio |
|||
Felber provided 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. Templating |
|||
is adapted by Marcus Downing from the FPDF\_Tpl library created by Jan |
|||
Slabon and Setasign. Jelmer Snoeck contributed packages that generate a |
|||
variety of barcodes and help with registering images on the web. Jelmer |
|||
Snoek and Guillermo Pascual augmented the basic HTML functionality with |
|||
aligned text. Kent Quirk implemented backwards-compatible support for |
|||
reading DPI from images that support it, and for setting DPI manually |
|||
and then having it properly taken into account when calculating image |
|||
size. Paulo Coutinho provided support for static embedded fonts. Dan |
|||
Meyers added support for embedded JavaScript. David Fish added a generic |
|||
alias-replacement function to enable, among other things, table of |
|||
contents functionality. Andy Bakun identified and corrected a problem in |
|||
which the internal catalogs were not sorted stably. Paul Montag added |
|||
encoding and decoding functionality for templates, including images that |
|||
are embedded in templates; this allows templates to be stored |
|||
independently of gofpdf. Paul also added support for page boxes used in |
|||
printing PDF documents. Wojciech Matusiak added supported for word |
|||
spacing. Artem Korotkiy added support of UTF-8 fonts. Dave Barnes added |
|||
support for imported objects and templates. Brigham Thompson added |
|||
support for rounded rectangles. Joe Westcott added underline |
|||
functionality and optimized image storage. Benoit KUGLER contributed |
|||
support for rectangles with corners of unequal radius, modification |
|||
times, and for file attachments and annotations. |
|||
|
|||
## Roadmap |
|||
|
|||
- Remove all legacy code page font support; use UTF-8 exclusively |
|||
- Improve test coverage as reported by the coverage tool. |
@ -0,0 +1,157 @@ |
|||
package gofpdf |
|||
|
|||
import ( |
|||
"crypto/md5" |
|||
"encoding/hex" |
|||
"fmt" |
|||
"strings" |
|||
) |
|||
|
|||
// Attachment defines a content to be included in the pdf, in one
|
|||
// of the following ways :
|
|||
// - associated with the document as a whole : see SetAttachments()
|
|||
// - accessible via a link localized on a page : see AddAttachmentAnnotation()
|
|||
type Attachment struct { |
|||
Content []byte |
|||
|
|||
// Filename is the displayed name of the attachment
|
|||
Filename string |
|||
|
|||
// Description is only displayed when using AddAttachmentAnnotation(),
|
|||
// and might be modified by the pdf reader.
|
|||
Description string |
|||
|
|||
objectNumber int // filled when content is included
|
|||
} |
|||
|
|||
// return the hex encoded checksum of `data`
|
|||
func checksum(data []byte) string { |
|||
tmp := md5.Sum(data) |
|||
sl := make([]byte, len(tmp)) |
|||
for i, v := range tmp { |
|||
sl[i] = v |
|||
} |
|||
return hex.EncodeToString(sl) |
|||
} |
|||
|
|||
// Writes a compressed file like object as ``/EmbeddedFile``. Compressing is
|
|||
// done with deflate. Includes length, compressed length and MD5 checksum.
|
|||
func (f *Fpdf) writeCompressedFileObject(content []byte) { |
|||
lenUncompressed := len(content) |
|||
sum := checksum(content) |
|||
compressed := sliceCompress(content) |
|||
lenCompressed := len(compressed) |
|||
f.newobj() |
|||
f.outf("<< /Type /EmbeddedFile /Length %d /Filter /FlateDecode /Params << /CheckSum <%s> /Size %d >> >>\n", |
|||
lenCompressed, sum, lenUncompressed) |
|||
f.putstream(compressed) |
|||
f.out("endobj") |
|||
} |
|||
|
|||
// Embed includes the content of `a`, and update its internal reference.
|
|||
func (f *Fpdf) embed(a *Attachment) { |
|||
if a.objectNumber != 0 { // already embedded (objectNumber start at 2)
|
|||
return |
|||
} |
|||
oldState := f.state |
|||
f.state = 1 // we write file content in the main buffer
|
|||
f.writeCompressedFileObject(a.Content) |
|||
streamID := f.n |
|||
f.newobj() |
|||
f.outf("<< /Type /Filespec /F () /UF %s /EF << /F %d 0 R >> /Desc %s\n>>", |
|||
f.textstring(utf8toutf16(a.Filename)), |
|||
streamID, |
|||
f.textstring(utf8toutf16(a.Description))) |
|||
f.out("endobj") |
|||
a.objectNumber = f.n |
|||
f.state = oldState |
|||
} |
|||
|
|||
// SetAttachments writes attachments as embedded files (document attachment).
|
|||
// These attachments are global, see AddAttachmentAnnotation() for a link
|
|||
// anchored in a page. Note that only the last call of SetAttachments is
|
|||
// useful, previous calls are discarded. Be aware that not all PDF readers
|
|||
// support document attachments. See the SetAttachment example for a
|
|||
// demonstration of this method.
|
|||
func (f *Fpdf) SetAttachments(as []Attachment) { |
|||
f.attachments = as |
|||
} |
|||
|
|||
// embed current attachments. store object numbers
|
|||
// for later use by getEmbeddedFiles()
|
|||
func (f *Fpdf) putAttachments() { |
|||
for i, a := range f.attachments { |
|||
f.embed(&a) |
|||
f.attachments[i] = a |
|||
} |
|||
} |
|||
|
|||
// return /EmbeddedFiles tree name catalog entry.
|
|||
func (f Fpdf) getEmbeddedFiles() string { |
|||
names := make([]string, len(f.attachments)) |
|||
for i, as := range f.attachments { |
|||
names[i] = fmt.Sprintf("(Attachement%d) %d 0 R ", i+1, as.objectNumber) |
|||
} |
|||
nameTree := fmt.Sprintf("<< /Names [\n %s \n] >>", strings.Join(names, "\n")) |
|||
return nameTree |
|||
} |
|||
|
|||
// ---------------------------------- Annotations ----------------------------------
|
|||
|
|||
type annotationAttach struct { |
|||
*Attachment |
|||
|
|||
x, y, w, h float64 // fpdf coordinates (y diff and scaling done)
|
|||
} |
|||
|
|||
// AddAttachmentAnnotation puts a link on the current page, on the rectangle
|
|||
// defined by `x`, `y`, `w`, `h`. This link points towards the content defined
|
|||
// in `a`, which is embedded in the document. Note than no drawing is done by
|
|||
// this method : a method like `Cell()` or `Rect()` should be called to
|
|||
// indicate to the reader that there is a link here. Requiring a pointer to an
|
|||
// Attachment avoids useless copies in the resulting pdf: attachment pointing
|
|||
// to the same data will have their content only be included once, and be
|
|||
// shared amongst all links. Be aware that not all PDF readers support
|
|||
// annotated attachments. See the AddAttachmentAnnotation example for a
|
|||
// demonstration of this method.
|
|||
func (f *Fpdf) AddAttachmentAnnotation(a *Attachment, x, y, w, h float64) { |
|||
if a == nil { |
|||
return |
|||
} |
|||
f.pageAttachments[f.page] = append(f.pageAttachments[f.page], annotationAttach{ |
|||
Attachment: a, |
|||
x: x * f.k, y: f.hPt - y*f.k, w: w * f.k, h: h * f.k, |
|||
}) |
|||
} |
|||
|
|||
// embed current annotations attachments. store object numbers
|
|||
// for later use by putAttachmentAnnotationLinks(), which is
|
|||
// called for each page.
|
|||
func (f *Fpdf) putAnnotationsAttachments() { |
|||
// avoid duplication
|
|||
m := map[*Attachment]bool{} |
|||
for _, l := range f.pageAttachments { |
|||
for _, an := range l { |
|||
if m[an.Attachment] { // already embedded
|
|||
continue |
|||
} |
|||
f.embed(an.Attachment) |
|||
} |
|||
} |
|||
} |
|||
|
|||
func (f *Fpdf) putAttachmentAnnotationLinks(out *fmtBuffer, page int) { |
|||
for _, an := range f.pageAttachments[page] { |
|||
x1, y1, x2, y2 := an.x, an.y, an.x+an.w, an.y-an.h |
|||
as := fmt.Sprintf("<< /Type /XObject /Subtype /Form /BBox [%.2f %.2f %.2f %.2f] /Length 0 >>", |
|||
x1, y1, x2, y2) |
|||
as += "\nstream\nendstream" |
|||
|
|||
out.printf("<< /Type /Annot /Subtype /FileAttachment /Rect [%.2f %.2f %.2f %.2f] /Border [0 0 0]\n", |
|||
x1, y1, x2, y2) |
|||
out.printf("/Contents %s ", f.textstring(utf8toutf16(an.Description))) |
|||
out.printf("/T %s ", f.textstring(utf8toutf16(an.Filename))) |
|||
out.printf("/AP << /N %s>>", as) |
|||
out.printf("/FS %d 0 R >>\n", an.objectNumber) |
|||
} |
|||
} |
@ -0,0 +1,146 @@ |
|||
/* |
|||
* Copyright (c) 2015 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 ( |
|||
"bytes" |
|||
"fmt" |
|||
"io" |
|||
"io/ioutil" |
|||
"sort" |
|||
) |
|||
|
|||
type sortType struct { |
|||
length int |
|||
less func(int, int) bool |
|||
swap func(int, int) |
|||
} |
|||
|
|||
func (s *sortType) Len() int { |
|||
return s.length |
|||
} |
|||
|
|||
func (s *sortType) Less(i, j int) bool { |
|||
return s.less(i, j) |
|||
} |
|||
|
|||
func (s *sortType) Swap(i, j int) { |
|||
s.swap(i, j) |
|||
} |
|||
|
|||
func gensort(Len int, Less func(int, int) bool, Swap func(int, int)) { |
|||
sort.Sort(&sortType{length: Len, less: Less, swap: Swap}) |
|||
} |
|||
|
|||
func writeBytes(leadStr string, startPos int, sl []byte) { |
|||
var pos, max int |
|||
var b byte |
|||
fmt.Printf("%s %07x", leadStr, startPos) |
|||
max = len(sl) |
|||
for pos < max { |
|||
fmt.Printf(" ") |
|||
for k := 0; k < 8; k++ { |
|||
if pos < max { |
|||
fmt.Printf(" %02x", sl[pos]) |
|||
} else { |
|||
fmt.Printf(" ") |
|||
} |
|||
pos++ |
|||
} |
|||
} |
|||
fmt.Printf(" |") |
|||
pos = 0 |
|||
for pos < max { |
|||
b = sl[pos] |
|||
if b < 32 || b >= 128 { |
|||
b = '.' |
|||
} |
|||
fmt.Printf("%c", b) |
|||
pos++ |
|||
} |
|||
fmt.Printf("|\n") |
|||
} |
|||
|
|||
func checkBytes(pos int, sl1, sl2 []byte, printDiff bool) (eq bool) { |
|||
eq = bytes.Equal(sl1, sl2) |
|||
if !eq && printDiff { |
|||
writeBytes("<", pos, sl1) |
|||
writeBytes(">", pos, sl2) |
|||
} |
|||
return |
|||
} |
|||
|
|||
// CompareBytes compares the bytes referred to by sl1 with those referred to by
|
|||
// sl2. Nil is returned if the buffers are equal, otherwise an error.
|
|||
func CompareBytes(sl1, sl2 []byte, printDiff bool) (err error) { |
|||
var posStart, posEnd, len1, len2, length int |
|||
var diffs bool |
|||
|
|||
len1 = len(sl1) |
|||
len2 = len(sl2) |
|||
length = len1 |
|||
if length > len2 { |
|||
length = len2 |
|||
} |
|||
for posStart < length-1 { |
|||
posEnd = posStart + 16 |
|||
if posEnd > length { |
|||
posEnd = length |
|||
} |
|||
if !checkBytes(posStart, sl1[posStart:posEnd], sl2[posStart:posEnd], printDiff) { |
|||
diffs = true |
|||
} |
|||
posStart = posEnd |
|||
} |
|||
if diffs { |
|||
err = fmt.Errorf("documents are different") |
|||
} |
|||
return |
|||
} |
|||
|
|||
// ComparePDFs reads and compares the full contents of the two specified
|
|||
// readers byte-for-byte. Nil is returned if the buffers are equal, otherwise
|
|||
// an error.
|
|||
func ComparePDFs(rdr1, rdr2 io.Reader, printDiff bool) (err error) { |
|||
var b1, b2 *bytes.Buffer |
|||
_, err = b1.ReadFrom(rdr1) |
|||
if err == nil { |
|||
_, err = b2.ReadFrom(rdr2) |
|||
if err == nil { |
|||
err = CompareBytes(b1.Bytes(), b2.Bytes(), printDiff) |
|||
} |
|||
} |
|||
return |
|||
} |
|||
|
|||
// ComparePDFFiles reads and compares the full contents of the two specified
|
|||
// files byte-for-byte. Nil is returned if the file contents are equal, or if
|
|||
// the second file is missing, otherwise an error.
|
|||
func ComparePDFFiles(file1Str, file2Str string, printDiff bool) (err error) { |
|||
var sl1, sl2 []byte |
|||
sl1, err = ioutil.ReadFile(file1Str) |
|||
if err == nil { |
|||
sl2, err = ioutil.ReadFile(file2Str) |
|||
if err == nil { |
|||
err = CompareBytes(sl1, sl2, printDiff) |
|||
} else { |
|||
// Second file is missing; treat this as success
|
|||
err = nil |
|||
} |
|||
} |
|||
return |
|||
} |
@ -0,0 +1,302 @@ |
|||
// Copyright (c) 2015 Jelmer Snoeck (Gmail: jelmer.snoeck)
|
|||
//
|
|||
// 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 barcode provides helper methods for adding barcodes of different
|
|||
// types to your pdf document. It relies on the github.com/boombuler/barcode
|
|||
// package for the barcode creation.
|
|||
package barcode |
|||
|
|||
import ( |
|||
"bytes" |
|||
"errors" |
|||
"image/jpeg" |
|||
"io" |
|||
"strconv" |
|||
"sync" |
|||
|
|||
"github.com/boombuler/barcode" |
|||
"github.com/boombuler/barcode/aztec" |
|||
"github.com/boombuler/barcode/codabar" |
|||
"github.com/boombuler/barcode/code128" |
|||
"github.com/boombuler/barcode/code39" |
|||
"github.com/boombuler/barcode/datamatrix" |
|||
"github.com/boombuler/barcode/ean" |
|||
"github.com/boombuler/barcode/qr" |
|||
"github.com/boombuler/barcode/twooffive" |
|||
"github.com/jung-kurt/gofpdf" |
|||
"github.com/ruudk/golang-pdf417" |
|||
) |
|||
|
|||
// barcodes represents the barcodes that have been registered through this
|
|||
// package. They will later be used to be scaled and put on the page.
|
|||
// RubenN: made this a struct with a mutex to prevent race condition
|
|||
var barcodes struct { |
|||
sync.Mutex |
|||
cache map[string]barcode.Barcode |
|||
} |
|||
|
|||
// barcodePdf is a partial PDF implementation that only implements a subset of
|
|||
// functions that are required to add the barcode to the PDF.
|
|||
type barcodePdf interface { |
|||
GetConversionRatio() float64 |
|||
GetImageInfo(imageStr string) *gofpdf.ImageInfoType |
|||
Image(imageNameStr string, x, y, w, h float64, flow bool, tp string, link int, linkStr string) |
|||
RegisterImageReader(imgName, tp string, r io.Reader) *gofpdf.ImageInfoType |
|||
SetError(err error) |
|||
} |
|||
|
|||
// printBarcode internally prints the scaled or unscaled barcode to the PDF. Used by both
|
|||
// Barcode() and BarcodeUnscalable().
|
|||
func printBarcode(pdf barcodePdf, code string, x, y float64, w, h *float64, flow bool) { |
|||
barcodes.Lock() |
|||
unscaled, ok := barcodes.cache[code] |
|||
barcodes.Unlock() |
|||
|
|||
if !ok { |
|||
err := errors.New("Barcode not found") |
|||
pdf.SetError(err) |
|||
return |
|||
} |
|||
|
|||
bname := uniqueBarcodeName(code, x, y) |
|||
info := pdf.GetImageInfo(bname) |
|||
scaleToWidth := unscaled.Bounds().Dx() |
|||
scaleToHeight := unscaled.Bounds().Dy() |
|||
|
|||
if info == nil { |
|||
bcode, err := barcode.Scale( |
|||
unscaled, |
|||
scaleToWidth, |
|||
scaleToHeight, |
|||
) |
|||
|
|||
if err != nil { |
|||
pdf.SetError(err) |
|||
return |
|||
} |
|||
|
|||
err = registerScaledBarcode(pdf, bname, bcode) |
|||
if err != nil { |
|||
pdf.SetError(err) |
|||
return |
|||
} |
|||
} |
|||
|
|||
scaleToWidthF := float64(scaleToWidth) |
|||
scaleToHeightF := float64(scaleToHeight) |
|||
|
|||
if w != nil { |
|||
scaleToWidthF = *w |
|||
} |
|||
if h != nil { |
|||
scaleToHeightF = *h |
|||
} |
|||
|
|||
pdf.Image(bname, x, y, scaleToWidthF, scaleToHeightF, flow, "jpg", 0, "") |
|||
|
|||
} |
|||
|
|||
// BarcodeUnscalable puts a registered barcode in the current page.
|
|||
//
|
|||
// Its arguments work in the same way as that of Barcode(). However, it allows for an unscaled
|
|||
// barcode in the width and/or height dimensions. This can be useful if you want to prevent
|
|||
// side effects of upscaling.
|
|||
func BarcodeUnscalable(pdf barcodePdf, code string, x, y float64, w, h *float64, flow bool) { |
|||
printBarcode(pdf, code, x, y, w, h, flow) |
|||
} |
|||
|
|||
// Barcode puts a registered barcode in the current page.
|
|||
//
|
|||
// The size should be specified in the units used to create the PDF document.
|
|||
// If width or height are left unspecfied, the barcode is not scaled in the unspecified dimensions.
|
|||
//
|
|||
// Positioning with x, y and flow is inherited from Fpdf.Image().
|
|||
func Barcode(pdf barcodePdf, code string, x, y, w, h float64, flow bool) { |
|||
printBarcode(pdf, code, x, y, &w, &h, flow) |
|||
} |
|||
|
|||
// GetUnscaledBarcodeDimensions returns the width and height of the
|
|||
// unscaled barcode associated with the given code.
|
|||
func GetUnscaledBarcodeDimensions(pdf barcodePdf, code string) (w, h float64) { |
|||
barcodes.Lock() |
|||
unscaled, ok := barcodes.cache[code] |
|||
barcodes.Unlock() |
|||
|
|||
if !ok { |
|||
err := errors.New("Barcode not found") |
|||
pdf.SetError(err) |
|||
return |
|||
} |
|||
|
|||
return convertFrom96Dpi(pdf, float64(unscaled.Bounds().Dx())), |
|||
convertFrom96Dpi(pdf, float64(unscaled.Bounds().Dy())) |
|||
} |
|||
|
|||
// Register registers a barcode but does not put it on the page. Use Barcode()
|
|||
// with the same code to put the barcode on the PDF page.
|
|||
func Register(bcode barcode.Barcode) string { |
|||
barcodes.Lock() |
|||
if len(barcodes.cache) == 0 { |
|||
barcodes.cache = make(map[string]barcode.Barcode) |
|||
} |
|||
|
|||
key := barcodeKey(bcode) |
|||
barcodes.cache[key] = bcode |
|||
barcodes.Unlock() |
|||
|
|||
return key |
|||
} |
|||
|
|||
// RegisterAztec registers a barcode of type Aztec to the PDF, but not to
|
|||
// the page. Use Barcode() with the return value to put the barcode on the page.
|
|||
// code is the string to be encoded. minECCPercent is the error correction percentage. 33 is the default.
|
|||
// userSpecifiedLayers can be a value between -4 and 32 inclusive.
|
|||
func RegisterAztec(pdf barcodePdf, code string, minECCPercent int, userSpecifiedLayers int) string { |
|||
bcode, err := aztec.Encode([]byte(code), minECCPercent, userSpecifiedLayers) |
|||
return registerBarcode(pdf, bcode, err) |
|||
} |
|||
|
|||
// RegisterCodabar registers a barcode of type Codabar to the PDF, but not to
|
|||
// the page. Use Barcode() with the return value to put the barcode on the page.
|
|||
func RegisterCodabar(pdf barcodePdf, code string) string { |
|||
bcode, err := codabar.Encode(code) |
|||
return registerBarcode(pdf, bcode, err) |
|||
} |
|||
|
|||
// RegisterCode128 registers a barcode of type Code128 to the PDF, but not to
|
|||
// the page. Use Barcode() with the return value to put the barcode on the page.
|
|||
func RegisterCode128(pdf barcodePdf, code string) string { |
|||
bcode, err := code128.Encode(code) |
|||
return registerBarcode(pdf, bcode, err) |
|||
} |
|||
|
|||
// RegisterCode39 registers a barcode of type Code39 to the PDF, but not to
|
|||
// the page. Use Barcode() with the return value to put the barcode on the page.
|
|||
//
|
|||
// includeChecksum and fullASCIIMode are inherited from code39.Encode().
|
|||
func RegisterCode39(pdf barcodePdf, code string, includeChecksum, fullASCIIMode bool) string { |
|||
bcode, err := code39.Encode(code, includeChecksum, fullASCIIMode) |
|||
return registerBarcode(pdf, bcode, err) |
|||
} |
|||
|
|||
// RegisterDataMatrix registers a barcode of type DataMatrix to the PDF, but not
|
|||
// to the page. Use Barcode() with the return value to put the barcode on the
|
|||
// page.
|
|||
func RegisterDataMatrix(pdf barcodePdf, code string) string { |
|||
bcode, err := datamatrix.Encode(code) |
|||
return registerBarcode(pdf, bcode, err) |
|||
} |
|||
|
|||
// RegisterPdf417 registers a barcode of type Pdf417 to the PDF, but not to the
|
|||
// page. code is the string to be encoded. columns specifies the number of
|
|||
// barcode columns; this should be a value between 1 and 30 inclusive.
|
|||
// securityLevel specifies an error correction level between zero and 8
|
|||
// inclusive. Barcodes for use with FedEx must set columns to 10 and
|
|||
// securityLevel to 5. Use Barcode() with the return value to put the barcode
|
|||
// on the page.
|
|||
func RegisterPdf417(pdf barcodePdf, code string, columns int, securityLevel int) string { |
|||
bcode := pdf417.Encode(code, columns, securityLevel) |
|||
return registerBarcode(pdf, bcode, nil) |
|||
} |
|||
|
|||
// RegisterEAN registers a barcode of type EAN to the PDF, but not to the page.
|
|||
// It will automatically detect if the barcode is EAN8 or EAN13. Use Barcode()
|
|||
// with the return value to put the barcode on the page.
|
|||
func RegisterEAN(pdf barcodePdf, code string) string { |
|||
bcode, err := ean.Encode(code) |
|||
return registerBarcode(pdf, bcode, err) |
|||
} |
|||
|
|||
// RegisterQR registers a barcode of type QR to the PDF, but not to the page.
|
|||
// Use Barcode() with the return value to put the barcode on the page.
|
|||
//
|
|||
// The ErrorCorrectionLevel and Encoding mode are inherited from qr.Encode().
|
|||
func RegisterQR(pdf barcodePdf, code string, ecl qr.ErrorCorrectionLevel, mode qr.Encoding) string { |
|||
bcode, err := qr.Encode(code, ecl, mode) |
|||
return registerBarcode(pdf, bcode, err) |
|||
} |
|||
|
|||
// RegisterTwoOfFive registers a barcode of type TwoOfFive to the PDF, but not
|
|||
// to the page. Use Barcode() with the return value to put the barcode on the
|
|||
// page.
|
|||
//
|
|||
// The interleaved bool is inherited from twooffive.Encode().
|
|||
func RegisterTwoOfFive(pdf barcodePdf, code string, interleaved bool) string { |
|||
bcode, err := twooffive.Encode(code, interleaved) |
|||
return registerBarcode(pdf, bcode, err) |
|||
} |
|||
|
|||
// registerBarcode registers a barcode internally using the Register() function.
|
|||
// In case of an error generating the barcode it will not be registered and will
|
|||
// set an error on the PDF. It will return a unique key for the barcode type and
|
|||
// content that can be used to put the barcode on the page.
|
|||
func registerBarcode(pdf barcodePdf, bcode barcode.Barcode, err error) string { |
|||
if err != nil { |
|||
pdf.SetError(err) |
|||
return "" |
|||
} |
|||
|
|||
return Register(bcode) |
|||
} |
|||
|
|||
// uniqueBarcodeName makes sure every barcode has a unique name for its
|
|||
// dimensions. Scaling a barcode image results in quality loss, which could be
|
|||
// a problem for barcode readers.
|
|||
func uniqueBarcodeName(code string, x, y float64) string { |
|||
xStr := strconv.FormatFloat(x, 'E', -1, 64) |
|||
yStr := strconv.FormatFloat(y, 'E', -1, 64) |
|||
|
|||
return "barcode-" + code + "-" + xStr + yStr |
|||
} |
|||
|
|||
// barcodeKey combines the code type and code value into a unique identifier for
|
|||
// a barcode type. This is so that we can store several barcodes with the same
|
|||
// code but different type in the barcodes map.
|
|||
func barcodeKey(bcode barcode.Barcode) string { |
|||
return bcode.Metadata().CodeKind + bcode.Content() |
|||
} |
|||
|
|||
// registerScaledBarcode registers a barcode with its exact dimensions to the
|
|||
// PDF but does not put it on the page. Use Fpdf.Image() with the same code to
|
|||
// add the barcode to the page.
|
|||
func registerScaledBarcode(pdf barcodePdf, code string, bcode barcode.Barcode) error { |
|||
buf := new(bytes.Buffer) |
|||
err := jpeg.Encode(buf, bcode, nil) |
|||
|
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
reader := bytes.NewReader(buf.Bytes()) |
|||
pdf.RegisterImageReader(code, "jpg", reader) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// convertTo96DPI converts the given value, which is based on a 72 DPI value
|
|||
// like the rest of the PDF document, to a 96 DPI value that is required for
|
|||
// an Image.
|
|||
//
|
|||
// Doing this through the Fpdf.Image() function would mean that it uses a 72 DPI
|
|||
// value and stretches it to a 96 DPI value. This results in quality loss which
|
|||
// could be problematic for barcode scanners.
|
|||
func convertTo96Dpi(pdf barcodePdf, value float64) float64 { |
|||
return value * pdf.GetConversionRatio() / 72 * 96 |
|||
} |
|||
|
|||
// convertFrom96Dpi converts the given value, which is based on a 96 DPI value
|
|||
// required for an Image, to a 72 DPI value like the rest of the PDF document.
|
|||
func convertFrom96Dpi(pdf barcodePdf, value float64) float64 { |
|||
return value / pdf.GetConversionRatio() * 72 / 96 |
|||
} |
@ -0,0 +1,222 @@ |
|||
package barcode_test |
|||
|
|||
import ( |
|||
"testing" |
|||
|
|||
"github.com/boombuler/barcode/code128" |
|||
"github.com/boombuler/barcode/qr" |
|||
"github.com/jung-kurt/gofpdf" |
|||
"github.com/jung-kurt/gofpdf/contrib/barcode" |
|||
"github.com/jung-kurt/gofpdf/internal/example" |
|||
) |
|||
|
|||
func createPdf() (pdf *gofpdf.Fpdf) { |
|||
pdf = gofpdf.New("L", "mm", "A4", "") |
|||
pdf.SetFont("Helvetica", "", 12) |
|||
pdf.SetFillColor(200, 200, 220) |
|||
pdf.AddPage() |
|||
return |
|||
} |
|||
|
|||
func ExampleRegister() { |
|||
pdf := createPdf() |
|||
|
|||
fileStr := example.Filename("contrib_barcode_Register") |
|||
|
|||
bcode, err := code128.Encode("gofpdf") |
|||
|
|||
if err == nil { |
|||
key := barcode.Register(bcode) |
|||
var width float64 = 100 |
|||
var height float64 = 10.0 |
|||
barcode.BarcodeUnscalable(pdf, key, 15, 15, &width, &height, false) |
|||
} |
|||
|
|||
err = pdf.OutputFileAndClose(fileStr) |
|||
example.Summary(err, fileStr) |
|||
// Output:
|
|||
// Successfully generated ../../pdf/contrib_barcode_Register.pdf
|
|||
} |
|||
|
|||
func ExampleRegisterCodabar() { |
|||
pdf := createPdf() |
|||
|
|||
key := barcode.RegisterCode128(pdf, "codabar") |
|||
var width float64 = 100 |
|||
var height float64 = 10 |
|||
barcode.BarcodeUnscalable(pdf, key, 15, 15, &width, &height, false) |
|||
|
|||
fileStr := example.Filename("contrib_barcode_RegisterCodabar") |
|||
err := pdf.OutputFileAndClose(fileStr) |
|||
example.Summary(err, fileStr) |
|||
// Output:
|
|||
// Successfully generated ../../pdf/contrib_barcode_RegisterCodabar.pdf
|
|||
} |
|||
|
|||
func ExampleRegisterAztec() { |
|||
pdf := createPdf() |
|||
|
|||
key := barcode.RegisterAztec(pdf, "aztec", 33, 0) |
|||
barcode.Barcode(pdf, key, 15, 15, 100, 100, false) |
|||
|
|||
fileStr := example.Filename("contrib_barcode_RegisterAztec") |
|||
err := pdf.OutputFileAndClose(fileStr) |
|||
example.Summary(err, fileStr) |
|||
// Output:
|
|||
// Successfully generated ../../pdf/contrib_barcode_RegisterAztec.pdf
|
|||
} |
|||
|
|||
func ExampleRegisterCode128() { |
|||
pdf := createPdf() |
|||
|
|||
key := barcode.RegisterCode128(pdf, "code128") |
|||
barcode.Barcode(pdf, key, 15, 15, 100, 10, false) |
|||
|
|||
fileStr := example.Filename("contrib_barcode_RegisterCode128") |
|||
err := pdf.OutputFileAndClose(fileStr) |
|||
example.Summary(err, fileStr) |
|||
// Output:
|
|||
// Successfully generated ../../pdf/contrib_barcode_RegisterCode128.pdf
|
|||
} |
|||
|
|||
func ExampleRegisterCode39() { |
|||
pdf := createPdf() |
|||
|
|||
key := barcode.RegisterCode39(pdf, "CODE39", false, true) |
|||
barcode.Barcode(pdf, key, 15, 15, 100, 10, false) |
|||
|
|||
fileStr := example.Filename("contrib_barcode_RegisterCode39") |
|||
err := pdf.OutputFileAndClose(fileStr) |
|||
example.Summary(err, fileStr) |
|||
// Output:
|
|||
// Successfully generated ../../pdf/contrib_barcode_RegisterCode39.pdf
|
|||
} |
|||
|
|||
func ExampleRegisterDataMatrix() { |
|||
pdf := createPdf() |
|||
|
|||
key := barcode.RegisterDataMatrix(pdf, "datamatrix") |
|||
barcode.Barcode(pdf, key, 15, 15, 20, 20, false) |
|||
|
|||
fileStr := example.Filename("contrib_barcode_RegisterDataMatrix") |
|||
err := pdf.OutputFileAndClose(fileStr) |
|||
example.Summary(err, fileStr) |
|||
// Output:
|
|||
// Successfully generated ../../pdf/contrib_barcode_RegisterDataMatrix.pdf
|
|||
} |
|||
|
|||
func ExampleRegisterEAN() { |
|||
pdf := createPdf() |
|||
|
|||
key := barcode.RegisterEAN(pdf, "96385074") |
|||
barcode.Barcode(pdf, key, 15, 15, 100, 10, false) |
|||
|
|||
fileStr := example.Filename("contrib_barcode_RegisterEAN") |
|||
err := pdf.OutputFileAndClose(fileStr) |
|||
example.Summary(err, fileStr) |
|||
// Output:
|
|||
// Successfully generated ../../pdf/contrib_barcode_RegisterEAN.pdf
|
|||
} |
|||
|
|||
func ExampleRegisterQR() { |
|||
pdf := createPdf() |
|||
|
|||
key := barcode.RegisterQR(pdf, "qrcode", qr.H, qr.Unicode) |
|||
barcode.Barcode(pdf, key, 15, 15, 100, 10, false) |
|||
|
|||
fileStr := example.Filename("contrib_barcode_RegisterQR") |
|||
err := pdf.OutputFileAndClose(fileStr) |
|||
example.Summary(err, fileStr) |
|||
// Output:
|
|||
// Successfully generated ../../pdf/contrib_barcode_RegisterQR.pdf
|
|||
} |
|||
|
|||
func ExampleRegisterTwoOfFive() { |
|||
pdf := createPdf() |
|||
|
|||
key := barcode.RegisterTwoOfFive(pdf, "1234567895", true) |
|||
barcode.Barcode(pdf, key, 15, 15, 100, 10, false) |
|||
|
|||
fileStr := example.Filename("contrib_barcode_RegisterTwoOfFive") |
|||
err := pdf.OutputFileAndClose(fileStr) |
|||
example.Summary(err, fileStr) |
|||
// Output:
|
|||
// Successfully generated ../../pdf/contrib_barcode_RegisterTwoOfFive.pdf
|
|||
} |
|||
|
|||
func ExampleRegisterPdf417() { |
|||
pdf := createPdf() |
|||
|
|||
key := barcode.RegisterPdf417(pdf, "1234567895", 10, 5) |
|||
barcode.Barcode(pdf, key, 15, 15, 100, 10, false) |
|||
|
|||
fileStr := example.Filename("contrib_barcode_RegisterPdf417") |
|||
err := pdf.OutputFileAndClose(fileStr) |
|||
example.Summary(err, fileStr) |
|||
// Output:
|
|||
// Successfully generated ../../pdf/contrib_barcode_RegisterPdf417.pdf
|
|||
} |
|||
|
|||
// TestRegisterCode128 ensures that no panic arises when an invalid barcode is registered.
|
|||
func TestRegisterCode128(t *testing.T) { |
|||
pdf := createPdf() |
|||
barcode.RegisterCode128(pdf, "Invalid character: รฉ") |
|||
} |
|||
|
|||
// TestBarcodeUnscalable shows that the barcode may be scaled or not by providing optional heights and widths.
|
|||
func TestBarcodeUnscalable(t *testing.T) { |
|||
pdf := createPdf() |
|||
|
|||
key := barcode.RegisterCode128(pdf, "code128") |
|||
var width float64 = 100 |
|||
var height float64 = 10 |
|||
barcode.BarcodeUnscalable(pdf, key, 15, 15, &width, &height, false) |
|||
barcode.BarcodeUnscalable(pdf, key, 15, 35, nil, &height, false) |
|||
barcode.BarcodeUnscalable(pdf, key, 15, 55, &width, nil, false) |
|||
barcode.BarcodeUnscalable(pdf, key, 15, 75, nil, nil, false) |
|||
|
|||
fileStr := example.Filename("contrib_barcode_Barcode") |
|||
err := pdf.OutputFileAndClose(fileStr) |
|||
example.Summary(err, fileStr) |
|||
// Output:
|
|||
// Successfully generated ../../pdf/contrib_barcode_Barcode.pdf
|
|||
} |
|||
|
|||
// TestGetUnscaledBarcodeDimensions shows that the width and height returned by the function match that of the barcode
|
|||
func TestGetUnscaledBarcodeDimensions(t *testing.T) { |
|||
pdf := createPdf() |
|||
|
|||
key := barcode.RegisterQR(pdf, "qrcode", qr.H, qr.Unicode) |
|||
barcode.BarcodeUnscalable(pdf, key, 15, 15, nil, nil, false) |
|||
w, h := barcode.GetUnscaledBarcodeDimensions(pdf, key) |
|||
|
|||
pdf.SetDrawColor(255, 0, 0) |
|||
pdf.Line(15, 15, 15+w, 15+h) |
|||
|
|||
fileStr := example.Filename("contrib_barcode_GetBarcodeDimensions") |
|||
err := pdf.OutputFileAndClose(fileStr) |
|||
example.Summary(err, fileStr) |
|||
// Output:
|
|||
// Successfully generated ../../pdf/contrib_barcode_GetBarcodeDimensions.pdf
|
|||
} |
|||
|
|||
// TestBarcodeNonIntegerScalingFactors shows that the barcode may be scaled to non-integer sizes
|
|||
func TestBarcodeNonIntegerScalingFactors(t *testing.T) { |
|||
pdf := gofpdf.New("L", "in", "A4", "") |
|||
pdf.SetFont("Helvetica", "", 12) |
|||
pdf.SetFillColor(200, 200, 220) |
|||
pdf.AddPage() |
|||
|
|||
key := barcode.RegisterQR(pdf, "qrcode", qr.H, qr.Unicode) |
|||
var scale float64 = 1.5 |
|||
barcode.BarcodeUnscalable(pdf, key, 0.5, 0.5, &scale, &scale, false) |
|||
|
|||
pdf.SetDrawColor(255, 0, 0) |
|||
pdf.Line(0.5, 0.5, 0.5+scale, 0.5+scale) |
|||
|
|||
fileStr := example.Filename("contrib_barcode_BarcodeScaling") |
|||
err := pdf.OutputFileAndClose(fileStr) |
|||
example.Summary(err, fileStr) |
|||
// Output:
|
|||
// Successfully generated ../../pdf/contrib_barcode_BarcodeScaling.pdf
|
|||
} |
@ -0,0 +1,68 @@ |
|||
package main |
|||
|
|||
// This command demonstrates the use of ghotscript to reduce the size
|
|||
// of generated PDFs. This is based on a comment made by farkerhaiku:
|
|||
// https://github.com/jung-kurt/gofpdf/issues/57#issuecomment-185843315
|
|||
|
|||
import ( |
|||
"fmt" |
|||
"os" |
|||
"os/exec" |
|||
|
|||
"github.com/jung-kurt/gofpdf" |
|||
) |
|||
|
|||
func report(fileStr string, err error) { |
|||
if err == nil { |
|||
var info os.FileInfo |
|||
info, err = os.Stat(fileStr) |
|||
if err == nil { |
|||
fmt.Printf("%s: OK, size %d\n", fileStr, info.Size()) |
|||
} else { |
|||
fmt.Printf("%s: bad stat\n", fileStr) |
|||
} |
|||
} else { |
|||
fmt.Printf("%s: %s\n", fileStr, err) |
|||
} |
|||
} |
|||
|
|||
func newPdf() (pdf *gofpdf.Fpdf) { |
|||
pdf = gofpdf.New("P", "mm", "A4", "../../font") |
|||
pdf.SetCompression(false) |
|||
pdf.AddFont("Calligrapher", "", "calligra.json") |
|||
pdf.AddPage() |
|||
pdf.SetFont("Calligrapher", "", 35) |
|||
pdf.Cell(0, 10, "Enjoy new fonts with FPDF!") |
|||
return |
|||
} |
|||
|
|||
func full(name string) { |
|||
report(name, newPdf().OutputFileAndClose(name)) |
|||
} |
|||
|
|||
func min(name string) { |
|||
cmd := exec.Command("gs", "-sDEVICE=pdfwrite", |
|||
"-dCompatibilityLevel=1.4", |
|||
"-dPDFSETTINGS=/screen", "-dNOPAUSE", "-dQUIET", |
|||
"-dBATCH", "-sOutputFile="+name, "-") |
|||
inPipe, err := cmd.StdinPipe() |
|||
if err == nil { |
|||
errChan := make(chan error, 1) |
|||
go func() { |
|||
errChan <- cmd.Start() |
|||
}() |
|||
err = newPdf().Output(inPipe) |
|||
if err == nil { |
|||
report(name, <-errChan) |
|||
} else { |
|||
report(name, err) |
|||
} |
|||
} else { |
|||
report(name, err) |
|||
} |
|||
} |
|||
|
|||
func main() { |
|||
full("full.pdf") |
|||
min("min.pdf") |
|||
} |
@ -0,0 +1,143 @@ |
|||
/* |
|||
Package gofpdi wraps the gofpdi PDF library to import existing PDFs as templates. See github.com/phpdave11/gofpdi |
|||
for further information and examples. |
|||
|
|||
Users should call NewImporter() to obtain their own Importer instance to work with. |
|||
To retain backwards compatibility, the package offers a default Importer that may be used via global functions. Note |
|||
however that use of the default Importer is not thread safe. |
|||
*/ |
|||
package gofpdi |
|||
|
|||
import ( |
|||
realgofpdi "github.com/phpdave11/gofpdi" |
|||
"io" |
|||
) |
|||
|
|||
// gofpdiPdf is a partial interface that only implements the functions we need
|
|||
// from the PDF generator to put the imported PDF templates on the PDF.
|
|||
type gofpdiPdf interface { |
|||
ImportObjects(objs map[string][]byte) |
|||
ImportObjPos(objs map[string]map[int]string) |
|||
ImportTemplates(tpls map[string]string) |
|||
UseImportedTemplate(tplName string, x float64, y float64, w float64, h float64) |
|||
SetError(err error) |
|||
} |
|||
|
|||
// Importer wraps an Importer from the gofpdi library.
|
|||
type Importer struct { |
|||
fpdi *realgofpdi.Importer |
|||
} |
|||
|
|||
// NewImporter creates a new Importer wrapping functionality from the gofpdi library.
|
|||
func NewImporter() *Importer { |
|||
return &Importer{ |
|||
fpdi: realgofpdi.NewImporter(), |
|||
} |
|||
} |
|||
|
|||
// ImportPage imports a page of a PDF file with the specified box (/MediaBox,
|
|||
// /TrimBox, /ArtBox, /CropBox, or /BleedBox). Returns a template id that can
|
|||
// be used with UseImportedTemplate to draw the template onto the page.
|
|||
func (i *Importer) ImportPage(f gofpdiPdf, sourceFile string, pageno int, box string) int { |
|||
// Set source file for fpdi
|
|||
i.fpdi.SetSourceFile(sourceFile) |
|||
// return template id
|
|||
return i.getTemplateID(f, pageno, box) |
|||
} |
|||
|
|||
// ImportPageFromStream imports a page of a PDF with the specified box
|
|||
// (/MediaBox, TrimBox, /ArtBox, /CropBox, or /BleedBox). Returns a template id
|
|||
// that can be used with UseImportedTemplate to draw the template onto the
|
|||
// page.
|
|||
func (i *Importer) ImportPageFromStream(f gofpdiPdf, rs *io.ReadSeeker, pageno int, box string) int { |
|||
// Set source stream for fpdi
|
|||
i.fpdi.SetSourceStream(rs) |
|||
// return template id
|
|||
return i.getTemplateID(f, pageno, box) |
|||
} |
|||
|
|||
func (i *Importer) getTemplateID(f gofpdiPdf, pageno int, box string) int { |
|||
// Import page
|
|||
tpl := i.fpdi.ImportPage(pageno, box) |
|||
|
|||
// Import objects into current pdf document
|
|||
// Unordered means that the objects will be returned with a sha1 hash instead of an integer
|
|||
// The objects themselves may have references to other hashes which will be replaced in ImportObjects()
|
|||
tplObjIDs := i.fpdi.PutFormXobjectsUnordered() |
|||
|
|||
// Set template names and ids (hashes) in gofpdf
|
|||
f.ImportTemplates(tplObjIDs) |
|||
|
|||
// Get a map[string]string of the imported objects.
|
|||
// The map keys will be the ID of each object.
|
|||
imported := i.fpdi.GetImportedObjectsUnordered() |
|||
|
|||
// Import gofpdi objects into gofpdf
|
|||
f.ImportObjects(imported) |
|||
|
|||
// Get a map[string]map[int]string of the object hashes and their positions within each object,
|
|||
// to be replaced with object ids (integers).
|
|||
importedObjPos := i.fpdi.GetImportedObjHashPos() |
|||
|
|||
// Import gofpdi object hashes and their positions into gopdf
|
|||
f.ImportObjPos(importedObjPos) |
|||
|
|||
return tpl |
|||
} |
|||
|
|||
// UseImportedTemplate draws the template onto the page at x,y. If w is 0, the
|
|||
// template will be scaled to fit based on h. If h is 0, the template will be
|
|||
// scaled to fit based on w.
|
|||
func (i *Importer) UseImportedTemplate(f gofpdiPdf, tplid int, x float64, y float64, w float64, h float64) { |
|||
// Get values from fpdi
|
|||
tplName, scaleX, scaleY, tX, tY := i.fpdi.UseTemplate(tplid, x, y, w, h) |
|||
|
|||
f.UseImportedTemplate(tplName, scaleX, scaleY, tX, tY) |
|||
} |
|||
|
|||
// GetPageSizes returns page dimensions for all pages of the imported pdf.
|
|||
// Result consists of map[<page number>]map[<box>]map[<dimension>]<value>.
|
|||
// <page number>: page number, note that page numbers start at 1
|
|||
// <box>: box identifier, e.g. "/MediaBox"
|
|||
// <dimension>: dimension string, either "w" or "h"
|
|||
func (i *Importer) GetPageSizes() map[int]map[string]map[string]float64 { |
|||
return i.fpdi.GetPageSizes() |
|||
} |
|||
|
|||
// Default Importer used by global functions
|
|||
var fpdi = NewImporter() |
|||
|
|||
// ImportPage imports a page of a PDF file with the specified box (/MediaBox,
|
|||
// /TrimBox, /ArtBox, /CropBox, or /BleedBox). Returns a template id that can
|
|||
// be used with UseImportedTemplate to draw the template onto the page.
|
|||
// Note: This uses the default Importer. Call NewImporter() to obtain a custom Importer.
|
|||
func ImportPage(f gofpdiPdf, sourceFile string, pageno int, box string) int { |
|||
return fpdi.ImportPage(f, sourceFile, pageno, box) |
|||
} |
|||
|
|||
// ImportPageFromStream imports a page of a PDF with the specified box
|
|||
// (/MediaBox, TrimBox, /ArtBox, /CropBox, or /BleedBox). Returns a template id
|
|||
// that can be used with UseImportedTemplate to draw the template onto the
|
|||
// page.
|
|||
// Note: This uses the default Importer. Call NewImporter() to obtain a custom Importer.
|
|||
func ImportPageFromStream(f gofpdiPdf, rs *io.ReadSeeker, pageno int, box string) int { |
|||
return fpdi.ImportPageFromStream(f, rs, pageno, box) |
|||
} |
|||
|
|||
// UseImportedTemplate draws the template onto the page at x,y. If w is 0, the
|
|||
// template will be scaled to fit based on h. If h is 0, the template will be
|
|||
// scaled to fit based on w.
|
|||
// Note: This uses the default Importer. Call NewImporter() to obtain a custom Importer.
|
|||
func UseImportedTemplate(f gofpdiPdf, tplid int, x float64, y float64, w float64, h float64) { |
|||
fpdi.UseImportedTemplate(f, tplid, x, y, w, h) |
|||
} |
|||
|
|||
// GetPageSizes returns page dimensions for all pages of the imported pdf.
|
|||
// Result consists of map[<page number>]map[<box>]map[<dimension>]<value>.
|
|||
// <page number>: page number, note that page numbers start at 1
|
|||
// <box>: box identifier, e.g. "/MediaBox"
|
|||
// <dimension>: dimension string, either "w" or "h"
|
|||
// Note: This uses the default Importer. Call NewImporter() to obtain a custom Importer.
|
|||
func GetPageSizes() map[int]map[string]map[string]float64 { |
|||
return fpdi.GetPageSizes() |
|||
} |
@ -0,0 +1,76 @@ |
|||
package gofpdi |
|||
|
|||
import ( |
|||
"bytes" |
|||
"github.com/jung-kurt/gofpdf" |
|||
"github.com/jung-kurt/gofpdf/internal/example" |
|||
"io" |
|||
"sync" |
|||
"testing" |
|||
) |
|||
|
|||
func ExampleNewImporter() { |
|||
// create new pdf
|
|||
pdf := gofpdf.New("P", "pt", "A4", "") |
|||
|
|||
// for testing purposes, get an arbitrary template pdf as stream
|
|||
rs, _ := getTemplatePdf() |
|||
|
|||
// create a new Importer instance
|
|||
imp := NewImporter() |
|||
|
|||
// import first page and determine page sizes
|
|||
tpl := imp.ImportPageFromStream(pdf, &rs, 1, "/MediaBox") |
|||
pageSizes := imp.GetPageSizes() |
|||
nrPages := len(imp.GetPageSizes()) |
|||
|
|||
// add all pages from template pdf
|
|||
for i := 1; i <= nrPages; i++ { |
|||
pdf.AddPage() |
|||
if i > 1 { |
|||
tpl = imp.ImportPageFromStream(pdf, &rs, i, "/MediaBox") |
|||
} |
|||
imp.UseImportedTemplate(pdf, tpl, 0, 0, pageSizes[i]["/MediaBox"]["w"], pageSizes[i]["/MediaBox"]["h"]) |
|||
} |
|||
|
|||
// output
|
|||
fileStr := example.Filename("contrib_gofpdi_Importer") |
|||
err := pdf.OutputFileAndClose(fileStr) |
|||
example.Summary(err, fileStr) |
|||
// Output:
|
|||
// Successfully generated ../../pdf/contrib_gofpdi_Importer.pdf
|
|||
} |
|||
|
|||
func TestGofpdiConcurrent(t *testing.T) { |
|||
wg := sync.WaitGroup{} |
|||
for i := 0; i < 100; i++ { |
|||
wg.Add(1) |
|||
go func() { |
|||
defer wg.Done() |
|||
pdf := gofpdf.New("P", "mm", "A4", "") |
|||
pdf.AddPage() |
|||
rs, _ := getTemplatePdf() |
|||
imp := NewImporter() |
|||
tpl := imp.ImportPageFromStream(pdf, &rs, 1, "/MediaBox") |
|||
imp.UseImportedTemplate(pdf, tpl, 0, 0, 210.0, 297.0) |
|||
// write to bytes buffer
|
|||
buf := bytes.Buffer{} |
|||
if err := pdf.Output(&buf); err != nil { |
|||
t.Fail() |
|||
} |
|||
}() |
|||
} |
|||
wg.Wait() |
|||
} |
|||
|
|||
func getTemplatePdf() (io.ReadSeeker, error) { |
|||
tpdf := gofpdf.New("P", "pt", "A4", "") |
|||
tpdf.AddPage() |
|||
tpdf.SetFont("Arial", "", 12) |
|||
tpdf.Text(20, 20, "Example Page 1") |
|||
tpdf.AddPage() |
|||
tpdf.Text(20, 20, "Example Page 2") |
|||
tbuf := bytes.Buffer{} |
|||
err := tpdf.Output(&tbuf) |
|||
return bytes.NewReader(tbuf.Bytes()), err |
|||
} |
@ -0,0 +1,43 @@ |
|||
package httpimg |
|||
|
|||
import ( |
|||
"io" |
|||
"net/http" |
|||
|
|||
"github.com/jung-kurt/gofpdf" |
|||
) |
|||
|
|||
// httpimgPdf is a partial interface that only implements the functions we need
|
|||
// from the PDF generator to put the HTTP images on the PDF.
|
|||
type httpimgPdf interface { |
|||
GetImageInfo(imageStr string) *gofpdf.ImageInfoType |
|||
ImageTypeFromMime(mimeStr string) string |
|||
RegisterImageReader(imgName, tp string, r io.Reader) *gofpdf.ImageInfoType |
|||
SetError(err error) |
|||
} |
|||
|
|||
// Register registers a HTTP image. Downloading the image from the provided URL
|
|||
// and adding it to the PDF but not adding it to the page. Use Image() with the
|
|||
// same URL to add the image to the page.
|
|||
func Register(f httpimgPdf, urlStr, tp string) (info *gofpdf.ImageInfoType) { |
|||
info = f.GetImageInfo(urlStr) |
|||
|
|||
if info != nil { |
|||
return |
|||
} |
|||
|
|||
resp, err := http.Get(urlStr) |
|||
|
|||
if err != nil { |
|||
f.SetError(err) |
|||
return |
|||
} |
|||
|
|||
defer resp.Body.Close() |
|||
|
|||
if tp == "" { |
|||
tp = f.ImageTypeFromMime(resp.Header["Content-Type"][0]) |
|||
} |
|||
|
|||
return f.RegisterImageReader(urlStr, tp, resp.Body) |
|||
} |
@ -0,0 +1,23 @@ |
|||
package httpimg_test |
|||
|
|||
import ( |
|||
"github.com/jung-kurt/gofpdf" |
|||
"github.com/jung-kurt/gofpdf/contrib/httpimg" |
|||
"github.com/jung-kurt/gofpdf/internal/example" |
|||
) |
|||
|
|||
func ExampleRegister() { |
|||
pdf := gofpdf.New("L", "mm", "A4", "") |
|||
pdf.SetFont("Helvetica", "", 12) |
|||
pdf.SetFillColor(200, 200, 220) |
|||
pdf.AddPage() |
|||
|
|||
url := "https://github.com/jung-kurt/gofpdf/raw/master/image/logo_gofpdf.jpg?raw=true" |
|||
httpimg.Register(pdf, url, "") |
|||
pdf.Image(url, 15, 15, 267, 0, false, "", 0, "") |
|||
fileStr := example.Filename("contrib_httpimg_Register") |
|||
err := pdf.OutputFileAndClose(fileStr) |
|||
example.Summary(err, fileStr) |
|||
// Output:
|
|||
// Successfully generated ../../pdf/contrib_httpimg_Register.pdf
|
|||
} |
@ -0,0 +1,83 @@ |
|||
/* |
|||
* Copyright (c) 2016 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 tiff allows standard (LZW-compressed) TIFF images to be used in
|
|||
// documents generated with gofpdf.
|
|||
package tiff |
|||
|
|||
import ( |
|||
"bytes" |
|||
"fmt" |
|||
"image" |
|||
"image/png" |
|||
"io" |
|||
"os" |
|||
|
|||
"github.com/jung-kurt/gofpdf" |
|||
"golang.org/x/image/tiff" |
|||
) |
|||
|
|||
// RegisterReader registers a TIFF image, adding it to the PDF file but not
|
|||
// adding it to the page. imgName specifies the name that will be used in the
|
|||
// call to Image() that actually places the image in the document. options
|
|||
// specifies various image properties; in this case, the ImageType property
|
|||
// should be set to "tiff". The TIFF image is a reader from the reader
|
|||
// specified by r.
|
|||
func RegisterReader(fpdf *gofpdf.Fpdf, imgName string, options gofpdf.ImageOptions, r io.Reader) (info *gofpdf.ImageInfoType) { |
|||
var err error |
|||
var img image.Image |
|||
var buf bytes.Buffer |
|||
if fpdf.Ok() { |
|||
if options.ImageType == "tiff" || options.ImageType == "tif" { |
|||
img, err = tiff.Decode(r) |
|||
if err == nil { |
|||
err = png.Encode(&buf, img) |
|||
if err == nil { |
|||
options.ImageType = "png" |
|||
info = fpdf.RegisterImageOptionsReader(imgName, options, &buf) |
|||
} |
|||
} |
|||
} else { |
|||
err = fmt.Errorf("expecting \"tiff\" or \"tif\" as image type, got \"%s\"", options.ImageType) |
|||
} |
|||
if err != nil { |
|||
fpdf.SetError(err) |
|||
} |
|||
} |
|||
return |
|||
} |
|||
|
|||
// RegisterFile registers a TIFF image, adding it to the PDF file but not
|
|||
// adding it to the page. imgName specifies the name that will be used in the
|
|||
// call to Image() that actually places the image in the document. options
|
|||
// specifies various image properties; in this case, the ImageType property
|
|||
// should be set to "tiff". The TIFF image is read from the file specified by
|
|||
// tiffFileStr.
|
|||
func RegisterFile(fpdf *gofpdf.Fpdf, imgName string, options gofpdf.ImageOptions, tiffFileStr string) (info *gofpdf.ImageInfoType) { |
|||
var f *os.File |
|||