mirror of https://github.com/grafana/grafana
Enterprise: add dependencies for upcoming features (#18793)
* Enterprise: add dependencies for upcoming features See enterprise issuespull/18814/head
parent
d6fb48c0ff
commit
1a4be4af8c
@ -0,0 +1 @@ |
||||
*.pdf binary |
||||
@ -0,0 +1,25 @@ |
||||
*.0 |
||||
coverage |
||||
font/CalligrapherRegular.json |
||||
font/CalligrapherRegular.z |
||||
font/Ubuntu-* |
||||
internal/files/bin/bin |
||||
look |
||||
makefont/makefont |
||||
open |
||||
**/*.out |
||||
pdf/*.pdf |
||||
pdf.txt |
||||
private |
||||
*.sublime* |
||||
*.swp |
||||
**/*.test |
||||
.idea/ |
||||
doc/body.html |
||||
doc/body.md |
||||
doc/index.html |
||||
doc/index.html.ok |
||||
coverage.html |
||||
|
||||
# macOS |
||||
.DS_Store |
||||
@ -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,256 @@ |
||||
# GoFPDF document generator |
||||
|
||||
[](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. |
||||
|
||||
## 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. |
||||
|
||||
## Roadmap |
||||
|
||||
- Improve test coverage as reported by the coverage tool. |
||||
@ -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,734 @@ |
||||
/* |
||||
* Copyright (c) 2013-2014 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" |
||||
"crypto/sha1" |
||||
"encoding/gob" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"time" |
||||
) |
||||
|
||||
// Version of FPDF from which this package is derived
|
||||
const ( |
||||
cnFpdfVersion = "1.7" |
||||
) |
||||
|
||||
type blendModeType struct { |
||||
strokeStr, fillStr, modeStr string |
||||
objNum int |
||||
} |
||||
|
||||
type gradientType struct { |
||||
tp int // 2: linear, 3: radial
|
||||
clr1Str, clr2Str string |
||||
x1, y1, x2, y2, r float64 |
||||
objNum int |
||||
} |
||||
|
||||
const ( |
||||
// OrientationPortrait represents the portrait orientation.
|
||||
OrientationPortrait = "portrait" |
||||
|
||||
// OrientationLandscape represents the landscape orientation.
|
||||
OrientationLandscape = "landscape" |
||||
) |
||||
|
||||
const ( |
||||
// UnitPoint represents the size unit point
|
||||
UnitPoint = "pt" |
||||
// UnitMillimeter represents the size unit millimeter
|
||||
UnitMillimeter = "mm" |
||||
// UnitCentimeter represents the size unit centimeter
|
||||
UnitCentimeter = "cm" |
||||
// UnitInch represents the size unit inch
|
||||
UnitInch = "inch" |
||||
) |
||||
|
||||
const ( |
||||
// PageSizeA3 represents DIN/ISO A3 page size
|
||||
PageSizeA3 = "A3" |
||||
// PageSizeA4 represents DIN/ISO A4 page size
|
||||
PageSizeA4 = "A4" |
||||
// PageSizeA5 represents DIN/ISO A5 page size
|
||||
PageSizeA5 = "A5" |
||||
// PageSizeLetter represents US Letter page size
|
||||
PageSizeLetter = "Letter" |
||||
// PageSizeLegal represents US Legal page size
|
||||
PageSizeLegal = "Legal" |
||||
) |
||||
|
||||
const ( |
||||
// BorderNone set no border
|
||||
BorderNone = "" |
||||
// BorderFull sets a full border
|
||||
BorderFull = "1" |
||||
// BorderLeft sets the border on the left side
|
||||
BorderLeft = "L" |
||||
// BorderTop sets the border at the top
|
||||
BorderTop = "T" |
||||
// BorderRight sets the border on the right side
|
||||
BorderRight = "R" |
||||
// BorderBottom sets the border on the bottom
|
||||
BorderBottom = "B" |
||||
) |
||||
|
||||
const ( |
||||
// LineBreakNone disables linebreak
|
||||
LineBreakNone = 0 |
||||
// LineBreakNormal enables normal linebreak
|
||||
LineBreakNormal = 1 |
||||
// LineBreakBelow enables linebreak below
|
||||
LineBreakBelow = 2 |
||||
) |
||||
|
||||
const ( |
||||
// AlignLeft left aligns the cell
|
||||
AlignLeft = "L" |
||||
// AlignRight right aligns the cell
|
||||
AlignRight = "R" |
||||
// AlignCenter centers the cell
|
||||
AlignCenter = "C" |
||||
// AlignTop aligns the cell to the top
|
||||
AlignTop = "T" |
||||
// AlignBottom aligns the cell to the bottom
|
||||
AlignBottom = "B" |
||||
// AlignMiddle aligns the cell to the middle
|
||||
AlignMiddle = "M" |
||||
// AlignBaseline aligns the cell to the baseline
|
||||
AlignBaseline = "B" |
||||
) |
||||
|
||||
type colorMode int |
||||
|
||||
const ( |
||||
colorModeRGB colorMode = iota |
||||
colorModeSpot |
||||
colorModeCMYK |
||||
) |
||||
|
||||
type colorType struct { |
||||
r, g, b float64 |
||||
ir, ig, ib int |
||||
mode colorMode |
||||
spotStr string // name of current spot color
|
||||
gray bool |
||||
str string |
||||
} |
||||
|
||||
// SpotColorType specifies a named spot color value
|
||||
type spotColorType struct { |
||||
id, objID int |
||||
val cmykColorType |
||||
} |
||||
|
||||
// CMYKColorType specifies an ink-based CMYK color value
|
||||
type cmykColorType struct { |
||||
c, m, y, k byte // 0% to 100%
|
||||
} |
||||
|
||||
// SizeType fields Wd and Ht specify the horizontal and vertical extents of a
|
||||
// document element such as a page.
|
||||
type SizeType struct { |
||||
Wd, Ht float64 |
||||
} |
||||
|
||||
// PointType fields X and Y specify the horizontal and vertical coordinates of
|
||||
// a point, typically used in drawing.
|
||||
type PointType struct { |
||||
X, Y float64 |
||||
} |
||||
|
||||
// XY returns the X and Y components of the receiver point.
|
||||
func (p PointType) XY() (float64, float64) { |
||||
return p.X, p.Y |
||||
} |
||||
|
||||
// ImageInfoType contains size, color and other information about an image.
|
||||
// Changes to this structure should be reflected in its GobEncode and GobDecode
|
||||
// methods.
|
||||
type ImageInfoType struct { |
||||
data []byte |
||||
smask []byte |
||||
n int |
||||
w float64 |
||||
h float64 |
||||
cs string |
||||
pal []byte |
||||
bpc int |
||||
f string |
||||
dp string |
||||
trns []int |
||||
scale float64 // document scaling factor
|
||||
dpi float64 |
||||
i string |
||||
} |
||||
|
||||
func generateImageID(info *ImageInfoType) (string, error) { |
||||
b, err := info.GobEncode() |
||||
return fmt.Sprintf("%x", sha1.Sum(b)), err |
||||
} |
||||
|
||||
// GobEncode encodes the receiving image to a byte slice.
|
||||
func (info *ImageInfoType) GobEncode() (buf []byte, err error) { |
||||
fields := []interface{}{info.data, info.smask, info.n, info.w, info.h, info.cs, |
||||
info.pal, info.bpc, info.f, info.dp, info.trns, info.scale, info.dpi} |
||||
w := new(bytes.Buffer) |
||||
encoder := gob.NewEncoder(w) |
||||
for j := 0; j < len(fields) && err == nil; j++ { |
||||
err = encoder.Encode(fields[j]) |
||||
} |
||||
if err == nil { |
||||
buf = w.Bytes() |
||||
} |
||||
return |
||||
} |
||||
|
||||
// GobDecode decodes the specified byte buffer (generated by GobEncode) into
|
||||
// the receiving image.
|
||||
func (info *ImageInfoType) GobDecode(buf []byte) (err error) { |
||||
fields := []interface{}{&info.data, &info.smask, &info.n, &info.w, &info.h, |
||||
&info.cs, &info.pal, &info.bpc, &info.f, &info.dp, &info.trns, &info.scale, &info.dpi} |
||||
r := bytes.NewBuffer(buf) |
||||
decoder := gob.NewDecoder(r) |
||||
for j := 0; j < len(fields) && err == nil; j++ { |
||||
err = decoder.Decode(fields[j]) |
||||
} |
||||
|
||||
info.i, err = generateImageID(info) |
||||
return |
||||
} |
||||
|
||||
// PointConvert returns the value of pt, expressed in points (1/72 inch), as a
|
||||
// value expressed in the unit of measure specified in New(). Since font
|
||||
// management in Fpdf uses points, this method can help with line height
|
||||
// calculations and other methods that require user units.
|
||||
func (f *Fpdf) PointConvert(pt float64) (u float64) { |
||||
return pt / f.k |
||||
} |
||||
|
||||
// PointToUnitConvert is an alias for PointConvert.
|
||||
func (f *Fpdf) PointToUnitConvert(pt float64) (u float64) { |
||||
return pt / f.k |
||||
} |
||||
|
||||
// UnitToPointConvert returns the value of u, expressed in the unit of measure
|
||||
// specified in New(), as a value expressed in points (1/72 inch). Since font
|
||||
// management in Fpdf uses points, this method can help with setting font sizes
|
||||
// based on the sizes of other non-font page elements.
|
||||
func (f *Fpdf) UnitToPointConvert(u float64) (pt float64) { |
||||
return u * f.k |
||||
} |
||||
|
||||
// Extent returns the width and height of the image in the units of the Fpdf
|
||||
// object.
|
||||
func (info *ImageInfoType) Extent() (wd, ht float64) { |
||||
return info.Width(), info.Height() |
||||
} |
||||
|
||||
// Width returns the width of the image in the units of the Fpdf object.
|
||||
func (info *ImageInfoType) Width() float64 { |
||||
return info.w / (info.scale * info.dpi / 72) |
||||
} |
||||
|
||||
// Height returns the height of the image in the units of the Fpdf object.
|
||||
func (info *ImageInfoType) Height() float64 { |
||||
return info.h / (info.scale * info.dpi / 72) |
||||
} |
||||
|
||||
// SetDpi sets the dots per inch for an image. PNG images MAY have their dpi
|
||||
// set automatically, if the image specifies it. DPI information is not
|
||||
// currently available automatically for JPG and GIF images, so if it's
|
||||
// important to you, you can set it here. It defaults to 72 dpi.
|
||||
func (info *ImageInfoType) SetDpi(dpi float64) { |
||||
info.dpi = dpi |
||||
} |
||||
|
||||
type fontFileType struct { |
||||
length1, length2 int64 |
||||
n int |
||||
embedded bool |
||||
content []byte |
||||
fontType string |
||||
} |
||||
|
||||
type linkType struct { |
||||
x, y, wd, ht float64 |
||||
link int // Auto-generated internal link ID or...
|
||||
linkStr string // ...application-provided external link string
|
||||
} |
||||
|
||||
type intLinkType struct { |
||||
page int |
||||
y float64 |
||||
} |
||||
|
||||
// outlineType is used for a sidebar outline of bookmarks
|
||||
type outlineType struct { |
||||
text string |
||||
level, parent, first, last, next, prev int |
||||
y float64 |
||||
p int |
||||
} |
||||
|
||||
// InitType is used with NewCustom() to customize an Fpdf instance.
|
||||
// OrientationStr, UnitStr, SizeStr and FontDirStr correspond to the arguments
|
||||
// accepted by New(). If the Wd and Ht fields of Size are each greater than
|
||||
// zero, Size will be used to set the default page size rather than SizeStr. Wd
|
||||
// and Ht are specified in the units of measure indicated by UnitStr.
|
||||
type InitType struct { |
||||
OrientationStr string |
||||
UnitStr string |
||||
SizeStr string |
||||
Size SizeType |
||||
FontDirStr string |
||||
} |
||||
|
||||
// FontLoader is used to read fonts (JSON font specification and zlib compressed font binaries)
|
||||
// from arbitrary locations (e.g. files, zip files, embedded font resources).
|
||||
//
|
||||
// Open provides an io.Reader for the specified font file (.json or .z). The file name
|
||||
// never includes a path. Open returns an error if the specified file cannot be opened.
|
||||
type FontLoader interface { |
||||
Open(name string) (io.Reader, error) |
||||
} |
||||
|
||||
// Pdf defines the interface used for various methods. It is implemented by the
|
||||
// main FPDF instance as well as templates.
|
||||
type Pdf interface { |
||||
AddFont(familyStr, styleStr, fileStr string) |
||||
AddFontFromBytes(familyStr, styleStr string, jsonFileBytes, zFileBytes []byte) |
||||
AddFontFromReader(familyStr, styleStr string, r io.Reader) |
||||
AddLayer(name string, visible bool) (layerID int) |
||||
AddLink() int |
||||
AddPage() |
||||
AddPageFormat(orientationStr string, size SizeType) |
||||
AddSpotColor(nameStr string, c, m, y, k byte) |
||||
AliasNbPages(aliasStr string) |
||||
ArcTo(x, y, rx, ry, degRotate, degStart, degEnd float64) |
||||
Arc(x, y, rx, ry, degRotate, degStart, degEnd float64, styleStr string) |
||||
BeginLayer(id int) |
||||
Beziergon(points []PointType, styleStr string) |
||||
Bookmark(txtStr string, level int, y float64) |
||||
CellFormat(w, h float64, txtStr, borderStr string, ln int, alignStr string, fill bool, link int, linkStr string) |
||||
Cellf(w, h float64, fmtStr string, args ...interface{}) |
||||
Cell(w, h float64, txtStr string) |
||||
Circle(x, y, r float64, styleStr string) |
||||
ClearError() |
||||
ClipCircle(x, y, r float64, outline bool) |
||||
ClipEllipse(x, y, rx, ry float64, outline bool) |
||||
ClipEnd() |
||||
ClipPolygon(points []PointType, outline bool) |
||||
ClipRect(x, y, w, h float64, outline bool) |
||||
ClipRoundedRect(x, y, w, h, r float64, outline bool) |
||||
ClipText(x, y float64, txtStr string, outline bool) |
||||
Close() |
||||
ClosePath() |
||||
CreateTemplateCustom(corner PointType, size SizeType, fn func(*Tpl)) Template |
||||
CreateTemplate(fn func(*Tpl)) Template |
||||
CurveBezierCubicTo(cx0, cy0, cx1, cy1, x, y float64) |
||||
CurveBezierCubic(x0, y0, cx0, cy0, cx1, cy1, x1, y1 float64, styleStr string) |
||||
CurveCubic(x0, y0, cx0, cy0, x1, y1, cx1, cy1 float64, styleStr string) |
||||
CurveTo(cx, cy, x, y float64) |
||||
Curve(x0, y0, cx, cy, x1, y1 float64, styleStr string) |
||||
DrawPath(styleStr string) |
||||
Ellipse(x, y, rx, ry, degRotate float64, styleStr string) |
||||
EndLayer() |
||||
Err() bool |
||||
Error() error |
||||
GetAlpha() (alpha float64, blendModeStr string) |
||||
GetAutoPageBreak() (auto bool, margin float64) |
||||
GetCellMargin() float64 |
||||
GetConversionRatio() float64 |
||||
GetDrawColor() (int, int, int) |
||||
GetDrawSpotColor() (name string, c, m, y, k byte) |
||||
GetFillColor() (int, int, int) |
||||
GetFillSpotColor() (name string, c, m, y, k byte) |
||||
GetFontDesc(familyStr, styleStr string) FontDescType |
||||
GetFontSize() (ptSize, unitSize float64) |
||||
GetImageInfo(imageStr string) (info *ImageInfoType) |
||||
GetLineWidth() float64 |
||||
GetMargins() (left, top, right, bottom float64) |
||||
GetPageSizeStr(sizeStr string) (size SizeType) |
||||
GetPageSize() (width, height float64) |
||||
GetStringWidth(s string) float64 |
||||
GetTextColor() (int, int, int) |
||||
GetTextSpotColor() (name string, c, m, y, k byte) |
||||
GetX() float64 |
||||
GetXY() (float64, float64) |
||||
GetY() float64 |
||||
HTMLBasicNew() (html HTMLBasicType) |
||||
Image(imageNameStr string, x, y, w, h float64, flow bool, tp string, link int, linkStr string) |
||||
ImageOptions(imageNameStr string, x, y, w, h float64, flow bool, options ImageOptions, link int, linkStr string) |
||||
ImageTypeFromMime(mimeStr string) (tp string) |
||||
LinearGradient(x, y, w, h float64, r1, g1, b1, r2, g2, b2 int, x1, y1, x2, y2 float64) |
||||
LineTo(x, y float64) |
||||
Line(x1, y1, x2, y2 float64) |
||||
LinkString(x, y, w, h float64, linkStr string) |
||||
Link(x, y, w, h float64, link int) |
||||
Ln(h float64) |
||||
MoveTo(x, y float64) |
||||
MultiCell(w, h float64, txtStr, borderStr, alignStr string, fill bool) |
||||
Ok() bool |
||||
OpenLayerPane() |
||||
OutputAndClose(w io.WriteCloser) error |
||||
OutputFileAndClose(fileStr string) error |
||||
Output(w io.Writer) error |
||||
PageCount() int |
||||
PageNo() int |
||||
PageSize(pageNum int) (wd, ht float64, unitStr string) |
||||
PointConvert(pt float64) (u float64) |
||||
PointToUnitConvert(pt float64) (u float64) |
||||
Polygon(points []PointType, styleStr string) |
||||
RadialGradient(x, y, w, h float64, r1, g1, b1, r2, g2, b2 int, x1, y1, x2, y2, r float64) |
||||
RawWriteBuf(r io.Reader) |
||||
RawWriteStr(str string) |
||||
Rect(x, y, w, h float64, styleStr string) |
||||
RegisterAlias(alias, replacement string) |
||||
RegisterImage(fileStr, tp string) (info *ImageInfoType) |
||||
RegisterImageOptions(fileStr string, options ImageOptions) (info *ImageInfoType) |
||||
RegisterImageOptionsReader(imgName string, options ImageOptions, r io.Reader) (info *ImageInfoType) |
||||
RegisterImageReader(imgName, tp string, r io.Reader) (info *ImageInfoType) |
||||
SetAcceptPageBreakFunc(fnc func() bool) |
||||
SetAlpha(alpha float64, blendModeStr string) |
||||
SetAuthor(authorStr string, isUTF8 bool) |
||||
SetAutoPageBreak(auto bool, margin float64) |
||||
SetCatalogSort(flag bool) |
||||
SetCellMargin(margin float64) |
||||
SetCompression(compress bool) |
||||
SetCreationDate(tm time.Time) |
||||
SetCreator(creatorStr string, isUTF8 bool) |
||||
SetDashPattern(dashArray []float64, dashPhase float64) |
||||
SetDisplayMode(zoomStr, layoutStr string) |
||||
SetDrawColor(r, g, b int) |
||||
SetDrawSpotColor(nameStr string, tint byte) |
||||
SetError(err error) |
||||
SetErrorf(fmtStr string, args ...interface{}) |
||||
SetFillColor(r, g, b int) |
||||
SetFillSpotColor(nameStr string, tint byte) |
||||
SetFont(familyStr, styleStr string, size float64) |
||||
SetFontLoader(loader FontLoader) |
||||
SetFontLocation(fontDirStr string) |
||||
SetFontSize(size float64) |
||||
SetFontStyle(styleStr string) |
||||
SetFontUnitSize(size float64) |
||||
SetFooterFunc(fnc func()) |
||||
SetFooterFuncLpi(fnc func(lastPage bool)) |
||||
SetHeaderFunc(fnc func()) |
||||
SetHeaderFuncMode(fnc func(), homeMode bool) |
||||
SetHomeXY() |
||||
SetJavascript(script string) |
||||
SetKeywords(keywordsStr string, isUTF8 bool) |
||||
SetLeftMargin(margin float64) |
||||
SetLineCapStyle(styleStr string) |
||||
SetLineJoinStyle(styleStr string) |
||||
SetLineWidth(width float64) |
||||
SetLink(link int, y float64, page int) |
||||
SetMargins(left, top, right float64) |
||||
SetPageBoxRec(t string, pb PageBox) |
||||
SetPageBox(t string, x, y, wd, ht float64) |
||||
SetPage(pageNum int) |
||||
SetProtection(actionFlag byte, userPassStr, ownerPassStr string) |
||||
SetRightMargin(margin float64) |
||||
SetSubject(subjectStr string, isUTF8 bool) |
||||
SetTextColor(r, g, b int) |
||||
SetTextSpotColor(nameStr string, tint byte) |
||||
SetTitle(titleStr string, isUTF8 bool) |
||||
SetTopMargin(margin float64) |
||||
SetUnderlineThickness(thickness float64) |
||||
SetXmpMetadata(xmpStream []byte) |
||||
SetX(x float64) |
||||
SetXY(x, y float64) |
||||
SetY(y float64) |
||||
SplitLines(txt []byte, w float64) [][]byte |
||||
String() string |
||||
SVGBasicWrite(sb *SVGBasicType, scale float64) |
||||
Text(x, y float64, txtStr string) |
||||
TransformBegin() |
||||
TransformEnd() |
||||
TransformMirrorHorizontal(x float64) |
||||
TransformMirrorLine(angle, x, y float64) |
||||
TransformMirrorPoint(x, y float64) |
||||
TransformMirrorVertical(y float64) |
||||
TransformRotate(angle, x, y float64) |
||||
TransformScale(scaleWd, scaleHt, x, y float64) |
||||
TransformScaleX(scaleWd, x, y float64) |
||||
TransformScaleXY(s, x, y float64) |
||||
TransformScaleY(scaleHt, x, y float64) |
||||
TransformSkew(angleX, angleY, x, y float64) |
||||
TransformSkewX(angleX, x, y float64) |
||||
TransformSkewY(angleY, x, y float64) |
||||
Transform(tm TransformMatrix) |
||||
TransformTranslate(tx, ty float64) |
||||
TransformTranslateX(tx float64) |
||||
TransformTranslateY(ty float64) |
||||
UnicodeTranslatorFromDescriptor(cpStr string) (rep func(string) string) |
||||
UnitToPointConvert(u float64) (pt float64) |
||||
UseTemplateScaled(t Template, corner PointType, size SizeType) |
||||
UseTemplate(t Template) |
||||
WriteAligned(width, lineHeight float64, textStr, alignStr string) |
||||
Writef(h float64, fmtStr string, args ...interface{}) |
||||
Write(h float64, txtStr string) |
||||
WriteLinkID(h float64, displayStr string, linkID int) |
||||
WriteLinkString(h float64, displayStr, targetStr string) |
||||
} |
||||
|
||||
// PageBox defines the coordinates and extent of the various page box types
|
||||
type PageBox struct { |
||||
SizeType |
||||
PointType |
||||
} |
||||
|
||||
// Fpdf is the principal structure for creating a single PDF document
|
||||
type Fpdf struct { |
||||
isCurrentUTF8 bool // is current font used in utf-8 mode
|
||||
isRTL bool // is is right to left mode enabled
|
||||
page int // current page number
|
||||
n int // current object number
|
||||
offsets []int // array of object offsets
|
||||
templates map[string]Template // templates used in this document
|
||||
templateObjects map[string]int // template object IDs within this document
|
||||
importedObjs map[string][]byte // imported template objects (gofpdi)
|
||||
importedObjPos map[string]map[int]string // imported template objects hashes and their positions (gofpdi)
|
||||
importedTplObjs map[string]string // imported template names and IDs (hashed) (gofpdi)
|
||||
importedTplIDs map[string]int // imported template ids hash to object id int (gofpdi)
|
||||
buffer fmtBuffer // buffer holding in-memory PDF
|
||||
pages []*bytes.Buffer // slice[page] of page content; 1-based
|
||||
state int // current document state
|
||||
compress bool // compression flag
|
||||
k float64 // scale factor (number of points in user unit)
|
||||
defOrientation string // default orientation
|
||||
curOrientation string // current orientation
|
||||
stdPageSizes map[string]SizeType // standard page sizes
|
||||
defPageSize SizeType // default page size
|
||||
defPageBoxes map[string]PageBox // default page size
|
||||
curPageSize SizeType // current page size
|
||||
pageSizes map[int]SizeType // used for pages with non default sizes or orientations
|
||||
pageBoxes map[int]map[string]PageBox // used to define the crop, trim, bleed and art boxes
|
||||
unitStr string // unit of measure for all rendered objects except fonts
|
||||
wPt, hPt float64 // dimensions of current page in points
|
||||
w, h float64 // dimensions of current page in user unit
|
||||
lMargin float64 // left margin
|
||||
tMargin float64 // top margin
|
||||
rMargin float64 // right margin
|
||||
bMargin float64 // page break margin
|
||||
cMargin float64 // cell margin
|
||||
x, y float64 // current position in user unit
|
||||
lasth float64 // height of last printed cell
|
||||
lineWidth float64 // line width in user unit
|
||||
fontpath string // path containing fonts
|
||||
fontLoader FontLoader // used to load font files from arbitrary locations
|
||||
coreFonts map[string]bool // array of core font names
|
||||
fonts map[string]fontDefType // array of used fonts
|
||||
fontFiles map[string]fontFileType // array of font files
|
||||
diffs []string // array of encoding differences
|
||||
fontFamily string // current font family
|
||||
fontStyle string // current font style
|
||||
underline bool // underlining flag
|
||||
currentFont fontDefType // current font info
|
||||
fontSizePt float64 // current font size in points
|
||||
fontSize float64 // current font size in user unit
|
||||
ws float64 // word spacing
|
||||
images map[string]*ImageInfoType // array of used images
|
||||
aliasMap map[string]string // map of alias->replacement
|
||||
pageLinks [][]linkType // pageLinks[page][link], both 1-based
|
||||
links []intLinkType // array of internal links
|
||||
outlines []outlineType // array of outlines
|
||||
outlineRoot int // root of outlines
|
||||
autoPageBreak bool // automatic page breaking
|
||||
acceptPageBreak func() bool // returns true to accept page break
|
||||
pageBreakTrigger float64 // threshold used to trigger page breaks
|
||||
inHeader bool // flag set when processing header
|
||||
headerFnc func() // function provided by app and called to write header
|
||||
headerHomeMode bool // set position to home after headerFnc is called
|
||||
inFooter bool // flag set when processing footer
|
||||
footerFnc func() // function provided by app and called to write footer
|
||||
footerFncLpi func(bool) // function provided by app and called to write footer with last page flag
|
||||
zoomMode string // zoom display mode
|
||||
layoutMode string // layout display mode
|
||||
xmp []byte // XMP metadata
|
||||
producer string // producer
|
||||
title string // title
|
||||
subject string // subject
|
||||
author string // author
|
||||
keywords string // keywords
|
||||
creator string // creator
|
||||
creationDate time.Time // override for dcoument CreationDate value
|
||||
aliasNbPagesStr string // alias for total number of pages
|
||||
pdfVersion string // PDF version number
|
||||
fontDirStr string // location of font definition files
|
||||
capStyle int // line cap style: butt 0, round 1, square 2
|
||||
joinStyle int // line segment join style: miter 0, round 1, bevel 2
|
||||
dashArray []float64 // dash array
|
||||
dashPhase float64 // dash phase
|
||||
blendList []blendModeType // slice[idx] of alpha transparency modes, 1-based
|
||||
blendMap map[string]int // map into blendList
|
||||
blendMode string // current blend mode
|
||||
alpha float64 // current transpacency
|
||||
gradientList []gradientType // slice[idx] of gradient records
|
||||
clipNest int // Number of active clipping contexts
|
||||
transformNest int // Number of active transformation contexts
|
||||
err error // Set if error occurs during life cycle of instance
|
||||
protect protectType // document protection structure
|
||||
layer layerRecType // manages optional layers in document
|
||||
catalogSort bool // sort resource catalogs in document
|
||||
nJs int // JavaScript object number
|
||||
javascript *string // JavaScript code to include in the PDF
|
||||
colorFlag bool // indicates whether fill and text colors are different
|
||||
color struct { |
||||
// Composite values of colors
|
||||
draw, fill, text colorType |
||||
} |
||||
spotColorMap map[string]spotColorType // Map of named ink-based colors
|
||||
userUnderlineThickness float64 // A custom user underline thickness multiplier.
|
||||
} |
||||
|
||||
type encType struct { |
||||
uv int |
||||
name string |
||||
} |
||||
|
||||
type encListType [256]encType |
||||
|
||||
type fontBoxType struct { |
||||
Xmin, Ymin, Xmax, Ymax int |
||||
} |
||||
|
||||
// Font flags for FontDescType.Flags as defined in the pdf specification.
|
||||
const ( |
||||
// FontFlagFixedPitch is set if all glyphs have the same width (as
|
||||
// opposed to proportional or variable-pitch fonts, which have
|
||||
// different widths).
|
||||
FontFlagFixedPitch = 1 << 0 |
||||
// FontFlagSerif is set if glyphs have serifs, which are short
|
||||
// strokes drawn at an angle on the top and bottom of glyph stems.
|
||||
// (Sans serif fonts do not have serifs.)
|
||||
FontFlagSerif = 1 << 1 |
||||
// FontFlagSymbolic is set if font contains glyphs outside the
|
||||
// Adobe standard Latin character set. This flag and the
|
||||
// Nonsymbolic flag shall not both be set or both be clear.
|
||||
FontFlagSymbolic = 1 << 2 |
||||
// FontFlagScript is set if glyphs resemble cursive handwriting.
|
||||
FontFlagScript = 1 << 3 |
||||
// FontFlagNonsymbolic is set if font uses the Adobe standard
|
||||
// Latin character set or a subset of it.
|
||||
FontFlagNonsymbolic = 1 << 5 |
||||
// FontFlagItalic is set if glyphs have dominant vertical strokes
|
||||
// that are slanted.
|
||||
FontFlagItalic = 1 << 6 |
||||
// FontFlagAllCap is set if font contains no lowercase letters;
|
||||
// typically used for display purposes, such as for titles or
|
||||
// headlines.
|
||||
FontFlagAllCap = 1 << 16 |
||||
// SmallCap is set if font contains both uppercase and lowercase
|
||||
// letters. The uppercase letters are similar to those in the
|
||||
// regular version of the same typeface family. The glyphs for the
|
||||
// lowercase letters have the same shapes as the corresponding
|
||||
// uppercase letters, but they are sized and their proportions
|
||||
// adjusted so that they have the same size and stroke weight as
|
||||
// lowercase glyphs in the same typeface family.
|
||||
SmallCap = 1 << 18 |
||||
// ForceBold determines whether bold glyphs shall be painted with
|
||||
// extra pixels even at very small text sizes by a conforming
|
||||
// reader. If the ForceBold flag is set, features of bold glyphs
|
||||
// may be thickened at small text sizes.
|
||||
ForceBold = 1 << 18 |
||||
) |
||||
|
||||
// FontDescType (font descriptor) specifies metrics and other
|
||||
// attributes of a font, as distinct from the metrics of individual
|
||||
// glyphs (as defined in the pdf specification).
|
||||
type FontDescType struct { |
||||
// The maximum height above the baseline reached by glyphs in this
|
||||
// font (for example for "S"). The height of glyphs for accented
|
||||
// characters shall be excluded.
|
||||
Ascent int |
||||
// The maximum depth below the baseline reached by glyphs in this
|
||||
// font. The value shall be a negative number.
|
||||
Descent int |
||||
// The vertical coordinate of the top of flat capital letters,
|
||||
// measured from the baseline (for example "H").
|
||||
CapHeight int |
||||
// A collection of flags defining various characteristics of the
|
||||
// font. (See the FontFlag* constants.)
|
||||
Flags int |
||||
// A rectangle, expressed in the glyph coordinate system, that
|
||||
// shall specify the font bounding box. This should be the smallest
|
||||
// rectangle enclosing the shape that would result if all of the
|
||||
// glyphs of the font were placed with their origins coincident
|
||||
// and then filled.
|
||||
FontBBox fontBoxType |
||||
// The angle, expressed in degrees counterclockwise from the
|
||||
// vertical, of the dominant vertical strokes of the font. (The
|
||||
// 9-o’clock position is 90 degrees, and the 3-o’clock position
|
||||
// is –90 degrees.) The value shall be negative for fonts that
|
||||
// slope to the right, as almost all italic fonts do.
|
||||
ItalicAngle int |
||||
// The thickness, measured horizontally, of the dominant vertical
|
||||
// stems of glyphs in the font.
|
||||
StemV int |
||||
// The width to use for character codes whose widths are not
|
||||
// specified in a font dictionary’s Widths array. This shall have
|
||||
// a predictable effect only if all such codes map to glyphs whose
|
||||
// actual widths are the same as the value of the MissingWidth
|
||||
// entry. (Default value: 0.)
|
||||
MissingWidth int |
||||
} |
||||
|
||||
type fontDefType struct { |
||||
Tp string // "Core", "TrueType", ...
|
||||
Name string // "Courier-Bold", ...
|
||||
Desc FontDescType // Font descriptor
|
||||
Up int // Underline position
|
||||
Ut int // Underline thickness
|
||||
Cw []int // Character width by ordinal
|
||||
Enc string // "cp1252", ...
|
||||
Diff string // Differences from reference encoding
|
||||
File string // "Redressed.z"
|
||||
Size1, Size2 int // Type1 values
|
||||
OriginalSize int // Size of uncompressed font file
|
||||
N int // Set by font loader
|
||||
DiffN int // Position of diff in app array, set by font loader
|
||||
i string // 1-based position in font list, set by font loader, not this program
|
||||
utf8File *utf8FontFile // UTF-8 font
|
||||
usedRunes map[int]int // Array of used runes
|
||||
} |
||||
|
||||
// generateFontID generates a font Id from the font definition
|
||||
func generateFontID(fdt fontDefType) (string, error) { |
||||
// file can be different if generated in different instance
|
||||
fdt.File = "" |
||||
b, err := json.Marshal(&fdt) |
||||
return fmt.Sprintf("%x", sha1.Sum(b)), err |
||||
} |
||||
|
||||
type fontInfoType struct { |
||||
Data []byte |
||||
File string |
||||
OriginalSize int |
||||
FontName string |
||||
Bold bool |
||||
IsFixedPitch bool |
||||
UnderlineThickness int |
||||
UnderlinePosition int |
||||
Widths []int |
||||
Size1, Size2 uint32 |
||||
Desc FontDescType |
||||
} |
||||
@ -0,0 +1,268 @@ |
||||
/* |
||||
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 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. |
||||
|
||||
|
||||
Installation |
||||
|
||||
To install the package on your system, run |
||||
|
||||
go get github.com/jung-kurt/gofpdf |
||||
|
||||
Later, to receive updates, run |
||||
|
||||
go get -u -v github.com/jung-kurt/gofpdf/... |
||||
|
||||
|
||||
Quick Start |
||||
|
||||
The following Go code generates a simple PDF file. |
||||
|
||||
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 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 library written in PHP (despite the caveat in the |
||||
introduction to Effective Go). 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 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. |
||||
|
||||
./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 and DejaVu Fonts. |
||||
|
||||
|
||||
Related Packages |
||||
|
||||
The 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 if appropriate |
||||
|
||||
- conform to the standards of golint and go vet, that is, golint . and |
||||
go vet . should not generate any warnings |
||||
|
||||
- not diminish test coverage |
||||
|
||||
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 |
||||
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. |
||||
|
||||
|
||||
Roadmap |
||||
|
||||
|
||||
- Improve test coverage as reported by the coverage tool. |
||||
*/ |
||||
package gofpdf |
||||
@ -0,0 +1,559 @@ |
||||
/* |
||||
* Copyright (c) 2014 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 |
||||
|
||||
// Embedded standard fonts
|
||||
|
||||
import ( |
||||
"strings" |
||||
) |
||||
|
||||
var embeddedFontList = map[string]string{ |
||||
"courierBI": `{"Tp":"Core","Name":"Courier-BoldOblique","Up":-100,"Ut":50,"I":256,"Cw":[600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600]}`, |
||||
"courierB": `{"Tp":"Core","Name":"Courier-Bold","Up":-100,"Ut":50,"I":256,"Cw":[600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600]}`, |
||||
"courierI": `{"Tp":"Core","Name":"Courier-Oblique","Up":-100,"Ut":50,"I":256,"Cw":[600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600]}`, |
||||
"courier": `{"Tp":"Core","Name":"Courier","Up":-100,"Ut":50,"I":256,"Cw":[600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600]}`, |
||||
"helveticaBI": `{"Tp":"Core","Name":"Helvetica-BoldOblique","Up":-100,"Ut":50,"Cw":[278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,333,474,556,556,889,722,238,333,333,389,584,278,333,278,278,556,556,556,556,556,556,556,556,556,556,333,333,584,584,584,611,975,722,722,722,722,667,611,778,722,278,556,722,611,833,722,778,667,778,722,667,611,722,667,944,667,667,611,333,278,333,584,556,333,556,611,556,611,556,333,611,611,278,278,556,278,889,611,611,611,611,389,556,333,611,556,778,556,556,500,389,280,389,584,350,556,350,278,556,500,1000,556,556,333,1000,667,333,1000,350,611,350,350,278,278,500,500,350,556,1000,333,1000,556,333,944,350,500,667,278,333,556,556,556,556,280,556,333,737,370,556,584,333,737,333,400,584,333,333,333,611,556,278,333,333,365,556,834,834,834,611,722,722,722,722,722,722,1000,722,667,667,667,667,278,278,278,278,722,722,778,778,778,778,778,584,778,722,722,722,722,667,667,611,556,556,556,556,556,556,889,556,556,556,556,556,278,278,278,278,611,611,611,611,611,611,611,584,611,611,611,611,611,556,611,556]}`, |
||||
"helveticaB": `{"Tp":"Core","Name":"Helvetica-Bold","Up":-100,"Ut":50,"Cw":[278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,333,474,556,556,889,722,238,333,333,389,584,278,333,278,278,556,556,556,556,556,556,556,556,556,556,333,333,584,584,584,611,975,722,722,722,722,667,611,778,722,278,556,722,611,833,722,778,667,778,722,667,611,722,667,944,667,667,611,333,278,333,584,556,333,556,611,556,611,556,333,611,611,278,278,556,278,889,611,611,611,611,389,556,333,611,556,778,556,556,500,389,280,389,584,350,556,350,278,556,500,1000,556,556,333,1000,667,333,1000,350,611,350,350,278,278,500,500,350,556,1000,333,1000,556,333,944,350,500,667,278,333,556,556,556,556,280,556,333,737,370,556,584,333,737,333,400,584,333,333,333,611,556,278,333,333,365,556,834,834,834,611,722,722,722,722,722,722,1000,722,667,667,667,667,278,278,278,278,722,722,778,778,778,778,778,584,778,722,722,722,722,667,667,611,556,556,556,556,556,556,889,556,556,556,556,556,278,278,278,278,611,611,611,611,611,611,611,584,611,611,611,611,611,556,611,556]}`, |
||||
"helveticaI": `{"Tp":"Core","Name":"Helvetica-Oblique","Up":-100,"Ut":50,"Cw":[278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,355,556,556,889,667,191,333,333,389,584,278,333,278,278,556,556,556,556,556,556,556,556,556,556,278,278,584,584,584,556,1015,667,667,722,722,667,611,778,722,278,500,667,556,833,722,778,667,778,722,667,611,722,667,944,667,667,611,278,278,278,469,556,333,556,556,500,556,556,278,556,556,222,222,500,222,833,556,556,556,556,333,500,278,556,500,722,500,500,500,334,260,334,584,350,556,350,222,556,333,1000,556,556,333,1000,667,333,1000,350,611,350,350,222,222,333,333,350,556,1000,333,1000,500,333,944,350,500,667,278,333,556,556,556,556,260,556,333,737,370,556,584,333,737,333,400,584,333,333,333,556,537,278,333,333,365,556,834,834,834,611,667,667,667,667,667,667,1000,722,667,667,667,667,278,278,278,278,722,722,778,778,778,778,778,584,778,722,722,722,722,667,667,611,556,556,556,556,556,556,889,500,556,556,556,556,278,278,278,278,556,556,556,556,556,556,556,584,611,556,556,556,556,500,556,500]}`, |
||||
"helvetica": `{"Tp":"Core","Name":"Helvetica","Up":-100,"Ut":50,"Cw":[278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,355,556,556,889,667,191,333,333,389,584,278,333,278,278,556,556,556,556,556,556,556,556,556,556,278,278,584,584,584,556,1015,667,667,722,722,667,611,778,722,278,500,667,556,833,722,778,667,778,722,667,611,722,667,944,667,667,611,278,278,278,469,556,333,556,556,500,556,556,278,556,556,222,222,500,222,833,556,556,556,556,333,500,278,556,500,722,500,500,500,334,260,334,584,350,556,350,222,556,333,1000,556,556,333,1000,667,333,1000,350,611,350,350,222,222,333,333,350,556,1000,333,1000,500,333,944,350,500,667,278,333,556,556,556,556,260,556,333,737,370,556,584,333,737,333,400,584,333,333,333,556,537,278,333,333,365,556,834,834,834,611,667,667,667,667,667,667,1000,722,667,667,667,667,278,278,278,278,722,722,778,778,778,778,778,584,778,722,722,722,722,667,667,611,556,556,556,556,556,556,889,500,556,556,556,556,278,278,278,278,556,556,556,556,556,556,556,584,611,556,556,556,556,500,556,500]}`, |
||||
"timesBI": `{"Tp":"Core","Name":"Times-BoldItalic","Up":-100,"Ut":50,"Cw":[250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,389,555,500,500,833,778,278,333,333,500,570,250,333,250,278,500,500,500,500,500,500,500,500,500,500,333,333,570,570,570,500,832,667,667,667,722,667,667,722,778,389,500,667,611,889,722,722,611,722,667,556,611,722,667,889,667,611,611,333,278,333,570,500,333,500,500,444,500,444,333,500,556,278,278,500,278,778,556,500,500,500,389,389,278,556,444,667,500,444,389,348,220,348,570,350,500,350,333,500,500,1000,500,500,333,1000,556,333,944,350,611,350,350,333,333,500,500,350,500,1000,333,1000,389,333,722,350,389,611,250,389,500,500,500,500,220,500,333,747,266,500,606,333,747,333,400,570,300,300,333,576,500,250,333,300,300,500,750,750,750,500,667,667,667,667,667,667,944,667,667,667,667,667,389,389,389,389,722,722,722,722,722,722,722,570,722,722,722,722,722,611,611,500,500,500,500,500,500,500,722,444,444,444,444,444,278,278,278,278,500,556,500,500,500,500,500,570,500,556,556,556,556,444,500,444]}`, |
||||
"timesB": `{"Tp":"Core","Name":"Times-Bold","Up":-100,"Ut":50,"Cw":[250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,333,555,500,500,1000,833,278,333,333,500,570,250,333,250,278,500,500,500,500,500,500,500,500,500,500,333,333,570,570,570,500,930,722,667,722,722,667,611,778,778,389,500,778,667,944,722,778,611,778,722,556,667,722,722,1000,722,722,667,333,278,333,581,500,333,500,556,444,556,444,333,500,556,278,333,556,278,833,556,500,556,556,444,389,333,556,500,722,500,500,444,394,220,394,520,350,500,350,333,500,500,1000,500,500,333,1000,556,333,1000,350,667,350,350,333,333,500,500,350,500,1000,333,1000,389,333,722,350,444,722,250,333,500,500,500,500,220,500,333,747,300,500,570,333,747,333,400,570,300,300,333,556,540,250,333,300,330,500,750,750,750,500,722,722,722,722,722,722,1000,722,667,667,667,667,389,389,389,389,722,722,778,778,778,778,778,570,778,722,722,722,722,722,611,556,500,500,500,500,500,500,722,444,444,444,444,444,278,278,278,278,500,556,500,500,500,500,500,570,500,556,556,556,556,500,556,500]}`, |
||||
"timesI": `{"Tp":"Core","Name":"Times-Italic","Up":-100,"Ut":50,"Cw":[250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,333,420,500,500,833,778,214,333,333,500,675,250,333,250,278,500,500,500,500,500,500,500,500,500,500,333,333,675,675,675,500,920,611,611,667,722,611,611,722,722,333,444,667,556,833,667,722,611,722,611,500,556,722,611,833,611,556,556,389,278,389,422,500,333,500,500,444,500,444,278,500,500,278,278,444,278,722,500,500,500,500,389,389,278,500,444,667,444,444,389,400,275,400,541,350,500,350,333,500,556,889,500,500,333,1000,500,333,944,350,556,350,350,333,333,556,556,350,500,889,333,980,389,333,667,350,389,556,250,389,500,500,500,500,275,500,333,760,276,500,675,333,760,333,400,675,300,300,333,500,523,250,333,300,310,500,750,750,750,500,611,611,611,611,611,611,889,667,611,611,611,611,333,333,333,333,722,667,722,722,722,722,722,675,722,722,722,722,722,556,611,500,500,500,500,500,500,500,667,444,444,444,444,444,278,278,278,278,500,500,500,500,500,500,500,675,500,500,500,500,500,444,500,444]}`, |
||||
"times": `{"Tp":"Core","Name":"Times-Roman","Up":-100,"Ut":50,"Cw":[250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,250,333,408,500,500,833,778,180,333,333,500,564,250,333,250,278,500,500,500,500,500,500,500,500,500,500,278,278,564,564,564,444,921,722,667,667,722,611,556,722,722,333,389,722,611,889,722,722,556,722,667,556,611,722,722,944,722,722,611,333,278,333,469,500,333,444,500,444,500,444,333,500,500,278,278,500,278,778,500,500,500,500,333,389,278,500,500,722,500,500,444,480,200,480,541,350,500,350,333,500,444,1000,500,500,333,1000,556,333,889,350,611,350,350,333,333,444,444,350,500,1000,333,980,389,333,722,350,444,722,250,333,500,500,500,500,200,500,333,760,276,500,564,333,760,333,400,564,300,300,333,500,453,250,333,300,310,500,750,750,750,444,722,722,722,722,722,722,889,667,611,611,611,611,333,333,333,333,722,722,722,722,722,722,722,564,722,722,722,722,722,722,556,500,444,444,444,444,444,444,667,444,444,444,444,444,278,278,278,278,500,500,500,500,500,500,500,564,500,500,500,500,500,500,500,500]}`, |
||||
"zapfdingbats": `{"Tp":"Core","Name":"ZapfDingbats","Up":-100,"Ut":50,"Cw":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,278,974,961,974,980,719,789,790,791,690,960,939,549,855,911,933,911,945,974,755,846,762,761,571,677,763,760,759,754,494,552,537,577,692,786,788,788,790,793,794,816,823,789,841,823,833,816,831,923,744,723,749,790,792,695,776,768,792,759,707,708,682,701,826,815,789,789,707,687,696,689,786,787,713,791,785,791,873,761,762,762,759,759,892,892,788,784,438,138,277,415,392,392,668,668,0,390,390,317,317,276,276,509,509,410,410,234,234,334,334,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,732,544,544,910,667,760,760,776,595,694,626,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,788,894,838,1016,458,748,924,748,918,927,928,928,834,873,828,924,924,917,930,931,463,883,836,836,867,867,696,696,874,0,874,760,946,771,865,771,888,967,888,831,873,927,970,918,0]}`, |
||||
} |
||||
|
||||
func (f *Fpdf) coreFontReader(familyStr, styleStr string) (r *strings.Reader) { |
||||
key := familyStr + styleStr |
||||
str, ok := embeddedFontList[key] |
||||
if ok { |
||||
r = strings.NewReader(str) |
||||
} else { |
||||
f.SetErrorf("could not locate \"%s\" among embedded core font definition files", key) |
||||
} |
||||
return |
||||
} |
||||
|
||||
var embeddedMapList = map[string]string{ |
||||
"cp1250": ` |
||||
!00 U+0000 .notdef |
||||
!01 U+0001 .notdef |
||||
!02 U+0002 .notdef |
||||
!03 U+0003 .notdef |
||||
!04 U+0004 .notdef |
||||
!05 U+0005 .notdef |
||||
!06 U+0006 .notdef |
||||
!07 U+0007 .notdef |
||||
!08 U+0008 .notdef |
||||
!09 U+0009 .notdef |
||||
!0A U+000A .notdef |
||||
!0B U+000B .notdef |
||||
!0C U+000C .notdef |
||||
!0D U+000D .notdef |
||||
!0E U+000E .notdef |
||||
!0F U+000F .notdef |
||||
!10 U+0010 .notdef |
||||
!11 U+0011 .notdef |
||||
!12 U+0012 .notdef |
||||
!13 U+0013 .notdef |
||||
!14 U+0014 .notdef |
||||
!15 U+0015 .notdef |
||||
!16 U+0016 .notdef |
||||
!17 U+0017 .notdef |
||||
!18 U+0018 .notdef |
||||
!19 U+0019 .notdef |
||||
!1A U+001A .notdef |
||||
!1B U+001B .notdef |
||||
!1C U+001C .notdef |
||||
!1D U+001D .notdef |
||||
!1E U+001E .notdef |
||||
!1F U+001F .notdef |
||||
!20 U+0020 space |
||||
!21 U+0021 exclam |
||||
!22 U+0022 quotedbl |
||||
!23 U+0023 numbersign |
||||
!24 U+0024 dollar |
||||
!25 U+0025 percent |
||||
!26 U+0026 ampersand |
||||
!27 U+0027 quotesingle |
||||
!28 U+0028 parenleft |
||||
!29 U+0029 parenright |
||||
!2A U+002A asterisk |
||||
!2B U+002B plus |
||||
!2C U+002C comma |
||||
!2D U+002D hyphen |
||||
!2E U+002E period |
||||
!2F U+002F slash |
||||
!30 U+0030 zero |
||||
!31 U+0031 one |
||||
!32 U+0032 two |
||||
!33 U+0033 three |
||||
!34 U+0034 four |
||||
!35 U+0035 five |
||||
!36 U+0036 six |
||||
!37 U+0037 seven |
||||
!38 U+0038 eight |
||||
!39 U+0039 nine |
||||
!3A U+003A colon |
||||
!3B U+003B semicolon |
||||
!3C U+003C less |
||||
!3D U+003D equal |
||||
!3E U+003E greater |
||||
!3F U+003F question |
||||
!40 U+0040 at |
||||
!41 U+0041 A |
||||
!42 U+0042 B |
||||
!43 U+0043 C |
||||
!44 U+0044 D |
||||
!45 U+0045 E |
||||
!46 U+0046 F |
||||
!47 U+0047 G |
||||
!48 U+0048 H |
||||
!49 U+0049 I |
||||
!4A U+004A J |
||||
!4B U+004B K |
||||
!4C U+004C L |
||||
!4D U+004D M |
||||
!4E U+004E N |
||||
!4F U+004F O |
||||
!50 U+0050 P |
||||
!51 U+0051 Q |
||||
!52 U+0052 R |
||||
!53 U+0053 S |
||||
!54 U+0054 T |
||||
!55 U+0055 U |
||||
!56 U+0056 V |
||||
!57 U+0057 W |
||||
!58 U+0058 X |
||||
!59 U+0059 Y |
||||
!5A U+005A Z |
||||
!5B U+005B bracketleft |
||||
!5C U+005C backslash |
||||
!5D U+005D bracketright |
||||
!5E U+005E asciicircum |
||||
!5F U+005F underscore |
||||
!60 U+0060 grave |
||||
!61 U+0061 a |
||||
!62 U+0062 b |
||||
!63 U+0063 c |
||||
!64 U+0064 d |
||||
!65 U+0065 e |
||||
!66 U+0066 f |
||||
!67 U+0067 g |
||||
!68 U+0068 h |
||||
!69 U+0069 i |
||||
!6A U+006A j |
||||
!6B U+006B k |
||||
!6C U+006C l |
||||
!6D U+006D m |
||||
!6E U+006E n |
||||
!6F U+006F o |
||||
!70 U+0070 p |
||||
!71 U+0071 q |
||||
!72 U+0072 r |
||||
!73 U+0073 s |
||||
!74 U+0074 t |
||||
!75 U+0075 u |
||||
!76 U+0076 v |
||||
!77 U+0077 w |
||||
!78 U+0078 x |
||||
!79 U+0079 y |
||||
!7A U+007A z |
||||
!7B U+007B braceleft |
||||
!7C U+007C bar |
||||
!7D U+007D braceright |
||||
!7E U+007E asciitilde |
||||
!7F U+007F .notdef |
||||
!80 U+20AC Euro |
||||
!82 U+201A quotesinglbase |
||||
!84 U+201E quotedblbase |
||||
!85 U+2026 ellipsis |
||||
!86 U+2020 dagger |
||||
!87 U+2021 daggerdbl |
||||
!89 U+2030 perthousand |
||||
!8A U+0160 Scaron |
||||
!8B U+2039 guilsinglleft |
||||
!8C U+015A Sacute |
||||
!8D U+0164 Tcaron |
||||
!8E U+017D Zcaron |
||||
!8F U+0179 Zacute |
||||
!91 U+2018 quoteleft |
||||
!92 U+2019 quoteright |
||||
!93 U+201C quotedblleft |
||||
!94 U+201D quotedblright |
||||
!95 U+2022 bullet |
||||
!96 U+2013 endash |
||||
!97 U+2014 emdash |
||||
!99 U+2122 trademark |
||||
!9A U+0161 scaron |
||||
!9B U+203A guilsinglright |
||||
!9C U+015B sacute |
||||
!9D U+0165 tcaron |
||||
!9E U+017E zcaron |
||||
!9F U+017A zacute |
||||
!A0 U+00A0 space |
||||
!A1 U+02C7 caron |
||||
!A2 U+02D8 breve |
||||
!A3 U+0141 Lslash |
||||
!A4 U+00A4 currency |
||||
!A5 U+0104 Aogonek |
||||
!A6 U+00A6 brokenbar |
||||
!A7 U+00A7 section |
||||
!A8 U+00A8 dieresis |
||||
!A9 U+00A9 copyright |
||||
!AA U+015E Scedilla |
||||
!AB U+00AB guillemotleft |
||||
!AC U+00AC logicalnot |
||||
!AD U+00AD hyphen |
||||
!AE U+00AE registered |
||||
!AF U+017B Zdotaccent |
||||
!B0 U+00B0 degree |
||||
!B1 U+00B1 plusminus |
||||
!B2 U+02DB ogonek |
||||
!B3 U+0142 lslash |
||||
!B4 U+00B4 acute |
||||
!B5 U+00B5 mu |
||||
!B6 U+00B6 paragraph |
||||
!B7 U+00B7 periodcentered |
||||
!B8 U+00B8 cedilla |
||||
!B9 U+0105 aogonek |
||||
!BA U+015F scedilla |
||||
!BB U+00BB guillemotright |
||||
!BC U+013D Lcaron |
||||
!BD U+02DD hungarumlaut |
||||
!BE U+013E lcaron |
||||
!BF U+017C zdotaccent |
||||
!C0 U+0154 Racute |
||||
!C1 U+00C1 Aacute |
||||
!C2 U+00C2 Acircumflex |
||||
!C3 U+0102 Abreve |
||||
!C4 U+00C4 Adieresis |
||||
!C5 U+0139 Lacute |
||||
!C6 U+0106 Cacute |
||||
!C7 U+00C7 Ccedilla |
||||
!C8 U+010C Ccaron |
||||
!C9 U+00C9 Eacute |
||||
!CA U+0118 Eogonek |
||||
!CB U+00CB Edieresis |
||||
!CC U+011A Ecaron |
||||
!CD U+00CD Iacute |
||||
!CE U+00CE Icircumflex |
||||
!CF U+010E Dcaron |
||||
!D0 U+0110 Dcroat |
||||
!D1 U+0143 Nacute |
||||
!D2 U+0147 Ncaron |
||||
!D3 U+00D3 Oacute |
||||
!D4 U+00D4 Ocircumflex |
||||
!D5 U+0150 Ohungarumlaut |
||||
!D6 U+00D6 Odieresis |
||||
!D7 U+00D7 multiply |
||||
!D8 U+0158 Rcaron |
||||
!D9 U+016E Uring |
||||
!DA U+00DA Uacute |
||||
!DB U+0170 Uhungarumlaut |
||||
!DC U+00DC Udieresis |
||||
!DD U+00DD Yacute |
||||
!DE U+0162 Tcommaaccent |
||||
!DF U+00DF germandbls |
||||
!E0 U+0155 racute |
||||
!E1 U+00E1 aacute |
||||
!E2 U+00E2 acircumflex |
||||
!E3 U+0103 abreve |
||||
!E4 U+00E4 adieresis |
||||
!E5 U+013A lacute |
||||
!E6 U+0107 cacute |
||||
!E7 U+00E7 ccedilla |
||||
!E8 U+010D ccaron |
||||
!E9 U+00E9 eacute |
||||
!EA U+0119 eogonek |
||||
!EB U+00EB edieresis |
||||
!EC U+011B ecaron |
||||
!ED U+00ED iacute |
||||
!EE U+00EE icircumflex |
||||
!EF U+010F dcaron |
||||
!F0 U+0111 dcroat |
||||
!F1 U+0144 nacute |
||||
!F2 U+0148 ncaron |
||||
!F3 U+00F3 oacute |
||||
!F4 U+00F4 ocircumflex |
||||
!F5 U+0151 ohungarumlaut |
||||
!F6 U+00F6 odieresis |
||||
!F7 U+00F7 divide |
||||
!F8 U+0159 rcaron |
||||
!F9 U+016F uring |
||||
!FA U+00FA uacute |
||||
!FB U+0171 uhungarumlaut |
||||
!FC U+00FC udieresis |
||||
!FD U+00FD yacute |
||||
!FE U+0163 tcommaaccent |
||||
!FF U+02D9 dotaccent |
||||
`, |
||||
"cp1252": ` |
||||
!00 U+0000 .notdef |
||||
!01 U+0001 .notdef |
||||
!02 U+0002 .notdef |
||||
!03 U+0003 .notdef |
||||
!04 U+0004 .notdef |
||||
!05 U+0005 .notdef |
||||
!06 U+0006 .notdef |
||||
!07 U+0007 .notdef |
||||
!08 U+0008 .notdef |
||||
!09 U+0009 .notdef |
||||
!0A U+000A .notdef |
||||
!0B U+000B .notdef |
||||
!0C U+000C .notdef |
||||
!0D U+000D .notdef |
||||
!0E U+000E .notdef |
||||
!0F U+000F .notdef |
||||
!10 U+0010 .notdef |
||||
!11 U+0011 .notdef |
||||
!12 U+0012 .notdef |
||||
!13 U+0013 .notdef |
||||
!14 U+0014 .notdef |
||||
!15 U+0015 .notdef |
||||
!16 U+0016 .notdef |
||||
!17 U+0017 .notdef |
||||
!18 U+0018 .notdef |
||||
!19 U+0019 .notdef |
||||
!1A U+001A .notdef |
||||
!1B U+001B .notdef |
||||
!1C U+001C .notdef |
||||
!1D U+001D .notdef |
||||
!1E U+001E .notdef |
||||
!1F U+001F .notdef |
||||
!20 U+0020 space |
||||
!21 U+0021 exclam |
||||
!22 U+0022 quotedbl |
||||
!23 U+0023 numbersign |
||||
!24 U+0024 dollar |
||||
!25 U+0025 percent |
||||
!26 U+0026 ampersand |
||||
!27 U+0027 quotesingle |
||||
!28 U+0028 parenleft |
||||
!29 U+0029 parenright |
||||
!2A U+002A asterisk |
||||
!2B U+002B plus |
||||
!2C U+002C comma |
||||
!2D U+002D hyphen |
||||
!2E U+002E period |
||||
!2F U+002F slash |
||||
!30 U+0030 zero |
||||
!31 U+0031 one |
||||
!32 U+0032 two |
||||
!33 U+0033 three |
||||
!34 U+0034 four |
||||
!35 U+0035 five |
||||
!36 U+0036 six |
||||
!37 U+0037 seven |
||||
!38 U+0038 eight |
||||
!39 U+0039 nine |
||||
!3A U+003A colon |
||||
!3B U+003B semicolon |
||||
!3C U+003C less |
||||
!3D U+003D equal |
||||
!3E U+003E greater |
||||
!3F U+003F question |
||||
!40 U+0040 at |
||||
!41 U+0041 A |
||||
!42 U+0042 B |
||||
!43 U+0043 C |
||||
!44 U+0044 D |
||||
!45 U+0045 E |
||||
!46 U+0046 F |
||||
!47 U+0047 G |
||||
!48 U+0048 H |
||||
!49 U+0049 I |
||||
!4A U+004A J |
||||
!4B U+004B K |
||||
!4C U+004C L |
||||
!4D U+004D M |
||||
!4E U+004E N |
||||
!4F U+004F O |
||||
!50 U+0050 P |
||||
!51 U+0051 Q |
||||
!52 U+0052 R |
||||
!53 U+0053 S |
||||
!54 U+0054 T |
||||
!55 U+0055 U |
||||
!56 U+0056 V |
||||
!57 U+0057 W |
||||
!58 U+0058 X |
||||
!59 U+0059 Y |
||||
!5A U+005A Z |
||||
!5B U+005B bracketleft |
||||
!5C U+005C backslash |
||||
!5D U+005D bracketright |
||||
!5E U+005E asciicircum |
||||
!5F U+005F underscore |
||||
!60 U+0060 grave |
||||
!61 U+0061 a |
||||
!62 U+0062 b |
||||
!63 U+0063 c |
||||
!64 U+0064 d |
||||
!65 U+0065 e |
||||
!66 U+0066 f |
||||
!67 U+0067 g |
||||
!68 U+0068 h |
||||
!69 U+0069 i |
||||
!6A U+006A j |
||||
!6B U+006B k |
||||
!6C U+006C l |
||||
!6D U+006D m |
||||
!6E U+006E n |
||||
!6F U+006F o |
||||
!70 U+0070 p |
||||
!71 U+0071 q |
||||
!72 U+0072 r |
||||
!73 U+0073 s |
||||
!74 U+0074 t |
||||
!75 U+0075 u |
||||
!76 U+0076 v |
||||
!77 U+0077 w |
||||
!78 U+0078 x |
||||
!79 U+0079 y |
||||
!7A U+007A z |
||||
!7B U+007B braceleft |
||||
!7C U+007C bar |
||||
!7D U+007D braceright |
||||
!7E U+007E asciitilde |
||||
!7F U+007F .notdef |
||||
!80 U+20AC Euro |
||||
!82 U+201A quotesinglbase |
||||
!83 U+0192 florin |
||||
!84 U+201E quotedblbase |
||||
!85 U+2026 ellipsis |
||||
!86 U+2020 dagger |
||||
!87 U+2021 daggerdbl |
||||
!88 U+02C6 circumflex |
||||
!89 U+2030 perthousand |
||||
!8A U+0160 Scaron |
||||
!8B U+2039 guilsinglleft |
||||
!8C U+0152 OE |
||||
!8E U+017D Zcaron |
||||
!91 U+2018 quoteleft |
||||
!92 U+2019 quoteright |
||||
!93 U+201C quotedblleft |
||||
!94 U+201D quotedblright |
||||
!95 U+2022 bullet |
||||
!96 U+2013 endash |
||||
!97 U+2014 emdash |
||||
!98 U+02DC tilde |
||||
!99 U+2122 trademark |
||||
!9A U+0161 scaron |
||||
!9B U+203A guilsinglright |
||||
!9C U+0153 oe |
||||
!9E U+017E zcaron |
||||
!9F U+0178 Ydieresis |
||||
!A0 U+00A0 space |
||||
!A1 U+00A1 exclamdown |
||||
!A2 U+00A2 cent |
||||
!A3 U+00A3 sterling |
||||
!A4 U+00A4 currency |
||||
!A5 U+00A5 yen |
||||
!A6 U+00A6 brokenbar |
||||
!A7 U+00A7 section |
||||
!A8 U+00A8 dieresis |
||||
!A9 U+00A9 copyright |
||||
!AA U+00AA ordfeminine |
||||
!AB U+00AB guillemotleft |
||||
!AC U+00AC logicalnot |
||||
!AD U+00AD hyphen |
||||
!AE U+00AE registered |
||||
!AF U+00AF macron |
||||
!B0 U+00B0 degree |
||||
!B1 U+00B1 plusminus |
||||
!B2 U+00B2 twosuperior |
||||
!B3 U+00B3 threesuperior |
||||
!B4 U+00B4 acute |
||||
!B5 U+00B5 mu |
||||
!B6 U+00B6 paragraph |
||||
!B7 U+00B7 periodcentered |
||||
!B8 U+00B8 cedilla |
||||
!B9 U+00B9 onesuperior |
||||
!BA U+00BA ordmasculine |
||||
!BB U+00BB guillemotright |
||||
!BC U+00BC onequarter |
||||
!BD U+00BD onehalf |
||||
!BE U+00BE threequarters |
||||
!BF U+00BF questiondown |
||||
!C0 U+00C0 Agrave |
||||
!C1 U+00C1 Aacute |
||||
!C2 U+00C2 Acircumflex |
||||
!C3 U+00C3 Atilde |
||||
!C4 U+00C4 Adieresis |
||||
!C5 U+00C5 Aring |
||||
!C6 U+00C6 AE |
||||
!C7 U+00C7 Ccedilla |
||||
!C8 U+00C8 Egrave |
||||
!C9 U+00C9 Eacute |
||||
!CA U+00CA Ecircumflex |
||||
!CB U+00CB Edieresis |
||||
!CC U+00CC Igrave |
||||
!CD U+00CD Iacute |
||||
!CE U+00CE Icircumflex |
||||
!CF U+00CF Idieresis |
||||
!D0 U+00D0 Eth |
||||
!D1 U+00D1 Ntilde |
||||
!D2 U+00D2 Ograve |
||||
!D3 U+00D3 Oacute |
||||
!D4 U+00D4 Ocircumflex |
||||
!D5 U+00D5 Otilde |
||||
!D6 U+00D6 Odieresis |
||||
!D7 U+00D7 multiply |
||||
!D8 U+00D8 Oslash |
||||
!D9 U+00D9 Ugrave |
||||
!DA U+00DA Uacute |
||||
!DB U+00DB Ucircumflex |
||||
!DC U+00DC Udieresis |
||||
!DD U+00DD Yacute |
||||
!DE U+00DE Thorn |
||||
!DF U+00DF germandbls |
||||
!E0 U+00E0 agrave |
||||
!E1 U+00E1 aacute |
||||
!E2 U+00E2 acircumflex |
||||
!E3 U+00E3 atilde |
||||
!E4 U+00E4 adieresis |
||||
!E5 U+00E5 aring |
||||
!E6 U+00E6 ae |
||||
!E7 U+00E7 ccedilla |
||||
!E8 U+00E8 egrave |
||||
!E9 U+00E9 eacute |
||||
!EA U+00EA ecircumflex |
||||
!EB U+00EB edieresis |
||||
!EC U+00EC igrave |
||||
!ED U+00ED iacute |
||||
!EE U+00EE icircumflex |
||||
!EF U+00EF idieresis |
||||
!F0 U+00F0 eth |
||||
!F1 U+00F1 ntilde |
||||
!F2 U+00F2 ograve |
||||
!F3 U+00F3 oacute |
||||
!F4 U+00F4 ocircumflex |
||||
!F5 U+00F5 otilde |
||||
!F6 U+00F6 odieresis |
||||
!F7 U+00F7 divide |
||||
!F8 U+00F8 oslash |
||||
!F9 U+00F9 ugrave |
||||
!FA U+00FA uacute |
||||
!FB U+00FB ucircumflex |
||||
!FC U+00FC udieresis |
||||
!FD U+00FD yacute |
||||
!FE U+00FE thorn |
||||
!FF U+00FF ydieresis |
||||
`, |
||||
} |
||||
@ -0,0 +1,474 @@ |
||||
/* |
||||
* Copyright (c) 2013 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 |
||||
|
||||
// Utility to generate font definition files
|
||||
|
||||
// Version: 1.2
|
||||
// Date: 2011-06-18
|
||||
// Author: Olivier PLATHEY
|
||||
// Port to Go: Kurt Jung, 2013-07-15
|
||||
|
||||
import ( |
||||
"bufio" |
||||
"compress/zlib" |
||||
"encoding/binary" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"io/ioutil" |
||||
"os" |
||||
"path/filepath" |
||||
"strconv" |
||||
"strings" |
||||
) |
||||
|
||||
func baseNoExt(fileStr string) string { |
||||
str := filepath.Base(fileStr) |
||||
extLen := len(filepath.Ext(str)) |
||||
if extLen > 0 { |
||||
str = str[:len(str)-extLen] |
||||
} |
||||
return str |
||||
} |
||||
|
||||
func loadMap(encodingFileStr string) (encList encListType, err error) { |
||||
// printf("Encoding file string [%s]\n", encodingFileStr)
|
||||
var f *os.File |
||||
// f, err = os.Open(encodingFilepath(encodingFileStr))
|
||||
f, err = os.Open(encodingFileStr) |
||||
if err == nil { |
||||
defer f.Close() |
||||
for j := range encList { |
||||
encList[j].uv = -1 |
||||
encList[j].name = ".notdef" |
||||
} |
||||
scanner := bufio.NewScanner(f) |
||||
var enc encType |
||||
var pos int |
||||
for scanner.Scan() { |
||||
// "!3F U+003F question"
|
||||
_, err = fmt.Sscanf(scanner.Text(), "!%x U+%x %s", &pos, &enc.uv, &enc.name) |
||||
if err == nil { |
||||
if pos < 256 { |
||||
encList[pos] = enc |
||||
} else { |
||||
err = fmt.Errorf("map position 0x%2X exceeds 0xFF", pos) |
||||
return |
||||
} |
||||
} else { |
||||
return |
||||
} |
||||
} |
||||
if err = scanner.Err(); err != nil { |
||||
return |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
// getInfoFromTrueType returns information from a TrueType font
|
||||
func getInfoFromTrueType(fileStr string, msgWriter io.Writer, embed bool, encList encListType) (info fontInfoType, err error) { |
||||
info.Widths = make([]int, 256) |
||||
var ttf TtfType |
||||
ttf, err = TtfParse(fileStr) |
||||
if err != nil { |
||||
return |
||||
} |
||||
if embed { |
||||
if !ttf.Embeddable { |
||||
err = fmt.Errorf("font license does not allow embedding") |
||||
return |
||||
} |
||||
info.Data, err = ioutil.ReadFile(fileStr) |
||||
if err != nil { |
||||
return |
||||
} |
||||
info.OriginalSize = len(info.Data) |
||||
} |
||||
k := 1000.0 / float64(ttf.UnitsPerEm) |
||||
info.FontName = ttf.PostScriptName |
||||
info.Bold = ttf.Bold |
||||
info.Desc.ItalicAngle = int(ttf.ItalicAngle) |
||||
info.IsFixedPitch = ttf.IsFixedPitch |
||||
info.Desc.Ascent = round(k * float64(ttf.TypoAscender)) |
||||
info.Desc.Descent = round(k * float64(ttf.TypoDescender)) |
||||
info.UnderlineThickness = round(k * float64(ttf.UnderlineThickness)) |
||||
info.UnderlinePosition = round(k * float64(ttf.UnderlinePosition)) |
||||
info.Desc.FontBBox = fontBoxType{ |
||||
round(k * float64(ttf.Xmin)), |
||||
round(k * float64(ttf.Ymin)), |
||||
round(k * float64(ttf.Xmax)), |
||||
round(k * float64(ttf.Ymax)), |
||||
} |
||||
// printf("FontBBox\n")
|
||||
// dump(info.Desc.FontBBox)
|
||||
info.Desc.CapHeight = round(k * float64(ttf.CapHeight)) |
||||
info.Desc.MissingWidth = round(k * float64(ttf.Widths[0])) |
||||
var wd int |
||||
for j := 0; j < len(info.Widths); j++ { |
||||
wd = info.Desc.MissingWidth |
||||
if encList[j].name != ".notdef" { |
||||
uv := encList[j].uv |
||||
pos, ok := ttf.Chars[uint16(uv)] |
||||
if ok { |
||||
wd = round(k * float64(ttf.Widths[pos])) |
||||
} else { |
||||
fmt.Fprintf(msgWriter, "Character %s is missing\n", encList[j].name) |
||||
} |
||||
} |
||||
info.Widths[j] = wd |
||||
} |
||||
// printf("getInfoFromTrueType/FontBBox\n")
|
||||
// dump(info.Desc.FontBBox)
|
||||
return |
||||
} |
||||
|
||||
type segmentType struct { |
||||
marker uint8 |
||||
tp uint8 |
||||
size uint32 |
||||
data []byte |
||||
} |
||||
|
||||
func segmentRead(r io.Reader) (s segmentType, err error) { |
||||
if err = binary.Read(r, binary.LittleEndian, &s.marker); err != nil { |
||||
return |
||||
} |
||||
if s.marker != 128 { |
||||
err = fmt.Errorf("font file is not a valid binary Type1") |
||||
return |
||||
} |
||||
if err = binary.Read(r, binary.LittleEndian, &s.tp); err != nil { |
||||
return |
||||
} |
||||
if err = binary.Read(r, binary.LittleEndian, &s.size); err != nil { |
||||
return |
||||
} |
||||
s.data = make([]byte, s.size) |
||||
_, err = r.Read(s.data) |
||||
return |
||||
} |
||||
|
||||
// -rw-r--r-- 1 root root 9532 2010-04-22 11:27 /usr/share/fonts/type1/mathml/Symbol.afm
|
||||
// -rw-r--r-- 1 root root 37744 2010-04-22 11:27 /usr/share/fonts/type1/mathml/Symbol.pfb
|
||||
|
||||
// getInfoFromType1 return information from a Type1 font
|
||||
func getInfoFromType1(fileStr string, msgWriter io.Writer, embed bool, encList encListType) (info fontInfoType, err error) { |
||||
info.Widths = make([]int, 256) |
||||
if embed { |
||||
var f *os.File |
||||
f, err = os.Open(fileStr) |
||||
if err != nil { |
||||
return |
||||
} |
||||
defer f.Close() |
||||
// Read first segment
|
||||
var s1, s2 segmentType |
||||
s1, err = segmentRead(f) |
||||
if err != nil { |
||||
return |
||||
} |
||||
s2, err = segmentRead(f) |
||||
if err != nil { |
||||
return |
||||
} |
||||
info.Data = s1.data |
||||
info.Data = append(info.Data, s2.data...) |
||||
info.Size1 = s1.size |
||||
info.Size2 = s2.size |
||||
} |
||||
afmFileStr := fileStr[0:len(fileStr)-3] + "afm" |
||||
size, ok := fileSize(afmFileStr) |
||||
if !ok { |
||||
err = fmt.Errorf("font file (ATM) %s not found", afmFileStr) |
||||
return |
||||
} else if size == 0 { |
||||
err = fmt.Errorf("font file (AFM) %s empty or not readable", afmFileStr) |
||||
return |
||||
} |
||||
var f *os.File |
||||
f, err = os.Open(afmFileStr) |
||||
if err != nil { |
||||
return |
||||
} |
||||
defer f.Close() |
||||
scanner := bufio.NewScanner(f) |
||||
var fields []string |
||||
var wd int |
||||
var wt, name string |
||||
wdMap := make(map[string]int) |
||||
for scanner.Scan() { |
||||
fields = strings.Fields(strings.TrimSpace(scanner.Text())) |
||||
// Comment Generated by FontForge 20080203
|
||||
// FontName Symbol
|
||||
// C 32 ; WX 250 ; N space ; B 0 0 0 0 ;
|
||||
if len(fields) >= 2 { |
||||
switch fields[0] { |
||||
case "C": |
||||
if wd, err = strconv.Atoi(fields[4]); err == nil { |
||||
name = fields[7] |
||||
wdMap[name] = wd |
||||
} |
||||
case "FontName": |
||||
info.FontName = fields[1] |
||||
case "Weight": |
||||
wt = strings.ToLower(fields[1]) |
||||
case "ItalicAngle": |
||||
info.Desc.ItalicAngle, err = strconv.Atoi(fields[1]) |
||||
case "Ascender": |
||||
info.Desc.Ascent, err = strconv.Atoi(fields[1]) |
||||
case "Descender": |
||||
info.Desc.Descent, err = strconv.Atoi(fields[1]) |
||||
case "UnderlineThickness": |
||||
info.UnderlineThickness, err = strconv.Atoi(fields[1]) |
||||
case "UnderlinePosition": |
||||
info.UnderlinePosition, err = strconv.Atoi(fields[1]) |
||||
case "IsFixedPitch": |
||||
info.IsFixedPitch = fields[1] == "true" |
||||
case "FontBBox": |
||||
if info.Desc.FontBBox.Xmin, err = strconv.Atoi(fields[1]); err == nil { |
||||
if info.Desc.FontBBox.Ymin, err = strconv.Atoi(fields[2]); err == nil { |
||||
if info.Desc.FontBBox.Xmax, err = strconv.Atoi(fields[3]); err == nil { |
||||
info.Desc.FontBBox.Ymax, err = strconv.Atoi(fields[4]) |
||||
} |
||||
} |
||||
} |
||||
case "CapHeight": |
||||
info.Desc.CapHeight, err = strconv.Atoi(fields[1]) |
||||
case "StdVW": |
||||
info.Desc.StemV, err = strconv.Atoi(fields[1]) |
||||
} |
||||
} |
||||
if err != nil { |
||||
return |
||||
} |
||||
} |
||||
if err = scanner.Err(); err != nil { |
||||
return |
||||
} |
||||
if info.FontName == "" { |
||||
err = fmt.Errorf("the field FontName missing in AFM file %s", afmFileStr) |
||||
return |
||||
} |
||||
info.Bold = wt == "bold" || wt == "black" |
||||
var missingWd int |
||||
missingWd, ok = wdMap[".notdef"] |
||||
if ok { |
||||
info.Desc.MissingWidth = missingWd |
||||
} |
||||
for j := 0; j < len(info.Widths); j++ { |
||||
info.Widths[j] = info.Desc.MissingWidth |
||||
} |
||||
for j := 0; j < len(info.Widths); j++ { |
||||
name = encList[j].name |
||||
if name != ".notdef" { |
||||
wd, ok = wdMap[name] |
||||
if ok { |
||||
info.Widths[j] = wd |
||||
} else { |
||||
fmt.Fprintf(msgWriter, "Character %s is missing\n", name) |
||||
} |
||||
} |
||||
} |
||||
// printf("getInfoFromType1/FontBBox\n")
|
||||
// dump(info.Desc.FontBBox)
|
||||
return |
||||
} |
||||
|
||||
func makeFontDescriptor(info *fontInfoType) { |
||||
if info.Desc.CapHeight == 0 { |
||||
info.Desc.CapHeight = info.Desc.Ascent |
||||
} |
||||
info.Desc.Flags = 1 << 5 |
||||
if info.IsFixedPitch { |
||||
info.Desc.Flags |= 1 |
||||
} |
||||
if info.Desc.ItalicAngle != 0 { |
||||
info.Desc.Flags |= 1 << 6 |
||||
} |
||||
if info.Desc.StemV == 0 { |
||||
if info.Bold { |
||||
info.Desc.StemV = 120 |
||||
} else { |
||||
info.Desc.StemV = 70 |
||||
} |
||||
} |
||||
// printf("makeFontDescriptor/FontBBox\n")
|
||||
// dump(info.Desc.FontBBox)
|
||||
} |
||||
|
||||
// makeFontEncoding builds differences from reference encoding
|
||||
func makeFontEncoding(encList encListType, refEncFileStr string) (diffStr string, err error) { |
||||
var refList encListType |
||||
if refList, err = loadMap(refEncFileStr); err != nil { |
||||
return |
||||
} |
||||
var buf fmtBuffer |
||||
last := 0 |
||||
for j := 32; j < 256; j++ { |
||||
if encList[j].name != refList[j].name { |
||||
if j != last+1 { |
||||
buf.printf("%d ", j) |
||||
} |
||||
last = j |
||||
buf.printf("/%s ", encList[j].name) |
||||
} |
||||
} |
||||
diffStr = strings.TrimSpace(buf.String()) |
||||
return |
||||
} |
||||
|
||||
func makeDefinitionFile(fileStr, tpStr, encodingFileStr string, embed bool, encList encListType, info fontInfoType) error { |
||||
var err error |
||||
var def fontDefType |
||||
def.Tp = tpStr |
||||
def.Name = info.FontName |
||||
makeFontDescriptor(&info) |
||||
def.Desc = info.Desc |
||||
// printf("makeDefinitionFile/FontBBox\n")
|
||||
// dump(def.Desc.FontBBox)
|
||||
def.Up = info.UnderlinePosition |
||||
def.Ut = info.UnderlineThickness |
||||
def.Cw = info.Widths |
||||
def.Enc = baseNoExt(encodingFileStr) |
||||
// fmt.Printf("encodingFileStr [%s], def.Enc [%s]\n", encodingFileStr, def.Enc)
|
||||
// fmt.Printf("reference [%s]\n", filepath.Join(filepath.Dir(encodingFileStr), "cp1252.map"))
|
||||
def.Diff, err = makeFontEncoding(encList, filepath.Join(filepath.Dir(encodingFileStr), "cp1252.map")) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
def.File = info.File |
||||
def.Size1 = int(info.Size1) |
||||
def.Size2 = int(info.Size2) |
||||
def.OriginalSize = info.OriginalSize |
||||
// printf("Font definition file [%s]\n", fileStr)
|
||||
var buf []byte |
||||
buf, err = json.Marshal(def) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
var f *os.File |
||||
f, err = os.Create(fileStr) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer f.Close() |
||||
_, err = f.Write(buf) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
err = f.Close() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return err |
||||
} |
||||
|
||||
// MakeFont generates a font definition file in JSON format. A definition file
|
||||
// of this type is required to use non-core fonts in the PDF documents that
|
||||
// gofpdf generates. See the makefont utility in the gofpdf package for a
|
||||
// command line interface to this function.
|
||||
//
|
||||
// fontFileStr is the name of the TrueType file (extension .ttf), OpenType file
|
||||
// (extension .otf) or binary Type1 file (extension .pfb) from which to
|
||||
// generate a definition file. If an OpenType file is specified, it must be one
|
||||
// that is based on TrueType outlines, not PostScript outlines; this cannot be
|
||||
// determined from the file extension alone. If a Type1 file is specified, a
|
||||
// metric file with the same pathname except with the extension .afm must be
|
||||
// present.
|
||||
//
|
||||
// encodingFileStr is the name of the encoding file that corresponds to the
|
||||
// font.
|
||||
//
|
||||
// dstDirStr is the name of the directory in which to save the definition file
|
||||
// and, if embed is true, the compressed font file.
|
||||
//
|
||||
// msgWriter is the writer that is called to display messages throughout the
|
||||
// process. Use nil to turn off messages.
|
||||
//
|
||||
// embed is true if the font is to be embedded in the PDF files.
|
||||
func MakeFont(fontFileStr, encodingFileStr, dstDirStr string, msgWriter io.Writer, embed bool) error { |
||||
if msgWriter == nil { |
||||
msgWriter = ioutil.Discard |
||||
} |
||||
if !fileExist(fontFileStr) { |
||||
return fmt.Errorf("font file not found: %s", fontFileStr) |
||||
} |
||||
extStr := strings.ToLower(fontFileStr[len(fontFileStr)-3:]) |
||||
// printf("Font file extension [%s]\n", extStr)
|
||||
var tpStr string |
||||
switch extStr { |
||||
case "ttf": |
||||
fallthrough |
||||
case "otf": |
||||
tpStr = "TrueType" |
||||
case "pfb": |
||||
tpStr = "Type1" |
||||
default: |
||||
return fmt.Errorf("unrecognized font file extension: %s", extStr) |
||||
} |
||||
|
||||
var info fontInfoType |
||||
encList, err := loadMap(encodingFileStr) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
// printf("Encoding table\n")
|
||||
// dump(encList)
|
||||
if tpStr == "TrueType" { |
||||
info, err = getInfoFromTrueType(fontFileStr, msgWriter, embed, encList) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} else { |
||||
info, err = getInfoFromType1(fontFileStr, msgWriter, embed, encList) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
baseStr := baseNoExt(fontFileStr) |
||||
// fmt.Printf("Base [%s]\n", baseStr)
|
||||
if embed { |
||||
var f *os.File |
||||
info.File = baseStr + ".z" |
||||
zFileStr := filepath.Join(dstDirStr, info.File) |
||||
f, err = os.Create(zFileStr) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer f.Close() |
||||
cmp := zlib.NewWriter(f) |
||||
_, err = cmp.Write(info.Data) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
err = cmp.Close() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
fmt.Fprintf(msgWriter, "Font file compressed: %s\n", zFileStr) |
||||
} |
||||
defFileStr := filepath.Join(dstDirStr, baseStr+".json") |
||||
err = makeDefinitionFile(defFileStr, tpStr, encodingFileStr, embed, encList, info) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
fmt.Fprintf(msgWriter, "Font definition file successfully generated: %s\n", defFileStr) |
||||
return nil |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,213 @@ |
||||
package gofpdf |
||||
|
||||
import ( |
||||
"fmt" |
||||
"math" |
||||
) |
||||
|
||||
// Routines in this file are translated from the work of Moritz Wagner and
|
||||
// Andreas Würmser.
|
||||
|
||||
// TransformMatrix is used for generalized transformations of text, drawings
|
||||
// and images.
|
||||
type TransformMatrix struct { |
||||
A, B, C, D, E, F float64 |
||||
} |
||||
|
||||
// TransformBegin sets up a transformation context for subsequent text,
|
||||
// drawings and images. The typical usage is to immediately follow a call to
|
||||
// this method with a call to one or more of the transformation methods such as
|
||||
// TransformScale(), TransformSkew(), etc. This is followed by text, drawing or
|
||||
// image output and finally a call to TransformEnd(). All transformation
|
||||
// contexts must be properly ended prior to outputting the document.
|
||||
func (f *Fpdf) TransformBegin() { |
||||
f.transformNest++ |
||||
f.out("q") |
||||
} |
||||
|
||||
// TransformScaleX scales the width of the following text, drawings and images.
|
||||
// scaleWd is the percentage scaling factor. (x, y) is center of scaling.
|
||||
//
|
||||
// The TransformBegin() example demonstrates this method.
|
||||
func (f *Fpdf) TransformScaleX(scaleWd, x, y float64) { |
||||
f.TransformScale(scaleWd, 100, x, y) |
||||
} |
||||
|
||||
// TransformScaleY scales the height of the following text, drawings and
|
||||
// images. scaleHt is the percentage scaling factor. (x, y) is center of
|
||||
// scaling.
|
||||
//
|
||||
// The TransformBegin() example demonstrates this method.
|
||||
func (f *Fpdf) TransformScaleY(scaleHt, x, y float64) { |
||||
f.TransformScale(100, scaleHt, x, y) |
||||
} |
||||
|
||||
// TransformScaleXY uniformly scales the width and height of the following
|
||||
// text, drawings and images. s is the percentage scaling factor for both width
|
||||
// and height. (x, y) is center of scaling.
|
||||
//
|
||||
// The TransformBegin() example demonstrates this method.
|
||||
func (f *Fpdf) TransformScaleXY(s, x, y float64) { |
||||
f.TransformScale(s, s, x, y) |
||||
} |
||||
|
||||
// TransformScale generally scales the following text, drawings and images.
|
||||
// scaleWd and scaleHt are the percentage scaling factors for width and height.
|
||||
// (x, y) is center of scaling.
|
||||
//
|
||||
// The TransformBegin() example demonstrates this method.
|
||||
func (f *Fpdf) TransformScale(scaleWd, scaleHt, x, y float64) { |
||||
if scaleWd == 0 || scaleHt == 0 { |
||||
f.err = fmt.Errorf("scale factor cannot be zero") |
||||
return |
||||
} |
||||
y = (f.h - y) * f.k |
||||
x *= f.k |
||||
scaleWd /= 100 |
||||
scaleHt /= 100 |
||||
f.Transform(TransformMatrix{scaleWd, 0, 0, |
||||
scaleHt, x * (1 - scaleWd), y * (1 - scaleHt)}) |
||||
} |
||||
|
||||
// TransformMirrorHorizontal horizontally mirrors the following text, drawings
|
||||
// and images. x is the axis of reflection.
|
||||
//
|
||||
// The TransformBegin() example demonstrates this method.
|
||||
func (f *Fpdf) TransformMirrorHorizontal(x float64) { |
||||
f.TransformScale(-100, 100, x, f.y) |
||||
} |
||||
|
||||
// TransformMirrorVertical vertically mirrors the following text, drawings and
|
||||
// images. y is the axis of reflection.
|
||||
//
|
||||
// The TransformBegin() example demonstrates this method.
|
||||
func (f *Fpdf) TransformMirrorVertical(y float64) { |
||||
f.TransformScale(100, -100, f.x, y) |
||||
} |
||||
|
||||
// TransformMirrorPoint symmetrically mirrors the following text, drawings and
|
||||
// images on the point specified by (x, y).
|
||||
//
|
||||
// The TransformBegin() example demonstrates this method.
|
||||
func (f *Fpdf) TransformMirrorPoint(x, y float64) { |
||||
f.TransformScale(-100, -100, x, y) |
||||
} |
||||
|
||||
// TransformMirrorLine symmetrically mirrors the following text, drawings and
|
||||
// images on the line defined by angle and the point (x, y). angles is
|
||||
// specified in degrees and measured counter-clockwise from the 3 o'clock
|
||||
// position.
|
||||
//
|
||||
// The TransformBegin() example demonstrates this method.
|
||||
func (f *Fpdf) TransformMirrorLine(angle, x, y float64) { |
||||
f.TransformScale(-100, 100, x, y) |
||||
f.TransformRotate(-2*(angle-90), x, y) |
||||
} |
||||
|
||||
// TransformTranslateX moves the following text, drawings and images
|
||||
// horizontally by the amount specified by tx.
|
||||
//
|
||||
// The TransformBegin() example demonstrates this method.
|
||||
func (f *Fpdf) TransformTranslateX(tx float64) { |
||||
f.TransformTranslate(tx, 0) |
||||
} |
||||
|
||||
// TransformTranslateY moves the following text, drawings and images vertically
|
||||
// by the amount specified by ty.
|
||||
//
|
||||
// The TransformBegin() example demonstrates this method.
|
||||
func (f *Fpdf) TransformTranslateY(ty float64) { |
||||
f.TransformTranslate(0, ty) |
||||
} |
||||
|
||||
// TransformTranslate moves the following text, drawings and images
|
||||
// horizontally and vertically by the amounts specified by tx and ty.
|
||||
//
|
||||
// The TransformBegin() example demonstrates this method.
|
||||
func (f *Fpdf) TransformTranslate(tx, ty float64) { |
||||
f.Transform(TransformMatrix{1, 0, 0, 1, tx * f.k, -ty * f.k}) |
||||
} |
||||
|
||||
// TransformRotate rotates the following text, drawings and images around the
|
||||
// center point (x, y). angle is specified in degrees and measured
|
||||
// counter-clockwise from the 3 o'clock position.
|
||||
//
|
||||
// The TransformBegin() example demonstrates this method.
|
||||
func (f *Fpdf) TransformRotate(angle, x, y float64) { |
||||
y = (f.h - y) * f.k |
||||
x *= f.k |
||||
angle = angle * math.Pi / 180 |
||||
var tm TransformMatrix |
||||
tm.A = math.Cos(angle) |
||||
tm.B = math.Sin(angle) |
||||
tm.C = -tm.B |
||||
tm.D = tm.A |
||||
tm.E = x + tm.B*y - tm.A*x |
||||
tm.F = y - tm.A*y - tm.B*x |
||||
f.Transform(tm) |
||||
} |
||||
|
||||
// TransformSkewX horizontally skews the following text, drawings and images
|
||||
// keeping the point (x, y) stationary. angleX ranges from -90 degrees (skew to
|
||||
// the left) to 90 degrees (skew to the right).
|
||||
//
|
||||
// The TransformBegin() example demonstrates this method.
|
||||
func (f *Fpdf) TransformSkewX(angleX, x, y float64) { |
||||
f.TransformSkew(angleX, 0, x, y) |
||||
} |
||||
|
||||
// TransformSkewY vertically skews the following text, drawings and images
|
||||
// keeping the point (x, y) stationary. angleY ranges from -90 degrees (skew to
|
||||
// the bottom) to 90 degrees (skew to the top).
|
||||
//
|
||||
// The TransformBegin() example demonstrates this method.
|
||||
func (f *Fpdf) TransformSkewY(angleY, x, y float64) { |
||||
f.TransformSkew(0, angleY, x, y) |
||||
} |
||||
|
||||
// TransformSkew generally skews the following text, drawings and images
|
||||
// keeping the point (x, y) stationary. angleX ranges from -90 degrees (skew to
|
||||
// the left) to 90 degrees (skew to the right). angleY ranges from -90 degrees
|
||||
// (skew to the bottom) to 90 degrees (skew to the top).
|
||||
//
|
||||
// The TransformBegin() example demonstrates this method.
|
||||
func (f *Fpdf) TransformSkew(angleX, angleY, x, y float64) { |
||||
if angleX <= -90 || angleX >= 90 || angleY <= -90 || angleY >= 90 { |
||||
f.err = fmt.Errorf("skew values must be between -90° and 90°") |
||||
return |
||||
} |
||||
x *= f.k |
||||
y = (f.h - y) * f.k |
||||
var tm TransformMatrix |
||||
tm.A = 1 |
||||
tm.B = math.Tan(angleY * math.Pi / 180) |
||||
tm.C = math.Tan(angleX * math.Pi / 180) |
||||
tm.D = 1 |
||||
tm.E = -tm.C * y |
||||
tm.F = -tm.B * x |
||||
f.Transform(tm) |
||||
} |
||||
|
||||
// Transform generally transforms the following text, drawings and images
|
||||
// according to the specified matrix. It is typically easier to use the various
|
||||
// methods such as TransformRotate() and TransformMirrorVertical() instead.
|
||||
func (f *Fpdf) Transform(tm TransformMatrix) { |
||||
if f.transformNest > 0 { |
||||
f.outf("%.5f %.5f %.5f %.5f %.5f %.5f cm", |
||||
tm.A, tm.B, tm.C, tm.D, tm.E, tm.F) |
||||
} else if f.err == nil { |
||||
f.err = fmt.Errorf("transformation context is not active") |
||||
} |
||||
} |
||||
|
||||
// TransformEnd applies a transformation that was begun with a call to TransformBegin().
|
||||
//
|
||||
// The TransformBegin() example demonstrates this method.
|
||||
func (f *Fpdf) TransformEnd() { |
||||
if f.transformNest > 0 { |
||||
f.transformNest-- |
||||
f.out("Q") |
||||
} else { |
||||
f.err = fmt.Errorf("error attempting to end transformation operation out of sequence") |
||||
} |
||||
} |
||||
@ -0,0 +1,12 @@ |
||||
module github.com/jung-kurt/gofpdf |
||||
|
||||
go 1.12 |
||||
|
||||
require ( |
||||
github.com/boombuler/barcode v1.0.0 |
||||
github.com/phpdave11/gofpdi v1.0.7 |
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58 |
||||
golang.org/x/image v0.0.0-20190507092727-e4e5bf290fec |
||||
) |
||||
|
||||
replace github.com/jung-kurt/gopdf => ./ |
||||
@ -0,0 +1,18 @@ |
||||
github.com/boombuler/barcode v1.0.0 h1:s1TvRnXwL2xJRaccrdcBQMZxq6X7DvsMogtmJeHDdrc= |
||||
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= |
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= |
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= |
||||
github.com/phpdave11/gofpdi v1.0.7 h1:k2oy4yhkQopCK+qW8KjCla0iU2RpDow+QUDmH9DDt44= |
||||
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= |
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= |
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= |
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58 h1:nlG4Wa5+minh3S9LVFtNoY+GVRiudA2e3EVfcCi3RCA= |
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= |
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= |
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= |
||||
golang.org/x/image v0.0.0-20190507092727-e4e5bf290fec h1:arXJwtMuk5vqI1NHX0UTnNw977rYk5Sl4jQqHj+hun4= |
||||
golang.org/x/image v0.0.0-20190507092727-e4e5bf290fec/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= |
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
||||
@ -0,0 +1,446 @@ |
||||
package gofpdf |
||||
|
||||
import ( |
||||
"math" |
||||
"strconv" |
||||
) |
||||
|
||||
func unused(args ...interface{}) { |
||||
} |
||||
|
||||
// RGBType holds fields for red, green and blue color components (0..255)
|
||||
type RGBType struct { |
||||
R, G, B int |
||||
} |
||||
|
||||
// RGBAType holds fields for red, green and blue color components (0..255) and
|
||||
// an alpha transparency value (0..1)
|
||||
type RGBAType struct { |
||||
R, G, B int |
||||
Alpha float64 |
||||
} |
||||
|
||||
// StateType holds various commonly used drawing values for convenient
|
||||
// retrieval (StateGet()) and restore (Put) methods.
|
||||
type StateType struct { |
||||
clrDraw, clrText, clrFill RGBType |
||||
lineWd float64 |
||||
fontSize float64 |
||||
alpha float64 |
||||
blendStr string |
||||
cellMargin float64 |
||||
} |
||||
|
||||
// StateGet returns a variable that contains common state values.
|
||||
func StateGet(pdf *Fpdf) (st StateType) { |
||||
st.clrDraw.R, st.clrDraw.G, st.clrDraw.B = pdf.GetDrawColor() |
||||
st.clrFill.R, st.clrFill.G, st.clrFill.B = pdf.GetFillColor() |
||||
st.clrText.R, st.clrText.G, st.clrText.B = pdf.GetTextColor() |
||||
st.lineWd = pdf.GetLineWidth() |
||||
_, st.fontSize = pdf.GetFontSize() |
||||
st.alpha, st.blendStr = pdf.GetAlpha() |
||||
st.cellMargin = pdf.GetCellMargin() |
||||
return |
||||
} |
||||
|
||||
// Put sets the common state values contained in the state structure
|
||||
// specified by st.
|
||||
func (st StateType) Put(pdf *Fpdf) { |
||||
pdf.SetDrawColor(st.clrDraw.R, st.clrDraw.G, st.clrDraw.B) |
||||
pdf.SetFillColor(st.clrFill.R, st.clrFill.G, st.clrFill.B) |
||||
pdf.SetTextColor(st.clrText.R, st.clrText.G, st.clrText.B) |
||||
pdf.SetLineWidth(st.lineWd) |
||||
pdf.SetFontUnitSize(st.fontSize) |
||||
pdf.SetAlpha(st.alpha, st.blendStr) |
||||
pdf.SetCellMargin(st.cellMargin) |
||||
} |
||||
|
||||
// TickFormatFncType defines a callback for label drawing.
|
||||
type TickFormatFncType func(val float64, precision int) string |
||||
|
||||
// defaultFormatter returns the string form of val with precision decimal places.
|
||||
func defaultFormatter(val float64, precision int) string { |
||||
return strconv.FormatFloat(val, 'f', precision, 64) |
||||
} |
||||
|
||||
// GridType assists with the generation of graphs. It allows the application to
|
||||
// work with logical data coordinates rather than page coordinates and assists
|
||||
// with the drawing of a background grid.
|
||||
type GridType struct { |
||||
// Chart coordinates in page units
|
||||
x, y, w, h float64 |
||||
// X, Y, Wd, Ht float64
|
||||
// Slopes and intercepts scale data points to graph coordinates linearly
|
||||
xm, xb, ym, yb float64 |
||||
// Tickmarks
|
||||
xTicks, yTicks []float64 |
||||
// Labels are inside of graph boundary
|
||||
XLabelIn, YLabelIn bool |
||||
// Labels on X-axis should be rotated
|
||||
XLabelRotate bool |
||||
// Formatters; use nil to eliminate labels
|
||||
XTickStr, YTickStr TickFormatFncType |
||||
// Subdivisions between tickmarks
|
||||
XDiv, YDiv int |
||||
// Formatting precision
|
||||
xPrecision, yPrecision int |
||||
// Line and label colors
|
||||
ClrText, ClrMain, ClrSub RGBAType |
||||
// Line thickness
|
||||
WdMain, WdSub float64 |
||||
// Label height in points
|
||||
TextSize float64 |
||||
} |
||||
|
||||
// linear returns the slope and y-intercept of the straight line joining the
|
||||
// two specified points. For scaling purposes, associate the arguments as
|
||||
// follows: x1: observed low value, y1: desired low value, x2: observed high
|
||||
// value, y2: desired high value.
|
||||
func linear(x1, y1, x2, y2 float64) (slope, intercept float64) { |
||||
if x2 != x1 { |
||||
slope = (y2 - y1) / (x2 - x1) |
||||
intercept = y2 - x2*slope |
||||
} |
||||
return |
||||
} |
||||
|
||||
// linearTickmark returns the slope and intercept that will linearly map data
|
||||
// values (the range of which is specified by the tickmark slice tm) to page
|
||||
// values (the range of which is specified by lo and hi).
|
||||
func linearTickmark(tm []float64, lo, hi float64) (slope, intercept float64) { |
||||
ln := len(tm) |
||||
if ln > 0 { |
||||
slope, intercept = linear(tm[0], lo, tm[ln-1], hi) |
||||
} |
||||
return |
||||
} |
||||
|
||||
// NewGrid returns a variable of type GridType that is initialized to draw on a
|
||||
// rectangle of width w and height h with the upper left corner positioned at
|
||||
// point (x, y). The coordinates are in page units, that is, the same as those
|
||||
// specified in New().
|
||||
//
|
||||
// The returned variable is initialized with a very simple default tickmark
|
||||
// layout that ranges from 0 to 1 in both axes. Prior to calling Grid(), the
|
||||
// application may establish a more suitable tickmark layout by calling the
|
||||
// methods TickmarksContainX() and TickmarksContainY(). These methods bound the
|
||||
// data range with appropriate boundaries and divisions. Alternatively, if the
|
||||
// exact extent and divisions of the tickmark layout are known, the methods
|
||||
// TickmarksExtentX() and TickmarksExtentY may be called instead.
|
||||
func NewGrid(x, y, w, h float64) (grid GridType) { |
||||
grid.x = x |
||||
grid.y = y |
||||
grid.w = w |
||||
grid.h = h |
||||
grid.TextSize = 7 // Points
|
||||
grid.TickmarksExtentX(0, 1, 1) |
||||
grid.TickmarksExtentY(0, 1, 1) |
||||
grid.XLabelIn = false |
||||
grid.YLabelIn = false |
||||
grid.XLabelRotate = false |
||||
grid.XDiv = 10 |
||||
grid.YDiv = 10 |
||||
grid.ClrText = RGBAType{R: 0, G: 0, B: 0, Alpha: 1} |
||||
grid.ClrMain = RGBAType{R: 128, G: 160, B: 128, Alpha: 1} |
||||
grid.ClrSub = RGBAType{R: 192, G: 224, B: 192, Alpha: 1} |
||||
grid.WdMain = 0.1 |
||||
grid.WdSub = 0.1 |
||||
grid.YTickStr = defaultFormatter |
||||
grid.XTickStr = defaultFormatter |
||||
return |
||||
} |
||||
|
||||
// WdAbs returns the absolute value of dataWd, specified in logical data units,
|
||||
// that has been converted to the unit of measure specified in New().
|
||||
func (g GridType) WdAbs(dataWd float64) float64 { |
||||
return math.Abs(g.xm * dataWd) |
||||
} |
||||
|
||||
// Wd converts dataWd, specified in logical data units, to the unit of measure
|
||||
// specified in New().
|
||||
func (g GridType) Wd(dataWd float64) float64 { |
||||
return g.xm * dataWd |
||||
} |
||||
|
||||
// XY converts dataX and dataY, specified in logical data units, to the X and Y
|
||||
// position on the current page.
|
||||
func (g GridType) XY(dataX, dataY float64) (x, y float64) { |
||||
return g.xm*dataX + g.xb, g.ym*dataY + g.yb |
||||
} |
||||
|
||||
// Pos returns the point, in page units, indicated by the relative positions
|
||||
// xRel and yRel. These are values between 0 and 1. xRel specifies the relative
|
||||
// position between the grid's left and right edges. yRel specifies the
|
||||
// relative position between the grid's bottom and top edges.
|
||||
func (g GridType) Pos(xRel, yRel float64) (x, y float64) { |
||||
x = g.w*xRel + g.x |
||||
y = g.h*(1-yRel) + g.y |
||||
return |
||||
} |
||||
|
||||
// X converts dataX, specified in logical data units, to the X position on the
|
||||
// current page.
|
||||
func (g GridType) X(dataX float64) float64 { |
||||
return g.xm*dataX + g.xb |
||||
} |
||||
|
||||
// HtAbs returns the absolute value of dataHt, specified in logical data units,
|
||||
// that has been converted to the unit of measure specified in New().
|
||||
func (g GridType) HtAbs(dataHt float64) float64 { |
||||
return math.Abs(g.ym * dataHt) |
||||
} |
||||
|
||||
// Ht converts dataHt, specified in logical data units, to the unit of measure
|
||||
// specified in New().
|
||||
func (g GridType) Ht(dataHt float64) float64 { |
||||
return g.ym * dataHt |
||||
} |
||||
|
||||
// Y converts dataY, specified in logical data units, to the Y position on the
|
||||
// current page.
|
||||
func (g GridType) Y(dataY float64) float64 { |
||||
return g.ym*dataY + g.yb |
||||
} |
||||
|
||||
// XRange returns the minimum and maximum values for the current tickmark
|
||||
// sequence. These correspond to the data values of the graph's left and right
|
||||
// edges.
|
||||
func (g GridType) XRange() (min, max float64) { |
||||
min = g.xTicks[0] |
||||
max = g.xTicks[len(g.xTicks)-1] |
||||
return |
||||
} |
||||
|
||||
// YRange returns the minimum and maximum values for the current tickmark
|
||||
// sequence. These correspond to the data values of the graph's bottom and top
|
||||
// edges.
|
||||
func (g GridType) YRange() (min, max float64) { |
||||
min = g.yTicks[0] |
||||
max = g.yTicks[len(g.yTicks)-1] |
||||
return |
||||
} |
||||
|
||||
// TickmarksContainX sets the tickmarks to be shown by Grid() in the horizontal
|
||||
// dimension. The argument min and max specify the minimum and maximum values
|
||||
// to be contained within the grid. The tickmark values that are generated are
|
||||
// suitable for general purpose graphs.
|
||||
//
|
||||
// See TickmarkExtentX() for an alternative to this method to be used when the
|
||||
// exact values of the tickmarks are to be set by the application.
|
||||
func (g *GridType) TickmarksContainX(min, max float64) { |
||||
g.xTicks, g.xPrecision = Tickmarks(min, max) |
||||
g.xm, g.xb = linearTickmark(g.xTicks, g.x, g.x+g.w) |
||||
} |
||||
|
||||
// TickmarksContainY sets the tickmarks to be shown by Grid() in the vertical
|
||||
// dimension. The argument min and max specify the minimum and maximum values
|
||||
// to be contained within the grid. The tickmark values that are generated are
|
||||
// suitable for general purpose graphs.
|
||||
//
|
||||
// See TickmarkExtentY() for an alternative to this method to be used when the
|
||||
// exact values of the tickmarks are to be set by the application.
|
||||
func (g *GridType) TickmarksContainY(min, max float64) { |
||||
g.yTicks, g.yPrecision = Tickmarks(min, max) |
||||
g.ym, g.yb = linearTickmark(g.yTicks, g.y+g.h, g.y) |
||||
} |
||||
|
||||
func extent(min, div float64, count int) (tm []float64, precision int) { |
||||
tm = make([]float64, count+1) |
||||
for j := 0; j <= count; j++ { |
||||
tm[j] = min |
||||
min += div |
||||
} |
||||
precision = TickmarkPrecision(div) |
||||
return |
||||
} |
||||
|
||||
// TickmarksExtentX sets the tickmarks to be shown by Grid() in the horizontal
|
||||
// dimension. count specifies number of major tickmark subdivisions to be
|
||||
// graphed. min specifies the leftmost data value. div specifies, in data
|
||||
// units, the extent of each major tickmark subdivision.
|
||||
//
|
||||
// See TickmarkContainX() for an alternative to this method to be used when
|
||||
// viewer-friendly tickmarks are to be determined automatically.
|
||||
func (g *GridType) TickmarksExtentX(min, div float64, count int) { |
||||
g.xTicks, g.xPrecision = extent(min, div, count) |
||||
g.xm, g.xb = linearTickmark(g.xTicks, g.x, g.x+g.w) |
||||
} |
||||
|
||||
// TickmarksExtentY sets the tickmarks to be shown by Grid() in the vertical
|
||||
// dimension. count specifies number of major tickmark subdivisions to be
|
||||
// graphed. min specifies the bottommost data value. div specifies, in data
|
||||
// units, the extent of each major tickmark subdivision.
|
||||
//
|
||||
// See TickmarkContainY() for an alternative to this method to be used when
|
||||
// viewer-friendly tickmarks are to be determined automatically.
|
||||
func (g *GridType) TickmarksExtentY(min, div float64, count int) { |
||||
g.yTicks, g.yPrecision = extent(min, div, count) |
||||
g.ym, g.yb = linearTickmark(g.yTicks, g.y+g.h, g.y) |
||||
} |
||||
|
||||
// func (g *GridType) SetXExtent(dataLf, paperLf, dataRt, paperRt float64) {
|
||||
// g.xm, g.xb = linear(dataLf, paperLf, dataRt, paperRt)
|
||||
// }
|
||||
|
||||
// func (g *GridType) SetYExtent(dataTp, paperTp, dataBt, paperBt float64) {
|
||||
// g.ym, g.yb = linear(dataTp, paperTp, dataBt, paperBt)
|
||||
// }
|
||||
|
||||
func lineAttr(pdf *Fpdf, clr RGBAType, lineWd float64) { |
||||
pdf.SetLineWidth(lineWd) |
||||
pdf.SetAlpha(clr.Alpha, "Normal") |
||||
pdf.SetDrawColor(clr.R, clr.G, clr.B) |
||||
} |
||||
|
||||
// Grid generates a graph-paperlike set of grid lines on the current page.
|
||||
func (g GridType) Grid(pdf *Fpdf) { |
||||
var st StateType |
||||
var yLen, xLen int |
||||
var textSz, halfTextSz, yMin, yMax, xMin, xMax, yDiv, xDiv float64 |
||||
var str string |
||||
var strOfs, strWd, tp, bt, lf, rt, drawX, drawY float64 |
||||
|
||||
xLen = len(g.xTicks) |
||||
yLen = len(g.yTicks) |
||||
if xLen > 1 && yLen > 1 { |
||||
|
||||
st = StateGet(pdf) |
||||
|
||||
line := func(x1, y1, x2, y2 float64, heavy bool) { |
||||
if heavy { |
||||
lineAttr(pdf, g.ClrMain, g.WdMain) |
||||
} else { |
||||
lineAttr(pdf, g.ClrSub, g.WdSub) |
||||
} |
||||
pdf.Line(x1, y1, x2, y2) |
||||
} |
||||
|
||||
textSz = pdf.PointToUnitConvert(g.TextSize) |
||||
halfTextSz = textSz / 2 |
||||
|
||||
pdf.SetAutoPageBreak(false, 0) |
||||
pdf.SetFontUnitSize(textSz) |
||||
strOfs = pdf.GetStringWidth("0") |
||||
pdf.SetFillColor(255, 255, 255) |
||||
pdf.SetCellMargin(0) |
||||
|
||||
xMin = g.xTicks[0] |
||||
xMax = g.xTicks[xLen-1] |
||||
|
||||
yMin = g.yTicks[0] |
||||
yMax = g.yTicks[yLen-1] |
||||
|
||||
lf = g.X(xMin) |
||||
rt = g.X(xMax) |
||||
bt = g.Y(yMin) |
||||
tp = g.Y(yMax) |
||||
|
||||
// Verticals along X axis
|
||||
xDiv = g.xTicks[1] - g.xTicks[0] |
||||
if g.XDiv > 0 { |
||||
xDiv = xDiv / float64(g.XDiv) |
||||
} |
||||
xDiv = g.Wd(xDiv) |
||||
for j, x := range g.xTicks { |
||||
drawX = g.X(x) |
||||
line(drawX, tp, drawX, bt, true) |
||||
if j < xLen-1 { |
||||
for k := 1; k < g.XDiv; k++ { |
||||
drawX += xDiv |
||||
line(drawX, tp, drawX, bt, false) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Horizontals along Y axis
|
||||
yDiv = g.yTicks[1] - g.yTicks[0] |
||||
if g.YDiv > 0 { |
||||
yDiv = yDiv / float64(g.YDiv) |
||||
} |
||||
yDiv = g.Ht(yDiv) |
||||
for j, y := range g.yTicks { |
||||
drawY = g.Y(y) |
||||
line(lf, drawY, rt, drawY, true) |
||||
if j < yLen-1 { |
||||
for k := 1; k < g.YDiv; k++ { |
||||
drawY += yDiv |
||||
line(lf, drawY, rt, drawY, false) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// X labels
|
||||
if g.XTickStr != nil { |
||||
drawY = bt |
||||
for _, x := range g.xTicks { |
||||
str = g.XTickStr(x, g.xPrecision) |
||||
strWd = pdf.GetStringWidth(str) |
||||
drawX = g.X(x) |
||||
if g.XLabelRotate { |
||||
pdf.TransformBegin() |
||||
pdf.TransformRotate(90, drawX, drawY) |
||||
if g.XLabelIn { |
||||
pdf.SetXY(drawX+strOfs, drawY-halfTextSz) |
||||
} else { |
||||
pdf.SetXY(drawX-strOfs-strWd, drawY-halfTextSz) |
||||
} |
||||
pdf.CellFormat(strWd, textSz, str, "", 0, "L", true, 0, "") |
||||
pdf.TransformEnd() |
||||
} else { |
||||
drawX -= strWd / 2.0 |
||||
if g.XLabelIn { |
||||
pdf.SetXY(drawX, drawY-textSz-strOfs) |
||||
} else { |
||||
pdf.SetXY(drawX, drawY+strOfs) |
||||
} |
||||
pdf.CellFormat(strWd, textSz, str, "", 0, "L", true, 0, "") |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Y labels
|
||||
if g.YTickStr != nil { |
||||
drawX = lf |
||||
for _, y := range g.yTicks { |
||||
// str = strconv.FormatFloat(y, 'f', g.yPrecision, 64)
|
||||
str = g.YTickStr(y, g.yPrecision) |
||||
strWd = pdf.GetStringWidth(str) |
||||
if g.YLabelIn { |
||||
pdf.SetXY(drawX+strOfs, g.Y(y)-halfTextSz) |
||||
} else { |
||||
pdf.SetXY(lf-strOfs-strWd, g.Y(y)-halfTextSz) |
||||
} |
||||
pdf.CellFormat(strWd, textSz, str, "", 0, "L", true, 0, "") |
||||
} |
||||
} |
||||
|
||||
// Restore drawing attributes
|
||||
st.Put(pdf) |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
// Plot plots a series of count line segments from xMin to xMax. It repeatedly
|
||||
// calls fnc(x) to retrieve the y value associate with x. The currently
|
||||
// selected line drawing attributes are used.
|
||||
func (g GridType) Plot(pdf *Fpdf, xMin, xMax float64, count int, fnc func(x float64) (y float64)) { |
||||
if count > 0 { |
||||
var x, delta, drawX0, drawY0, drawX1, drawY1 float64 |
||||
delta = (xMax - xMin) / float64(count) |
||||
x = xMin |
||||
for j := 0; j <= count; j++ { |
||||
if j == 0 { |
||||
drawX1 = g.X(x) |
||||
drawY1 = g.Y(fnc(x)) |
||||
} else { |
||||
pdf.Line(drawX0, drawY0, drawX1, drawY1) |
||||
} |
||||
x += delta |
||||
drawX0 = drawX1 |
||||
drawY0 = drawY1 |
||||
drawX1 = g.X(x) |
||||
drawY1 = g.Y(fnc(x)) |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,220 @@ |
||||
/* |
||||
* Copyright (c) 2014 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 ( |
||||
"regexp" |
||||
"strings" |
||||
) |
||||
|
||||
// HTMLBasicSegmentType defines a segment of literal text in which the current
|
||||
// attributes do not vary, or an open tag or a close tag.
|
||||
type HTMLBasicSegmentType struct { |
||||
Cat byte // 'O' open tag, 'C' close tag, 'T' text
|
||||
Str string // Literal text unchanged, tags are lower case
|
||||
Attr map[string]string // Attribute keys are lower case
|
||||
} |
||||
|
||||
// HTMLBasicTokenize returns a list of HTML tags and literal elements. This is
|
||||
// done with regular expressions, so the result is only marginally better than
|
||||
// useless.
|
||||
func HTMLBasicTokenize(htmlStr string) (list []HTMLBasicSegmentType) { |
||||
// This routine is adapted from http://www.fpdf.org/
|
||||
list = make([]HTMLBasicSegmentType, 0, 16) |
||||
htmlStr = strings.Replace(htmlStr, "\n", " ", -1) |
||||
htmlStr = strings.Replace(htmlStr, "\r", "", -1) |
||||
tagRe, _ := regexp.Compile(`(?U)<.*>`) |
||||
attrRe, _ := regexp.Compile(`([^=]+)=["']?([^"']+)`) |
||||
capList := tagRe.FindAllStringIndex(htmlStr, -1) |
||||
if capList != nil { |
||||
var seg HTMLBasicSegmentType |
||||
var parts []string |
||||
pos := 0 |
||||
for _, cap := range capList { |
||||
if pos < cap[0] { |
||||
seg.Cat = 'T' |
||||
seg.Str = htmlStr[pos:cap[0]] |
||||
seg.Attr = nil |
||||
list = append(list, seg) |
||||
} |
||||
if htmlStr[cap[0]+1] == '/' { |
||||
seg.Cat = 'C' |
||||
seg.Str = strings.ToLower(htmlStr[cap[0]+2 : cap[1]-1]) |
||||
seg.Attr = nil |
||||
list = append(list, seg) |
||||
} else { |
||||
// Extract attributes
|
||||
parts = strings.Split(htmlStr[cap[0]+1:cap[1]-1], " ") |
||||
if len(parts) > 0 { |
||||
for j, part := range parts { |
||||
if j == 0 { |
||||
seg.Cat = 'O' |
||||
seg.Str = strings.ToLower(parts[0]) |
||||
seg.Attr = make(map[string]string) |
||||
} else { |
||||
attrList := attrRe.FindAllStringSubmatch(part, -1) |
||||
if attrList != nil { |
||||
for _, attr := range attrList { |
||||
seg.Attr[strings.ToLower(attr[1])] = attr[2] |
||||
} |
||||
} |
||||
} |
||||
} |
||||
list = append(list, seg) |
||||
} |
||||
} |
||||
pos = cap[1] |
||||
} |
||||
if len(htmlStr) > pos { |
||||
seg.Cat = 'T' |
||||
seg.Str = htmlStr[pos:] |
||||
seg.Attr = nil |
||||
list = append(list, seg) |
||||
} |
||||
} else { |
||||
list = append(list, HTMLBasicSegmentType{Cat: 'T', Str: htmlStr, Attr: nil}) |
||||
} |
||||
return |
||||
} |
||||
|
||||
// HTMLBasicType is used for rendering a very basic subset of HTML. It supports
|
||||
// only hyperlinks and bold, italic and underscore attributes. In the Link
|
||||
// structure, the ClrR, ClrG and ClrB fields (0 through 255) define the color
|
||||
// of hyperlinks. The Bold, Italic and Underscore values define the hyperlink
|
||||
// style.
|
||||
type HTMLBasicType struct { |
||||
pdf *Fpdf |
||||
Link struct { |
||||
ClrR, ClrG, ClrB int |
||||
Bold, Italic, Underscore bool |
||||
} |
||||
} |
||||
|
||||
// HTMLBasicNew returns an instance that facilitates writing basic HTML in the
|
||||
// specified PDF file.
|
||||
func (f *Fpdf) HTMLBasicNew() (html HTMLBasicType) { |
||||
html.pdf = f |
||||
html.Link.ClrR, html.Link.ClrG, html.Link.ClrB = 0, 0, 128 |
||||
html.Link.Bold, html.Link.Italic, html.Link.Underscore = false, false, true |
||||
return |
||||
} |
||||
|
||||
// Write prints text from the current position using the currently selected
|
||||
// font. See HTMLBasicNew() to create a receiver that is associated with the
|
||||
// PDF document instance. The text can be encoded with a basic subset of HTML
|
||||
// that includes hyperlinks and tags for italic (I), bold (B), underscore
|
||||
// (U) and center (CENTER) attributes. When the right margin is reached a line
|
||||
// break occurs and text continues from the left margin. Upon method exit, the
|
||||
// current position is left at the end of the text.
|
||||
//
|
||||
// lineHt indicates the line height in the unit of measure specified in New().
|
||||
func (html *HTMLBasicType) Write(lineHt float64, htmlStr string) { |
||||
var boldLvl, italicLvl, underscoreLvl, linkBold, linkItalic, linkUnderscore int |
||||
var textR, textG, textB = html.pdf.GetTextColor() |
||||
var hrefStr string |
||||
if html.Link.Bold { |
||||
linkBold = 1 |
||||
} |
||||
if html.Link.Italic { |
||||
linkItalic = 1 |
||||
} |
||||
if html.Link.Underscore { |
||||
linkUnderscore = 1 |
||||
} |
||||
setStyle := func(boldAdj, italicAdj, underscoreAdj int) { |
||||
styleStr := "" |
||||
boldLvl += boldAdj |
||||
if boldLvl > 0 { |
||||
styleStr += "B" |
||||
} |
||||
italicLvl += italicAdj |
||||
if italicLvl > 0 { |
||||
styleStr += "I" |
||||
} |
||||
underscoreLvl += underscoreAdj |
||||
if underscoreLvl > 0 { |
||||
styleStr += "U" |
||||
} |
||||
html.pdf.SetFont("", styleStr, 0) |
||||
} |
||||
putLink := func(urlStr, txtStr string) { |
||||
// Put a hyperlink
|
||||
html.pdf.SetTextColor(html.Link.ClrR, html.Link.ClrG, html.Link.ClrB) |
||||
setStyle(linkBold, linkItalic, linkUnderscore) |
||||
html.pdf.WriteLinkString(lineHt, txtStr, urlStr) |
||||
setStyle(-linkBold, -linkItalic, -linkUnderscore) |
||||
html.pdf.SetTextColor(textR, textG, textB) |
||||
} |
||||
list := HTMLBasicTokenize(htmlStr) |
||||
var ok bool |
||||
alignStr := "L" |
||||
for _, el := range list { |
||||
switch el.Cat { |
||||
case 'T': |
||||
if len(hrefStr) > 0 { |
||||
putLink(hrefStr, el.Str) |
||||
hrefStr = "" |
||||
} else { |
||||
if alignStr == "C" || alignStr == "R" { |
||||
html.pdf.WriteAligned(0, lineHt, el.Str, alignStr) |
||||
} else { |
||||
html.pdf.Write(lineHt, el.Str) |
||||
} |
||||
} |
||||
case 'O': |
||||
switch el.Str { |
||||
case "b": |
||||
setStyle(1, 0, 0) |
||||
case "i": |
||||
setStyle(0, 1, 0) |
||||
case "u": |
||||
setStyle(0, 0, 1) |
||||
case "br": |
||||
html.pdf.Ln(lineHt) |
||||
case "center": |
||||
html.pdf.Ln(lineHt) |
||||
alignStr = "C" |
||||
case "right": |
||||
html.pdf.Ln(lineHt) |
||||
alignStr = "R" |
||||
case "left": |
||||
html.pdf.Ln(lineHt) |
||||
alignStr = "L" |
||||
case "a": |
||||
hrefStr, ok = el.Attr["href"] |
||||
if !ok { |
||||
hrefStr = "" |
||||
} |
||||
} |
||||
case 'C': |
||||
switch el.Str { |
||||
case "b": |
||||
setStyle(-1, 0, 0) |
||||
case "i": |
||||
setStyle(0, -1, 0) |
||||
case "u": |
||||
setStyle(0, 0, -1) |
||||
case "center": |
||||
html.pdf.Ln(lineHt) |
||||
alignStr = "L" |
||||
case "right": |
||||
html.pdf.Ln(lineHt) |
||||
alignStr = "L" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,82 @@ |
||||
package gofpdf |
||||
|
||||
// Adapted from Nice Numbers for Graph Labels by Paul Heckbert from "Graphics
|
||||
// Gems", Academic Press, 1990
|
||||
|
||||
// Paul Heckbert 2 Dec 88
|
||||
|
||||
// https://github.com/erich666/GraphicsGems
|
||||
|
||||
// LICENSE
|
||||
|
||||
// This code repository predates the concept of Open Source, and predates most
|
||||
// licenses along such lines. As such, the official license truly is:
|
||||
|
||||
// EULA: The Graphics Gems code is copyright-protected. In other words, you
|
||||
// cannot claim the text of the code as your own and resell it. Using the code
|
||||
// is permitted in any program, product, or library, non-commercial or
|
||||
// commercial. Giving credit is not required, though is a nice gesture. The
|
||||
// code comes as-is, and if there are any flaws or problems with any Gems code,
|
||||
// nobody involved with Gems - authors, editors, publishers, or webmasters -
|
||||
// are to be held responsible. Basically, don't be a jerk, and remember that
|
||||
// anything free comes with no guarantee.
|
||||
|
||||
import ( |
||||
"math" |
||||
) |
||||
|
||||
// niceNum returns a "nice" number approximately equal to x. The number is
|
||||
// rounded if round is true, converted to its ceiling otherwise.
|
||||
func niceNum(val float64, round bool) float64 { |
||||
var nf float64 |
||||
|
||||
exp := int(math.Floor(math.Log10(val))) |
||||
f := val / math.Pow10(exp) |
||||
if round { |
||||
switch { |
||||
case f < 1.5: |
||||
nf = 1 |
||||
case f < 3.0: |
||||
nf = 2 |
||||
case f < 7.0: |
||||
nf = 5 |
||||
default: |
||||
nf = 10 |
||||
} |
||||
} else { |
||||
switch { |
||||
case f <= 1: |
||||
nf = 1 |
||||
case f <= 2.0: |
||||
nf = 2 |
||||
case f <= 5.0: |
||||
nf = 5 |
||||
default: |
||||
nf = 10 |
||||
} |
||||
} |
||||
return nf * math.Pow10(exp) |
||||
} |
||||
|
||||
// TickmarkPrecision returns an appropriate precision value for label
|
||||
// formatting.
|
||||
func TickmarkPrecision(div float64) int { |
||||
return int(math.Max(-math.Floor(math.Log10(div)), 0)) |
||||
} |
||||
|
||||
// Tickmarks returns a slice of tickmarks appropriate for a chart axis and an
|
||||
// appropriate precision for formatting purposes. The values min and max will
|
||||
// be contained within the tickmark range.
|
||||
func Tickmarks(min, max float64) (list []float64, precision int) { |
||||
if max > min { |
||||
spread := niceNum(max-min, false) |
||||
d := niceNum((spread / 4), true) |
||||
graphMin := math.Floor(min/d) * d |
||||
graphMax := math.Ceil(max/d) * d |
||||
precision = TickmarkPrecision(d) |
||||
for x := graphMin; x < graphMax+0.5*d; x += d { |
||||
list = append(list, x) |
||||
} |
||||
} |
||||
return |
||||
} |
||||
@ -0,0 +1,121 @@ |
||||
/* |
||||
* Copyright (c) 2014 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 |
||||
|
||||
// Routines in this file are translated from
|
||||
// http://www.fpdf.org/en/script/script97.php
|
||||
|
||||
type layerType struct { |
||||
name string |
||||
visible bool |
||||
objNum int // object number
|
||||
} |
||||
|
||||
type layerRecType struct { |
||||
list []layerType |
||||
currentLayer int |
||||
openLayerPane bool |
||||
} |
||||
|
||||
func (f *Fpdf) layerInit() { |
||||
f.layer.list = make([]layerType, 0) |
||||
f.layer.currentLayer = -1 |
||||
f.layer.openLayerPane = false |
||||
} |
||||
|
||||
// AddLayer defines a layer that can be shown or hidden when the document is
|
||||
// displayed. name specifies the layer name that the document reader will
|
||||
// display in the layer list. visible specifies whether the layer will be
|
||||
// initially visible. The return value is an integer ID that is used in a call
|
||||
// to BeginLayer().
|
||||
func (f *Fpdf) AddLayer(name string, visible bool) (layerID int) { |
||||
layerID = len(f.layer.list) |
||||
f.layer.list = append(f.layer.list, layerType{name: name, visible: visible}) |
||||
return |
||||
} |
||||
|
||||
// BeginLayer is called to begin adding content to the specified layer. All
|
||||
// content added to the page between a call to BeginLayer and a call to
|
||||
// EndLayer is added to the layer specified by id. See AddLayer for more
|
||||
// details.
|
||||
func (f *Fpdf) BeginLayer(id int) { |
||||
f.EndLayer() |
||||
if id >= 0 && id < len(f.layer.list) { |
||||
f.outf("/OC /OC%d BDC", id) |
||||
f.layer.currentLayer = id |
||||
} |
||||
} |
||||
|
||||
// EndLayer is called to stop adding content to the currently active layer. See
|
||||
// BeginLayer for more details.
|
||||
func (f *Fpdf) EndLayer() { |
||||
if f.layer.currentLayer >= 0 { |
||||
f.out("EMC") |
||||
f.layer.currentLayer = -1 |
||||
} |
||||
} |
||||
|
||||
// OpenLayerPane advises the document reader to open the layer pane when the
|
||||
// document is initially displayed.
|
||||
func (f *Fpdf) OpenLayerPane() { |
||||
f.layer.openLayerPane = true |
||||
} |
||||
|
||||
func (f *Fpdf) layerEndDoc() { |
||||
if len(f.layer.list) > 0 { |
||||
if f.pdfVersion < "1.5" { |
||||
f.pdfVersion = "1.5" |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (f *Fpdf) layerPutLayers() { |
||||
for j, l := range f.layer.list { |
||||
f.newobj() |
||||
f.layer.list[j].objNum = f.n |
||||
f.outf("<</Type /OCG /Name %s>>", f.textstring(utf8toutf16(l.name))) |
||||
f.out("endobj") |
||||
} |
||||
} |
||||
|
||||
func (f *Fpdf) layerPutResourceDict() { |
||||
if len(f.layer.list) > 0 { |
||||
f.out("/Properties <<") |
||||
for j, layer := range f.layer.list { |
||||
f.outf("/OC%d %d 0 R", j, layer.objNum) |
||||
} |
||||
f.out(">>") |
||||
} |
||||
|
||||
} |
||||
|
||||
func (f *Fpdf) layerPutCatalog() { |
||||
if len(f.layer.list) > 0 { |
||||
onStr := "" |
||||
offStr := "" |
||||
for _, layer := range f.layer.list { |
||||
onStr += sprintf("%d 0 R ", layer.objNum) |
||||
if !layer.visible { |
||||
offStr += sprintf("%d 0 R ", layer.objNum) |
||||
} |
||||
} |
||||
f.outf("/OCProperties <</OCGs [%s] /D <</OFF [%s] /Order [%s]>>>>", onStr, offStr, onStr) |
||||
if f.layer.openLayerPane { |
||||
f.out("/PageMode /UseOC") |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,213 @@ |
||||
/* |
||||
* Copyright (c) 2013-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 gofpdf |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
"strings" |
||||
) |
||||
|
||||
func (f *Fpdf) pngColorSpace(ct byte) (colspace string, colorVal int) { |
||||
colorVal = 1 |
||||
switch ct { |
||||
case 0, 4: |
||||
colspace = "DeviceGray" |
||||
case 2, 6: |
||||
colspace = "DeviceRGB" |
||||
colorVal = 3 |
||||
case 3: |
||||
colspace = "Indexed" |
||||
default: |
||||
f.err = fmt.Errorf("unknown color type in PNG buffer: %d", ct) |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (f *Fpdf) parsepngstream(buf *bytes.Buffer, readdpi bool) (info *ImageInfoType) { |
||||
info = f.newImageInfo() |
||||
// Check signature
|
||||
if string(buf.Next(8)) != "\x89PNG\x0d\x0a\x1a\x0a" { |
||||
f.err = fmt.Errorf("not a PNG buffer") |
||||
return |
||||
} |
||||
// Read header chunk
|
||||
_ = buf.Next(4) |
||||
if string(buf.Next(4)) != "IHDR" { |
||||
f.err = fmt.Errorf("incorrect PNG buffer") |
||||
return |
||||
} |
||||
w := f.readBeInt32(buf) |
||||
h := f.readBeInt32(buf) |
||||
bpc := f.readByte(buf) |
||||
if bpc > 8 { |
||||
f.err = fmt.Errorf("16-bit depth not supported in PNG file") |
||||
} |
||||
ct := f.readByte(buf) |
||||
var colspace string |
||||
var colorVal int |
||||
colspace, colorVal = f.pngColorSpace(ct) |
||||
if f.err != nil { |
||||
return |
||||
} |
||||
if f.readByte(buf) != 0 { |
||||
f.err = fmt.Errorf("'unknown compression method in PNG buffer") |
||||
return |
||||
} |
||||
if f.readByte(buf) != 0 { |
||||
f.err = fmt.Errorf("'unknown filter method in PNG buffer") |
||||
return |
||||
} |
||||
if f.readByte(buf) != 0 { |
||||
f.err = fmt.Errorf("interlacing not supported in PNG buffer") |
||||
return |
||||
} |
||||
_ = buf.Next(4) |
||||
dp := sprintf("/Predictor 15 /Colors %d /BitsPerComponent %d /Columns %d", colorVal, bpc, w) |
||||
// Scan chunks looking for palette, transparency and image data
|
||||
pal := make([]byte, 0, 32) |
||||
var trns []int |
||||
data := make([]byte, 0, 32) |
||||
loop := true |
||||
for loop { |
||||
n := int(f.readBeInt32(buf)) |
||||
// dbg("Loop [%d]", n)
|
||||
switch string(buf.Next(4)) { |
||||
case "PLTE": |
||||
// dbg("PLTE")
|
||||
// Read palette
|
||||
pal = buf.Next(n) |
||||
_ = buf.Next(4) |
||||
case "tRNS": |
||||
// dbg("tRNS")
|
||||
// Read transparency info
|
||||
t := buf.Next(n) |
||||
switch ct { |
||||
case 0: |
||||
trns = []int{int(t[1])} // ord(substr($t,1,1)));
|
||||
case 2: |
||||
trns = []int{int(t[1]), int(t[3]), int(t[5])} // array(ord(substr($t,1,1)), ord(substr($t,3,1)), ord(substr($t,5,1)));
|
||||
default: |
||||
pos := strings.Index(string(t), "\x00") |
||||
if pos >= 0 { |
||||
trns = []int{pos} // array($pos);
|
||||
} |
||||
} |
||||
_ = buf.Next(4) |
||||
case "IDAT": |
||||
// dbg("IDAT")
|
||||
// Read image data block
|
||||
data = append(data, buf.Next(n)...) |
||||
_ = buf.Next(4) |
||||
case "IEND": |
||||
// dbg("IEND")
|
||||
loop = false |
||||
case "pHYs": |
||||
// dbg("pHYs")
|
||||
// png files theoretically support different x/y dpi
|
||||
// but we ignore files like this
|
||||
// but if they're the same then we can stamp our info
|
||||
// object with it
|
||||
x := int(f.readBeInt32(buf)) |
||||
y := int(f.readBeInt32(buf)) |
||||
units := buf.Next(1)[0] |
||||
// fmt.Printf("got a pHYs block, x=%d, y=%d, u=%d, readdpi=%t\n",
|
||||
// x, y, int(units), readdpi)
|
||||
// only modify the info block if the user wants us to
|
||||
if x == y && readdpi { |
||||
switch units { |
||||
// if units is 1 then measurement is px/meter
|
||||
case 1: |
||||
info.dpi = float64(x) / 39.3701 // inches per meter
|
||||
default: |
||||
info.dpi = float64(x) |
||||
} |
||||
} |
||||
_ = buf.Next(4) |
||||
default: |
||||
// dbg("default")
|
||||
_ = buf.Next(n + 4) |
||||
} |
||||
if loop { |
||||
loop = n > 0 |
||||
} |
||||
} |
||||
if colspace == "Indexed" && len(pal) == 0 { |
||||
f.err = fmt.Errorf("missing palette in PNG buffer") |
||||
} |
||||
info.w = float64(w) |
||||
info.h = float64(h) |
||||
info.cs = colspace |
||||
info.bpc = int(bpc) |
||||
info.f = "FlateDecode" |
||||
info.dp = dp |
||||
info.pal = pal |
||||
info.trns = trns |
||||
// dbg("ct [%d]", ct)
|
||||
if ct >= 4 { |
||||
// Separate alpha and color channels
|
||||
var err error |
||||
data, err = sliceUncompress(data) |
||||
if err != nil { |
||||
f.err = err |
||||
return |
||||
} |
||||
var color, alpha bytes.Buffer |
||||
if ct == 4 { |
||||
// Gray image
|
||||
width := int(w) |
||||
height := int(h) |
||||
length := 2 * width |
||||
var pos, elPos int |
||||
for i := 0; i < height; i++ { |
||||
pos = (1 + length) * i |
||||
color.WriteByte(data[pos]) |
||||
alpha.WriteByte(data[pos]) |
||||
elPos = pos + 1 |
||||
for k := 0; k < width; k++ { |
||||
color.WriteByte(data[elPos]) |
||||
alpha.WriteByte(data[elPos+1]) |
||||
elPos += 2 |
||||
} |
||||
} |
||||
} else { |
||||
// RGB image
|
||||
width := int(w) |
||||
height := int(h) |
||||
length := 4 * width |
||||
var pos, elPos int |
||||
for i := 0; i < height; i++ { |
||||
pos = (1 + length) * i |
||||
color.WriteByte(data[pos]) |
||||
alpha.WriteByte(data[pos]) |
||||
elPos = pos + 1 |
||||
for k := 0; k < width; k++ { |
||||
color.Write(data[elPos : elPos+3]) |
||||
alpha.WriteByte(data[elPos+3]) |
||||
elPos += 4 |
||||
} |
||||
} |
||||
} |
||||
data = sliceCompress(color.Bytes()) |
||||
info.smask = sliceCompress(alpha.Bytes()) |
||||
if f.pdfVersion < "1.4" { |
||||
f.pdfVersion = "1.4" |
||||
} |
||||
} |
||||
info.data = data |
||||
return |
||||
} |
||||
@ -0,0 +1,114 @@ |
||||
/* |
||||
* Copyright (c) 2013-2014 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. |
||||
*/ |
||||
|
||||
// PDF protection is adapted from the work of Klemen VODOPIVEC for the fpdf
|
||||
// product.
|
||||
|
||||
package gofpdf |
||||
|
||||
import ( |
||||
"crypto/md5" |
||||
"crypto/rc4" |
||||
"encoding/binary" |
||||
"math/rand" |
||||
) |
||||
|
||||
// Advisory bitflag constants that control document activities
|
||||
const ( |
||||
CnProtectPrint = 4 |
||||
CnProtectModify = 8 |
||||
CnProtectCopy = 16 |
||||
CnProtectAnnotForms = 32 |
||||
) |
||||
|
||||
type protectType struct { |
||||
encrypted bool |
||||
uValue []byte |
||||
oValue []byte |
||||
pValue int |
||||
padding []byte |
||||
encryptionKey []byte |
||||
objNum int |
||||
rc4cipher *rc4.Cipher |
||||
rc4n uint32 // Object number associated with rc4 cipher
|
||||
} |
||||
|
||||
func (p *protectType) rc4(n uint32, buf *[]byte) { |
||||
if p.rc4cipher == nil || p.rc4n != n { |
||||
p.rc4cipher, _ = rc4.NewCipher(p.objectKey(n)) |
||||
p.rc4n = n |
||||
} |
||||
p.rc4cipher.XORKeyStream(*buf, *buf) |
||||
} |
||||
|
||||
func (p *protectType) objectKey(n uint32) []byte { |
||||
var nbuf, b []byte |
||||
nbuf = make([]byte, 8, 8) |
||||
binary.LittleEndian.PutUint32(nbuf, n) |
||||
b = append(b, p.encryptionKey...) |
||||
b = append(b, nbuf[0], nbuf[1], nbuf[2], 0, 0) |
||||
s := md5.Sum(b) |
||||
return s[0:10] |
||||
} |
||||
|
||||
func oValueGen(userPass, ownerPass []byte) (v []byte) { |
||||
var c *rc4.Cipher |
||||
tmp := md5.Sum(ownerPass) |
||||
c, _ = rc4.NewCipher(tmp[0:5]) |
||||
size := len(userPass) |
||||
v = make([]byte, size, size) |
||||
c.XORKeyStream(v, userPass) |
||||
return |
||||
} |
||||
|
||||
func (p *protectType) uValueGen() (v []byte) { |
||||
var c *rc4.Cipher |
||||
c, _ = rc4.NewCipher(p.encryptionKey) |
||||
size := len(p.padding) |
||||
v = make([]byte, size, size) |
||||
c.XORKeyStream(v, p.padding) |
||||
return |
||||
} |
||||
|
||||
func (p *protectType) setProtection(privFlag byte, userPassStr, ownerPassStr string) { |
||||
privFlag = 192 | (privFlag & (CnProtectCopy | CnProtectModify | CnProtectPrint | CnProtectAnnotForms)) |
||||
p.padding = []byte{ |
||||
0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, |
||||
0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08, |
||||
0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, |
||||
0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A, |
||||
} |
||||
userPass := []byte(userPassStr) |
||||
var ownerPass []byte |
||||
if ownerPassStr == "" { |
||||
ownerPass = make([]byte, 8, 8) |
||||
binary.LittleEndian.PutUint64(ownerPass, uint64(rand.Int63())) |
||||
} else { |
||||
ownerPass = []byte(ownerPassStr) |
||||
} |
||||
userPass = append(userPass, p.padding...)[0:32] |
||||
ownerPass = append(ownerPass, p.padding...)[0:32] |
||||
p.encrypted = true |
||||
p.oValue = oValueGen(userPass, ownerPass) |
||||
var buf []byte |
||||
buf = append(buf, userPass...) |
||||
buf = append(buf, p.oValue...) |
||||
buf = append(buf, privFlag, 0xff, 0xff, 0xff) |
||||
sum := md5.Sum(buf) |
||||
p.encryptionKey = sum[0:5] |
||||
p.uValue = p.uValueGen() |
||||
p.pValue = -(int(privFlag^255) + 1) |
||||
} |
||||
@ -0,0 +1,53 @@ |
||||
package gofpdf |
||||
|
||||
import ( |
||||
"math" |
||||
// "strings"
|
||||
"unicode" |
||||
) |
||||
|
||||
// SplitText splits UTF-8 encoded text into several lines using the current
|
||||
// font. Each line has its length limited to a maximum width given by w. This
|
||||
// function can be used to determine the total height of wrapped text for
|
||||
// vertical placement purposes.
|
||||
func (f *Fpdf) SplitText(txt string, w float64) (lines []string) { |
||||
cw := f.currentFont.Cw |
||||
wmax := int(math.Ceil((w - 2*f.cMargin) * 1000 / f.fontSize)) |
||||
s := []rune(txt) // Return slice of UTF-8 runes
|
||||
nb := len(s) |
||||
for nb > 0 && s[nb-1] == '\n' { |
||||
nb-- |
||||
} |
||||
s = s[0:nb] |
||||
sep := -1 |
||||
i := 0 |
||||
j := 0 |
||||
l := 0 |
||||
for i < nb { |
||||
c := s[i] |
||||
l += cw[c] |
||||
if unicode.IsSpace(c) || isChinese(c) { |
||||
sep = i |
||||
} |
||||
if c == '\n' || l > wmax { |
||||
if sep == -1 { |
||||
if i == j { |
||||
i++ |
||||
} |
||||
sep = i |
||||
} else { |
||||
i = sep + 1 |
||||
} |
||||
lines = append(lines, string(s[j:sep])) |
||||
sep = -1 |
||||
j = i |
||||
l = 0 |
||||
} else { |
||||
i++ |
||||
} |
||||
} |
||||
if i != j { |
||||
lines = append(lines, string(s[j:i])) |
||||
} |
||||
return lines |
||||
} |
||||
@ -0,0 +1,184 @@ |
||||
// Copyright (c) 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.
|
||||
|
||||
// Adapted from http://www.fpdf.org/en/script/script89.php by Olivier PLATHEY
|
||||
|
||||
package gofpdf |
||||
|
||||
import ( |
||||
"fmt" |
||||
"strings" |
||||
) |
||||
|
||||
func byteBound(v byte) byte { |
||||
if v > 100 { |
||||
return 100 |
||||
} |
||||
return v |
||||
} |
||||
|
||||
// AddSpotColor adds an ink-based CMYK color to the gofpdf instance and
|
||||
// associates it with the specified name. The individual components specify
|
||||
// percentages ranging from 0 to 100. Values above this are quietly capped to
|
||||
// 100. An error occurs if the specified name is already associated with a
|
||||
// color.
|
||||
func (f *Fpdf) AddSpotColor(nameStr string, c, m, y, k byte) { |
||||
if f.err == nil { |
||||
_, ok := f.spotColorMap[nameStr] |
||||
if !ok { |
||||
id := len(f.spotColorMap) + 1 |
||||
f.spotColorMap[nameStr] = spotColorType{ |
||||
id: id, |
||||
val: cmykColorType{ |
||||
c: byteBound(c), |
||||
m: byteBound(m), |
||||
y: byteBound(y), |
||||
k: byteBound(k), |
||||
}, |
||||
} |
||||
} else { |
||||
f.err = fmt.Errorf("name \"%s\" is already associated with a spot color", nameStr) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (f *Fpdf) getSpotColor(nameStr string) (clr spotColorType, ok bool) { |
||||
if f.err == nil { |
||||
clr, ok = f.spotColorMap[nameStr] |
||||
if !ok { |
||||
f.err = fmt.Errorf("spot color name \"%s\" is not registered", nameStr) |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
// SetDrawSpotColor sets the current draw color to the spot color associated
|
||||
// with nameStr. An error occurs if the name is not associated with a color.
|
||||
// The value for tint ranges from 0 (no intensity) to 100 (full intensity). It
|
||||
// is quietly bounded to this range.
|
||||
func (f *Fpdf) SetDrawSpotColor(nameStr string, tint byte) { |
||||
var clr spotColorType |
||||
var ok bool |
||||
|
||||
clr, ok = f.getSpotColor(nameStr) |
||||
if ok { |
||||
f.color.draw.mode = colorModeSpot |
||||
f.color.draw.spotStr = nameStr |
||||
f.color.draw.str = sprintf("/CS%d CS %.3f SCN", clr.id, float64(byteBound(tint))/100) |
||||
if f.page > 0 { |
||||
f.out(f.color.draw.str) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// SetFillSpotColor sets the current fill color to the spot color associated
|
||||
// with nameStr. An error occurs if the name is not associated with a color.
|
||||
// The value for tint ranges from 0 (no intensity) to 100 (full intensity). It
|
||||
// is quietly bounded to this range.
|
||||
func (f *Fpdf) SetFillSpotColor(nameStr string, tint byte) { |
||||
var clr spotColorType |
||||
var ok bool |
||||
|
||||
clr, ok = f.getSpotColor(nameStr) |
||||
if ok { |
||||
f.color.fill.mode = colorModeSpot |
||||
f.color.fill.spotStr = nameStr |
||||
f.color.fill.str = sprintf("/CS%d cs %.3f scn", clr.id, float64(byteBound(tint))/100) |
||||
f.colorFlag = f.color.fill.str != f.color.text.str |
||||
if f.page > 0 { |
||||
f.out(f.color.fill.str) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// SetTextSpotColor sets the current text color to the spot color associated
|
||||
// with nameStr. An error occurs if the name is not associated with a color.
|
||||
// The value for tint ranges from 0 (no intensity) to 100 (full intensity). It
|
||||
// is quietly bounded to this range.
|
||||
func (f *Fpdf) SetTextSpotColor(nameStr string, tint byte) { |
||||
var clr spotColorType |
||||
var ok bool |
||||
|
||||
clr, ok = f.getSpotColor(nameStr) |
||||
if ok { |
||||
f.color.text.mode = colorModeSpot |
||||
f.color.text.spotStr = nameStr |
||||
f.color.text.str = sprintf("/CS%d cs %.3f scn", clr.id, float64(byteBound(tint))/100) |
||||
f.colorFlag = f.color.text.str != f.color.text.str |
||||
} |
||||
} |
||||
|
||||
func (f *Fpdf) returnSpotColor(clr colorType) (name string, c, m, y, k byte) { |
||||
var spotClr spotColorType |
||||
var ok bool |
||||
|
||||
name = clr.spotStr |
||||
if name != "" { |
||||
spotClr, ok = f.getSpotColor(name) |
||||
if ok { |
||||
c = spotClr.val.c |
||||
m = spotClr.val.m |
||||
y = spotClr.val.y |
||||
k = spotClr.val.k |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
// GetDrawSpotColor returns the most recently used spot color information for
|
||||
// drawing. This will not be the current drawing color if some other color type
|
||||
// such as RGB is active. If no spot color has been set for drawing, zero
|
||||
// values are returned.
|
||||
func (f *Fpdf) GetDrawSpotColor() (name string, c, m, y, k byte) { |
||||
return f.returnSpotColor(f.color.draw) |
||||
} |
||||
|
||||
// GetTextSpotColor returns the most recently used spot color information for
|
||||
// text output. This will not be the current text color if some other color
|
||||
// type such as RGB is active. If no spot color has been set for text, zero
|
||||
// values are returned.
|
||||
func (f *Fpdf) GetTextSpotColor() (name string, c, m, y, k byte) { |
||||
return f.returnSpotColor(f.color.text) |
||||
} |
||||
|
||||
// GetFillSpotColor returns the most recently used spot color information for
|
||||
// fill output. This will not be the current fill color if some other color
|
||||
// type such as RGB is active. If no fill spot color has been set, zero values
|
||||
// are returned.
|
||||
func (f *Fpdf) GetFillSpotColor() (name string, c, m, y, k byte) { |
||||
return f.returnSpotColor(f.color.fill) |
||||
} |
||||
|
||||
func (f *Fpdf) putSpotColors() { |
||||
for k, v := range f.spotColorMap { |
||||
f.newobj() |
||||
f.outf("[/Separation /%s", strings.Replace(k, " ", "#20", -1)) |
||||
f.out("/DeviceCMYK <<") |
||||
f.out("/Range [0 1 0 1 0 1 0 1] /C0 [0 0 0 0] ") |
||||
f.outf("/C1 [%.3f %.3f %.3f %.3f] ", float64(v.val.c)/100, float64(v.val.m)/100, |
||||
float64(v.val.y)/100, float64(v.val.k)/100) |
||||
f.out("/FunctionType 2 /Domain [0 1] /N 1>>]") |
||||
f.out("endobj") |
||||
v.objID = f.n |
||||
f.spotColorMap[k] = v |
||||
} |
||||
} |
||||
|
||||
func (f *Fpdf) spotColorPutResourceDict() { |
||||
f.out("/ColorSpace <<") |
||||
for _, clr := range f.spotColorMap { |
||||
f.outf("/CS%d %d 0 R", clr.id, clr.objID) |
||||
} |
||||
f.out(">>") |
||||
} |
||||
@ -0,0 +1,35 @@ |
||||
package gofpdf |
||||
|
||||
// Adapted from http://www.fpdf.org/en/script/script61.php by Wirus and released with the FPDF license.
|
||||
|
||||
// SubWrite prints text from the current position in the same way as Write().
|
||||
// ht is the line height in the unit of measure specified in New(). str
|
||||
// specifies the text to write. subFontSize is the size of the font in points.
|
||||
// subOffset is the vertical offset of the text in points; a positive value
|
||||
// indicates a superscript, a negative value indicates a subscript. link is the
|
||||
// identifier returned by AddLink() or 0 for no internal link. linkStr is a
|
||||
// target URL or empty for no external link. A non--zero value for link takes
|
||||
// precedence over linkStr.
|
||||
//
|
||||
// The SubWrite example demonstrates this method.
|
||||
func (f *Fpdf) SubWrite(ht float64, str string, subFontSize, subOffset float64, link int, linkStr string) { |
||||
if f.err != nil { |
||||
return |
||||
} |
||||
// resize font
|
||||
subFontSizeOld := f.fontSizePt |
||||
f.SetFontSize(subFontSize) |
||||
// reposition y
|
||||
subOffset = (((subFontSize - subFontSizeOld) / f.k) * 0.3) + (subOffset / f.k) |
||||
subX := f.x |
||||
subY := f.y |
||||
f.SetXY(subX, subY-subOffset) |
||||
//Output text
|
||||
f.write(ht, str, link, linkStr) |
||||
// restore y position
|
||||
subX = f.x |
||||
subY = f.y |
||||
f.SetXY(subX, subY+subOffset) |
||||
// restore font size
|
||||
f.SetFontSize(subFontSizeOld) |
||||
} |
||||
@ -0,0 +1,246 @@ |
||||
/* |
||||
* Copyright (c) 2014 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 ( |
||||
"encoding/xml" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"strconv" |
||||
"strings" |
||||
) |
||||
|
||||
var pathCmdSub *strings.Replacer |
||||
|
||||
func init() { |
||||
// Handle permitted constructions like "100L200,230"
|
||||
pathCmdSub = strings.NewReplacer(",", " ", |
||||
"L", " L ", "l", " l ", |
||||
"C", " C ", "c", " c ", |
||||
"M", " M ", "m", " m ", |
||||
"H", " H ", "h", " h ", |
||||
"V", " V ", "v", " v ", |
||||
"Q", " Q ", "q", " q ", |
||||
"Z", " Z ", "z", " z ") |
||||
} |
||||
|
||||
// SVGBasicSegmentType describes a single curve or position segment
|
||||
type SVGBasicSegmentType struct { |
||||
Cmd byte // See http://www.w3.org/TR/SVG/paths.html for path command structure
|
||||
Arg [6]float64 |
||||
} |
||||
|
||||
func absolutizePath(segs []SVGBasicSegmentType) { |
||||
var x, y float64 |
||||
var segPtr *SVGBasicSegmentType |
||||
adjust := func(pos int, adjX, adjY float64) { |
||||
segPtr.Arg[pos] += adjX |
||||
segPtr.Arg[pos+1] += adjY |
||||
} |
||||
for j, seg := range segs { |
||||
segPtr = &segs[j] |
||||
if j == 0 && seg.Cmd == 'm' { |
||||
segPtr.Cmd = 'M' |
||||
} |
||||
switch segPtr.Cmd { |
||||
case 'M': |
||||
x = seg.Arg[0] |
||||
y = seg.Arg[1] |
||||
case 'm': |
||||
adjust(0, x, y) |
||||
segPtr.Cmd = 'M' |
||||
x = segPtr.Arg[0] |
||||
y = segPtr.Arg[1] |
||||
case 'L': |
||||
x = seg.Arg[0] |
||||
y = seg.Arg[1] |
||||
case 'l': |
||||
adjust(0, x, y) |
||||
segPtr.Cmd = 'L' |
||||
x = segPtr.Arg[0] |
||||
y = segPtr.Arg[1] |
||||
case 'C': |
||||
x = seg.Arg[4] |
||||
y = seg.Arg[5] |
||||
case 'c': |
||||
adjust(0, x, y) |
||||
adjust(2, x, y) |
||||
adjust(4, x, y) |
||||
segPtr.Cmd = 'C' |
||||
x = segPtr.Arg[4] |
||||
y = segPtr.Arg[5] |
||||
case 'Q': |
||||
x = seg.Arg[2] |
||||
y = seg.Arg[3] |
||||
case 'q': |
||||
adjust(0, x, y) |
||||
adjust(2, x, y) |
||||
segPtr.Cmd = 'Q' |
||||
x = segPtr.Arg[2] |
||||
y = segPtr.Arg[3] |
||||
case 'H': |
||||
x = seg.Arg[0] |
||||
case 'h': |
||||
segPtr.Arg[0] += x |
||||
segPtr.Cmd = 'H' |
||||
x += seg.Arg[0] |
||||
case 'V': |
||||
y = seg.Arg[0] |
||||
case 'v': |
||||
segPtr.Arg[0] += y |
||||
segPtr.Cmd = 'V' |
||||
y += seg.Arg[0] |
||||
case 'z': |
||||
segPtr.Cmd = 'Z' |
||||
} |
||||
} |
||||
} |
||||
|
||||
func pathParse(pathStr string) (segs []SVGBasicSegmentType, err error) { |
||||
var seg SVGBasicSegmentType |
||||
var j, argJ, argCount, prevArgCount int |
||||
setup := func(n int) { |
||||
// It is not strictly necessary to clear arguments, but result may be clearer
|
||||
// to caller
|
||||
for j := 0; j < len(seg.Arg); j++ { |
||||
seg.Arg[j] = 0.0 |
||||
} |
||||
argJ = 0 |
||||
argCount = n |
||||
prevArgCount = n |
||||
} |
||||
var str string |
||||
var c byte |
||||
pathStr = pathCmdSub.Replace(pathStr) |
||||
strList := strings.Fields(pathStr) |
||||
count := len(strList) |
||||
for j = 0; j < count && err == nil; j++ { |
||||
str = strList[j] |
||||
if argCount == 0 { // Look for path command or argument continuation
|
||||
c = str[0] |
||||
if c == '-' || (c >= '0' && c <= '9') { // More arguments
|
||||
if j > 0 { |
||||
setup(prevArgCount) |
||||
// Repeat previous action
|
||||
if seg.Cmd == 'M' { |
||||
seg.Cmd = 'L' |
||||
} else if seg.Cmd == 'm' { |
||||
seg.Cmd = 'l' |
||||
} |
||||
} else { |
||||
err = fmt.Errorf("expecting SVG path command at first position, got %s", str) |
||||
} |
||||
} |
||||
} |
||||
if err == nil { |
||||
if argCount == 0 { |
||||
seg.Cmd = str[0] |
||||
switch seg.Cmd { |
||||
case 'M', 'm': // Absolute/relative moveto: x, y
|
||||
setup(2) |
||||
case 'C', 'c': // Absolute/relative Bézier curve: cx0, cy0, cx1, cy1, x1, y1
|
||||
setup(6) |
||||
case 'H', 'h': // Absolute/relative horizontal line to: x
|
||||
setup(1) |
||||
case 'L', 'l': // Absolute/relative lineto: x, y
|
||||
setup(2) |
||||
case 'Q', 'q': // Absolute/relative quadratic curve: x0, y0, x1, y1
|
||||
setup(4) |
||||
case 'V', 'v': // Absolute/relative vertical line to: y
|
||||
setup(1) |
||||
case 'Z', 'z': // closepath instruction (takes no arguments)
|
||||
segs = append(segs, seg) |
||||
default: |
||||
err = fmt.Errorf("expecting SVG path command at position %d, got %s", j, str) |
||||
} |
||||
} else { |
||||
seg.Arg[argJ], err = strconv.ParseFloat(str, 64) |
||||
if err == nil { |
||||
argJ++ |
||||
argCount-- |
||||
if argCount == 0 { |
||||
segs = append(segs, seg) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
if err == nil { |
||||
if argCount == 0 { |
||||
absolutizePath(segs) |
||||
} else { |
||||
err = fmt.Errorf("expecting additional (%d) numeric arguments", argCount) |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
// SVGBasicType aggregates the information needed to describe a multi-segment
|
||||
// basic vector image
|
||||
type SVGBasicType struct { |
||||
Wd, Ht float64 |
||||
Segments [][]SVGBasicSegmentType |
||||
} |
||||
|
||||
// SVGBasicParse parses a simple scalable vector graphics (SVG) buffer into a
|
||||
// descriptor. Only a small subset of the SVG standard, in particular the path
|
||||
// information generated by jSignature, is supported. The returned path data
|
||||
// includes only the commands 'M' (absolute moveto: x, y), 'L' (absolute
|
||||
// lineto: x, y), 'C' (absolute cubic Bézier curve: cx0, cy0, cx1, cy1,
|
||||
// x1,y1), 'Q' (absolute quadratic Bézier curve: x0, y0, x1, y1) and 'Z'
|
||||
// (closepath).
|
||||
func SVGBasicParse(buf []byte) (sig SVGBasicType, err error) { |
||||
type pathType struct { |
||||
D string `xml:"d,attr"` |
||||
} |
||||
type srcType struct { |
||||
Wd float64 `xml:"width,attr"` |
||||
Ht float64 `xml:"height,attr"` |
||||
Paths []pathType `xml:"path"` |
||||
} |
||||
var src srcType |
||||
err = xml.Unmarshal(buf, &src) |
||||
if err == nil { |
||||
if src.Wd > 0 && src.Ht > 0 { |
||||
sig.Wd, sig.Ht = src.Wd, src.Ht |
||||
var segs []SVGBasicSegmentType |
||||
for _, path := range src.Paths { |
||||
if err == nil { |
||||
segs, err = pathParse(path.D) |
||||
if err == nil { |
||||
sig.Segments = append(sig.Segments, segs) |
||||
} |
||||
} |
||||
} |
||||
} else { |
||||
err = fmt.Errorf("unacceptable values for basic SVG extent: %.2f x %.2f", |
||||
sig.Wd, sig.Ht) |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
// SVGBasicFileParse parses a simple scalable vector graphics (SVG) file into a
|
||||
// basic descriptor. The SVGBasicWrite() example demonstrates this method.
|
||||
func SVGBasicFileParse(svgFileStr string) (sig SVGBasicType, err error) { |
||||
var buf []byte |
||||
buf, err = ioutil.ReadFile(svgFileStr) |
||||
if err == nil { |
||||
sig, err = SVGBasicParse(buf) |
||||
} |
||||
return |
||||
} |
||||
@ -0,0 +1,85 @@ |
||||
/* |
||||
* Copyright (c) 2014 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 |
||||
|
||||
// SVGBasicWrite renders the paths encoded in the basic SVG image specified by
|
||||
// sb. The scale value is used to convert the coordinates in the path to the
|
||||
// unit of measure specified in New(). The current position (as set with a call
|
||||
// to SetXY()) is used as the origin of the image. The current line cap style
|
||||
// (as set with SetLineCapStyle()), line width (as set with SetLineWidth()),
|
||||
// and draw color (as set with SetDrawColor()) are used in drawing the image
|
||||
// paths.
|
||||
func (f *Fpdf) SVGBasicWrite(sb *SVGBasicType, scale float64) { |
||||
originX, originY := f.GetXY() |
||||
var x, y, newX, newY float64 |
||||
var cx0, cy0, cx1, cy1 float64 |
||||
var path []SVGBasicSegmentType |
||||
var seg SVGBasicSegmentType |
||||
var startX, startY float64 |
||||
sval := func(origin float64, arg int) float64 { |
||||
return origin + scale*seg.Arg[arg] |
||||
} |
||||
xval := func(arg int) float64 { |
||||
return sval(originX, arg) |
||||
} |
||||
yval := func(arg int) float64 { |
||||
return sval(originY, arg) |
||||
} |
||||
val := func(arg int) (float64, float64) { |
||||
return xval(arg), yval(arg + 1) |
||||
} |
||||
for j := 0; j < len(sb.Segments) && f.Ok(); j++ { |
||||
path = sb.Segments[j] |
||||
for k := 0; k < len(path) && f.Ok(); k++ { |
||||
seg = path[k] |
||||
switch seg.Cmd { |
||||
case 'M': |
||||
x, y = val(0) |
||||
startX, startY = x, y |
||||
f.SetXY(x, y) |
||||
case 'L': |
||||
newX, newY = val(0) |
||||
f.Line(x, y, newX, newY) |
||||
x, y = newX, newY |
||||
case 'C': |
||||
cx0, cy0 = val(0) |
||||
cx1, cy1 = val(2) |
||||
newX, newY = val(4) |
||||
f.CurveCubic(x, y, cx0, cy0, newX, newY, cx1, cy1, "D") |
||||
x, y = newX, newY |
||||
case 'Q': |
||||
cx0, cy0 = val(0) |
||||
newX, newY = val(2) |
||||
f.Curve(x, y, cx0, cy0, newX, newY, "D") |
||||
x, y = newX, newY |
||||
case 'H': |
||||
newX = xval(0) |
||||
f.Line(x, y, newX, y) |
||||
x = newX |
||||
case 'V': |
||||
newY = yval(0) |
||||
f.Line(x, y, x, newY) |
||||
y = newY |
||||
case 'Z': |
||||
f.Line(x, y, startX, startY) |
||||
x, y = startX, startY |
||||
default: |
||||
f.SetErrorf("Unexpected path command '%c'", seg.Cmd) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,273 @@ |
||||
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. |
||||
*/ |
||||
|
||||
import ( |
||||
"encoding/gob" |
||||
"sort" |
||||
) |
||||
|
||||
// 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.defOrientation, 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.defOrientation, f.unitStr, f.fontDirStr, fn, f) |
||||
} |
||||
|
||||
// CreateTemplate creates a template that is not attached to any document.
|
||||
//
|
||||
// This function is deprecated; it incorrectly assumes that a page with a width
|
||||
// smaller than its height is oriented in portrait mode, otherwise it assumes
|
||||
// landscape mode. This causes problems when placing the template in a master
|
||||
// document where this condition does not apply. CreateTpl() is a similar
|
||||
// function that lets you specify the orientation to avoid this problem.
|
||||
func CreateTemplate(corner PointType, size SizeType, unitStr, fontDirStr string, fn func(*Tpl)) Template { |
||||
orientationStr := "p" |
||||
if size.Wd > size.Ht { |
||||
orientationStr = "l" |
||||
} |
||||
|
||||
return CreateTpl(corner, size, orientationStr, unitStr, fontDirStr, fn) |
||||
} |
||||
|
||||
// CreateTpl creates a template not attached to any document
|
||||
func CreateTpl(corner PointType, size SizeType, orientationStr, unitStr, fontDirStr string, fn func(*Tpl)) Template { |
||||
return newTpl(corner, size, orientationStr, 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%s-%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%s Do Q", t.ID()) |
||||
} |
||||
|
||||
// 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() string |
||||
Size() (PointType, SizeType) |
||||
Bytes() []byte |
||||
Images() map[string]*ImageInfoType |
||||
Templates() []Template |
||||
NumPages() int |
||||
FromPage(int) (Template, error) |
||||
FromPages() []Template |
||||
Serialize() ([]byte, error) |
||||
gob.GobDecoder |
||||
gob.GobEncoder |
||||
} |
||||
|
||||
func (f *Fpdf) templateFontCatalog() { |
||||
var keyList []string |
||||
var font fontDefType |
||||
var key string |
||||
f.out("/Font <<") |
||||
for key = range f.fonts { |
||||
keyList = append(keyList, key) |
||||
} |
||||
if f.catalogSort { |
||||
sort.Strings(keyList) |
||||
} |
||||
for _, key = range keyList { |
||||
font = f.fonts[key] |
||||
f.outf("/F%s %d 0 R", font.i, font.N) |
||||
} |
||||
f.out(">>") |
||||
} |
||||
|
||||
// putTemplates writes the templates to the PDF
|
||||
func (f *Fpdf) putTemplates() { |
||||
filter := "" |
||||
if f.compress { |
||||
filter = "/Filter /FlateDecode " |
||||
} |
||||
|
||||
templates := sortTemplates(f.templates, f.catalogSort) |
||||
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]") |
||||
|
||||
f.templateFontCatalog() |
||||
|
||||
tImages := t.Images() |
||||
tTemplates := t.Templates() |
||||
if len(tImages) > 0 || len(tTemplates) > 0 { |
||||
f.out("/XObject <<") |
||||
{ |
||||
var key string |
||||
var keyList []string |
||||
var ti *ImageInfoType |
||||
for key = range tImages { |
||||
keyList = append(keyList, key) |
||||
} |
||||
if gl.catalogSort { |
||||
sort.Strings(keyList) |
||||
} |
||||
for _, key = range keyList { |
||||
// for _, ti := range tImages {
|
||||
ti = tImages[key] |
||||
f.outf("/I%s %d 0 R", ti.i, ti.n) |
||||
} |
||||
} |
||||
for _, tt := range tTemplates { |
||||
id := tt.ID() |
||||
if objID, ok := f.templateObjects[id]; ok { |
||||
f.outf("/TPL%s %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") |
||||
} |
||||
} |
||||
|
||||
func templateKeyList(mp map[string]Template, sort bool) (keyList []string) { |
||||
var key string |
||||
for key = range mp { |
||||
keyList = append(keyList, key) |
||||
} |
||||
if sort { |
||||
gensort(len(keyList), |
||||
func(a, b int) bool { |
||||
return keyList[a] < keyList[b] |
||||
}, |
||||
func(a, b int) { |
||||
keyList[a], keyList[b] = keyList[b], keyList[a] |
||||
}) |
||||
} |
||||
return |
||||
} |
||||
|
||||
// sortTemplates puts templates in a suitable order based on dependices
|
||||
func sortTemplates(templates map[string]Template, catalogSort bool) []Template { |
||||
chain := make([]Template, 0, len(templates)*2) |
||||
|
||||
// build a full set of dependency chains
|
||||
var keyList []string |
||||
var key string |
||||
var t Template |
||||
keyList = templateKeyList(templates, catalogSort) |
||||
for _, key = range keyList { |
||||
t = templates[key] |
||||
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 { |
||||
chain = append(chain, templateChainDependencies(req)...) |
||||
} |
||||
chain = append(chain, template) |
||||
return chain |
||||
} |
||||
|
||||
// < 0002640 31 20 31 32 20 30 20 52 0a 2f 54 50 4c 32 20 31 |1 12 0 R./TPL2 1|
|
||||
// < 0002650 35 20 30 20 52 0a 2f 54 50 4c 31 20 31 34 20 30 |5 0 R./TPL1 14 0|
|
||||
|
||||
// > 0002640 31 20 31 32 20 30 20 52 0a 2f 54 50 4c 31 20 31 |1 12 0 R./TPL1 1|
|
||||
// > 0002650 34 20 30 20 52 0a 2f 54 50 4c 32 20 31 35 20 30 |4 0 R./TPL2 15 0|
|
||||
@ -0,0 +1,299 @@ |
||||
package gofpdf |
||||
|
||||
import ( |
||||
"bytes" |
||||
"crypto/sha1" |
||||
"encoding/gob" |
||||
"errors" |
||||
"fmt" |
||||
) |
||||
|
||||
/* |
||||
* 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, orientationStr, unitStr, fontDirStr string, fn func(*Tpl), copyFrom *Fpdf) Template { |
||||
sizeStr := "" |
||||
|
||||
fpdf := fpdfNew(orientationStr, unitStr, sizeStr, fontDirStr, size) |
||||
tpl := Tpl{*fpdf} |
||||
if copyFrom != nil { |
||||
tpl.loadParamsFromFpdf(copyFrom) |
||||
} |
||||
tpl.Fpdf.AddPage() |
||||
fn(&tpl) |
||||
|
||||
bytes := make([][]byte, len(tpl.Fpdf.pages)) |
||||
// skip the first page as it will always be empty
|
||||
for x := 1; x < len(bytes); x++ { |
||||
bytes[x] = tpl.Fpdf.pages[x].Bytes() |
||||
} |
||||
|
||||
templates := make([]Template, 0, len(tpl.Fpdf.templates)) |
||||
for _, key := range templateKeyList(tpl.Fpdf.templates, true) { |
||||
templates = append(templates, tpl.Fpdf.templates[key]) |
||||
} |
||||
images := tpl.Fpdf.images |
||||
|
||||
template := FpdfTpl{corner, size, bytes, images, templates, tpl.Fpdf.page} |
||||
return &template |
||||
} |
||||
|
||||
// FpdfTpl is a concrete implementation of the Template interface.
|
||||
type FpdfTpl struct { |
||||
corner PointType |
||||
size SizeType |
||||
bytes [][]byte |
||||
images map[string]*ImageInfoType |
||||
templates []Template |
||||
page int |
||||
} |
||||
|
||||
// ID returns the global template identifier
|
||||
func (t *FpdfTpl) ID() string { |
||||
return fmt.Sprintf("%x", sha1.Sum(t.Bytes())) |
||||
} |
||||
|
||||
// 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[t.page] |
||||
} |
||||
|
||||
// FromPage creates a new template from a specific Page
|
||||
func (t *FpdfTpl) FromPage(page int) (Template, error) { |
||||
// pages start at 1
|
||||
if page == 0 { |
||||
return nil, errors.New("Pages start at 1 No template will have a page 0") |
||||
} |
||||
|
||||
if page > t.NumPages() { |
||||
return nil, fmt.Errorf("The template does not have a page %d", page) |
||||
} |
||||
// if it is already pointing to the correct page
|
||||
// there is no need to create a new template
|
||||
if t.page == page { |
||||
return t, nil |
||||
} |
||||
|
||||
t2 := *t |
||||
t2.page = page |
||||
return &t2, nil |
||||
} |
||||
|
||||
// FromPages creates a template slice with all the pages within a template.
|
||||
func (t *FpdfTpl) FromPages() []Template { |
||||
p := make([]Template, t.NumPages()) |
||||
for x := 1; x <= t.NumPages(); x++ { |
||||
// the only error is when accessing a
|
||||
// non existing template... that can't happen
|
||||
// here
|
||||
p[x-1], _ = t.FromPage(x) |
||||
} |
||||
|
||||
return p |
||||
} |
||||
|
||||
// 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 |
||||
} |
||||
|
||||
// NumPages returns the number of available pages within the template. Look at FromPage and FromPages on access to that content.
|
||||
func (t *FpdfTpl) NumPages() int { |
||||
// the first page is empty to
|
||||
// make the pages begin at one
|
||||
return len(t.bytes) - 1 |
||||
} |
||||
|
||||
// Serialize turns a template into a byte string for later deserialization
|
||||
func (t *FpdfTpl) Serialize() ([]byte, error) { |
||||
b := new(bytes.Buffer) |
||||
enc := gob.NewEncoder(b) |
||||
err := enc.Encode(t) |
||||
|
||||
return b.Bytes(), err |
||||
} |
||||
|
||||
// DeserializeTemplate creaties a template from a previously serialized
|
||||
// template
|
||||
func DeserializeTemplate(b []byte) (Template, error) { |
||||
tpl := new(FpdfTpl) |
||||
dec := gob.NewDecoder(bytes.NewBuffer(b)) |
||||
err := dec.Decode(tpl) |
||||
return tpl, err |
||||
} |
||||
|
||||
// childrenImages returns the next layer of children images, it doesn't dig into
|
||||
// children of children. Applies template namespace to keys to ensure
|
||||
// no collisions. See UseTemplateScaled
|
||||
func (t *FpdfTpl) childrenImages() map[string]*ImageInfoType { |
||||
childrenImgs := make(map[string]*ImageInfoType) |
||||
|
||||
for x := 0; x < len(t.templates); x++ { |
||||
imgs := t.templates[x].Images() |
||||
for key, val := range imgs { |
||||
name := sprintf("t%s-%s", t.templates[x].ID(), key) |
||||
childrenImgs[name] = val |
||||
} |
||||
} |
||||
|
||||
return childrenImgs |
||||
} |
||||
|
||||
// childrensTemplates returns the next layer of children templates, it doesn't dig into
|
||||
// children of children.
|
||||
func (t *FpdfTpl) childrensTemplates() []Template { |
||||
childrenTmpls := make([]Template, 0) |
||||
|
||||
for x := 0; x < len(t.templates); x++ { |
||||
tmpls := t.templates[x].Templates() |
||||
childrenTmpls = append(childrenTmpls, tmpls...) |
||||
} |
||||
|
||||
return childrenTmpls |
||||
} |
||||
|
||||
// GobEncode encodes the receiving template into a byte buffer. Use GobDecode
|
||||
// to decode the byte buffer back to a template.
|
||||
func (t *FpdfTpl) GobEncode() ([]byte, error) { |
||||
w := new(bytes.Buffer) |
||||
encoder := gob.NewEncoder(w) |
||||
|
||||
childrensTemplates := t.childrensTemplates() |
||||
firstClassTemplates := make([]Template, 0) |
||||
|
||||
found_continue: |
||||
for x := 0; x < len(t.templates); x++ { |
||||
for y := 0; y < len(childrensTemplates); y++ { |
||||
if childrensTemplates[y].ID() == t.templates[x].ID() { |
||||
continue found_continue |
||||
} |
||||
} |
||||
|
||||
firstClassTemplates = append(firstClassTemplates, t.templates[x]) |
||||
} |
||||
err := encoder.Encode(firstClassTemplates) |
||||
|
||||
childrenImgs := t.childrenImages() |
||||
firstClassImgs := make(map[string]*ImageInfoType) |
||||
|
||||
for key, img := range t.images { |
||||
if _, ok := childrenImgs[key]; !ok { |
||||
firstClassImgs[key] = img |
||||
} |
||||
} |
||||
|
||||
if err == nil { |
||||
err = encoder.Encode(firstClassImgs) |
||||
} |
||||
if err == nil { |
||||
err = encoder.Encode(t.corner) |
||||
} |
||||
if err == nil { |
||||
err = encoder.Encode(t.size) |
||||
} |
||||
if err == nil { |
||||
err = encoder.Encode(t.bytes) |
||||
} |
||||
if err == nil { |
||||
err = encoder.Encode(t.page) |
||||
} |
||||
|
||||
return w.Bytes(), err |
||||
} |
||||
|
||||
// GobDecode decodes the specified byte buffer into the receiving template.
|
||||
func (t *FpdfTpl) GobDecode(buf []byte) error { |
||||
r := bytes.NewBuffer(buf) |
||||
decoder := gob.NewDecoder(r) |
||||
|
||||
firstClassTemplates := make([]*FpdfTpl, 0) |
||||
err := decoder.Decode(&firstClassTemplates) |
||||
t.templates = make([]Template, len(firstClassTemplates)) |
||||
|
||||
for x := 0; x < len(t.templates); x++ { |
||||
t.templates[x] = Template(firstClassTemplates[x]) |
||||
} |
||||
|
||||
firstClassImages := t.childrenImages() |
||||
|
||||
t.templates = append(t.childrensTemplates(), t.templates...) |
||||
|
||||
t.images = make(map[string]*ImageInfoType) |
||||
if err == nil { |
||||
err = decoder.Decode(&t.images) |
||||
} |
||||
|
||||
for k, v := range firstClassImages { |
||||
t.images[k] = v |
||||
} |
||||
|
||||
if err == nil { |
||||
err = decoder.Decode(&t.corner) |
||||
} |
||||
if err == nil { |
||||
err = decoder.Decode(&t.size) |
||||
} |
||||
if err == nil { |
||||
err = decoder.Decode(&t.bytes) |
||||
} |
||||
if err == nil { |
||||
err = decoder.Decode(&t.page) |
||||
} |
||||
|
||||
return err |
||||
} |
||||
|
||||
// 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.fonts = f.fonts |
||||
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 |
||||
} |
||||
@ -0,0 +1,374 @@ |
||||
/* |
||||
* Copyright (c) 2013 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 |
||||
|
||||
// Utility to parse TTF font files
|
||||
// Version: 1.0
|
||||
// Date: 2011-06-18
|
||||
// Author: Olivier PLATHEY
|
||||
// Port to Go: Kurt Jung, 2013-07-15
|
||||
|
||||
import ( |
||||
"encoding/binary" |
||||
"fmt" |
||||
"os" |
||||
"regexp" |
||||
"strings" |
||||
) |
||||
|
||||
// TtfType contains metrics of a TrueType font.
|
||||
type TtfType struct { |
||||
Embeddable bool |
||||
UnitsPerEm uint16 |
||||
PostScriptName string |
||||
Bold bool |
||||
ItalicAngle int16 |
||||
IsFixedPitch bool |
||||
TypoAscender int16 |
||||
TypoDescender int16 |
||||
UnderlinePosition int16 |
||||
UnderlineThickness int16 |
||||
Xmin, Ymin, Xmax, Ymax int16 |
||||
CapHeight int16 |
||||
Widths []uint16 |
||||
Chars map[uint16]uint16 |
||||
} |
||||
|
||||
type ttfParser struct { |
||||
rec TtfType |
||||
f *os.File |
||||
tables map[string]uint32 |
||||
numberOfHMetrics uint16 |
||||
numGlyphs uint16 |
||||
} |
||||
|
||||
// TtfParse extracts various metrics from a TrueType font file.
|
||||
func TtfParse(fileStr string) (TtfRec TtfType, err error) { |
||||
var t ttfParser |
||||
t.f, err = os.Open(fileStr) |
||||
if err != nil { |
||||
return |
||||
} |
||||
version, err := t.ReadStr(4) |
||||
if err != nil { |
||||
return |
||||
} |
||||
if version == "OTTO" { |
||||
err = fmt.Errorf("fonts based on PostScript outlines are not supported") |
||||
return |
||||
} |
||||
if version != "\x00\x01\x00\x00" { |
||||
err = fmt.Errorf("unrecognized file format") |
||||
return |
||||
} |
||||
numTables := int(t.ReadUShort()) |
||||
t.Skip(3 * 2) // searchRange, entrySelector, rangeShift
|
||||
t.tables = make(map[string]uint32) |
||||
var tag string |
||||
for j := 0; j < numTables; j++ { |
||||
tag, err = t.ReadStr(4) |
||||
if err != nil { |
||||
return |
||||
} |
||||
t.Skip(4) // checkSum
|
||||
offset := t.ReadULong() |
||||
t.Skip(4) // length
|
||||
t.tables[tag] = offset |
||||
} |
||||
err = t.ParseComponents() |
||||
if err != nil { |
||||
return |
||||
} |
||||
t.f.Close() |
||||
TtfRec = t.rec |
||||
return |
||||
} |
||||
|
||||
func (t *ttfParser) ParseComponents() (err error) { |
||||
err = t.ParseHead() |
||||
if err == nil { |
||||
err = t.ParseHhea() |
||||
if err == nil { |
||||
err = t.ParseMaxp() |
||||
if err == nil { |
||||
err = t.ParseHmtx() |
||||
if err == nil { |
||||
err = t.ParseCmap() |
||||
if err == nil { |
||||
err = t.ParseName() |
||||
if err == nil { |
||||
err = t.ParseOS2() |
||||
if err == nil { |
||||
err = t.ParsePost() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (t *ttfParser) ParseHead() (err error) { |
||||
err = t.Seek("head") |
||||
t.Skip(3 * 4) // version, fontRevision, checkSumAdjustment
|
||||
magicNumber := t.ReadULong() |
||||
if magicNumber != 0x5F0F3CF5 { |
||||
err = fmt.Errorf("incorrect magic number") |
||||
return |
||||
} |
||||
t.Skip(2) // flags
|
||||
t.rec.UnitsPerEm = t.ReadUShort() |
||||
t.Skip(2 * 8) // created, modified
|
||||
t.rec.Xmin = t.ReadShort() |
||||
t.rec.Ymin = t.ReadShort() |
||||
t.rec.Xmax = t.ReadShort() |
||||
t.rec.Ymax = t.ReadShort() |
||||
return |
||||
} |
||||
|
||||
func (t *ttfParser) ParseHhea() (err error) { |
||||
err = t.Seek("hhea") |
||||
if err == nil { |
||||
t.Skip(4 + 15*2) |
||||
t.numberOfHMetrics = t.ReadUShort() |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (t *ttfParser) ParseMaxp() (err error) { |
||||
err = t.Seek("maxp") |
||||
if err == nil { |
||||
t.Skip(4) |
||||
t.numGlyphs = t.ReadUShort() |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (t *ttfParser) ParseHmtx() (err error) { |
||||
err = t.Seek("hmtx") |
||||
if err == nil { |
||||
t.rec.Widths = make([]uint16, 0, 8) |
||||
for j := uint16(0); j < t.numberOfHMetrics; j++ { |
||||
t.rec.Widths = append(t.rec.Widths, t.ReadUShort()) |
||||
t.Skip(2) // lsb
|
||||
} |
||||
if t.numberOfHMetrics < t.numGlyphs { |
||||
lastWidth := t.rec.Widths[t.numberOfHMetrics-1] |
||||
for j := t.numberOfHMetrics; j < t.numGlyphs; j++ { |
||||
t.rec.Widths = append(t.rec.Widths, lastWidth) |
||||
} |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (t *ttfParser) ParseCmap() (err error) { |
||||
var offset int64 |
||||
if err = t.Seek("cmap"); err != nil { |
||||
return |
||||
} |
||||
t.Skip(2) // version
|
||||
numTables := int(t.ReadUShort()) |
||||
offset31 := int64(0) |
||||
for j := 0; j < numTables; j++ { |
||||
platformID := t.ReadUShort() |
||||
encodingID := t.ReadUShort() |
||||
offset = int64(t.ReadULong()) |
||||
if platformID == 3 && encodingID == 1 { |
||||
offset31 = offset |
||||
} |
||||
} |
||||
if offset31 == 0 { |
||||
err = fmt.Errorf("no Unicode encoding found") |
||||
return |
||||
} |
||||
startCount := make([]uint16, 0, 8) |
||||
endCount := make([]uint16, 0, 8) |
||||
idDelta := make([]int16, 0, 8) |
||||
idRangeOffset := make([]uint16, 0, 8) |
||||
t.rec.Chars = make(map[uint16]uint16) |
||||
t.f.Seek(int64(t.tables["cmap"])+offset31, os.SEEK_SET) |
||||
format := t.ReadUShort() |
||||
if format != 4 { |
||||
err = fmt.Errorf("unexpected subtable format: %d", format) |
||||
return |
||||
} |
||||
t.Skip(2 * 2) // length, language
|
||||
segCount := int(t.ReadUShort() / 2) |
||||
t.Skip(3 * 2) // searchRange, entrySelector, rangeShift
|
||||
for j := 0; j < segCount; j++ { |
||||
endCount = append(endCount, t.ReadUShort()) |
||||
} |
||||
t.Skip(2) // reservedPad
|
||||
for j := 0; j < segCount; j++ { |
||||
startCount = append(startCount, t.ReadUShort()) |
||||
} |
||||
for j := 0; j < segCount; j++ { |
||||
idDelta = append(idDelta, t.ReadShort()) |
||||
} |
||||
offset, _ = t.f.Seek(int64(0), os.SEEK_CUR) |
||||
for j := 0; j < segCount; j++ { |
||||
idRangeOffset = append(idRangeOffset, t.ReadUShort()) |
||||
} |
||||
for j := 0; j < segCount; j++ { |
||||
c1 := startCount[j] |
||||
c2 := endCount[j] |
||||
d := idDelta[j] |
||||
ro := idRangeOffset[j] |
||||
if ro > 0 { |
||||
t.f.Seek(offset+2*int64(j)+int64(ro), os.SEEK_SET) |
||||
} |
||||
for c := c1; c <= c2; c++ { |
||||
if c == 0xFFFF { |
||||
break |
||||
} |
||||
var gid int32 |
||||
if ro > 0 { |
||||
gid = int32(t.ReadUShort()) |
||||
if gid > 0 { |
||||
gid += int32(d) |
||||
} |
||||
} else { |
||||
gid = int32(c) + int32(d) |
||||
} |
||||
if gid >= 65536 { |
||||
gid -= 65536 |
||||
} |
||||
if gid > 0 { |
||||
t.rec.Chars[c] = uint16(gid) |
||||
} |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (t *ttfParser) ParseName() (err error) { |
||||
err = t.Seek("name") |
||||
if err == nil { |
||||
tableOffset, _ := t.f.Seek(0, os.SEEK_CUR) |
||||
t.rec.PostScriptName = "" |
||||
t.Skip(2) // format
|
||||
count := t.ReadUShort() |
||||
stringOffset := t.ReadUShort() |
||||
for j := uint16(0); j < count && t.rec.PostScriptName == ""; j++ { |
||||
t.Skip(3 * 2) // platformID, encodingID, languageID
|
||||
nameID := t.ReadUShort() |
||||
length := t.ReadUShort() |
||||
offset := t.ReadUShort() |
||||
if nameID == 6 { |
||||
// PostScript name
|
||||
t.f.Seek(int64(tableOffset)+int64(stringOffset)+int64(offset), os.SEEK_SET) |
||||
var s string |
||||
s, err = t.ReadStr(int(length)) |
||||
if err != nil { |
||||
return |
||||
} |
||||
s = strings.Replace(s, "\x00", "", -1) |
||||
var re *regexp.Regexp |
||||
if re, err = regexp.Compile("[(){}<> /%[\\]]"); err != nil { |
||||
return |
||||
} |
||||
t.rec.PostScriptName = re.ReplaceAllString(s, "") |
||||
} |
||||
} |
||||
if t.rec.PostScriptName == "" { |
||||
err = fmt.Errorf("the name PostScript was not found") |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (t *ttfParser) ParseOS2() (err error) { |
||||
err = t.Seek("OS/2") |
||||
if err == nil { |
||||
version := t.ReadUShort() |
||||
t.Skip(3 * 2) // xAvgCharWidth, usWeightClass, usWidthClass
|
||||
fsType := t.ReadUShort() |
||||
t.rec.Embeddable = (fsType != 2) && (fsType&0x200) == 0 |
||||
t.Skip(11*2 + 10 + 4*4 + 4) |
||||
fsSelection := t.ReadUShort() |
||||
t.rec.Bold = (fsSelection & 32) != 0 |
||||
t.Skip(2 * 2) // usFirstCharIndex, usLastCharIndex
|
||||
t.rec.TypoAscender = t.ReadShort() |
||||
t.rec.TypoDescender = t.ReadShort() |
||||
if version >= 2 { |
||||
t.Skip(3*2 + 2*4 + 2) |
||||
t.rec.CapHeight = t.ReadShort() |
||||
} else { |
||||
t.rec.CapHeight = 0 |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (t *ttfParser) ParsePost() (err error) { |
||||
err = t.Seek("post") |
||||
if err == nil { |
||||
t.Skip(4) // version
|
||||
t.rec.ItalicAngle = t.ReadShort() |
||||
t.Skip(2) // Skip decimal part
|
||||
t.rec.UnderlinePosition = t.ReadShort() |
||||
t.rec.UnderlineThickness = t.ReadShort() |
||||
t.rec.IsFixedPitch = t.ReadULong() != 0 |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (t *ttfParser) Seek(tag string) (err error) { |
||||
ofs, ok := t.tables[tag] |
||||
if ok { |
||||
t.f.Seek(int64(ofs), os.SEEK_SET) |
||||
} else { |
||||
err = fmt.Errorf("table not found: %s", tag) |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (t *ttfParser) Skip(n int) { |
||||
t.f.Seek(int64(n), os.SEEK_CUR) |
||||
} |
||||
|
||||
func (t *ttfParser) ReadStr(length int) (str string, err error) { |
||||
var n int |
||||
buf := make([]byte, length) |
||||
n, err = t.f.Read(buf) |
||||
if err == nil { |
||||
if n == length { |
||||
str = string(buf) |
||||
} else { |
||||
err = fmt.Errorf("unable to read %d bytes", length) |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (t *ttfParser) ReadUShort() (val uint16) { |
||||
binary.Read(t.f, binary.BigEndian, &val) |
||||
return |
||||
} |
||||
|
||||
func (t *ttfParser) ReadShort() (val int16) { |
||||
binary.Read(t.f, binary.BigEndian, &val) |
||||
return |
||||
} |
||||
|
||||
func (t *ttfParser) ReadULong() (val uint32) { |
||||
binary.Read(t.f, binary.BigEndian, &val) |
||||
return |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,454 @@ |
||||
/* |
||||
* Copyright (c) 2013 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 ( |
||||
"bufio" |
||||
"bytes" |
||||
"compress/zlib" |
||||
"fmt" |
||||
"io" |
||||
"math" |
||||
"os" |
||||
"path/filepath" |
||||
"strings" |
||||
) |
||||
|
||||
func round(f float64) int { |
||||
if f < 0 { |
||||
return -int(math.Floor(-f + 0.5)) |
||||
} |
||||
return int(math.Floor(f + 0.5)) |
||||
} |
||||
|
||||
func sprintf(fmtStr string, args ...interface{}) string { |
||||
return fmt.Sprintf(fmtStr, args...) |
||||
} |
||||
|
||||
// fileExist returns true if the specified normal file exists
|
||||
func fileExist(filename string) (ok bool) { |
||||
info, err := os.Stat(filename) |
||||
if err == nil { |
||||
if ^os.ModePerm&info.Mode() == 0 { |
||||
ok = true |
||||
} |
||||
} |
||||
return ok |
||||
} |
||||
|
||||
// fileSize returns the size of the specified file; ok will be false
|
||||
// if the file does not exist or is not an ordinary file
|
||||
func fileSize(filename string) (size int64, ok bool) { |
||||
info, err := os.Stat(filename) |
||||
ok = err == nil |
||||
if ok { |
||||
size = info.Size() |
||||
} |
||||
return |
||||
} |
||||
|
||||
// bufferFromReader returns a new buffer populated with the contents of the specified Reader
|
||||
func bufferFromReader(r io.Reader) (b *bytes.Buffer, err error) { |
||||
b = new(bytes.Buffer) |
||||
_, err = b.ReadFrom(r) |
||||
return |
||||
} |
||||
|
||||
// slicesEqual returns true if the two specified float slices are equal
|
||||
func slicesEqual(a, b []float64) bool { |
||||
if len(a) != len(b) { |
||||
return false |
||||
} |
||||
for i := range a { |
||||
if a[i] != b[i] { |
||||
return false |
||||
} |
||||
} |
||||
return true |
||||
} |
||||
|
||||
// sliceCompress returns a zlib-compressed copy of the specified byte array
|
||||
func sliceCompress(data []byte) []byte { |
||||
var buf bytes.Buffer |
||||
cmp, _ := zlib.NewWriterLevel(&buf, zlib.BestSpeed) |
||||
cmp.Write(data) |
||||
cmp.Close() |
||||
return buf.Bytes() |
||||
} |
||||
|
||||
// sliceUncompress returns an uncompressed copy of the specified zlib-compressed byte array
|
||||
func sliceUncompress(data []byte) (outData []byte, err error) { |
||||
inBuf := bytes.NewReader(data) |
||||
r, err := zlib.NewReader(inBuf) |
||||
defer r.Close() |
||||
if err == nil { |
||||
var outBuf bytes.Buffer |
||||
_, err = outBuf.ReadFrom(r) |
||||
if err == nil { |
||||
outData = outBuf.Bytes() |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
// utf8toutf16 converts UTF-8 to UTF-16BE; from http://www.fpdf.org/
|
||||
func utf8toutf16(s string, withBOM ...bool) string { |
||||
bom := true |
||||
if len(withBOM) > 0 { |
||||
bom = withBOM[0] |
||||
} |
||||
res := make([]byte, 0, 8) |
||||
if bom { |
||||
res = append(res, 0xFE, 0xFF) |
||||
} |
||||
nb := len(s) |
||||
i := 0 |
||||
for i < nb { |
||||
c1 := byte(s[i]) |
||||
i++ |
||||
switch { |
||||
case c1 >= 224: |
||||
// 3-byte character
|
||||
c2 := byte(s[i]) |
||||
i++ |
||||
c3 := byte(s[i]) |
||||
i++ |
||||
res = append(res, ((c1&0x0F)<<4)+((c2&0x3C)>>2), |
||||
((c2&0x03)<<6)+(c3&0x3F)) |
||||
case c1 >= 192: |
||||
// 2-byte character
|
||||
c2 := byte(s[i]) |
||||
i++ |
||||
res = append(res, ((c1 & 0x1C) >> 2), |
||||
((c1&0x03)<<6)+(c2&0x3F)) |
||||
default: |
||||
// Single-byte character
|
||||
res = append(res, 0, c1) |
||||
} |
||||
} |
||||
return string(res) |
||||
} |
||||
|
||||
// intIf returns a if cnd is true, otherwise b
|
||||
func intIf(cnd bool, a, b int) int { |
||||
if cnd { |
||||
return a |
||||
} |
||||
return b |
||||
} |
||||
|
||||
// strIf returns aStr if cnd is true, otherwise bStr
|
||||
func strIf(cnd bool, aStr, bStr string) string { |
||||
if cnd { |
||||
return aStr |
||||
} |
||||
return bStr |
||||
} |
||||
|
||||
// doNothing returns the passed string with no translation.
|
||||
func doNothing(s string) string { |
||||
return s |
||||
} |
||||
|
||||
// Dump the internals of the specified values
|
||||
// func dump(fileStr string, a ...interface{}) {
|
||||
// fl, err := os.OpenFile(fileStr, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
// if err == nil {
|
||||
// fmt.Fprintf(fl, "----------------\n")
|
||||
// spew.Fdump(fl, a...)
|
||||
// fl.Close()
|
||||
// }
|
||||
// }
|
||||
|
||||
func repClosure(m map[rune]byte) func(string) string { |
||||
var buf bytes.Buffer |
||||
return func(str string) string { |
||||
var ch byte |
||||
var ok bool |
||||
buf.Truncate(0) |
||||
for _, r := range str { |
||||
if r < 0x80 { |
||||
ch = byte(r) |
||||
} else { |
||||
ch, ok = m[r] |
||||
if !ok { |
||||
ch = byte('.') |
||||
} |
||||
} |
||||
buf.WriteByte(ch) |
||||
} |
||||
return buf.String() |
||||
} |
||||
} |
||||
|
||||
// UnicodeTranslator returns a function that can be used to translate, where
|
||||
// possible, utf-8 strings to a form that is compatible with the specified code
|
||||
// page. The returned function accepts a string and returns a string.
|
||||
//
|
||||
// r is a reader that should read a buffer made up of content lines that
|
||||
// pertain to the code page of interest. Each line is made up of three
|
||||
// whitespace separated fields. The first begins with "!" and is followed by
|
||||
// two hexadecimal digits that identify the glyph position in the code page of
|
||||
// interest. The second field begins with "U+" and is followed by the unicode
|
||||
// code point value. The third is the glyph name. A number of these code page
|
||||
// map files are packaged with the gfpdf library in the font directory.
|
||||
//
|
||||
// An error occurs only if a line is read that does not conform to the expected
|
||||
// format. In this case, the returned function is valid but does not perform
|
||||
// any rune translation.
|
||||
func UnicodeTranslator(r io.Reader) (f func(string) string, err error) { |
||||
m := make(map[rune]byte) |
||||
var uPos, cPos uint32 |
||||
var lineStr, nameStr string |
||||
sc := bufio.NewScanner(r) |
||||
for sc.Scan() { |
||||
lineStr = sc.Text() |
||||
lineStr = strings.TrimSpace(lineStr) |
||||
if len(lineStr) > 0 { |
||||
_, err = fmt.Sscanf(lineStr, "!%2X U+%4X %s", &cPos, &uPos, &nameStr) |
||||
if err == nil { |
||||
if cPos >= 0x80 { |
||||
m[rune(uPos)] = byte(cPos) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
if err == nil { |
||||
f = repClosure(m) |
||||
} else { |
||||
f = doNothing |
||||
} |
||||
return |
||||
} |
||||
|
||||
// UnicodeTranslatorFromFile returns a function that can be used to translate,
|
||||
// where possible, utf-8 strings to a form that is compatible with the
|
||||
// specified code page. See UnicodeTranslator for more details.
|
||||
//
|
||||
// fileStr identifies a font descriptor file that maps glyph positions to names.
|
||||
//
|
||||
// If an error occurs reading the file, the returned function is valid but does
|
||||
// not perform any rune translation.
|
||||
func UnicodeTranslatorFromFile(fileStr string) (f func(string) string, err error) { |
||||
var fl *os.File |
||||
fl, err = os.Open(fileStr) |
||||
if err == nil { |
||||
f, err = UnicodeTranslator(fl) |
||||
fl.Close() |
||||
} else { |
||||
f = doNothing |
||||
} |
||||
return |
||||
} |
||||
|
||||
// UnicodeTranslatorFromDescriptor returns a function that can be used to
|
||||
// translate, where possible, utf-8 strings to a form that is compatible with
|
||||
// the specified code page. See UnicodeTranslator for more details.
|
||||
//
|
||||
// cpStr identifies a code page. A descriptor file in the font directory, set
|
||||
// with the fontDirStr argument in the call to New(), should have this name
|
||||
// plus the extension ".map". If cpStr is empty, it will be replaced with
|
||||
// "cp1252", the gofpdf code page default.
|
||||
//
|
||||
// If an error occurs reading the descriptor, the returned function is valid
|
||||
// but does not perform any rune translation.
|
||||
//
|
||||
// The CellFormat_codepage example demonstrates this method.
|
||||
func (f *Fpdf) UnicodeTranslatorFromDescriptor(cpStr string) (rep func(string) string) { |
||||
var str string |
||||
var ok bool |
||||
if f.err == nil { |
||||
if len(cpStr) == 0 { |
||||
cpStr = "cp1252" |
||||
} |
||||
str, ok = embeddedMapList[cpStr] |
||||
if ok { |
||||
rep, f.err = UnicodeTranslator(strings.NewReader(str)) |
||||
} else { |
||||
rep, f.err = UnicodeTranslatorFromFile(filepath.Join(f.fontpath, cpStr) + ".map") |
||||
} |
||||
} else { |
||||
rep = doNothing |
||||
} |
||||
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 adjusts 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} |
||||
} |
||||
|
||||
//The untypedKeyMap structure and its methods are copyrighted 2019 by Arteom Korotkiy (Gmail: arteomkorotkiy).
|
||||
//Imitation of untyped Map Array
|
||||
type untypedKeyMap struct { |
||||
keySet []interface{} |
||||
valueSet []int |
||||
} |
||||
|
||||
//Get position of key=>value in PHP Array
|
||||
func (pa *untypedKeyMap) getIndex(key interface{}) int { |
||||
if key != nil { |
||||
for i, mKey := range pa.keySet { |
||||
if mKey == key { |
||||
return i |
||||
} |
||||
} |
||||
return -1 |
||||
} |
||||
return -1 |
||||
} |
||||
|
||||
//Put key=>value in PHP Array
|
||||
func (pa *untypedKeyMap) put(key interface{}, value int) { |
||||
if key == nil { |
||||
var i int |
||||
for n := 0; ; n++ { |
||||
i = pa.getIndex(n) |
||||
if i < 0 { |
||||
key = n |
||||
break |
||||
} |
||||
} |
||||
pa.keySet = append(pa.keySet, key) |
||||
pa.valueSet = append(pa.valueSet, value) |
||||
} else { |
||||
i := pa.getIndex(key) |
||||
if i < 0 { |
||||
pa.keySet = append(pa.keySet, key) |
||||
pa.valueSet = append(pa.valueSet, value) |
||||
} else { |
||||
pa.valueSet[i] = value |
||||
} |
||||
} |
||||
} |
||||
|
||||
//Delete value in PHP Array
|
||||
func (pa *untypedKeyMap) delete(key interface{}) { |
||||
if pa == nil || pa.keySet == nil || pa.valueSet == nil { |
||||
return |
||||
} |
||||
i := pa.getIndex(key) |
||||
if i >= 0 { |
||||
if i == 0 { |
||||
pa.keySet = pa.keySet[1:] |
||||
pa.valueSet = pa.valueSet[1:] |
||||
} else if i == len(pa.keySet)-1 { |
||||
pa.keySet = pa.keySet[:len(pa.keySet)-1] |
||||
pa.valueSet = pa.valueSet[:len(pa.valueSet)-1] |
||||
} else { |
||||
pa.keySet = append(pa.keySet[:i], pa.keySet[i+1:]...) |
||||
pa.valueSet = append(pa.valueSet[:i], pa.valueSet[i+1:]...) |
||||
} |
||||
} |
||||
} |
||||
|
||||
//Get value from PHP Array
|
||||
func (pa *untypedKeyMap) get(key interface{}) int { |
||||
i := pa.getIndex(key) |
||||
if i >= 0 { |
||||
return pa.valueSet[i] |
||||
} |
||||
return 0 |
||||
} |
||||
|
||||
//Imitation of PHP function pop()
|
||||
func (pa *untypedKeyMap) pop() { |
||||
pa.keySet = pa.keySet[:len(pa.keySet)-1] |
||||
pa.valueSet = pa.valueSet[:len(pa.valueSet)-1] |
||||
} |
||||
|
||||
//Imitation of PHP function array_merge()
|
||||
func arrayMerge(arr1, arr2 *untypedKeyMap) *untypedKeyMap { |
||||
answer := untypedKeyMap{} |
||||
if arr1 == nil && arr2 == nil { |
||||
answer = untypedKeyMap{ |
||||
make([]interface{}, 0), |
||||
make([]int, 0), |
||||
} |
||||
} else if arr2 == nil { |
||||
answer.keySet = arr1.keySet[:] |
||||
answer.valueSet = arr1.valueSet[:] |
||||
} else if arr1 == nil { |
||||
answer.keySet = arr2.keySet[:] |
||||
answer.valueSet = arr2.valueSet[:] |
||||
} else { |
||||
answer.keySet = arr1.keySet[:] |
||||
answer.valueSet = arr1.valueSet[:] |
||||
for i := 0; i < len(arr2.keySet); i++ { |
||||
if arr2.keySet[i] == "interval" { |
||||
if arr1.getIndex("interval") < 0 { |
||||
answer.put("interval", arr2.valueSet[i]) |
||||
} |
||||
} else { |
||||
answer.put(nil, arr2.valueSet[i]) |
||||
} |
||||
} |
||||
} |
||||
return &answer |
||||
} |
||||
|
||||
func remove(arr []int, key int) []int { |
||||
n := 0 |
||||
for i, mKey := range arr { |
||||
if mKey == key { |
||||
n = i |
||||
} |
||||
} |
||||
if n == 0 { |
||||
return arr[1:] |
||||
} else if n == len(arr)-1 { |
||||
return arr[:len(arr)-1] |
||||
} |
||||
return append(arr[:n], arr[n+1:]...) |
||||
} |
||||
|
||||
func isChinese(rune2 rune) bool { |
||||
// chinese unicode: 4e00-9fa5
|
||||
if rune2 >= rune(0x4e00) && rune2 <= rune(0x9fa5) { |
||||
return true |
||||
} |
||||
return false |
||||
} |
||||
@ -0,0 +1,22 @@ |
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects) |
||||
*.o |
||||
*.a |
||||
*.so |
||||
|
||||
# Folders |
||||
_obj |
||||
_test |
||||
|
||||
# Architecture specific extensions/prefixes |
||||
*.[568vq] |
||||
[568vq].out |
||||
|
||||
*.cgo1.go |
||||
*.cgo2.c |
||||
_cgo_defun.c |
||||
_cgo_gotypes.go |
||||
_cgo_export.* |
||||
|
||||
_testmain.go |
||||
|
||||
*.exe |
||||
@ -0,0 +1,21 @@ |
||||
Copyright (C) 2012 Rob Figueiredo |
||||
All Rights Reserved. |
||||
|
||||
MIT LICENSE |
||||
|
||||
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,125 @@ |
||||
[](http://godoc.org/github.com/robfig/cron) |
||||
[](https://travis-ci.org/robfig/cron) |
||||
|
||||
# cron |
||||
|
||||
Cron V3 has been released! |
||||
|
||||
To download the specific tagged release, run: |
||||
|
||||
go get github.com/robfig/cron/v3@v3.0.0 |
||||
|
||||
Import it in your program as: |
||||
|
||||
import "github.com/robfig/cron/v3" |
||||
|
||||
It requires Go 1.11 or later due to usage of Go Modules. |
||||
|
||||
Refer to the documentation here: |
||||
http://godoc.org/github.com/robfig/cron |
||||
|
||||
The rest of this document describes the the advances in v3 and a list of |
||||
breaking changes for users that wish to upgrade from an earlier version. |
||||
|
||||
## Upgrading to v3 (June 2019) |
||||
|
||||
cron v3 is a major upgrade to the library that addresses all outstanding bugs, |
||||
feature requests, and rough edges. It is based on a merge of master which |
||||
contains various fixes to issues found over the years and the v2 branch which |
||||
contains some backwards-incompatible features like the ability to remove cron |
||||
jobs. In addition, v3 adds support for Go Modules, cleans up rough edges like |
||||
the timezone support, and fixes a number of bugs. |
||||
|
||||
New features: |
||||
|
||||
- Support for Go modules. Callers must now import this library as |
||||
`github.com/robfig/cron/v3`, instead of `gopkg.in/...` |
||||
|
||||
- Fixed bugs: |
||||
- 0f01e6b parser: fix combining of Dow and Dom (#70) |
||||
- dbf3220 adjust times when rolling the clock forward to handle non-existent midnight (#157) |
||||
- eeecf15 spec_test.go: ensure an error is returned on 0 increment (#144) |
||||
- 70971dc cron.Entries(): update request for snapshot to include a reply channel (#97) |
||||
- 1cba5e6 cron: fix: removing a job causes the next scheduled job to run too late (#206) |
||||
|
||||
- Standard cron spec parsing by default (first field is "minute"), with an easy |
||||
way to opt into the seconds field (quartz-compatible). Although, note that the |
||||
year field (optional in Quartz) is not supported. |
||||
|
||||
- Extensible, key/value logging via an interface that complies with |
||||
the https://github.com/go-logr/logr project. |
||||
|
||||
- The new Chain & JobWrapper types allow you to install "interceptors" to add |
||||
cross-cutting behavior like the following: |
||||
- Recover any panics from jobs |
||||
- Delay a job's execution if the previous run hasn't completed yet |
||||
- Skip a job's execution if the previous run hasn't completed yet |
||||
- Log each job's invocations |
||||
- Notification when jobs are completed |
||||
|
||||
It is backwards incompatible with both v1 and v2. These updates are required: |
||||
|
||||
- The v1 branch accepted an optional seconds field at the beginning of the cron |
||||
spec. This is non-standard and has led to a lot of confusion. The new default |
||||
parser conforms to the standard as described by [the Cron wikipedia page]. |
||||
|
||||
UPDATING: To retain the old behavior, construct your Cron with a custom |
||||
parser: |
||||
|
||||
// Seconds field, required |
||||
cron.New(cron.WithSeconds()) |
||||
|
||||
// Seconds field, optional |
||||
cron.New( |
||||
cron.WithParser( |
||||
cron.SecondOptional | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)) |
||||
|
||||
- The Cron type now accepts functional options on construction rather than the |
||||
previous ad-hoc behavior modification mechanisms (setting a field, calling a setter). |
||||
|
||||
UPDATING: Code that sets Cron.ErrorLogger or calls Cron.SetLocation must be |
||||
updated to provide those values on construction. |
||||
|
||||
- CRON_TZ is now the recommended way to specify the timezone of a single |
||||
schedule, which is sanctioned by the specification. The legacy "TZ=" prefix |
||||
will continue to be supported since it is unambiguous and easy to do so. |
||||
|
||||
UPDATING: No update is required. |
||||
|
||||
- By default, cron will no longer recover panics in jobs that it runs. |
||||
Recovering can be surprising (see issue #192) and seems to be at odds with |
||||
typical behavior of libraries. Relatedly, the `cron.WithPanicLogger` option |
||||
has been removed to accommodate the more general JobWrapper type. |
||||
|
||||
UPDATING: To opt into panic recovery and configure the panic logger: |
||||
|
||||
cron.New(cron.WithChain( |
||||
cron.Recover(logger), // or use cron.DefaultLogger |
||||
)) |
||||
|
||||
- In adding support for https://github.com/go-logr/logr, `cron.WithVerboseLogger` was |
||||
removed, since it is duplicative with the leveled logging. |
||||
|
||||
UPDATING: Callers should use `WithLogger` and specify a logger that does not |
||||
discard `Info` logs. For convenience, one is provided that wraps `*log.Logger`: |
||||
|
||||
cron.New( |
||||
cron.WithLogger(cron.VerbosePrintfLogger(logger))) |
||||
|
||||
|
||||
### Background - Cron spec format |
||||
|
||||
There are two cron spec formats in common usage: |
||||
|
||||
- The "standard" cron format, described on [the Cron wikipedia page] and used by |
||||
the cron Linux system utility. |
||||
|
||||
- The cron format used by [the Quartz Scheduler], commonly used for scheduled |
||||
jobs in Java software |
||||
|
||||
[the Cron wikipedia page]: https://en.wikipedia.org/wiki/Cron |
||||
[the Quartz Scheduler]: http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger.html |
||||
|
||||
The original version of this package included an optional "seconds" field, which |
||||
made it incompatible with both of these formats. Now, the "standard" format is |
||||
the default format accepted, and the Quartz format is opt-in. |
||||
@ -0,0 +1,92 @@ |
||||
package cron |
||||
|
||||
import ( |
||||
"fmt" |
||||
"runtime" |
||||
"sync" |
||||
"time" |
||||
) |
||||
|
||||
// JobWrapper decorates the given Job with some behavior.
|
||||
type JobWrapper func(Job) Job |
||||
|
||||
// Chain is a sequence of JobWrappers that decorates submitted jobs with
|
||||
// cross-cutting behaviors like logging or synchronization.
|
||||
type Chain struct { |
||||
wrappers []JobWrapper |
||||
} |
||||
|
||||
// NewChain returns a Chain consisting of the given JobWrappers.
|
||||
func NewChain(c ...JobWrapper) Chain { |
||||
return Chain{c} |
||||
} |
||||
|
||||
// Then decorates the given job with all JobWrappers in the chain.
|
||||
//
|
||||
// This:
|
||||
// NewChain(m1, m2, m3).Then(job)
|
||||
// is equivalent to:
|
||||
// m1(m2(m3(job)))
|
||||
func (c Chain) Then(j Job) Job { |
||||
for i := range c.wrappers { |
||||
j = c.wrappers[len(c.wrappers)-i-1](j) |
||||
} |
||||
return j |
||||
} |
||||
|
||||
// Recover panics in wrapped jobs and log them with the provided logger.
|
||||
func Recover(logger Logger) JobWrapper { |
||||
return func(j Job) Job { |
||||
return FuncJob(func() { |
||||
defer func() { |
||||
if r := recover(); r != nil { |
||||
const size = 64 << 10 |
||||
buf := make([]byte, size) |
||||
buf = buf[:runtime.Stack(buf, false)] |
||||
err, ok := r.(error) |
||||
if !ok { |
||||
err = fmt.Errorf("%v", r) |
||||
} |
||||
logger.Error(err, "panic", "stack", "...\n"+string(buf)) |
||||
} |
||||
}() |
||||
j.Run() |
||||
}) |
||||
} |
||||
} |
||||
|
||||
// DelayIfStillRunning serializes jobs, delaying subsequent runs until the
|
||||
// previous one is complete. Jobs running after a delay of more than a minute
|
||||
// have the delay logged at Info.
|
||||
func DelayIfStillRunning(logger Logger) JobWrapper { |
||||
return func(j Job) Job { |
||||
var mu sync.Mutex |
||||
return FuncJob(func() { |
||||
start := time.Now() |
||||
mu.Lock() |
||||
defer mu.Unlock() |
||||
if dur := time.Since(start); dur > time.Minute { |
||||
logger.Info("delay", "duration", dur) |
||||
} |
||||
j.Run() |
||||
}) |
||||
} |
||||
} |
||||
|
||||
// SkipIfStillRunning skips an invocation of the Job if a previous invocation is
|
||||
// still running. It logs skips to the given logger at Info level.
|
||||
func SkipIfStillRunning(logger Logger) JobWrapper { |
||||
var ch = make(chan struct{}, 1) |
||||
ch <- struct{}{} |
||||
return func(j Job) Job { |
||||
return FuncJob(func() { |
||||
select { |
||||
case v := <-ch: |
||||
j.Run() |
||||
ch <- v |
||||
default: |
||||
logger.Info("skip") |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
@ -0,0 +1,27 @@ |
||||
package cron |
||||
|
||||
import "time" |
||||
|
||||
// ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes".
|
||||
// It does not support jobs more frequent than once a second.
|
||||
type ConstantDelaySchedule struct { |
||||
Delay time.Duration |
||||
} |
||||
|
||||
// Every returns a crontab Schedule that activates once every duration.
|
||||
// Delays of less than a second are not supported (will round up to 1 second).
|
||||
// Any fields less than a Second are truncated.
|
||||
func Every(duration time.Duration) ConstantDelaySchedule { |
||||
if duration < time.Second { |
||||
duration = time.Second |
||||
} |
||||
return ConstantDelaySchedule{ |
||||
Delay: duration - time.Duration(duration.Nanoseconds())%time.Second, |
||||
} |
||||
} |
||||
|
||||
// Next returns the next time this should be run.
|
||||
// This rounds so that the next activation time will be on the second.
|
||||
func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time { |
||||
return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond) |
||||
} |
||||
@ -0,0 +1,350 @@ |
||||
package cron |
||||
|
||||
import ( |
||||
"context" |
||||
"sort" |
||||
"sync" |
||||
"time" |
||||
) |
||||
|
||||
// Cron keeps track of any number of entries, invoking the associated func as
|
||||
// specified by the schedule. It may be started, stopped, and the entries may
|
||||
// be inspected while running.
|
||||
type Cron struct { |
||||
entries []*Entry |
||||
chain Chain |
||||
stop chan struct{} |
||||
add chan *Entry |
||||
remove chan EntryID |
||||
snapshot chan chan []Entry |
||||
running bool |
||||
logger Logger |
||||
runningMu sync.Mutex |
||||
location *time.Location |
||||
parser Parser |
||||
nextID EntryID |
||||
jobWaiter sync.WaitGroup |
||||
} |
||||
|
||||
// Job is an interface for submitted cron jobs.
|
||||
type Job interface { |
||||
Run() |
||||
} |
||||
|
||||
// Schedule describes a job's duty cycle.
|
||||
type Schedule interface { |
||||
// Next returns the next activation time, later than the given time.
|
||||
// Next is invoked initially, and then each time the job is run.
|
||||
Next(time.Time) time.Time |
||||
} |
||||
|
||||
// EntryID identifies an entry within a Cron instance
|
||||
type EntryID int |
||||
|
||||
// Entry consists of a schedule and the func to execute on that schedule.
|
||||
type Entry struct { |
||||
// ID is the cron-assigned ID of this entry, which may be used to look up a
|
||||
// snapshot or remove it.
|
||||
ID EntryID |
||||
|
||||
// Schedule on which this job should be run.
|
||||
Schedule Schedule |
||||
|
||||
// Next time the job will run, or the zero time if Cron has not been
|
||||
// started or this entry's schedule is unsatisfiable
|
||||
Next time.Time |
||||
|
||||
// Prev is the last time this job was run, or the zero time if never.
|
||||
Prev time.Time |
||||
|
||||
// WrappedJob is the thing to run when the Schedule is activated.
|
||||
WrappedJob Job |
||||
|
||||
// Job is the thing that was submitted to cron.
|
||||
// It is kept around so that user code that needs to get at the job later,
|
||||
// e.g. via Entries() can do so.
|
||||
Job Job |
||||
} |
||||
|
||||
// Valid returns true if this is not the zero entry.
|
||||
func (e Entry) Valid() bool { return e.ID != 0 } |
||||
|
||||
// byTime is a wrapper for sorting the entry array by time
|
||||
// (with zero time at the end).
|
||||
type byTime []*Entry |
||||
|
||||
func (s byTime) Len() int { return len(s) } |
||||
func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } |
||||
func (s byTime) Less(i, j int) bool { |
||||
// Two zero times should return false.
|
||||
// Otherwise, zero is "greater" than any other time.
|
||||
// (To sort it at the end of the list.)
|
||||
if s[i].Next.IsZero() { |
||||
return false |
||||
} |
||||
if s[j].Next.IsZero() { |
||||
return true |
||||
} |
||||
return s[i].Next.Before(s[j].Next) |
||||
} |
||||
|
||||
// New returns a new Cron job runner, modified by the given options.
|
||||
//
|
||||
// Available Settings
|
||||
//
|
||||
// Time Zone
|
||||
// Description: The time zone in which schedules are interpreted
|
||||
// Default: time.Local
|
||||
//
|
||||
// Parser
|
||||
// Description: Parser converts cron spec strings into cron.Schedules.
|
||||
// Default: Accepts this spec: https://en.wikipedia.org/wiki/Cron
|
||||
//
|
||||
// Chain
|
||||
// Description: Wrap submitted jobs to customize behavior.
|
||||
// Default: A chain that recovers panics and logs them to stderr.
|
||||
//
|
||||
// See "cron.With*" to modify the default behavior.
|
||||
func New(opts ...Option) *Cron { |
||||
c := &Cron{ |
||||
entries: nil, |
||||
chain: NewChain(), |
||||
add: make(chan *Entry), |
||||
stop: make(chan struct{}), |
||||
snapshot: make(chan chan []Entry), |
||||
remove: make(chan EntryID), |
||||
running: false, |
||||
runningMu: sync.Mutex{}, |
||||
logger: DefaultLogger, |
||||
location: time.Local, |
||||
parser: standardParser, |
||||
} |
||||
for _, opt := range opts { |
||||
opt(c) |
||||
} |
||||
return c |
||||
} |
||||
|
||||
// FuncJob is a wrapper that turns a func() into a cron.Job
|
||||
type FuncJob func() |
||||
|
||||
func (f FuncJob) Run() { f() } |
||||
|
||||
// AddFunc adds a func to the Cron to be run on the given schedule.
|
||||
// The spec is parsed using the time zone of this Cron instance as the default.
|
||||
// An opaque ID is returned that can be used to later remove it.
|
||||
func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error) { |
||||
return c.AddJob(spec, FuncJob(cmd)) |
||||
} |
||||
|
||||
// AddJob adds a Job to the Cron to be run on the given schedule.
|
||||
// The spec is parsed using the time zone of this Cron instance as the default.
|
||||
// An opaque ID is returned that can be used to later remove it.
|
||||
func (c *Cron) AddJob(spec string, cmd Job) (EntryID, error) { |
||||
schedule, err := c.parser.Parse(spec) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
return c.Schedule(schedule, cmd), nil |
||||
} |
||||
|
||||
// Schedule adds a Job to the Cron to be run on the given schedule.
|
||||
// The job is wrapped with the configured Chain.
|
||||
func (c *Cron) Schedule(schedule Schedule, cmd Job) EntryID { |
||||
c.runningMu.Lock() |
||||
defer c.runningMu.Unlock() |
||||
c.nextID++ |
||||
entry := &Entry{ |
||||
ID: c.nextID, |
||||
Schedule: schedule, |
||||
WrappedJob: c.chain.Then(cmd), |
||||
Job: cmd, |
||||
} |
||||
if !c.running { |
||||
c.entries = append(c.entries, entry) |
||||
} else { |
||||
c.add <- entry |
||||
} |
||||
return entry.ID |
||||
} |
||||
|
||||
// Entries returns a snapshot of the cron entries.
|
||||
func (c *Cron) Entries() []Entry { |
||||
c.runningMu.Lock() |
||||
defer c.runningMu.Unlock() |
||||
if c.running { |
||||
replyChan := make(chan []Entry, 1) |
||||
c.snapshot <- replyChan |
||||
return <-replyChan |
||||
} |
||||
return c.entrySnapshot() |
||||
} |
||||
|
||||
// Location gets the time zone location
|
||||
func (c *Cron) Location() *time.Location { |
||||
return c.location |
||||
} |
||||
|
||||
// Entry returns a snapshot of the given entry, or nil if it couldn't be found.
|
||||
func (c *Cron) Entry(id EntryID) Entry { |
||||
for _, entry := range c.Entries() { |
||||
if id == entry.ID { |
||||
return entry |
||||
} |
||||
} |
||||
return Entry{} |
||||
} |
||||
|
||||
// Remove an entry from being run in the future.
|
||||
func (c *Cron) Remove(id EntryID) { |
||||
c.runningMu.Lock() |
||||
defer c.runningMu.Unlock() |
||||
if c.running { |
||||
c.remove <- id |
||||
} else { |
||||
c.removeEntry(id) |
||||
} |
||||
} |
||||
|
||||
// Start the cron scheduler in its own goroutine, or no-op if already started.
|
||||
func (c *Cron) Start() { |
||||
c.runningMu.Lock() |
||||
defer c.runningMu.Unlock() |
||||
if c.running { |
||||
return |
||||
} |
||||
c.running = true |
||||
go c.run() |
||||
} |
||||
|
||||
// Run the cron scheduler, or no-op if already running.
|
||||
func (c *Cron) Run() { |
||||
c.runningMu.Lock() |
||||
if c.running { |
||||
c.runningMu.Unlock() |
||||
return |
||||
} |
||||
c.running = true |
||||
c.runningMu.Unlock() |
||||
c.run() |
||||
} |
||||
|
||||
// run the scheduler.. this is private just due to the need to synchronize
|
||||
// access to the 'running' state variable.
|
||||
func (c *Cron) run() { |
||||
c.logger.Info("start") |
||||
|
||||
// Figure out the next activation times for each entry.
|
||||
now := c.now() |
||||
for _, entry := range c.entries { |
||||
entry.Next = entry.Schedule.Next(now) |
||||
c.logger.Info("schedule", "now", now, "entry", entry.ID, "next", entry.Next) |
||||
} |
||||
|
||||
for { |
||||
// Determine the next entry to run.
|
||||
sort.Sort(byTime(c.entries)) |
||||
|
||||
var timer *time.Timer |
||||
if len(c.entries) == 0 || c.entries[0].Next.IsZero() { |
||||
// If there are no entries yet, just sleep - it still handles new entries
|
||||
// and stop requests.
|
||||
timer = time.NewTimer(100000 * time.Hour) |
||||
} else { |
||||
timer = time.NewTimer(c.entries[0].Next.Sub(now)) |
||||
} |
||||
|
||||
for { |
||||
select { |
||||
case now = <-timer.C: |
||||
now = now.In(c.location) |
||||
c.logger.Info("wake", "now", now) |
||||
|
||||
// Run every entry whose next time was less than now
|
||||
for _, e := range c.entries { |
||||
if e.Next.After(now) || e.Next.IsZero() { |
||||
break |
||||
} |
||||
c.startJob(e.WrappedJob) |
||||
e.Prev = e.Next |
||||
e.Next = e.Schedule.Next(now) |
||||
c.logger.Info("run", "now", now, "entry", e.ID, "next", e.Next) |
||||
} |
||||
|
||||
case newEntry := <-c.add: |
||||
timer.Stop() |
||||
now = c.now() |
||||
newEntry.Next = newEntry.Schedule.Next(now) |
||||
c.entries = append(c.entries, newEntry) |
||||
c.logger.Info("added", "now", now, "entry", newEntry.ID, "next", newEntry.Next) |
||||
|
||||
case replyChan := <-c.snapshot: |
||||
replyChan <- c.entrySnapshot() |
||||
continue |
||||
|
||||
case <-c.stop: |
||||
timer.Stop() |
||||
c.logger.Info("stop") |
||||
return |
||||
|
||||
case id := <-c.remove: |
||||
timer.Stop() |
||||
now = c.now() |
||||
c.removeEntry(id) |
||||
c.logger.Info("removed", "entry", id) |
||||
} |
||||
|
||||
break |
||||
} |
||||
} |
||||
} |
||||
|
||||
// startJob runs the given job in a new goroutine.
|
||||
func (c *Cron) startJob(j Job) { |
||||
c.jobWaiter.Add(1) |
||||
go func() { |
||||
defer c.jobWaiter.Done() |
||||
j.Run() |
||||
}() |
||||
} |
||||
|
||||
// now returns current time in c location
|
||||
func (c *Cron) now() time.Time { |
||||
return time.Now().In(c.location) |
||||
} |
||||
|
||||
// Stop stops the cron scheduler if it is running; otherwise it does nothing.
|
||||
// A context is returned so the caller can wait for running jobs to complete.
|
||||
func (c *Cron) Stop() context.Context { |
||||
c.runningMu.Lock() |
||||
defer c.runningMu.Unlock() |
||||
if c.running { |
||||
c.stop <- struct{}{} |
||||
c.running = false |
||||
} |
||||
ctx, cancel := context.WithCancel(context.Background()) |
||||
go func() { |
||||
c.jobWaiter.Wait() |
||||
cancel() |
||||
}() |
||||
return ctx |
||||
} |
||||
|
||||
// entrySnapshot returns a copy of the current cron entry list.
|
||||
func (c *Cron) entrySnapshot() []Entry { |
||||
var entries = make([]Entry, len(c.entries)) |
||||
for i, e := range c.entries { |
||||
entries[i] = *e |
||||
} |
||||
return entries |
||||
} |
||||
|
||||
func (c *Cron) removeEntry(id EntryID) { |
||||
var entries []*Entry |
||||
for _, e := range c.entries { |
||||
if e.ID != id { |
||||
entries = append(entries, e) |
||||
} |
||||
} |
||||
c.entries = entries |
||||
} |
||||
@ -0,0 +1,212 @@ |
||||
/* |
||||
Package cron implements a cron spec parser and job runner. |
||||
|
||||
Usage |
||||
|
||||
Callers may register Funcs to be invoked on a given schedule. Cron will run |
||||
them in their own goroutines. |
||||
|
||||
c := cron.New() |
||||
c.AddFunc("30 * * * *", func() { fmt.Println("Every hour on the half hour") }) |
||||
c.AddFunc("30 3-6,20-23 * * *", func() { fmt.Println(".. in the range 3-6am, 8-11pm") }) |
||||
c.AddFunc("CRON_TZ=Asia/Tokyo 30 04 * * * *", func() { fmt.Println("Runs at 04:30 Tokyo time every day") }) |
||||
c.AddFunc("@hourly", func() { fmt.Println("Every hour, starting an hour from now") }) |
||||
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty, starting an hour thirty from now") }) |
||||
c.Start() |
||||
.. |
||||
// Funcs are invoked in their own goroutine, asynchronously.
|
||||
... |
||||
// Funcs may also be added to a running Cron
|
||||
c.AddFunc("@daily", func() { fmt.Println("Every day") }) |
||||
.. |
||||
// Inspect the cron job entries' next and previous run times.
|
||||
inspect(c.Entries()) |
||||
.. |
||||
c.Stop() // Stop the scheduler (does not stop any jobs already running).
|
||||
|
||||
CRON Expression Format |
||||
|
||||
A cron expression represents a set of times, using 5 space-separated fields. |
||||
|
||||
Field name | Mandatory? | Allowed values | Allowed special characters |
||||
---------- | ---------- | -------------- | -------------------------- |
||||
Minutes | Yes | 0-59 | * / , - |
||||
Hours | Yes | 0-23 | * / , - |
||||
Day of month | Yes | 1-31 | * / , - ? |
||||
Month | Yes | 1-12 or JAN-DEC | * / , - |
||||
Day of week | Yes | 0-6 or SUN-SAT | * / , - ? |
||||
|
||||
Month and Day-of-week field values are case insensitive. "SUN", "Sun", and |
||||
"sun" are equally accepted. |
||||
|
||||
The specific interpretation of the format is based on the Cron Wikipedia page: |
||||
https://en.wikipedia.org/wiki/Cron
|
||||
|
||||
Alternative Formats |
||||
|
||||
Alternative Cron expression formats support other fields like seconds. You can |
||||
implement that by creating a custom Parser as follows. |
||||
|
||||
cron.New( |
||||
cron.WithParser( |
||||
cron.SecondOptional | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)) |
||||
|
||||
The most popular alternative Cron expression format is Quartz: |
||||
http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger.html
|
||||
|
||||
Special Characters |
||||
|
||||
Asterisk ( * ) |
||||
|
||||
The asterisk indicates that the cron expression will match for all values of the |
||||
field; e.g., using an asterisk in the 5th field (month) would indicate every |
||||
month. |
||||
|
||||
Slash ( / ) |
||||
|
||||
Slashes are used to describe increments of ranges. For example 3-59/15 in the |
||||
1st field (minutes) would indicate the 3rd minute of the hour and every 15 |
||||
minutes thereafter. The form "*\/..." is equivalent to the form "first-last/...", |
||||
that is, an increment over the largest possible range of the field. The form |
||||
"N/..." is accepted as meaning "N-MAX/...", that is, starting at N, use the |
||||
increment until the end of that specific range. It does not wrap around. |
||||
|
||||
Comma ( , ) |
||||
|
||||
Commas are used to separate items of a list. For example, using "MON,WED,FRI" in |
||||
the 5th field (day of week) would mean Mondays, Wednesdays and Fridays. |
||||
|
||||
Hyphen ( - ) |
||||
|
||||
Hyphens are used to define ranges. For example, 9-17 would indicate every |
||||
hour between 9am and 5pm inclusive. |
||||
|
||||
Question mark ( ? ) |
||||
|
||||
Question mark may be used instead of '*' for leaving either day-of-month or |
||||
day-of-week blank. |
||||
|
||||
Predefined schedules |
||||
|
||||
You may use one of several pre-defined schedules in place of a cron expression. |
||||
|
||||
Entry | Description | Equivalent To |
||||
----- | ----------- | ------------- |
||||
@yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 1 1 * |
||||
@monthly | Run once a month, midnight, first of month | 0 0 1 * * |
||||
@weekly | Run once a week, midnight between Sat/Sun | 0 0 * * 0 |
||||
@daily (or @midnight) | Run once a day, midnight | 0 0 * * * |
||||
@hourly | Run once an hour, beginning of hour | 0 * * * * |
||||
|
||||
Intervals |
||||
|
||||
You may also schedule a job to execute at fixed intervals, starting at the time it's added |
||||
or cron is run. This is supported by formatting the cron spec like this: |
||||
|
||||
@every <duration> |
||||
|
||||
where "duration" is a string accepted by time.ParseDuration |
||||
(http://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
For example, "@every 1h30m10s" would indicate a schedule that activates after |
||||
1 hour, 30 minutes, 10 seconds, and then every interval after that. |
||||
|
||||
Note: The interval does not take the job runtime into account. For example, |
||||
if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes, |
||||
it will have only 2 minutes of idle time between each run. |
||||
|
||||
Time zones |
||||
|
||||
By default, all interpretation and scheduling is done in the machine's local |
||||
time zone (time.Local). You can specify a different time zone on construction: |
||||
|
||||
cron.New( |
||||
cron.WithLocation(time.UTC)) |
||||
|
||||
Individual cron schedules may also override the time zone they are to be |
||||
interpreted in by providing an additional space-separated field at the beginning |
||||
of the cron spec, of the form "CRON_TZ=Asia/Tokyo". |
||||
|
||||
For example: |
||||
|
||||
# Runs at 6am in time.Local |
||||
cron.New().AddFunc("0 6 * * ?", ...) |
||||
|
||||
# Runs at 6am in America/New_York |
||||
nyc, _ := time.LoadLocation("America/New_York") |
||||
c := cron.New(cron.WithLocation(nyc)) |
||||
c.AddFunc("0 6 * * ?", ...) |
||||
|
||||
# Runs at 6am in Asia/Tokyo |
||||
cron.New().AddFunc("CRON_TZ=Asia/Tokyo 0 6 * * ?", ...) |
||||
|
||||
# Runs at 6am in Asia/Tokyo |
||||
c := cron.New(cron.WithLocation(nyc)) |
||||
c.SetLocation("America/New_York") |
||||
c.AddFunc("CRON_TZ=Asia/Tokyo 0 6 * * ?", ...) |
||||
|
||||
The prefix "TZ=(TIME ZONE)" is also supported for legacy compatibility. |
||||
|
||||
Be aware that jobs scheduled during daylight-savings leap-ahead transitions will |
||||
not be run! |
||||
|
||||
Job Wrappers / Chain |
||||
|
||||
A Cron runner may be configured with a chain of job wrappers to add |
||||
cross-cutting functionality to all submitted jobs. For example, they may be used |
||||
to achieve the following effects: |
||||
|
||||
- Recover any panics from jobs (activated by default) |
||||
- Delay a job's execution if the previous run hasn't completed yet |
||||
- Skip a job's execution if the previous run hasn't completed yet |
||||
- Log each job's invocations |
||||
|
||||
Install wrappers for all jobs added to a cron using the `cron.WithChain` option: |
||||
|
||||
cron.New(cron.WithChain( |
||||
cron.SkipIfStillRunning(logger), |
||||
)) |
||||
|
||||
Install wrappers for individual jobs by explicitly wrapping them: |
||||
|
||||
job = cron.NewChain( |
||||
cron.SkipIfStillRunning(logger), |
||||
).Then(job) |
||||
|
||||
Thread safety |
||||
|
||||
Since the Cron service runs concurrently with the calling code, some amount of |
||||
care must be taken to ensure proper synchronization. |
||||
|
||||
All cron methods are designed to be correctly synchronized as long as the caller |
||||
ensures that invocations have a clear happens-before ordering between them. |
||||
|
||||
Logging |
||||
|
||||
Cron defines a Logger interface that is a subset of the one defined in |
||||
github.com/go-logr/logr. It has two logging levels (Info and Error), and |
||||
parameters are key/value pairs. This makes it possible for cron logging to plug |
||||
into structured logging systems. An adapter, [Verbose]PrintfLogger, is provided |
||||
to wrap the standard library *log.Logger. |
||||
|
||||
For additional insight into Cron operations, verbose logging may be activated |
||||
which will record job runs, scheduling decisions, and added or removed jobs. |
||||
Activate it with a one-off logger as follows: |
||||
|
||||
cron.New( |
||||
cron.WithLogger( |
||||
cron.VerbosePrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags)))) |
||||
|
||||
|
||||
Implementation |
||||
|
||||
Cron entries are stored in an array, sorted by their next activation time. Cron |
||||
sleeps until the next job is due to be run. |
||||
|
||||
Upon waking: |
||||
- it runs each entry that is active on that second |
||||
- it calculates the next run times for the jobs that were run |
||||
- it re-sorts the array of entries by next activation time. |
||||
- it goes to sleep until the soonest job. |
||||
*/ |
||||
package cron |
||||
@ -0,0 +1,3 @@ |
||||
module github.com/robfig/cron/v3 |
||||
|
||||
go 1.12 |
||||
@ -0,0 +1,86 @@ |
||||
package cron |
||||
|
||||
import ( |
||||
"io/ioutil" |
||||
"log" |
||||
"os" |
||||
"strings" |
||||
"time" |
||||
) |
||||
|
||||
// DefaultLogger is used by Cron if none is specified.
|
||||
var DefaultLogger Logger = PrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags)) |
||||
|
||||
// DiscardLogger can be used by callers to discard all log messages.
|
||||
var DiscardLogger Logger = PrintfLogger(log.New(ioutil.Discard, "", 0)) |
||||
|
||||
// Logger is the interface used in this package for logging, so that any backend
|
||||
// can be plugged in. It is a subset of the github.com/go-logr/logr interface.
|
||||
type Logger interface { |
||||
// Info logs routine messages about cron's operation.
|
||||
Info(msg string, keysAndValues ...interface{}) |
||||
// Error logs an error condition.
|
||||
Error(err error, msg string, keysAndValues ...interface{}) |
||||
} |
||||
|
||||
// PrintfLogger wraps a Printf-based logger (such as the standard library "log")
|
||||
// into an implementation of the Logger interface which logs errors only.
|
||||
func PrintfLogger(l interface{ Printf(string, ...interface{}) }) Logger { |
||||
return printfLogger{l, false} |
||||
} |
||||
|
||||
// VerbosePrintfLogger wraps a Printf-based logger (such as the standard library
|
||||
// "log") into an implementation of the Logger interface which logs everything.
|
||||
func VerbosePrintfLogger(l interface{ Printf(string, ...interface{}) }) Logger { |
||||
return printfLogger{l, true} |
||||
} |
||||
|
||||
type printfLogger struct { |
||||
logger interface{ Printf(string, ...interface{}) } |
||||
logInfo bool |
||||
} |
||||
|
||||
func (pl printfLogger) Info(msg string, keysAndValues ...interface{}) { |
||||
if pl.logInfo { |
||||
keysAndValues = formatTimes(keysAndValues) |
||||
pl.logger.Printf( |
||||
formatString(len(keysAndValues)), |
||||
append([]interface{}{msg}, keysAndValues...)...) |
||||
} |
||||
} |
||||
|
||||
func (pl printfLogger) Error(err error, msg string, keysAndValues ...interface{}) { |
||||
keysAndValues = formatTimes(keysAndValues) |
||||
pl.logger.Printf( |
||||
formatString(len(keysAndValues)+2), |
||||
append([]interface{}{msg, "error", err}, keysAndValues...)...) |
||||
} |
||||
|
||||
// formatString returns a logfmt-like format string for the number of
|
||||
// key/values.
|
||||
func formatString(numKeysAndValues int) string { |
||||
var sb strings.Builder |
||||
sb.WriteString("%s") |
||||
if numKeysAndValues > 0 { |
||||
sb.WriteString(", ") |
||||
} |
||||
for i := 0; i < numKeysAndValues/2; i++ { |
||||
if i > 0 { |
||||
sb.WriteString(", ") |
||||
} |
||||
sb.WriteString("%v=%v") |
||||
} |
||||
return sb.String() |
||||
} |
||||
|
||||
// formatTimes formats any time.Time values as RFC3339.
|
||||
func formatTimes(keysAndValues []interface{}) []interface{} { |
||||
var formattedArgs []interface{} |
||||
for _, arg := range keysAndValues { |
||||
if t, ok := arg.(time.Time); ok { |
||||
arg = t.Format(time.RFC3339) |
||||
} |
||||
formattedArgs = append(formattedArgs, arg) |
||||
} |
||||
return formattedArgs |
||||
} |
||||
@ -0,0 +1,45 @@ |
||||
package cron |
||||
|
||||
import ( |
||||
"time" |
||||
) |
||||
|
||||
// Option represents a modification to the default behavior of a Cron.
|
||||
type Option func(*Cron) |
||||
|
||||
// WithLocation overrides the timezone of the cron instance.
|
||||
func WithLocation(loc *time.Location) Option { |
||||
return func(c *Cron) { |
||||
c.location = loc |
||||
} |
||||
} |
||||
|
||||
// WithSeconds overrides the parser used for interpreting job schedules to
|
||||
// include a seconds field as the first one.
|
||||
func WithSeconds() Option { |
||||
return WithParser(NewParser( |
||||
Second | Minute | Hour | Dom | Month | Dow | Descriptor, |
||||
)) |
||||
} |
||||
|
||||
// WithParser overrides the parser used for interpreting job schedules.
|
||||
func WithParser(p Parser) Option { |
||||
return func(c *Cron) { |
||||
c.parser = p |
||||
} |
||||
} |
||||
|
||||
// WithChain specifies Job wrappers to apply to all jobs added to this cron.
|
||||
// Refer to the Chain* functions in this package for provided wrappers.
|
||||
func WithChain(wrappers ...JobWrapper) Option { |
||||
return func(c *Cron) { |
||||
c.chain = NewChain(wrappers...) |
||||
} |
||||
} |
||||
|
||||
// WithLogger uses the provided logger.
|
||||
func WithLogger(logger Logger) Option { |
||||
return func(c *Cron) { |
||||
c.logger = logger |
||||
} |
||||
} |
||||
@ -0,0 +1,434 @@ |
||||
package cron |
||||
|
||||
import ( |
||||
"fmt" |
||||
"math" |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
) |
||||
|
||||
// Configuration options for creating a parser. Most options specify which
|
||||
// fields should be included, while others enable features. If a field is not
|
||||
// included the parser will assume a default value. These options do not change
|
||||
// the order fields are parse in.
|
||||
type ParseOption int |
||||
|
||||
const ( |
||||
Second ParseOption = 1 << iota // Seconds field, default 0
|
||||
SecondOptional // Optional seconds field, default 0
|
||||
Minute // Minutes field, default 0
|
||||
Hour // Hours field, default 0
|
||||
Dom // Day of month field, default *
|
||||
Month // Month field, default *
|
||||
Dow // Day of week field, default *
|
||||
DowOptional // Optional day of week field, default *
|
||||
Descriptor // Allow descriptors such as @monthly, @weekly, etc.
|
||||
) |
||||
|
||||
var places = []ParseOption{ |
||||
Second, |
||||
Minute, |
||||
Hour, |
||||
Dom, |
||||
Month, |
||||
Dow, |
||||
} |
||||
|
||||
var defaults = []string{ |
||||
"0", |
||||
"0", |
||||
"0", |
||||
"*", |
||||
"*", |
||||
"*", |
||||
} |
||||
|
||||
// A custom Parser that can be configured.
|
||||
type Parser struct { |
||||
options ParseOption |
||||
} |
||||
|
||||
// NewParser creates a Parser with custom options.
|
||||
//
|
||||
// It panics if more than one Optional is given, since it would be impossible to
|
||||
// correctly infer which optional is provided or missing in general.
|
||||
//
|
||||
// Examples
|
||||
//
|
||||
// // Standard parser without descriptors
|
||||
// specParser := NewParser(Minute | Hour | Dom | Month | Dow)
|
||||
// sched, err := specParser.Parse("0 0 15 */3 *")
|
||||
//
|
||||
// // Same as above, just excludes time fields
|
||||
// subsParser := NewParser(Dom | Month | Dow)
|
||||
// sched, err := specParser.Parse("15 */3 *")
|
||||
//
|
||||
// // Same as above, just makes Dow optional
|
||||
// subsParser := NewParser(Dom | Month | DowOptional)
|
||||
// sched, err := specParser.Parse("15 */3")
|
||||
//
|
||||
func NewParser(options ParseOption) Parser { |
||||
optionals := 0 |
||||
if options&DowOptional > 0 { |
||||
optionals++ |
||||
} |
||||
if options&SecondOptional > 0 { |
||||
optionals++ |
||||
} |
||||
if optionals > 1 { |
||||
panic("multiple optionals may not be configured") |
||||
} |
||||
return Parser{options} |
||||
} |
||||
|
||||
// Parse returns a new crontab schedule representing the given spec.
|
||||
// It returns a descriptive error if the spec is not valid.
|
||||
// It accepts crontab specs and features configured by NewParser.
|
||||
func (p Parser) Parse(spec string) (Schedule, error) { |
||||
if len(spec) == 0 { |
||||
return nil, fmt.Errorf("empty spec string") |
||||
} |
||||
|
||||
// Extract timezone if present
|
||||
var loc = time.Local |
||||
if strings.HasPrefix(spec, "TZ=") || strings.HasPrefix(spec, "CRON_TZ=") { |
||||
var err error |
||||
i := strings.Index(spec, " ") |
||||
eq := strings.Index(spec, "=") |
||||
if loc, err = time.LoadLocation(spec[eq+1 : i]); err != nil { |
||||
return nil, fmt.Errorf("provided bad location %s: %v", spec[eq+1:i], err) |
||||
} |
||||
spec = strings.TrimSpace(spec[i:]) |
||||
} |
||||
|
||||
// Handle named schedules (descriptors), if configured
|
||||
if strings.HasPrefix(spec, "@") { |
||||
if p.options&Descriptor == 0 { |
||||
return nil, fmt.Errorf("parser does not accept descriptors: %v", spec) |
||||
} |
||||
return parseDescriptor(spec, loc) |
||||
} |
||||
|
||||
// Split on whitespace.
|
||||
fields := strings.Fields(spec) |
||||
|
||||
// Validate & fill in any omitted or optional fields
|
||||
var err error |
||||
fields, err = normalizeFields(fields, p.options) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
field := func(field string, r bounds) uint64 { |
||||
if err != nil { |
||||
return 0 |
||||
} |
||||
var bits uint64 |
||||
bits, err = getField(field, r) |
||||
return bits |
||||
} |
||||
|
||||
var ( |
||||
second = field(fields[0], seconds) |
||||
minute = field(fields[1], minutes) |
||||
hour = field(fields[2], hours) |
||||
dayofmonth = field(fields[3], dom) |
||||
month = field(fields[4], months) |
||||
dayofweek = field(fields[5], dow) |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &SpecSchedule{ |
||||
Second: second, |
||||
Minute: minute, |
||||
Hour: hour, |
||||
Dom: dayofmonth, |
||||
Month: month, |
||||
Dow: dayofweek, |
||||
Location: loc, |
||||
}, nil |
||||
} |
||||
|
||||
// normalizeFields takes a subset set of the time fields and returns the full set
|
||||
// with defaults (zeroes) populated for unset fields.
|
||||
//
|
||||
// As part of performing this function, it also validates that the provided
|
||||
// fields are compatible with the configured options.
|
||||
func normalizeFields(fields []string, options ParseOption) ([]string, error) { |
||||
// Validate optionals & add their field to options
|
||||
optionals := 0 |
||||
if options&SecondOptional > 0 { |
||||
options |= Second |
||||
optionals++ |
||||
} |
||||
if options&DowOptional > 0 { |
||||
options |= Dow |
||||
optionals++ |
||||
} |
||||
if optionals > 1 { |
||||
return nil, fmt.Errorf("multiple optionals may not be configured") |
||||
} |
||||
|
||||
// Figure out how many fields we need
|
||||
max := 0 |
||||
for _, place := range places { |
||||
if options&place > 0 { |
||||
max++ |
||||
} |
||||
} |
||||
min := max - optionals |
||||
|
||||
// Validate number of fields
|
||||
if count := len(fields); count < min || count > max { |
||||
if min == max { |
||||
return nil, fmt.Errorf("expected exactly %d fields, found %d: %s", min, count, fields) |
||||
} |
||||
return nil, fmt.Errorf("expected %d to %d fields, found %d: %s", min, max, count, fields) |
||||
} |
||||
|
||||
// Populate the optional field if not provided
|
||||
if min < max && len(fields) == min { |
||||
switch { |
||||
case options&DowOptional > 0: |
||||
fields = append(fields, defaults[5]) // TODO: improve access to default
|
||||
case options&SecondOptional > 0: |
||||
fields = append([]string{defaults[0]}, fields...) |
||||
default: |
||||
return nil, fmt.Errorf("unknown optional field") |
||||
} |
||||
} |
||||
|
||||
// Populate all fields not part of options with their defaults
|
||||
n := 0 |
||||
expandedFields := make([]string, len(places)) |
||||
copy(expandedFields, defaults) |
||||
for i, place := range places { |
||||
if options&place > 0 { |
||||
expandedFields[i] = fields[n] |
||||
n++ |
||||
} |
||||
} |
||||
return expandedFields, nil |
||||
} |
||||
|
||||
var standardParser = NewParser( |
||||
Minute | Hour | Dom | Month | Dow | Descriptor, |
||||
) |
||||
|
||||
// ParseStandard returns a new crontab schedule representing the given
|
||||
// standardSpec (https://en.wikipedia.org/wiki/Cron). It requires 5 entries
|
||||
// representing: minute, hour, day of month, month and day of week, in that
|
||||
// order. It returns a descriptive error if the spec is not valid.
|
||||
//
|
||||
// It accepts
|
||||
// - Standard crontab specs, e.g. "* * * * ?"
|
||||
// - Descriptors, e.g. "@midnight", "@every 1h30m"
|
||||
func ParseStandard(standardSpec string) (Schedule, error) { |
||||
return standardParser.Parse(standardSpec) |
||||
} |
||||
|
||||
// getField returns an Int with the bits set representing all of the times that
|
||||
// the field represents or error parsing field value. A "field" is a comma-separated
|
||||
// list of "ranges".
|
||||
func getField(field string, r bounds) (uint64, error) { |
||||
var bits uint64 |
||||
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' }) |
||||
for _, expr := range ranges { |
||||
bit, err := getRange(expr, r) |
||||
if err != nil { |
||||
return bits, err |
||||
} |
||||
bits |= bit |
||||
} |
||||
return bits, nil |
||||
} |
||||
|
||||
// getRange returns the bits indicated by the given expression:
|
||||
// number | number "-" number [ "/" number ]
|
||||
// or error parsing range.
|
||||
func getRange(expr string, r bounds) (uint64, error) { |
||||
var ( |
||||
start, end, step uint |
||||
rangeAndStep = strings.Split(expr, "/") |
||||
lowAndHigh = strings.Split(rangeAndStep[0], "-") |
||||
singleDigit = len(lowAndHigh) == 1 |
||||
err error |
||||
) |
||||
|
||||
var extra uint64 |
||||
if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" { |
||||
start = r.min |
||||
end = r.max |
||||
extra = starBit |
||||
} else { |
||||
start, err = parseIntOrName(lowAndHigh[0], r.names) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
switch len(lowAndHigh) { |
||||
case 1: |
||||
end = start |
||||
case 2: |
||||
end, err = parseIntOrName(lowAndHigh[1], r.names) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
default: |
||||
return 0, fmt.Errorf("too many hyphens: %s", expr) |
||||
} |
||||
} |
||||
|
||||
switch len(rangeAndStep) { |
||||
case 1: |
||||
step = 1 |
||||
case 2: |
||||
step, err = mustParseInt(rangeAndStep[1]) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
// Special handling: "N/step" means "N-max/step".
|
||||
if singleDigit { |
||||
end = r.max |
||||
} |
||||
if step > 1 { |
||||
extra = 0 |
||||
} |
||||
default: |
||||
return 0, fmt.Errorf("too many slashes: %s", expr) |
||||
} |
||||
|
||||
if start < r.min { |
||||
return 0, fmt.Errorf("beginning of range (%d) below minimum (%d): %s", start, r.min, expr) |
||||
} |
||||
if end > r.max { |
||||
return 0, fmt.Errorf("end of range (%d) above maximum (%d): %s", end, r.max, expr) |
||||
} |
||||
if start > end { |
||||
return 0, fmt.Errorf("beginning of range (%d) beyond end of range (%d): %s", start, end, expr) |
||||
} |
||||
if step == 0 { |
||||
return 0, fmt.Errorf("step of range should be a positive number: %s", expr) |
||||
} |
||||
|
||||
return getBits(start, end, step) | extra, nil |
||||
} |
||||
|
||||
// parseIntOrName returns the (possibly-named) integer contained in expr.
|
||||
func parseIntOrName(expr string, names map[string]uint) (uint, error) { |
||||
if names != nil { |
||||
if namedInt, ok := names[strings.ToLower(expr)]; ok { |
||||
return namedInt, nil |
||||
} |
||||
} |
||||
return mustParseInt(expr) |
||||
} |
||||
|
||||
// mustParseInt parses the given expression as an int or returns an error.
|
||||
func mustParseInt(expr string) (uint, error) { |
||||
num, err := strconv.Atoi(expr) |
||||
if err != nil { |
||||
return 0, fmt.Errorf("failed to parse int from %s: %s", expr, err) |
||||
} |
||||
if num < 0 { |
||||
return 0, fmt.Errorf("negative number (%d) not allowed: %s", num, expr) |
||||
} |
||||
|
||||
return uint(num), nil |
||||
} |
||||
|
||||
// getBits sets all bits in the range [min, max], modulo the given step size.
|
||||
func getBits(min, max, step uint) uint64 { |
||||
var bits uint64 |
||||
|
||||
// If step is 1, use shifts.
|
||||
if step == 1 { |
||||
return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min) |
||||
} |
||||
|
||||
// Else, use a simple loop.
|
||||
for i := min; i <= max; i += step { |
||||
bits |= 1 << i |
||||
} |
||||
return bits |
||||
} |
||||
|
||||
// all returns all bits within the given bounds. (plus the star bit)
|
||||
func all(r bounds) uint64 { |
||||
return getBits(r.min, r.max, 1) | starBit |
||||
} |
||||
|
||||
// parseDescriptor returns a predefined schedule for the expression, or error if none matches.
|
||||
func parseDescriptor(descriptor string, loc *time.Location) (Schedule, error) { |
||||
switch descriptor { |
||||
case "@yearly", "@annually": |
||||
return &SpecSchedule{ |
||||
Second: 1 << seconds.min, |
||||
Minute: 1 << minutes.min, |
||||
Hour: 1 << hours.min, |
||||
Dom: 1 << dom.min, |
||||
Month: 1 << months.min, |
||||
Dow: all(dow), |
||||
Location: loc, |
||||
}, nil |
||||
|
||||
case "@monthly": |
||||
return &SpecSchedule{ |
||||
Second: 1 << seconds.min, |
||||
Minute: 1 << minutes.min, |
||||
Hour: 1 << hours.min, |
||||
Dom: 1 << dom.min, |
||||
Month: all(months), |
||||
Dow: all(dow), |
||||
Location: loc, |
||||
}, nil |
||||
|
||||
case "@weekly": |
||||
return &SpecSchedule{ |
||||
Second: 1 << seconds.min, |
||||
Minute: 1 << minutes.min, |
||||
Hour: 1 << hours.min, |
||||
Dom: all(dom), |
||||
Month: all(months), |
||||
Dow: 1 << dow.min, |
||||
Location: loc, |
||||
}, nil |
||||
|
||||
case "@daily", "@midnight": |
||||
return &SpecSchedule{ |
||||
Second: 1 << seconds.min, |
||||
Minute: 1 << minutes.min, |
||||
Hour: 1 << hours.min, |
||||
Dom: all(dom), |
||||
Month: all(months), |
||||
Dow: all(dow), |
||||
Location: loc, |
||||
}, nil |
||||
|
||||
case "@hourly": |
||||
return &SpecSchedule{ |
||||
Second: 1 << seconds.min, |
||||
Minute: 1 << minutes.min, |
||||
Hour: all(hours), |
||||
Dom: all(dom), |
||||
Month: all(months), |
||||
Dow: all(dow), |
||||
Location: loc, |
||||
}, nil |
||||
|
||||
} |
||||
|
||||
const every = "@every " |
||||
if strings.HasPrefix(descriptor, every) { |
||||
duration, err := time.ParseDuration(descriptor[len(every):]) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to parse duration %s: %s", descriptor, err) |
||||
} |
||||
return Every(duration), nil |
||||
} |
||||
|
||||
return nil, fmt.Errorf("unrecognized descriptor: %s", descriptor) |
||||
} |
||||
@ -0,0 +1,188 @@ |
||||
package cron |
||||
|
||||
import "time" |
||||
|
||||
// SpecSchedule specifies a duty cycle (to the second granularity), based on a
|
||||
// traditional crontab specification. It is computed initially and stored as bit sets.
|
||||
type SpecSchedule struct { |
||||
Second, Minute, Hour, Dom, Month, Dow uint64 |
||||
|
||||
// Override location for this schedule.
|
||||
Location *time.Location |
||||
} |
||||
|
||||
// bounds provides a range of acceptable values (plus a map of name to value).
|
||||
type bounds struct { |
||||
min, max uint |
||||
names map[string]uint |
||||
} |
||||
|
||||
// The bounds for each field.
|
||||
var ( |
||||
seconds = bounds{0, 59, nil} |
||||
minutes = bounds{0, 59, nil} |
||||
hours = bounds{0, 23, nil} |
||||
dom = bounds{1, 31, nil} |
||||
months = bounds{1, 12, map[string]uint{ |
||||
"jan": 1, |
||||
"feb": 2, |
||||
"mar": 3, |
||||
"apr": 4, |
||||
"may": 5, |
||||
"jun": 6, |
||||
"jul": 7, |
||||
"aug": 8, |
||||
"sep": 9, |
||||
"oct": 10, |
||||
"nov": 11, |
||||
"dec": 12, |
||||
}} |
||||
dow = bounds{0, 6, map[string]uint{ |
||||
"sun": 0, |
||||
"mon": 1, |
||||
"tue": 2, |
||||
"wed": 3, |
||||
"thu": 4, |
||||
"fri": 5, |
||||
"sat": 6, |
||||
}} |
||||
) |
||||
|
||||
const ( |
||||
// Set the top bit if a star was included in the expression.
|
||||
starBit = 1 << 63 |
||||
) |
||||
|
||||
// Next returns the next time this schedule is activated, greater than the given
|
||||
// time. If no time can be found to satisfy the schedule, return the zero time.
|
||||
func (s *SpecSchedule) Next(t time.Time) time.Time { |
||||
// General approach
|
||||
//
|
||||
// For Month, Day, Hour, Minute, Second:
|
||||
// Check if the time value matches. If yes, continue to the next field.
|
||||
// If the field doesn't match the schedule, then increment the field until it matches.
|
||||
// While incrementing the field, a wrap-around brings it back to the beginning
|
||||
// of the field list (since it is necessary to re-verify previous field
|
||||
// values)
|
||||
|
||||
// Convert the given time into the schedule's timezone, if one is specified.
|
||||
// Save the original timezone so we can convert back after we find a time.
|
||||
// Note that schedules without a time zone specified (time.Local) are treated
|
||||
// as local to the time provided.
|
||||
origLocation := t.Location() |
||||
loc := s.Location |
||||
if loc == time.Local { |
||||
loc = t.Location() |
||||
} |
||||
if s.Location != time.Local { |
||||
t = t.In(s.Location) |
||||
} |
||||
|
||||
// Start at the earliest possible time (the upcoming second).
|
||||
t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond) |
||||
|
||||
// This flag indicates whether a field has been incremented.
|
||||
added := false |
||||
|
||||
// If no time is found within five years, return zero.
|
||||
yearLimit := t.Year() + 5 |
||||
|
||||
WRAP: |
||||
if t.Year() > yearLimit { |
||||
return time.Time{} |
||||
} |
||||
|
||||
// Find the first applicable month.
|
||||
// If it's this month, then do nothing.
|
||||
for 1<<uint(t.Month())&s.Month == 0 { |
||||
// If we have to add a month, reset the other parts to 0.
|
||||
if !added { |
||||
added = true |
||||
// Otherwise, set the date at the beginning (since the current time is irrelevant).
|
||||
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc) |
||||
} |
||||
t = t.AddDate(0, 1, 0) |
||||
|
||||
// Wrapped around.
|
||||
if t.Month() == time.January { |
||||
goto WRAP |
||||
} |
||||
} |
||||
|
||||
// Now get a day in that month.
|
||||
//
|
||||
// NOTE: This causes issues for daylight savings regimes where midnight does
|
||||
// not exist. For example: Sao Paulo has DST that transforms midnight on
|
||||
// 11/3 into 1am. Handle that by noticing when the Hour ends up != 0.
|
||||
for !dayMatches(s, t) { |
||||
if !added { |
||||
added = true |
||||
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc) |
||||
} |
||||
t = t.AddDate(0, 0, 1) |
||||
// Notice if the hour is no longer midnight due to DST.
|
||||
// Add an hour if it's 23, subtract an hour if it's 1.
|
||||
if t.Hour() != 0 { |
||||
if t.Hour() > 12 { |
||||
t = t.Add(time.Duration(24-t.Hour()) * time.Hour) |
||||
} else { |
||||
t = t.Add(time.Duration(-t.Hour()) * time.Hour) |
||||
} |
||||
} |
||||
|
||||
if t.Day() == 1 { |
||||
goto WRAP |
||||
} |
||||
} |
||||
|
||||
for 1<<uint(t.Hour())&s.Hour == 0 { |
||||
if !added { |
||||
added = true |
||||
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, loc) |
||||
} |
||||
t = t.Add(1 * time.Hour) |
||||
|
||||
if t.Hour() == 0 { |
||||
goto WRAP |
||||
} |
||||
} |
||||
|
||||
for 1<<uint(t.Minute())&s.Minute == 0 { |
||||
if !added { |
||||
added = true |
||||
t = t.Truncate(time.Minute) |
||||
} |
||||
t = t.Add(1 * time.Minute) |
||||
|
||||
if t.Minute() == 0 { |
||||
goto WRAP |
||||
} |
||||
} |
||||
|
||||
for 1<<uint(t.Second())&s.Second == 0 { |
||||
if !added { |
||||
added = true |
||||
t = t.Truncate(time.Second) |
||||
} |
||||
t = t.Add(1 * time.Second) |
||||
|
||||
if t.Second() == 0 { |
||||
goto WRAP |
||||
} |
||||
} |
||||
|
||||
return t.In(origLocation) |
||||
} |
||||
|
||||
// dayMatches returns true if the schedule's day-of-week and day-of-month
|
||||
// restrictions are satisfied by the given time.
|
||||
func dayMatches(s *SpecSchedule, t time.Time) bool { |
||||
var ( |
||||
domMatch bool = 1<<uint(t.Day())&s.Dom > 0 |
||||
dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0 |
||||
) |
||||
if s.Dom&starBit > 0 || s.Dow&starBit > 0 { |
||||
return domMatch && dowMatch |
||||
} |
||||
return domMatch || dowMatch |
||||
} |
||||
Loading…
Reference in new issue