/* * 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 // Version: 1.7a1 // Date: 2011-06-18 // Author: Olivier PLATHEY // Port to Go: Kurt Jung, 2013-07-15 // Additions: Klaus Wendel, archium GmbH/Gera, 2021-03-02 import ( "bytes" "encoding/binary" "encoding/json" "fmt" "image" "image/color" "image/gif" "image/jpeg" "image/png" "io" "io/ioutil" "math" "os" "path" "sort" "strconv" "strings" "time" ) var gl struct { catalogSort bool noCompress bool // Initial zero value indicates compression creationDate time.Time modDate time.Time } type fmtBuffer struct { bytes.Buffer } func (b *fmtBuffer) printf(fmtStr string, args ...interface{}) { b.Buffer.WriteString(fmt.Sprintf(fmtStr, args...)) } func fpdfNew(orientationStr, unitStr, sizeStr, fontDirStr string, size SizeType, density float64) (f *Fpdf) { f = new(Fpdf) if orientationStr == "" { orientationStr = "p" } else { orientationStr = strings.ToLower(orientationStr) } if unitStr == "" { unitStr = "mm" } if sizeStr == "" { sizeStr = "A4" } if fontDirStr == "" { fontDirStr = "." } // Page density in dpi if density == 0.0 { density = 72.0 } f.page = 0 f.n = 2 f.pages = make([]*bytes.Buffer, 0, 8) f.pages = append(f.pages, bytes.NewBufferString("")) // pages[0] is unused (1-based) f.pageSizes = make(map[int]SizeType) f.pageBoxes = make(map[int]map[string]PageBox) f.defPageBoxes = make(map[string]PageBox) f.state = 0 f.fonts = make(map[string]fontDefType) f.fontFiles = make(map[string]fontFileType) f.diffs = make([]string, 0, 8) f.templates = make(map[string]Template) f.templateObjects = make(map[string]int) f.importedObjs = make(map[string][]byte, 0) f.importedObjPos = make(map[string]map[int]string, 0) f.importedTplObjs = make(map[string]string) f.importedTplIDs = make(map[string]int, 0) f.images = make(map[string]*ImageInfoType) f.pageLinks = make([][]linkType, 0, 8) f.pageLinks = append(f.pageLinks, make([]linkType, 0, 0)) // pageLinks[0] is unused (1-based) f.links = make([]intLinkType, 0, 8) f.links = append(f.links, intLinkType{}) // links[0] is unused (1-based) f.pageAttachments = make([][]annotationAttach, 0, 8) f.pageAttachments = append(f.pageAttachments, []annotationAttach{}) // f.aliasMap = make(map[string]string) f.inHeader = false f.inFooter = false f.lasth = 0 f.fontFamily = "" f.fontStyle = "" f.SetFontSize(12) f.underline = false f.strikeout = false f.setDrawColor(0, 0, 0) f.setFillColor(0, 0, 0) f.setTextColor(0, 0, 0) f.colorFlag = false f.ws = 0 f.fontpath = fontDirStr // Core fonts f.coreFonts = map[string]bool{ "courier": true, "helvetica": true, "times": true, "symbol": true, "zapfdingbats": true, } // Scale factor switch unitStr { case "pt", "point": f.k = 1.0 case "mm": f.k = density / 25.4 case "cm": f.k = density / 2.54 case "in", "inch": f.k = density default: f.err = fmt.Errorf("incorrect unit %s", unitStr) return } f.unitStr = unitStr // Page sizes f.stdPageSizes = make(map[string]SizeType) f.stdPageSizes["a3"] = SizeType{841.89, 1190.55} f.stdPageSizes["a4"] = SizeType{595.28, 841.89} f.stdPageSizes["a5"] = SizeType{420.94, 595.28} f.stdPageSizes["a6"] = SizeType{297.64, 420.94} f.stdPageSizes["a2"] = SizeType{1190.55, 1683.78} f.stdPageSizes["a1"] = SizeType{1683.78, 2383.94} f.stdPageSizes["letter"] = SizeType{612, 792} f.stdPageSizes["legal"] = SizeType{612, 1008} f.stdPageSizes["tabloid"] = SizeType{792, 1224} if size.Wd > 0 && size.Ht > 0 { f.defPageSize = size } else { f.defPageSize = f.getpagesizestr(sizeStr) if f.err != nil { return } } f.curPageSize = f.defPageSize // Page orientation switch orientationStr { case "p", "portrait": f.defOrientation = "P" f.w = f.defPageSize.Wd f.h = f.defPageSize.Ht // dbg("Assign h: %8.2f", f.h) case "l", "landscape": f.defOrientation = "L" f.w = f.defPageSize.Ht f.h = f.defPageSize.Wd default: f.err = fmt.Errorf("incorrect orientation: %s", orientationStr) return } f.curOrientation = f.defOrientation f.wPt = f.w * f.k f.hPt = f.h * f.k // Page margins (1 cm) margin := 28.35 / f.k f.SetMargins(margin, margin, margin) // Interior cell margin (1 mm) f.cMargin = margin / 10 // Line width (0.2 mm) f.lineWidth = 0.567 / f.k // Automatic page break f.SetAutoPageBreak(true, 2*margin) // Default display mode f.SetDisplayMode("default", "default") if f.err != nil { return } f.acceptPageBreak = func() bool { return f.autoPageBreak } // Enable compression f.SetCompression(!gl.noCompress) f.spotColorMap = make(map[string]spotColorType) f.blendList = make([]blendModeType, 0, 8) f.blendList = append(f.blendList, blendModeType{}) // blendList[0] is unused (1-based) f.blendMap = make(map[string]int) f.blendMode = "Normal" f.alpha = 1 f.gradientList = make([]gradientType, 0, 8) f.gradientList = append(f.gradientList, gradientType{}) // gradientList[0] is unused // Set default PDF version number f.pdfVersion = "1.3" f.SetProducer("FPDF "+cnFpdfVersion, true) f.layerInit() f.catalogSort = gl.catalogSort f.creationDate = gl.creationDate f.modDate = gl.modDate f.userUnderlineThickness = 1 return } // NewCustom returns a pointer to a new Fpdf instance. Its methods are // subsequently called to produce a single PDF document. NewCustom() is an // alternative to New() that provides additional customization. The PageSize() // example demonstrates this method. func NewCustom(init *InitType) (f *Fpdf) { return fpdfNew(init.OrientationStr, init.UnitStr, init.SizeStr, init.FontDirStr, init.Size, init.Density) } // New returns a pointer to a new Fpdf instance. Its methods are subsequently // called to produce a single PDF document. // // orientationStr specifies the default page orientation. For portrait mode, // specify "P" or "Portrait". For landscape mode, specify "L" or "Landscape". // An empty string will be replaced with "P". // // unitStr specifies the unit of length used in size parameters for elements // other than fonts, which are always measured in points. Specify "pt" for // point, "mm" for millimeter, "cm" for centimeter, or "in" for inch. An empty // string will be replaced with "mm". // // sizeStr specifies the page size. Acceptable values are "A3", "A4", "A5", // "Letter", "Legal", or "Tabloid". An empty string will be replaced with "A4". // // fontDirStr specifies the file system location in which font resources will // be found. An empty string is replaced with ".". This argument only needs to // reference an actual directory if a font other than one of the core // fonts is used. The core fonts are "courier", "helvetica" (also called // "arial"), "times", and "zapfdingbats" (also called "symbol"). func New(orientationStr, unitStr, sizeStr, fontDirStr string, density float64) (f *Fpdf) { return fpdfNew(orientationStr, unitStr, sizeStr, fontDirStr, SizeType{0, 0}, density) } // Ok returns true if no processing errors have occurred. func (f *Fpdf) Ok() bool { return f.err == nil } // Err returns true if a processing error has occurred. func (f *Fpdf) Err() bool { return f.err != nil } // ClearError unsets the internal Fpdf error. This method should be used with // care, as an internal error condition usually indicates an unrecoverable // problem with the generation of a document. It is intended to deal with cases // in which an error is used to select an alternate form of the document. func (f *Fpdf) ClearError() { f.err = nil } // SetErrorf sets the internal Fpdf error with formatted text to halt PDF // generation; this may facilitate error handling by application. If an error // condition is already set, this call is ignored. // // See the documentation for printing in the standard fmt package for details // about fmtStr and args. func (f *Fpdf) SetErrorf(fmtStr string, args ...interface{}) { if f.err == nil { f.err = fmt.Errorf(fmtStr, args...) } } // String satisfies the fmt.Stringer interface and summarizes the Fpdf // instance. func (f *Fpdf) String() string { return "Fpdf " + cnFpdfVersion } // SetError sets an error to halt PDF generation. This may facilitate error // handling by application. See also Ok(), Err() and Error(). func (f *Fpdf) SetError(err error) { if f.err == nil && err != nil { f.err = err } } // Error returns the internal Fpdf error; this will be nil if no error has occurred. func (f *Fpdf) Error() error { return f.err } // GetPageSize returns the current page's width and height. This is the paper's // size. To compute the size of the area being used, subtract the margins (see // GetMargins()). func (f *Fpdf) GetPageSize() (width, height float64) { width = f.w height = f.h return } // GetMargins returns the left, top, right, and bottom margins. The first three // are set with the SetMargins() method. The bottom margin is set with the // SetAutoPageBreak() method. func (f *Fpdf) GetMargins() (left, top, right, bottom float64) { left = f.lMargin top = f.tMargin right = f.rMargin bottom = f.bMargin return } // SetMargins defines the left, top and right margins. By default, they equal 1 // cm. Call this method to change them. If the value of the right margin is // less than zero, it is set to the same as the left margin. func (f *Fpdf) SetMargins(left, top, right float64) { f.lMargin = left f.tMargin = top if right < 0 { right = left } f.rMargin = right } // SetLeftMargin defines the left margin. The method can be called before // creating the first page. If the current abscissa gets out of page, it is // brought back to the margin. func (f *Fpdf) SetLeftMargin(margin float64) { f.lMargin = margin if f.page > 0 && f.x < margin { f.x = margin } } // GetCellMargin returns the cell margin. This is the amount of space before // and after the text within a cell that's left blank, and is in units passed // to New(). It defaults to 1mm. func (f *Fpdf) GetCellMargin() float64 { return f.cMargin } // SetCellMargin sets the cell margin. This is the amount of space before and // after the text within a cell that's left blank, and is in units passed to // New(). func (f *Fpdf) SetCellMargin(margin float64) { f.cMargin = margin } // SetPageBoxRec sets the page box for the current page, and any following // pages. Allowable types are trim, trimbox, crop, cropbox, bleed, bleedbox, // art and artbox box types are case insensitive. See SetPageBox() for a method // that specifies the coordinates and extent of the page box individually. func (f *Fpdf) SetPageBoxRec(t string, pb PageBox) { switch strings.ToLower(t) { case "trim": fallthrough case "trimbox": t = "TrimBox" case "crop": fallthrough case "cropbox": t = "CropBox" case "bleed": fallthrough case "bleedbox": t = "BleedBox" case "art": fallthrough case "artbox": t = "ArtBox" default: f.err = fmt.Errorf("%s is not a valid page box type", t) return } pb.X = pb.X * f.k pb.Y = pb.Y * f.k pb.Wd = (pb.Wd * f.k) + pb.X pb.Ht = (pb.Ht * f.k) + pb.Y if f.page > 0 { f.pageBoxes[f.page][t] = pb } // always override. page defaults are supplied in addPage function f.defPageBoxes[t] = pb } // SetPageBox sets the page box for the current page, and any following pages. // Allowable types are trim, trimbox, crop, cropbox, bleed, bleedbox, art and // artbox box types are case insensitive. func (f *Fpdf) SetPageBox(t string, x, y, wd, ht float64) { f.SetPageBoxRec(t, PageBox{SizeType{Wd: wd, Ht: ht}, PointType{X: x, Y: y}}) } // SetPage sets the current page to that of a valid page in the PDF document. // pageNum is one-based. The SetPage() example demonstrates this method. func (f *Fpdf) SetPage(pageNum int) { if (pageNum > 0) && (pageNum < len(f.pages)) { f.page = pageNum } } // PageCount returns the number of pages currently in the document. Since page // numbers in gofpdf are one-based, the page count is the same as the page // number of the current last page. func (f *Fpdf) PageCount() int { return len(f.pages) - 1 } // SetFontLocation sets the location in the file system of the font and font // definition files. func (f *Fpdf) SetFontLocation(fontDirStr string) { f.fontpath = fontDirStr } // SetFontLoader sets a loader used to read font files (.json and .z) from an // arbitrary source. If a font loader has been specified, it is used to load // the named font resources when AddFont() is called. If this operation fails, // an attempt is made to load the resources from the configured font directory // (see SetFontLocation()). func (f *Fpdf) SetFontLoader(loader FontLoader) { f.fontLoader = loader } // SetHeaderFuncMode sets the function that lets the application render the // page header. See SetHeaderFunc() for more details. The value for homeMode // should be set to true to have the current position set to the left and top // margin after the header function is called. func (f *Fpdf) SetHeaderFuncMode(fnc func(), homeMode bool) { f.headerFnc = fnc f.headerHomeMode = homeMode } // SetHeaderFunc sets the function that lets the application render the page // header. The specified function is automatically called by AddPage() and // should not be called directly by the application. The implementation in Fpdf // is empty, so you have to provide an appropriate function if you want page // headers. fnc will typically be a closure that has access to the Fpdf // instance and other document generation variables. // // A header is a convenient place to put background content that repeats on // each page such as a watermark. When this is done, remember to reset the X // and Y values so the normal content begins where expected. Including a // watermark on each page is demonstrated in the example for TransformRotate. // // This method is demonstrated in the example for AddPage(). func (f *Fpdf) SetHeaderFunc(fnc func()) { f.headerFnc = fnc } // SetFooterFunc sets the function that lets the application render the page // footer. The specified function is automatically called by AddPage() and // Close() and should not be called directly by the application. The // implementation in Fpdf is empty, so you have to provide an appropriate // function if you want page footers. fnc will typically be a closure that has // access to the Fpdf instance and other document generation variables. See // SetFooterFuncLpi for a similar function that passes a last page indicator. // // This method is demonstrated in the example for AddPage(). func (f *Fpdf) SetFooterFunc(fnc func()) { f.footerFnc = fnc f.footerFncLpi = nil } // SetFooterFuncLpi sets the function that lets the application render the page // footer. The specified function is automatically called by AddPage() and // Close() and should not be called directly by the application. It is passed a // boolean that is true if the last page of the document is being rendered. The // implementation in Fpdf is empty, so you have to provide an appropriate // function if you want page footers. fnc will typically be a closure that has // access to the Fpdf instance and other document generation variables. func (f *Fpdf) SetFooterFuncLpi(fnc func(lastPage bool)) { f.footerFncLpi = fnc f.footerFnc = nil } // SetTopMargin defines the top margin. The method can be called before // creating the first page. func (f *Fpdf) SetTopMargin(margin float64) { f.tMargin = margin } // SetRightMargin defines the right margin. The method can be called before // creating the first page. func (f *Fpdf) SetRightMargin(margin float64) { f.rMargin = margin } // GetAutoPageBreak returns true if automatic pages breaks are enabled, false // otherwise. This is followed by the triggering limit from the bottom of the // page. This value applies only if automatic page breaks are enabled. func (f *Fpdf) GetAutoPageBreak() (auto bool, margin float64) { auto = f.autoPageBreak margin = f.bMargin return } // SetAutoPageBreak enables or disables the automatic page breaking mode. When // enabling, the second parameter is the distance from the bottom of the page // that defines the triggering limit. By default, the mode is on and the margin // is 2 cm. func (f *Fpdf) SetAutoPageBreak(auto bool, margin float64) { f.autoPageBreak = auto f.bMargin = margin f.pageBreakTrigger = f.h - margin } // SetDisplayMode sets advisory display directives for the document viewer. // Pages can be displayed entirely on screen, occupy the full width of the // window, use real size, be scaled by a specific zooming factor or use viewer // default (configured in the Preferences menu of Adobe Reader). The page // layout can be specified so that pages are displayed individually or in // pairs. // // zoomStr can be "fullpage" to display the entire page on screen, "fullwidth" // to use maximum width of window, "real" to use real size (equivalent to 100% // zoom) or "default" to use viewer default mode. // // layoutStr can be "single" (or "SinglePage") to display one page at once, // "continuous" (or "OneColumn") to display pages continuously, "two" (or // "TwoColumnLeft") to display two pages on two columns with odd-numbered pages // on the left, or "TwoColumnRight" to display two pages on two columns with // odd-numbered pages on the right, or "TwoPageLeft" to display pages two at a // time with odd-numbered pages on the left, or "TwoPageRight" to display pages // two at a time with odd-numbered pages on the right, or "default" to use // viewer default mode. func (f *Fpdf) SetDisplayMode(zoomStr, layoutStr string) { if f.err != nil { return } if layoutStr == "" { layoutStr = "default" } switch zoomStr { case "fullpage", "fullwidth", "real", "default": f.zoomMode = zoomStr default: f.err = fmt.Errorf("incorrect zoom display mode: %s", zoomStr) return } switch layoutStr { case "single", "continuous", "two", "default", "SinglePage", "OneColumn", "TwoColumnLeft", "TwoColumnRight", "TwoPageLeft", "TwoPageRight": f.layoutMode = layoutStr default: f.err = fmt.Errorf("incorrect layout display mode: %s", layoutStr) return } } // SetDefaultCompression controls the default setting of the internal // compression flag. See SetCompression() for more details. Compression is on // by default. func SetDefaultCompression(compress bool) { gl.noCompress = !compress } // SetCompression activates or deactivates page compression with zlib. When // activated, the internal representation of each page is compressed, which // leads to a compression ratio of about 2 for the resulting document. // Compression is on by default. func (f *Fpdf) SetCompression(compress bool) { f.compress = compress } // SetProducer defines the producer of the document. isUTF8 indicates if the string // is encoded in ISO-8859-1 (false) or UTF-8 (true). func (f *Fpdf) SetProducer(producerStr string, isUTF8 bool) { if isUTF8 { producerStr = utf8toutf16(producerStr) } f.producer = producerStr } // SetTitle defines the title of the document. isUTF8 indicates if the string // is encoded in ISO-8859-1 (false) or UTF-8 (true). func (f *Fpdf) SetTitle(titleStr string, isUTF8 bool) { if isUTF8 { titleStr = utf8toutf16(titleStr) } f.title = titleStr } // SetSubject defines the subject of the document. isUTF8 indicates if the // string is encoded in ISO-8859-1 (false) or UTF-8 (true). func (f *Fpdf) SetSubject(subjectStr string, isUTF8 bool) { if isUTF8 { subjectStr = utf8toutf16(subjectStr) } f.subject = subjectStr } // SetAuthor defines the author of the document. isUTF8 indicates if the string // is encoded in ISO-8859-1 (false) or UTF-8 (true). func (f *Fpdf) SetAuthor(authorStr string, isUTF8 bool) { if isUTF8 { authorStr = utf8toutf16(authorStr) } f.author = authorStr } // SetKeywords defines the keywords of the document. keywordStr is a // space-delimited string, for example "invoice August". isUTF8 indicates if // the string is encoded func (f *Fpdf) SetKeywords(keywordsStr string, isUTF8 bool) { if isUTF8 { keywordsStr = utf8toutf16(keywordsStr) } f.keywords = keywordsStr } // SetCreator defines the creator of the document. isUTF8 indicates if the // string is encoded in ISO-8859-1 (false) or UTF-8 (true). func (f *Fpdf) SetCreator(creatorStr string, isUTF8 bool) { if isUTF8 { creatorStr = utf8toutf16(creatorStr) } f.creator = creatorStr } // SetXmpMetadata defines XMP metadata that will be embedded with the document. func (f *Fpdf) SetXmpMetadata(xmpStream []byte) { f.xmp = xmpStream } // AliasNbPages defines an alias for the total number of pages. It will be // substituted as the document is closed. An empty string is replaced with the // string "{nb}". // // See the example for AddPage() for a demonstration of this method. func (f *Fpdf) AliasNbPages(aliasStr string) { if aliasStr == "" { aliasStr = "{nb}" } f.aliasNbPagesStr = aliasStr } // RTL enables right-to-left mode func (f *Fpdf) RTL() { f.isRTL = true } // LTR disables right-to-left mode func (f *Fpdf) LTR() { f.isRTL = false } // open begins a document func (f *Fpdf) open() { f.state = 1 } // Close terminates the PDF document. It is not necessary to call this method // explicitly because Output(), OutputAndClose() and OutputFileAndClose() do it // automatically. If the document contains no page, AddPage() is called to // prevent the generation of an invalid document. func (f *Fpdf) Close() { if f.err == nil { if f.clipNest > 0 { f.err = fmt.Errorf("clip procedure must be explicitly ended") } else if f.transformNest > 0 { f.err = fmt.Errorf("transformation procedure must be explicitly ended") } } if f.err != nil { return } if f.state == 3 { return } if f.page == 0 { f.AddPage() if f.err != nil { return } } // Page footer f.inFooter = true if f.footerFnc != nil { f.footerFnc() } else if f.footerFncLpi != nil { f.footerFncLpi(true) } f.inFooter = false // Close page f.endpage() // Close document f.enddoc() return } // PageSize returns the width and height of the specified page in the units // established in New(). These return values are followed by the unit of // measure itself. If pageNum is zero or otherwise out of bounds, it returns // the default page size, that is, the size of the page that would be added by // AddPage(). func (f *Fpdf) PageSize(pageNum int) (wd, ht float64, unitStr string) { sz, ok := f.pageSizes[pageNum] if ok { sz.Wd, sz.Ht = sz.Wd/f.k, sz.Ht/f.k } else { sz = f.defPageSize // user units } return sz.Wd, sz.Ht, f.unitStr } // AddPageFormat adds a new page with non-default orientation or size. See // AddPage() for more details. // // See New() for a description of orientationStr. // // size specifies the size of the new page in the units established in New(). // // The PageSize() example demonstrates this method. func (f *Fpdf) AddPageFormat(orientationStr string, size SizeType) { if f.err != nil { return } if f.page != len(f.pages)-1 { f.page = len(f.pages) - 1 } if f.state == 0 { f.open() } familyStr := f.fontFamily style := f.fontStyle if f.underline { style += "U" } if f.strikeout { style += "S" } fontsize := f.fontSizePt lw := f.lineWidth dc := f.color.draw fc := f.color.fill tc := f.color.text cf := f.colorFlag if f.page > 0 { f.inFooter = true // Page footer avoid double call on footer. if f.footerFnc != nil { f.footerFnc() } else if f.footerFncLpi != nil { f.footerFncLpi(false) // not last page. } f.inFooter = false // Close page f.endpage() } // Start new page f.beginpage(orientationStr, size) // Set line cap style to current value // f.out("2 J") f.outf("%d J", f.capStyle) // Set line join style to current value f.outf("%d j", f.joinStyle) // Set line width f.lineWidth = lw f.outf("%.2f w", lw*f.k) // Set dash pattern if len(f.dashArray) > 0 { f.outputDashPattern() } // Set font if familyStr != "" { f.SetFont(familyStr, style, fontsize) if f.err != nil { return } } // Set colors f.color.draw = dc if dc.str != "0 G" { f.out(dc.str) } f.color.fill = fc if fc.str != "0 g" { f.out(fc.str) } f.color.text = tc f.colorFlag = cf // Page header if f.headerFnc != nil { f.inHeader = true f.headerFnc() f.inHeader = false if f.headerHomeMode { f.SetHomeXY() } } // Restore line width if f.lineWidth != lw { f.lineWidth = lw f.outf("%.2f w", lw*f.k) } // Restore font if familyStr != "" { f.SetFont(familyStr, style, fontsize) if f.err != nil { return } } // Restore colors if f.color.draw.str != dc.str { f.color.draw = dc f.out(dc.str) } if f.color.fill.str != fc.str { f.color.fill = fc f.out(fc.str) } f.color.text = tc f.colorFlag = cf return } // AddPage adds a new page to the document. If a page is already present, the // Footer() method is called first to output the footer. Then the page is // added, the current position set to the top-left corner according to the left // and top margins, and Header() is called to display the header. // // The font which was set before calling is automatically restored. There is no // need to call SetFont() again if you want to continue with the same font. The // same is true for colors and line width. // // The origin of the coordinate system is at the top-left corner and increasing // ordinates go downwards. // // See AddPageFormat() for a version of this method that allows the page size // and orientation to be different than the default. func (f *Fpdf) AddPage() { if f.err != nil { return } // dbg("AddPage") f.AddPageFormat(f.defOrientation, f.defPageSize) return } // PageNo returns the current page number. // // See the example for AddPage() for a demonstration of this method. func (f *Fpdf) PageNo() int { return f.page } func colorComp(v int) (int, float64) { if v < 0 { v = 0 } else if v > 255 { v = 255 } return v, float64(v) / 255.0 } func rgbColorValue(r, g, b int, grayStr, fullStr string) (clr colorType) { clr.ir, clr.r = colorComp(r) clr.ig, clr.g = colorComp(g) clr.ib, clr.b = colorComp(b) clr.mode = colorModeRGB clr.gray = clr.ir == clr.ig && clr.r == clr.b if len(grayStr) > 0 { if clr.gray { clr.str = sprintf("%.3f %s", clr.r, grayStr) } else { clr.str = sprintf("%.3f %.3f %.3f %s", clr.r, clr.g, clr.b, fullStr) } } else { clr.str = sprintf("%.3f %.3f %.3f", clr.r, clr.g, clr.b) } return } // SetDrawColor defines the color used for all drawing operations (lines, // rectangles and cell borders). It is expressed in RGB components (0 - 255). // The method can be called before the first page is created. The value is // retained from page to page. func (f *Fpdf) SetDrawColor(r, g, b int) { f.setDrawColor(r, g, b) } func (f *Fpdf) setDrawColor(r, g, b int) { f.color.draw = rgbColorValue(r, g, b, "G", "RG") if f.page > 0 { f.out(f.color.draw.str) } } // GetDrawColor returns the most recently set draw color as RGB components (0 - // 255). This will not be the current value if a draw color of some other type // (for example, spot) has been more recently set. func (f *Fpdf) GetDrawColor() (int, int, int) { return f.color.draw.ir, f.color.draw.ig, f.color.draw.ib } // SetFillColor defines the color used for all filling operations (filled // rectangles and cell backgrounds). It is expressed in RGB components (0 // -255). The method can be called before the first page is created and the // value is retained from page to page. func (f *Fpdf) SetFillColor(r, g, b int) { f.setFillColor(r, g, b) } func (f *Fpdf) setFillColor(r, g, b int) { f.color.fill = rgbColorValue(r, g, b, "g", "rg") f.colorFlag = f.color.fill.str != f.color.text.str if f.page > 0 { f.out(f.color.fill.str) } } // GetFillColor returns the most recently set fill color as RGB components (0 - // 255). This will not be the current value if a fill color of some other type // (for example, spot) has been more recently set. func (f *Fpdf) GetFillColor() (int, int, int) { return f.color.fill.ir, f.color.fill.ig, f.color.fill.ib } // SetTextColor defines the color used for text. It is expressed in RGB // components (0 - 255). The method can be called before the first page is // created. The value is retained from page to page. func (f *Fpdf) SetTextColor(r, g, b int) { f.setTextColor(r, g, b) } func (f *Fpdf) setTextColor(r, g, b int) { f.color.text = rgbColorValue(r, g, b, "g", "rg") f.colorFlag = f.color.fill.str != f.color.text.str } // GetTextColor returns the most recently set text color as RGB components (0 - // 255). This will not be the current value if a text color of some other type // (for example, spot) has been more recently set. func (f *Fpdf) GetTextColor() (int, int, int) { return f.color.text.ir, f.color.text.ig, f.color.text.ib } // GetStringWidth returns the length of a string in user units. A font must be // currently selected. func (f *Fpdf) GetStringWidth(s string) float64 { if f.err != nil { return 0 } w := f.GetStringSymbolWidth(s) return float64(w) * f.fontSize / 1000 } // GetStringSymbolWidth returns the length of a string in glyf units. A font must be // currently selected. func (f *Fpdf) GetStringSymbolWidth(s string) int { if f.err != nil { return 0 } w := 0 if f.isCurrentUTF8 { unicode := []rune(s) for _, char := range unicode { intChar := int(char) if len(f.currentFont.Cw) >= intChar && f.currentFont.Cw[intChar] > 0 { if f.currentFont.Cw[intChar] != 65535 { w += f.currentFont.Cw[intChar] } } else if f.currentFont.Desc.MissingWidth != 0 { w += f.currentFont.Desc.MissingWidth } else { w += 500 } } } else { for _, ch := range []byte(s) { if ch == 0 { break } w += f.currentFont.Cw[ch] } } return w } // SetLineWidth defines the line width. By default, the value equals 0.2 mm. // The method can be called before the first page is created. The value is // retained from page to page. func (f *Fpdf) SetLineWidth(width float64) { f.setLineWidth(width) } func (f *Fpdf) setLineWidth(width float64) { f.lineWidth = width if f.page > 0 { f.outf("%.2f w", width*f.k) } } // GetLineWidth returns the current line thickness. func (f *Fpdf) GetLineWidth() float64 { return f.lineWidth } // SetLineCapStyle defines the line cap style. styleStr should be "butt", // "round" or "square". A square style projects from the end of the line. The // method can be called before the first page is created. The value is // retained from page to page. func (f *Fpdf) SetLineCapStyle(styleStr string) { var capStyle int switch styleStr { case "round": capStyle = 1 case "square": capStyle = 2 default: capStyle = 0 } f.capStyle = capStyle if f.page > 0 { f.outf("%d J", f.capStyle) } } // SetLineJoinStyle defines the line cap style. styleStr should be "miter", // "round" or "bevel". The method can be called before the first page // is created. The value is retained from page to page. func (f *Fpdf) SetLineJoinStyle(styleStr string) { var joinStyle int switch styleStr { case "round": joinStyle = 1 case "bevel": joinStyle = 2 default: joinStyle = 0 } f.joinStyle = joinStyle if f.page > 0 { f.outf("%d j", f.joinStyle) } } // SetDashPattern sets the dash pattern that is used to draw lines. The // dashArray elements are numbers that specify the lengths, in units // established in New(), of alternating dashes and gaps. The dash phase // specifies the distance into the dash pattern at which to start the dash. The // dash pattern is retained from page to page. Call this method with an empty // array to restore solid line drawing. // // The Beziergon() example demonstrates this method. func (f *Fpdf) SetDashPattern(dashArray []float64, dashPhase float64) { scaled := make([]float64, len(dashArray)) for i, value := range dashArray { scaled[i] = value * f.k } dashPhase *= f.k f.dashArray = scaled f.dashPhase = dashPhase if f.page > 0 { f.outputDashPattern() } } func (f *Fpdf) outputDashPattern() { var buf bytes.Buffer buf.WriteByte('[') for i, value := range f.dashArray { if i > 0 { buf.WriteByte(' ') } buf.WriteString(strconv.FormatFloat(value, 'f', 2, 64)) } buf.WriteString("] ") buf.WriteString(strconv.FormatFloat(f.dashPhase, 'f', 2, 64)) buf.WriteString(" d") f.outbuf(&buf) } // Line draws a line between points (x1, y1) and (x2, y2) using the current // draw color, line width and cap style. func (f *Fpdf) Line(x1, y1, x2, y2 float64) { f.outf("%.2f %.2f m %.2f %.2f l S", x1*f.k, (f.h-y1)*f.k, x2*f.k, (f.h-y2)*f.k) } // fillDrawOp corrects path painting operators func fillDrawOp(styleStr string) (opStr string) { switch strings.ToUpper(styleStr) { case "", "D": // Stroke the path. opStr = "S" case "F": // fill the path, using the nonzero winding number rule opStr = "f" case "F*": // fill the path, using the even-odd rule opStr = "f*" case "FD", "DF": // fill and then stroke the path, using the nonzero winding number rule opStr = "B" case "FD*", "DF*": // fill and then stroke the path, using the even-odd rule opStr = "B*" default: opStr = styleStr } return } // Rect outputs a rectangle of width w and height h with the upper left corner // positioned at point (x, y). // // It can be drawn (border only), filled (with no border) or both. styleStr can // be "F" for filled, "D" for outlined only, or "DF" or "FD" for outlined and // filled. An empty string will be replaced with "D". Drawing uses the current // draw color and line width centered on the rectangle's perimeter. Filling // uses the current fill color. func (f *Fpdf) Rect(x, y, w, h float64, styleStr string) { f.outf("%.2f %.2f %.2f %.2f re %s", x*f.k, (f.h-y)*f.k, w*f.k, -h*f.k, fillDrawOp(styleStr)) } // RoundedRect outputs a rectangle of width w and height h with the upper left // corner positioned at point (x, y). It can be drawn (border only), filled // (with no border) or both. styleStr can be "F" for filled, "D" for outlined // only, or "DF" or "FD" for outlined and filled. An empty string will be // replaced with "D". Drawing uses the current draw color and line width // centered on the rectangle's perimeter. Filling uses the current fill color. // The rounded corners of the rectangle are specified by radius r. corners is a // string that includes "1" to round the upper left corner, "2" to round the // upper right corner, "3" to round the lower right corner, and "4" to round // the lower left corner. The RoundedRect example demonstrates this method. func (f *Fpdf) RoundedRect(x, y, w, h, r float64, corners string, stylestr string) { // This routine was adapted by Brigham Thompson from a script by Christophe Prugnaud var rTL, rTR, rBR, rBL float64 // zero means no rounded corner if strings.Contains(corners, "1") { rTL = r } if strings.Contains(corners, "2") { rTR = r } if strings.Contains(corners, "3") { rBR = r } if strings.Contains(corners, "4") { rBL = r } f.RoundedRectExt(x, y, w, h, rTL, rTR, rBR, rBL, stylestr) } // RoundedRectExt behaves the same as RoundedRect() but supports a different // radius for each corner. A zero radius means squared corner. See // RoundedRect() for more details. This method is demonstrated in the // RoundedRect() example. func (f *Fpdf) RoundedRectExt(x, y, w, h, rTL, rTR, rBR, rBL float64, stylestr string) { f.roundedRectPath(x, y, w, h, rTL, rTR, rBR, rBL) f.out(fillDrawOp(stylestr)) } // Circle draws a circle centered on point (x, y) with radius r. // // styleStr can be "F" for filled, "D" for outlined only, or "DF" or "FD" for // outlined and filled. An empty string will be replaced with "D". Drawing uses // the current draw color and line width centered on the circle's perimeter. // Filling uses the current fill color. func (f *Fpdf) Circle(x, y, r float64, styleStr string) { f.Ellipse(x, y, r, r, 0, styleStr) } // Ellipse draws an ellipse centered at point (x, y). rx and ry specify its // horizontal and vertical radii. // // degRotate specifies the counter-clockwise angle in degrees that the ellipse // will be rotated. // // styleStr can be "F" for filled, "D" for outlined only, or "DF" or "FD" for // outlined and filled. An empty string will be replaced with "D". Drawing uses // the current draw color and line width centered on the ellipse's perimeter. // Filling uses the current fill color. // // The Circle() example demonstrates this method. func (f *Fpdf) Ellipse(x, y, rx, ry, degRotate float64, styleStr string) { f.arc(x, y, rx, ry, degRotate, 0, 360, styleStr, false) } // Polygon draws a closed figure defined by a series of vertices specified by // points. The x and y fields of the points use the units established in New(). // The last point in the slice will be implicitly joined to the first to close // the polygon. // // styleStr can be "F" for filled, "D" for outlined only, or "DF" or "FD" for // outlined and filled. An empty string will be replaced with "D". Drawing uses // the current draw color and line width centered on the ellipse's perimeter. // Filling uses the current fill color. func (f *Fpdf) Polygon(points []PointType, styleStr string) { if len(points) > 2 { for j, pt := range points { if j == 0 { f.point(pt.X, pt.Y) } else { f.outf("%.5f %.5f l ", pt.X*f.k, (f.h-pt.Y)*f.k) } } f.outf("%.5f %.5f l ", points[0].X*f.k, (f.h-points[0].Y)*f.k) f.DrawPath(styleStr) } } // Beziergon draws a closed figure defined by a series of cubic Bézier curve // segments. The first point in the slice defines the starting point of the // figure. Each three following points p1, p2, p3 represent a curve segment to // the point p3 using p1 and p2 as the Bézier control points. // // The x and y fields of the points use the units established in New(). // // styleStr can be "F" for filled, "D" for outlined only, or "DF" or "FD" for // outlined and filled. An empty string will be replaced with "D". Drawing uses // the current draw color and line width centered on the ellipse's perimeter. // Filling uses the current fill color. func (f *Fpdf) Beziergon(points []PointType, styleStr string) { // Thanks, Robert Lillack, for contributing this function. if len(points) < 4 { return } f.point(points[0].XY()) points = points[1:] for len(points) >= 3 { cx0, cy0 := points[0].XY() cx1, cy1 := points[1].XY() x1, y1 := points[2].XY() f.curve(cx0, cy0, cx1, cy1, x1, y1) points = points[3:] } f.DrawPath(styleStr) } // point outputs current point func (f *Fpdf) point(x, y float64) { f.outf("%.2f %.2f m", x*f.k, (f.h-y)*f.k) } // curve outputs a single cubic Bézier curve segment from current point func (f *Fpdf) curve(cx0, cy0, cx1, cy1, x, y float64) { // Thanks, Robert Lillack, for straightening this out f.outf("%.5f %.5f %.5f %.5f %.5f %.5f c", cx0*f.k, (f.h-cy0)*f.k, cx1*f.k, (f.h-cy1)*f.k, x*f.k, (f.h-y)*f.k) } // Curve draws a single-segment quadratic Bézier curve. The curve starts at // the point (x0, y0) and ends at the point (x1, y1). The control point (cx, // cy) specifies the curvature. At the start point, the curve is tangent to the // straight line between the start point and the control point. At the end // point, the curve is tangent to the straight line between the end point and // the control point. // // styleStr can be "F" for filled, "D" for outlined only, or "DF" or "FD" for // outlined and filled. An empty string will be replaced with "D". Drawing uses // the current draw color, line width, and cap style centered on the curve's // path. Filling uses the current fill color. // // The Circle() example demonstrates this method. func (f *Fpdf) Curve(x0, y0, cx, cy, x1, y1 float64, styleStr string) { f.point(x0, y0) f.outf("%.5f %.5f %.5f %.5f v %s", cx*f.k, (f.h-cy)*f.k, x1*f.k, (f.h-y1)*f.k, fillDrawOp(styleStr)) } // CurveCubic draws a single-segment cubic Bézier curve. This routine performs // the same function as CurveBezierCubic() but has a nonstandard argument order. // It is retained to preserve backward compatibility. func (f *Fpdf) CurveCubic(x0, y0, cx0, cy0, x1, y1, cx1, cy1 float64, styleStr string) { // f.point(x0, y0) // f.outf("%.5f %.5f %.5f %.5f %.5f %.5f c %s", cx0*f.k, (f.h-cy0)*f.k, // cx1*f.k, (f.h-cy1)*f.k, x1*f.k, (f.h-y1)*f.k, fillDrawOp(styleStr)) f.CurveBezierCubic(x0, y0, cx0, cy0, cx1, cy1, x1, y1, styleStr) } // CurveBezierCubic draws a single-segment cubic Bézier curve. The curve starts at // the point (x0, y0) and ends at the point (x1, y1). The control points (cx0, // cy0) and (cx1, cy1) specify the curvature. At the start point, the curve is // tangent to the straight line between the start point and the control point // (cx0, cy0). At the end point, the curve is tangent to the straight line // between the end point and the control point (cx1, cy1). // // styleStr can be "F" for filled, "D" for outlined only, or "DF" or "FD" for // outlined and filled. An empty string will be replaced with "D". Drawing uses // the current draw color, line width, and cap style centered on the curve's // path. Filling uses the current fill color. // // This routine performs the same function as CurveCubic() but uses standard // argument order. // // The Circle() example demonstrates this method. func (f *Fpdf) CurveBezierCubic(x0, y0, cx0, cy0, cx1, cy1, x1, y1 float64, styleStr string) { f.point(x0, y0) f.outf("%.5f %.5f %.5f %.5f %.5f %.5f c %s", cx0*f.k, (f.h-cy0)*f.k, cx1*f.k, (f.h-cy1)*f.k, x1*f.k, (f.h-y1)*f.k, fillDrawOp(styleStr)) } // Arc draws an elliptical arc centered at point (x, y). rx and ry specify its // horizontal and vertical radii. // // degRotate specifies the angle that the arc will be rotated. degStart and // degEnd specify the starting and ending angle of the arc. All angles are // specified in degrees and measured counter-clockwise from the 3 o'clock // position. // // styleStr can be "F" for filled, "D" for outlined only, or "DF" or "FD" for // outlined and filled. An empty string will be replaced with "D". Drawing uses // the current draw color, line width, and cap style centered on the arc's // path. Filling uses the current fill color. // // The Circle() example demonstrates this method. func (f *Fpdf) Arc(x, y, rx, ry, degRotate, degStart, degEnd float64, styleStr string) { f.arc(x, y, rx, ry, degRotate, degStart, degEnd, styleStr, false) } // GetAlpha returns the alpha blending channel, which consists of the // alpha transparency value and the blend mode. See SetAlpha for more // details. func (f *Fpdf) GetAlpha() (alpha float64, blendModeStr string) { return f.alpha, f.blendMode } // SetAlpha sets the alpha blending channel. The blending effect applies to // text, drawings and images. // // alpha must be a value between 0.0 (fully transparent) to 1.0 (fully opaque). // Values outside of this range result in an error. // // blendModeStr must be one of "Normal", "Multiply", "Screen", "Overlay", // "Darken", "Lighten", "ColorDodge", "ColorBurn","HardLight", "SoftLight", // "Difference", "Exclusion", "Hue", "Saturation", "Color", or "Luminosity". An // empty string is replaced with "Normal". // // To reset normal rendering after applying a blending mode, call this method // with alpha set to 1.0 and blendModeStr set to "Normal". func (f *Fpdf) SetAlpha(alpha float64, blendModeStr string) { if f.err != nil { return } var bl blendModeType switch blendModeStr { case "Normal", "Multiply", "Screen", "Overlay", "Darken", "Lighten", "ColorDodge", "ColorBurn", "HardLight", "SoftLight", "Difference", "Exclusion", "Hue", "Saturation", "Color", "Luminosity": bl.modeStr = blendModeStr case "": bl.modeStr = "Normal" default: f.err = fmt.Errorf("unrecognized blend mode \"%s\"", blendModeStr) return } if alpha < 0.0 || alpha > 1.0 { f.err = fmt.Errorf("alpha value (0.0 - 1.0) is out of range: %.3f", alpha) return } f.alpha = alpha f.blendMode = blendModeStr alphaStr := sprintf("%.3f", alpha) keyStr := sprintf("%s %s", alphaStr, blendModeStr) pos, ok := f.blendMap[keyStr] if !ok { pos = len(f.blendList) // at least 1 f.blendList = append(f.blendList, blendModeType{alphaStr, alphaStr, blendModeStr, 0}) f.blendMap[keyStr] = pos } f.outf("/GS%d gs", pos) } func (f *Fpdf) gradientClipStart(x, y, w, h float64) { // Save current graphic state and set clipping area f.outf("q %.2f %.2f %.2f %.2f re W n", x*f.k, (f.h-y)*f.k, w*f.k, -h*f.k) // Set up transformation matrix for gradient f.outf("%.5f 0 0 %.5f %.5f %.5f cm", w*f.k, h*f.k, x*f.k, (f.h-(y+h))*f.k) } func (f *Fpdf) gradientClipEnd() { // Restore previous graphic state f.out("Q") } func (f *Fpdf) gradient(tp, r1, g1, b1, r2, g2, b2 int, x1, y1, x2, y2, r float64) { pos := len(f.gradientList) clr1 := rgbColorValue(r1, g1, b1, "", "") clr2 := rgbColorValue(r2, g2, b2, "", "") f.gradientList = append(f.gradientList, gradientType{tp, clr1.str, clr2.str, x1, y1, x2, y2, r, 0}) f.outf("/Sh%d sh", pos) } // LinearGradient draws a rectangular area with a blending of one color to // another. The rectangle is of width w and height h. Its upper left corner is // positioned at point (x, y). // // Each color is specified with three component values, one each for red, green // and blue. The values range from 0 to 255. The first color is specified by // (r1, g1, b1) and the second color by (r2, g2, b2). // // The blending is controlled with a gradient vector that uses normalized // coordinates in which the lower left corner is position (0, 0) and the upper // right corner is (1, 1). The vector's origin and destination are specified by // the points (x1, y1) and (x2, y2). In a linear gradient, blending occurs // perpendicularly to the vector. The vector does not necessarily need to be // anchored on the rectangle edge. Color 1 is used up to the origin of the // vector and color 2 is used beyond the vector's end point. Between the points // the colors are gradually blended. func (f *Fpdf) LinearGradient(x, y, w, h float64, r1, g1, b1, r2, g2, b2 int, x1, y1, x2, y2 float64) { f.gradientClipStart(x, y, w, h) f.gradient(2, r1, g1, b1, r2, g2, b2, x1, y1, x2, y2, 0) f.gradientClipEnd() } // RadialGradient draws a rectangular area with a blending of one color to // another. The rectangle is of width w and height h. Its upper left corner is // positioned at point (x, y). // // Each color is specified with three component values, one each for red, green // and blue. The values range from 0 to 255. The first color is specified by // (r1, g1, b1) and the second color by (r2, g2, b2). // // The blending is controlled with a point and a circle, both specified with // normalized coordinates in which the lower left corner of the rendered // rectangle is position (0, 0) and the upper right corner is (1, 1). Color 1 // begins at the origin point specified by (x1, y1). Color 2 begins at the // circle specified by the center point (x2, y2) and radius r. Colors are // gradually blended from the origin to the circle. The origin and the circle's // center do not necessarily have to coincide, but the origin must be within // the circle to avoid rendering problems. // // The LinearGradient() example demonstrates this method. func (f *Fpdf) RadialGradient(x, y, w, h float64, r1, g1, b1, r2, g2, b2 int, x1, y1, x2, y2, r float64) { f.gradientClipStart(x, y, w, h) f.gradient(3, r1, g1, b1, r2, g2, b2, x1, y1, x2, y2, r) f.gradientClipEnd() } // ClipRect begins a rectangular clipping operation. The rectangle is of width // w and height h. Its upper left corner is positioned at point (x, y). outline // is true to draw a border with the current draw color and line width centered // on the rectangle's perimeter. Only the outer half of the border will be // shown. After calling this method, all rendering operations (for example, // Image(), LinearGradient(), etc) will be clipped by the specified rectangle. // Call ClipEnd() to restore unclipped operations. // // This ClipText() example demonstrates this method. func (f *Fpdf) ClipRect(x, y, w, h float64, outline bool) { f.clipNest++ f.outf("q %.2f %.2f %.2f %.2f re W %s", x*f.k, (f.h-y)*f.k, w*f.k, -h*f.k, strIf(outline, "S", "n")) } // ClipText begins a clipping operation in which rendering is confined to the // character string specified by txtStr. The origin (x, y) is on the left of // the first character at the baseline. The current font is used. outline is // true to draw a border with the current draw color and line width centered on // the perimeters of the text characters. Only the outer half of the border // will be shown. After calling this method, all rendering operations (for // example, Image(), LinearGradient(), etc) will be clipped. Call ClipEnd() to // restore unclipped operations. func (f *Fpdf) ClipText(x, y float64, txtStr string, outline bool) { f.clipNest++ f.outf("q BT %.5f %.5f Td %d Tr (%s) Tj ET", x*f.k, (f.h-y)*f.k, intIf(outline, 5, 7), f.escape(txtStr)) } func (f *Fpdf) clipArc(x1, y1, x2, y2, x3, y3 float64) { h := f.h f.outf("%.5f %.5f %.5f %.5f %.5f %.5f c ", x1*f.k, (h-y1)*f.k, x2*f.k, (h-y2)*f.k, x3*f.k, (h-y3)*f.k) } // ClipRoundedRect begins a rectangular clipping operation. The rectangle is of // width w and height h. Its upper left corner is positioned at point (x, y). // The rounded corners of the rectangle are specified by radius r. outline is // true to draw a border with the current draw color and line width centered on // the rectangle's perimeter. Only the outer half of the border will be shown. // After calling this method, all rendering operations (for example, Image(), // LinearGradient(), etc) will be clipped by the specified rectangle. Call // ClipEnd() to restore unclipped operations. // // This ClipText() example demonstrates this method. func (f *Fpdf) ClipRoundedRect(x, y, w, h, r float64, outline bool) { f.ClipRoundedRectExt(x, y, w, h, r, r, r, r, outline) } // ClipRoundedRectExt behaves the same as ClipRoundedRect() but supports a // different radius for each corner, given by rTL (top-left), rTR (top-right) // rBR (bottom-right), rBL (bottom-left). See ClipRoundedRect() for more // details. This method is demonstrated in the ClipText() example. func (f *Fpdf) ClipRoundedRectExt(x, y, w, h, rTL, rTR, rBR, rBL float64, outline bool) { f.clipNest++ f.roundedRectPath(x, y, w, h, rTL, rTR, rBR, rBL) f.outf(" W %s", strIf(outline, "S", "n")) } // add a rectangle path with rounded corners. // routine shared by RoundedRect() and ClipRoundedRect(), which add the // drawing operation func (f *Fpdf) roundedRectPath(x, y, w, h, rTL, rTR, rBR, rBL float64) { k := f.k hp := f.h myArc := (4.0 / 3.0) * (math.Sqrt2 - 1.0) f.outf("q %.5f %.5f m", (x+rTL)*k, (hp-y)*k) xc := x + w - rTR yc := y + rTR f.outf("%.5f %.5f l", xc*k, (hp-y)*k) if rTR != 0 { f.clipArc(xc+rTR*myArc, yc-rTR, xc+rTR, yc-rTR*myArc, xc+rTR, yc) } xc = x + w - rBR yc = y + h - rBR f.outf("%.5f %.5f l", (x+w)*k, (hp-yc)*k) if rBR != 0 { f.clipArc(xc+rBR, yc+rBR*myArc, xc+rBR*myArc, yc+rBR, xc, yc+rBR) } xc = x + rBL yc = y + h - rBL f.outf("%.5f %.5f l", xc*k, (hp-(y+h))*k) if rBL != 0 { f.clipArc(xc-rBL*myArc, yc+rBL, xc-rBL, yc+rBL*myArc, xc-rBL, yc) } xc = x + rTL yc = y + rTL f.outf("%.5f %.5f l", x*k, (hp-yc)*k) if rTL != 0 { f.clipArc(xc-rTL, yc-rTL*myArc, xc-rTL*myArc, yc-rTL, xc, yc-rTL) } } // ClipEllipse begins an elliptical clipping operation. The ellipse is centered // at (x, y). Its horizontal and vertical radii are specified by rx and ry. // outline is true to draw a border with the current draw color and line width // centered on the ellipse's perimeter. Only the outer half of the border will // be shown. After calling this method, all rendering operations (for example, // Image(), LinearGradient(), etc) will be clipped by the specified ellipse. // Call ClipEnd() to restore unclipped operations. // // This ClipText() example demonstrates this method. func (f *Fpdf) ClipEllipse(x, y, rx, ry float64, outline bool) { f.clipNest++ lx := (4.0 / 3.0) * rx * (math.Sqrt2 - 1) ly := (4.0 / 3.0) * ry * (math.Sqrt2 - 1) k := f.k h := f.h f.outf("q %.5f %.5f m %.5f %.5f %.5f %.5f %.5f %.5f c", (x+rx)*k, (h-y)*k, (x+rx)*k, (h-(y-ly))*k, (x+lx)*k, (h-(y-ry))*k, x*k, (h-(y-ry))*k) f.outf("%.5f %.5f %.5f %.5f %.5f %.5f c", (x-lx)*k, (h-(y-ry))*k, (x-rx)*k, (h-(y-ly))*k, (x-rx)*k, (h-y)*k) f.outf("%.5f %.5f %.5f %.5f %.5f %.5f c", (x-rx)*k, (h-(y+ly))*k, (x-lx)*k, (h-(y+ry))*k, x*k, (h-(y+ry))*k) f.outf("%.5f %.5f %.5f %.5f %.5f %.5f c W %s", (x+lx)*k, (h-(y+ry))*k, (x+rx)*k, (h-(y+ly))*k, (x+rx)*k, (h-y)*k, strIf(outline, "S", "n")) } // ClipCircle begins a circular clipping operation. The circle is centered at // (x, y) and has radius r. outline is true to draw a border with the current // draw color and line width centered on the circle's perimeter. Only the outer // half of the border will be shown. After calling this method, all rendering // operations (for example, Image(), LinearGradient(), etc) will be clipped by // the specified circle. Call ClipEnd() to restore unclipped operations. // // The ClipText() example demonstrates this method. func (f *Fpdf) ClipCircle(x, y, r float64, outline bool) { f.ClipEllipse(x, y, r, r, outline) } // ClipPolygon begins a clipping operation within a polygon. The figure is // defined by a series of vertices specified by points. The x and y fields of // the points use the units established in New(). The last point in the slice // will be implicitly joined to the first to close the polygon. outline is true // to draw a border with the current draw color and line width centered on the // polygon's perimeter. Only the outer half of the border will be shown. After // calling this method, all rendering operations (for example, Image(), // LinearGradient(), etc) will be clipped by the specified polygon. Call // ClipEnd() to restore unclipped operations. // // The ClipText() example demonstrates this method. func (f *Fpdf) ClipPolygon(points []PointType, outline bool) { f.clipNest++ var s fmtBuffer h := f.h k := f.k s.printf("q ") for j, pt := range points { s.printf("%.5f %.5f %s ", pt.X*k, (h-pt.Y)*k, strIf(j == 0, "m", "l")) } s.printf("h W %s", strIf(outline, "S", "n")) f.out(s.String()) } // ClipEnd ends a clipping operation that was started with a call to // ClipRect(), ClipRoundedRect(), ClipText(), ClipEllipse(), ClipCircle() or // ClipPolygon(). Clipping operations can be nested. The document cannot be // successfully output while a clipping operation is active. // // The ClipText() example demonstrates this method. func (f *Fpdf) ClipEnd() { if f.err == nil { if f.clipNest > 0 { f.clipNest-- f.out("Q") } else { f.err = fmt.Errorf("error attempting to end clip operation out of sequence") } } } // AddFont imports a TrueType, OpenType or Type1 font and makes it available. // It is necessary to generate a font definition file first with the makefont // utility. It is not necessary to call this function for the core PDF fonts // (courier, helvetica, times, zapfdingbats). // // The JSON definition file (and the font file itself when embedding) must be // present in the font directory. If it is not found, the error "Could not // include font definition file" is set. // // family specifies the font family. The name can be chosen arbitrarily. If it // is a standard family name, it will override the corresponding font. This // string is used to subsequently set the font with the SetFont method. // // style specifies the font style. Acceptable values are (case insensitive) the // empty string for regular style, "B" for bold, "I" for italic, or "BI" or // "IB" for bold and italic combined. // // fileStr specifies the base name with ".json" extension of the font // definition file to be added. The file will be loaded from the font directory // specified in the call to New() or SetFontLocation(). func (f *Fpdf) AddFont(familyStr, styleStr, fileStr string) { f.addFont(fontFamilyEscape(familyStr), styleStr, fileStr, false) } // AddUTF8Font imports a TrueType font with utf-8 symbols and makes it available. // It is necessary to generate a font definition file first with the makefont // utility. It is not necessary to call this function for the core PDF fonts // (courier, helvetica, times, zapfdingbats). // // The JSON definition file (and the font file itself when embedding) must be // present in the font directory. If it is not found, the error "Could not // include font definition file" is set. // // family specifies the font family. The name can be chosen arbitrarily. If it // is a standard family name, it will override the corresponding font. This // string is used to subsequently set the font with the SetFont method. // // style specifies the font style. Acceptable values are (case insensitive) the // empty string for regular style, "B" for bold, "I" for italic, or "BI" or // "IB" for bold and italic combined. // // fileStr specifies the base name with ".json" extension of the font // definition file to be added. The file will be loaded from the font directory // specified in the call to New() or SetFontLocation(). func (f *Fpdf) AddUTF8Font(familyStr, styleStr, fileStr string) { f.addFont(fontFamilyEscape(familyStr), styleStr, fileStr, true) } func (f *Fpdf) addFont(familyStr, styleStr, fileStr string, isUTF8 bool) { if fileStr == "" { if isUTF8 { fileStr = strings.Replace(familyStr, " ", "", -1) + strings.ToLower(styleStr) + ".ttf" } else { fileStr = strings.Replace(familyStr, " ", "", -1) + strings.ToLower(styleStr) + ".json" } } if isUTF8 { fontKey := getFontKey(familyStr, styleStr) _, ok := f.fonts[fontKey] if ok { return } var ttfStat os.FileInfo var err error fileStr = path.Join(f.fontpath, fileStr) ttfStat, err = os.Stat(fileStr) if err != nil { f.SetError(err) return } originalSize := ttfStat.Size() Type := "UTF8" var utf8Bytes []byte utf8Bytes, err = ioutil.ReadFile(fileStr) if err != nil { f.SetError(err) return } reader := fileReader{readerPosition: 0, array: utf8Bytes} utf8File := newUTF8Font(&reader) err = utf8File.parseFile() if err != nil { f.SetError(err) return } desc := FontDescType{ Ascent: int(utf8File.Ascent), Descent: int(utf8File.Descent), CapHeight: utf8File.CapHeight, Flags: utf8File.Flags, FontBBox: utf8File.Bbox, ItalicAngle: utf8File.ItalicAngle, StemV: utf8File.StemV, MissingWidth: round(utf8File.DefaultWidth), } var sbarr map[int]int if f.aliasNbPagesStr == "" { sbarr = makeSubsetRange(57) } else { sbarr = makeSubsetRange(32) } def := fontDefType{ Tp: Type, Name: fontKey, Desc: desc, Up: int(round(utf8File.UnderlinePosition)), Ut: round(utf8File.UnderlineThickness), Cw: utf8File.CharWidths, usedRunes: sbarr, File: fileStr, utf8File: utf8File, } def.i, _ = generateFontID(def) f.fonts[fontKey] = def f.fontFiles[fontKey] = fontFileType{ length1: originalSize, fontType: "UTF8", } f.fontFiles[fileStr] = fontFileType{ fontType: "UTF8", } } else { if f.fontLoader != nil { reader, err := f.fontLoader.Open(fileStr) if err == nil { f.AddFontFromReader(familyStr, styleStr, reader) if closer, ok := reader.(io.Closer); ok { closer.Close() } return } } fileStr = path.Join(f.fontpath, fileStr) file, err := os.Open(fileStr) if err != nil { f.err = err return } defer file.Close() f.AddFontFromReader(familyStr, styleStr, file) } } func makeSubsetRange(end int) map[int]int { answer := make(map[int]int) for i := 0; i < end; i++ { answer[i] = 0 } return answer } // AddFontFromBytes imports a TrueType, OpenType or Type1 font from static // bytes within the executable and makes it available for use in the generated // document. // // family specifies the font family. The name can be chosen arbitrarily. If it // is a standard family name, it will override the corresponding font. This // string is used to subsequently set the font with the SetFont method. // // style specifies the font style. Acceptable values are (case insensitive) the // empty string for regular style, "B" for bold, "I" for italic, or "BI" or // "IB" for bold and italic combined. // // jsonFileBytes contain all bytes of JSON file. // // zFileBytes contain all bytes of Z file. func (f *Fpdf) AddFontFromBytes(familyStr, styleStr string, jsonFileBytes, zFileBytes []byte) { f.addFontFromBytes(fontFamilyEscape(familyStr), styleStr, jsonFileBytes, zFileBytes, nil) } // AddUTF8FontFromBytes imports a TrueType font with utf-8 symbols from static // bytes within the executable and makes it available for use in the generated // document. // // family specifies the font family. The name can be chosen arbitrarily. If it // is a standard family name, it will override the corresponding font. This // string is used to subsequently set the font with the SetFont method. // // style specifies the font style. Acceptable values are (case insensitive) the // empty string for regular style, "B" for bold, "I" for italic, or "BI" or // "IB" for bold and italic combined. // // jsonFileBytes contain all bytes of JSON file. // // zFileBytes contain all bytes of Z file. func (f *Fpdf) AddUTF8FontFromBytes(familyStr, styleStr string, utf8Bytes []byte) { f.addFontFromBytes(fontFamilyEscape(familyStr), styleStr, nil, nil, utf8Bytes) } func (f *Fpdf) addFontFromBytes(familyStr, styleStr string, jsonFileBytes, zFileBytes, utf8Bytes []byte) { if f.err != nil { return } // load font key var ok bool fontkey := getFontKey(familyStr, styleStr) _, ok = f.fonts[fontkey] if ok { return } if utf8Bytes != nil { // if styleStr == "IB" { // styleStr = "BI" // } Type := "UTF8" reader := fileReader{readerPosition: 0, array: utf8Bytes} utf8File := newUTF8Font(&reader) err := utf8File.parseFile() if err != nil { fmt.Printf("get metrics Error: %e\n", err) return } desc := FontDescType{ Ascent: int(utf8File.Ascent), Descent: int(utf8File.Descent), CapHeight: utf8File.CapHeight, Flags: utf8File.Flags, FontBBox: utf8File.Bbox, ItalicAngle: utf8File.ItalicAngle, StemV: utf8File.StemV, MissingWidth: round(utf8File.DefaultWidth), } var sbarr map[int]int if f.aliasNbPagesStr == "" { sbarr = makeSubsetRange(57) } else { sbarr = makeSubsetRange(32) } def := fontDefType{ Tp: Type, Name: fontkey, Desc: desc, Up: int(round(utf8File.UnderlinePosition)), Ut: round(utf8File.UnderlineThickness), Cw: utf8File.CharWidths, utf8File: utf8File, usedRunes: sbarr, } def.i, _ = generateFontID(def) f.fonts[fontkey] = def } else { // load font definitions var info fontDefType err := json.Unmarshal(jsonFileBytes, &info) if err != nil { f.err = err } if f.err != nil { return } if info.i, err = generateFontID(info); err != nil { f.err = err return } // search existing encodings if len(info.Diff) > 0 { n := -1 for j, str := range f.diffs { if str == info.Diff { n = j + 1 break } } if n < 0 { f.diffs = append(f.diffs, info.Diff) n = len(f.diffs) } info.DiffN = n } // embed font if len(info.File) > 0 { if info.Tp == "TrueType" { f.fontFiles[info.File] = fontFileType{ length1: int64(info.OriginalSize), embedded: true, content: zFileBytes, } } else { f.fontFiles[info.File] = fontFileType{ length1: int64(info.Size1), length2: int64(info.Size2), embedded: true, content: zFileBytes, } } } f.fonts[fontkey] = info } } // getFontKey is used by AddFontFromReader and GetFontDesc func getFontKey(familyStr, styleStr string) string { familyStr = strings.ToLower(familyStr) styleStr = strings.ToUpper(styleStr) if styleStr == "IB" { styleStr = "BI" } return familyStr + styleStr } // AddFontFromReader imports a TrueType, OpenType or Type1 font and makes it // available using a reader that satisifies the io.Reader interface. See // AddFont for details about familyStr and styleStr. func (f *Fpdf) AddFontFromReader(familyStr, styleStr string, r io.Reader) { if f.err != nil { return } // dbg("Adding family [%s], style [%s]", familyStr, styleStr) familyStr = fontFamilyEscape(familyStr) var ok bool fontkey := getFontKey(familyStr, styleStr) _, ok = f.fonts[fontkey] if ok { return } var info fontDefType info = f.loadfont(r) if f.err != nil { return } if len(info.Diff) > 0 { // Search existing encodings n := -1 for j, str := range f.diffs { if str == info.Diff { n = j + 1 break } } if n < 0 { f.diffs = append(f.diffs, info.Diff) n = len(f.diffs) } info.DiffN = n } // dbg("font [%s], type [%s]", info.File, info.Tp) if len(info.File) > 0 { // Embedded font if info.Tp == "TrueType" { f.fontFiles[info.File] = fontFileType{length1: int64(info.OriginalSize)} } else { f.fontFiles[info.File] = fontFileType{length1: int64(info.Size1), length2: int64(info.Size2)} } } f.fonts[fontkey] = info return } // GetFontDesc returns the font descriptor, which can be used for // example to find the baseline of a font. If familyStr is empty // current font descriptor will be returned. // See FontDescType for documentation about the font descriptor. // See AddFont for details about familyStr and styleStr. func (f *Fpdf) GetFontDesc(familyStr, styleStr string) FontDescType { if familyStr == "" { return f.currentFont.Desc } return f.fonts[getFontKey(fontFamilyEscape(familyStr), styleStr)].Desc } // SetFont sets the font used to print character strings. It is mandatory to // call this method at least once before printing text or the resulting // document will not be valid. // // The font can be either a standard one or a font added via the AddFont() // method or AddFontFromReader() method. Standard fonts use the Windows // encoding cp1252 (Western Europe). // // The method can be called before the first page is created and the font is // kept from page to page. If you just wish to change the current font size, it // is simpler to call SetFontSize(). // // Note: the font definition file must be accessible. An error is set if the // file cannot be read. // // familyStr specifies the font family. It can be either a name defined by // AddFont(), AddFontFromReader() or one of the standard families (case // insensitive): "Courier" for fixed-width, "Helvetica" or "Arial" for sans // serif, "Times" for serif, "Symbol" or "ZapfDingbats" for symbolic. // // styleStr can be "B" (bold), "I" (italic), "U" (underscore), "S" (strike-out) // or any combination. The default value (specified with an empty string) is // regular. Bold and italic styles do not apply to Symbol and ZapfDingbats. // // size is the font size measured in points. The default value is the current // size. If no size has been specified since the beginning of the document, the // value taken is 12. func (f *Fpdf) SetFont(familyStr, styleStr string, size float64) { // dbg("SetFont x %.2f, lMargin %.2f", f.x, f.lMargin) if f.err != nil { return } // dbg("SetFont") familyStr = fontFamilyEscape(familyStr) var ok bool if familyStr == "" { familyStr = f.fontFamily } else { familyStr = strings.ToLower(familyStr) } styleStr = strings.ToUpper(styleStr) f.underline = strings.Contains(styleStr, "U") if f.underline { styleStr = strings.Replace(styleStr, "U", "", -1) } f.strikeout = strings.Contains(styleStr, "S") if f.strikeout { styleStr = strings.Replace(styleStr, "S", "", -1) } if styleStr == "IB" { styleStr = "BI" } if size == 0.0 { size = f.fontSizePt } // Test if font is already loaded fontKey := familyStr + styleStr _, ok = f.fonts[fontKey] if !ok { // Test if one of the core fonts if familyStr == "arial" { familyStr = "helvetica" } _, ok = f.coreFonts[familyStr] if ok { if familyStr == "symbol" { familyStr = "zapfdingbats" } if familyStr == "zapfdingbats" { styleStr = "" } fontKey = familyStr + styleStr _, ok = f.fonts[fontKey] if !ok { rdr := f.coreFontReader(familyStr, styleStr) if f.err == nil { f.AddFontFromReader(familyStr, styleStr, rdr) } if f.err != nil { return } } } else { f.err = fmt.Errorf("undefined font: %s %s", familyStr, styleStr) return } } // Select it f.fontFamily = familyStr f.fontStyle = styleStr f.fontSizePt = size f.fontSize = size / f.k f.currentFont = f.fonts[fontKey] if f.currentFont.Tp == "UTF8" { f.isCurrentUTF8 = true } else { f.isCurrentUTF8 = false } if f.page > 0 { f.outf("BT /F%s %.2f Tf ET", f.currentFont.i, f.fontSizePt) } return } // SetFontStyle sets the style of the current font. See also SetFont() func (f *Fpdf) SetFontStyle(styleStr string) { f.SetFont(f.fontFamily, styleStr, f.fontSizePt) } // SetFontSize defines the size of the current font. Size is specified in // points (1/ 72 inch). See also SetFontUnitSize(). func (f *Fpdf) SetFontSize(size float64) { f.fontSizePt = size f.fontSize = size / f.k if f.page > 0 { f.outf("BT /F%s %.2f Tf ET", f.currentFont.i, f.fontSizePt) } } // SetFontUnitSize defines the size of the current font. Size is specified in // the unit of measure specified in New(). See also SetFontSize(). func (f *Fpdf) SetFontUnitSize(size float64) { f.fontSizePt = size * f.k f.fontSize = size if f.page > 0 { f.outf("BT /F%s %.2f Tf ET", f.currentFont.i, f.fontSizePt) } } // GetFontSize returns the size of the current font in points followed by the // size in the unit of measure specified in New(). The second value can be used // as a line height value in drawing operations. func (f *Fpdf) GetFontSize() (ptSize, unitSize float64) { return f.fontSizePt, f.fontSize } // AddLink creates a new internal link and returns its identifier. An internal // link is a clickable area which directs to another place within the document. // The identifier can then be passed to Cell(), Write(), Image() or Link(). The // destination is defined with SetLink(). func (f *Fpdf) AddLink() int { f.links = append(f.links, intLinkType{}) return len(f.links) - 1 } // SetLink defines the page and position a link points to. See AddLink(). func (f *Fpdf) SetLink(link int, y float64, page int) { if y == -1 { y = f.y } if page == -1 { page = f.page } f.links[link] = intLinkType{page, y} } // newLink adds a new clickable link on current page func (f *Fpdf) newLink(x, y, w, h float64, link int, linkStr string) { // linkList, ok := f.pageLinks[f.page] // if !ok { // linkList = make([]linkType, 0, 8) // f.pageLinks[f.page] = linkList // } f.pageLinks[f.page] = append(f.pageLinks[f.page], linkType{x * f.k, f.hPt - y*f.k, w * f.k, h * f.k, link, linkStr}) } // Link puts a link on a rectangular area of the page. Text or image links are // generally put via Cell(), Write() or Image(), but this method can be useful // for instance to define a clickable area inside an image. link is the value // returned by AddLink(). func (f *Fpdf) Link(x, y, w, h float64, link int) { f.newLink(x, y, w, h, link, "") } // LinkString puts a link on a rectangular area of the page. Text or image // links are generally put via Cell(), Write() or Image(), but this method can // be useful for instance to define a clickable area inside an image. linkStr // is the target URL. func (f *Fpdf) LinkString(x, y, w, h float64, linkStr string) { f.newLink(x, y, w, h, 0, linkStr) } // Bookmark sets a bookmark that will be displayed in a sidebar outline. txtStr // is the title of the bookmark. level specifies the level of the bookmark in // the outline; 0 is the top level, 1 is just below, and so on. y specifies the // vertical position of the bookmark destination in the current page; -1 // indicates the current position. func (f *Fpdf) Bookmark(txtStr string, level int, y float64) { if y == -1 { y = f.y } if f.isCurrentUTF8 { txtStr = utf8toutf16(txtStr) } f.outlines = append(f.outlines, outlineType{text: txtStr, level: level, y: y, p: f.PageNo(), prev: -1, last: -1, next: -1, first: -1}) } // Text prints a character string. The origin (x, y) is on the left of the // first character at the baseline. This method permits a string to be placed // precisely on the page, but it is usually easier to use Cell(), MultiCell() // or Write() which are the standard methods to print text. func (f *Fpdf) Text(x, y float64, txtStr string) { var txt2 string if f.isCurrentUTF8 { if f.isRTL { txtStr = reverseText(txtStr) x -= f.GetStringWidth(txtStr) } txt2 = f.escape(utf8toutf16(txtStr, false)) for _, uni := range []rune(txtStr) { f.currentFont.usedRunes[int(uni)] = int(uni) } } else { txt2 = f.escape(txtStr) } s := sprintf("BT %.2f %.2f Td (%s) Tj ET", x*f.k, (f.h-y)*f.k, txt2) if f.underline && txtStr != "" { s += " " + f.dounderline(x, y, txtStr) } if f.strikeout && txtStr != "" { s += " " + f.dostrikeout(x, y, txtStr) } if f.colorFlag { s = sprintf("q %s %s Q", f.color.text.str, s) } f.out(s) } // SetWordSpacing sets spacing between words of following text. See the // WriteAligned() example for a demonstration of its use. func (f *Fpdf) SetWordSpacing(space float64) { f.out(sprintf("%.5f Tw", space*f.k)) } // SetTextRenderingMode sets the rendering mode of following text. // The mode can be as follows: // 0: Fill text // 1: Stroke text // 2: Fill, then stroke text // 3: Neither fill nor stroke text (invisible) // 4: Fill text and add to path for clipping // 5: Stroke text and add to path for clipping // 6: Fills then stroke text and add to path for clipping // 7: Add text to path for clipping // This method is demonstrated in the SetTextRenderingMode example. func (f *Fpdf) SetTextRenderingMode(mode int) { if mode >= 0 && mode <= 7 { f.out(sprintf("%d Tr", mode)) } } // SetAcceptPageBreakFunc allows the application to control where page breaks // occur. // // fnc is an application function (typically a closure) that is called by the // library whenever a page break condition is met. The break is issued if true // is returned. The default implementation returns a value according to the // mode selected by SetAutoPageBreak. The function provided should not be // called by the application. // // See the example for SetLeftMargin() to see how this function can be used to // manage multiple columns. func (f *Fpdf) SetAcceptPageBreakFunc(fnc func() bool) { f.acceptPageBreak = fnc } // CellFormat prints a rectangular cell with optional borders, background color // and character string. The upper-left corner of the cell corresponds to the // current position. The text can be aligned or centered. After the call, the // current position moves to the right or to the next line. It is possible to // put a link on the text. // // An error will be returned if a call to SetFont() has not already taken // place before this method is called. // // If automatic page breaking is enabled and the cell goes beyond the limit, a // page break is done before outputting. // // w and h specify the width and height of the cell. If w is 0, the cell // extends up to the right margin. Specifying 0 for h will result in no output, // but the current position will be advanced by w. // // txtStr specifies the text to display. // // borderStr specifies how the cell border will be drawn. An empty string // indicates no border, "1" indicates a full border, and one or more of "L", // "T", "R" and "B" indicate the left, top, right and bottom sides of the // border. // // ln indicates where the current position should go after the call. Possible // values are 0 (to the right), 1 (to the beginning of the next line), and 2 // (below). Putting 1 is equivalent to putting 0 and calling Ln() just after. // // alignStr specifies how the text is to be positioned within the cell. // Horizontal alignment is controlled by including "L", "C" or "R" (left, // center, right) in alignStr. Vertical alignment is controlled by including // "T", "M", "B" or "A" (top, middle, bottom, baseline) in alignStr. The default // alignment is left middle. // // fill is true to paint the cell background or false to leave it transparent. // // 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. func (f *Fpdf) CellFormat(w, h float64, txtStr, borderStr string, ln int, alignStr string, fill bool, link int, linkStr string) { // dbg("CellFormat. h = %.2f, borderStr = %s", h, borderStr) if f.err != nil { return } if f.currentFont.Name == "" { f.err = fmt.Errorf("font has not been set; unable to render text") return } borderStr = strings.ToUpper(borderStr) k := f.k if f.y+h > f.pageBreakTrigger && !f.inHeader && !f.inFooter && f.acceptPageBreak() { // Automatic page break x := f.x ws := f.ws // dbg("auto page break, x %.2f, ws %.2f", x, ws) if ws > 0 { f.ws = 0 f.out("0 Tw") } f.AddPageFormat(f.curOrientation, f.curPageSize) if f.err != nil { return } f.x = x if ws > 0 { f.ws = ws f.outf("%.3f Tw", ws*k) } } if w == 0 { w = f.w - f.rMargin - f.x } var s fmtBuffer if fill || borderStr == "1" { var op string if fill { if borderStr == "1" { op = "B" // dbg("border is '1', fill") } else { op = "f" // dbg("border is empty, fill") } } else { // dbg("border is '1', no fill") op = "S" } /// dbg("(CellFormat) f.x %.2f f.k %.2f", f.x, f.k) s.printf("%.2f %.2f %.2f %.2f re %s ", f.x*k, (f.h-f.y)*k, w*k, -h*k, op) } if len(borderStr) > 0 && borderStr != "1" { // fmt.Printf("border is '%s', no fill\n", borderStr) x := f.x y := f.y left := x * k top := (f.h - y) * k right := (x + w) * k bottom := (f.h - (y + h)) * k if strings.Contains(borderStr, "L") { s.printf("%.2f %.2f m %.2f %.2f l S ", left, top, left, bottom) } if strings.Contains(borderStr, "T") { s.printf("%.2f %.2f m %.2f %.2f l S ", left, top, right, top) } if strings.Contains(borderStr, "R") { s.printf("%.2f %.2f m %.2f %.2f l S ", right, top, right, bottom) } if strings.Contains(borderStr, "B") { s.printf("%.2f %.2f m %.2f %.2f l S ", left, bottom, right, bottom) } } if len(txtStr) > 0 { var dx, dy float64 // Horizontal alignment switch { case strings.Contains(alignStr, "R"): dx = w - f.cMargin - f.GetStringWidth(txtStr) case strings.Contains(alignStr, "C"): dx = (w - f.GetStringWidth(txtStr)) / 2 default: dx = f.cMargin } // Vertical alignment switch { case strings.Contains(alignStr, "T"): dy = (f.fontSize - h) / 2.0 case strings.Contains(alignStr, "B"): dy = (h - f.fontSize) / 2.0 case strings.Contains(alignStr, "A"): var descent float64 d := f.currentFont.Desc if d.Descent == 0 { // not defined (standard font?), use average of 19% descent = -0.19 * f.fontSize } else { descent = float64(d.Descent) * f.fontSize / float64(d.Ascent-d.Descent) } dy = (h-f.fontSize)/2.0 - descent default: dy = 0 } if f.colorFlag { s.printf("q %s ", f.color.text.str) } //If multibyte, Tw has no effect - do word spacing using an adjustment before each space if (f.ws != 0 || alignStr == "J") && f.isCurrentUTF8 { // && f.ws != 0 if f.isRTL { txtStr = reverseText(txtStr) } wmax := int(math.Ceil((w - 2*f.cMargin) * 1000 / f.fontSize)) for _, uni := range []rune(txtStr) { f.currentFont.usedRunes[int(uni)] = int(uni) } space := f.escape(utf8toutf16(" ", false)) strSize := f.GetStringSymbolWidth(txtStr) s.printf("BT 0 Tw %.2f %.2f Td [", (f.x+dx)*k, (f.h-(f.y+.5*h+.3*f.fontSize))*k) t := strings.Split(txtStr, " ") shift := float64((wmax - strSize)) / float64(len(t)-1) numt := len(t) for i := 0; i < numt; i++ { tx := t[i] tx = "(" + f.escape(utf8toutf16(tx, false)) + ")" s.printf("%s ", tx) if (i + 1) < numt { s.printf("%.3f(%s) ", -shift, space) } } s.printf("] TJ ET") } else { var txt2 string if f.isCurrentUTF8 { if f.isRTL { txtStr = reverseText(txtStr) } txt2 = f.escape(utf8toutf16(txtStr, false)) for _, uni := range []rune(txtStr) { f.currentFont.usedRunes[int(uni)] = int(uni) } } else { txt2 = strings.Replace(txtStr, "\\", "\\\\", -1) txt2 = strings.Replace(txt2, "(", "\\(", -1) txt2 = strings.Replace(txt2, ")", "\\)", -1) } bt := (f.x + dx) * k td := (f.h - (f.y + dy + .5*h + .3*f.fontSize)) * k s.printf("BT %.2f %.2f Td (%s)Tj ET", bt, td, txt2) //BT %.2F %.2F Td (%s) Tj ET',(f.x+dx)*k,(f.h-(f.y+.5*h+.3*f.FontSize))*k,txt2); } if f.underline { s.printf(" %s", f.dounderline(f.x+dx, f.y+dy+.5*h+.3*f.fontSize, txtStr)) } if f.strikeout { s.printf(" %s", f.dostrikeout(f.x+dx, f.y+dy+.5*h+.3*f.fontSize, txtStr)) } if f.colorFlag { s.printf(" Q") } if link > 0 || len(linkStr) > 0 { f.newLink(f.x+dx, f.y+dy+.5*h-.5*f.fontSize, f.GetStringWidth(txtStr), f.fontSize, link, linkStr) } } str := s.String() if len(str) > 0 { f.out(str) } f.lasth = h if ln > 0 { // Go to next line f.y += h if ln == 1 { f.x = f.lMargin } } else { f.x += w } return } // Revert string to use in RTL languages func reverseText(text string) string { oldText := []rune(text) newText := make([]rune, len(oldText)) length := len(oldText) - 1 for i, r := range oldText { newText[length-i] = r } return string(newText) } // Cell is a simpler version of CellFormat with no fill, border, links or // special alignment. The Cell_strikeout() example demonstrates this method. func (f *Fpdf) Cell(w, h float64, txtStr string) { f.CellFormat(w, h, txtStr, "", 0, "L", false, 0, "") } // Cellf is a simpler printf-style version of CellFormat with no fill, border, // links or special alignment. See documentation for the fmt package for // details on fmtStr and args. func (f *Fpdf) Cellf(w, h float64, fmtStr string, args ...interface{}) { f.CellFormat(w, h, sprintf(fmtStr, args...), "", 0, "L", false, 0, "") } // SplitLines splits 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. // // This method is useful for codepage-based fonts only. For UTF-8 encoded text, // use SplitText(). // // You can use MultiCell if you want to print a text on several lines in a // simple way. func (f *Fpdf) SplitLines(txt []byte, w float64) [][]byte { // Function contributed by Bruno Michel lines := [][]byte{} cw := f.currentFont.Cw wmax := int(math.Ceil((w - 2*f.cMargin) * 1000 / f.fontSize)) s := bytes.Replace(txt, []byte("\r"), []byte{}, -1) 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 c == ' ' || c == '\t' || c == '\n' { sep = i } if c == '\n' || l > wmax { if sep == -1 { if i == j { i++ } sep = i } else { i = sep + 1 } lines = append(lines, s[j:sep]) sep = -1 j = i l = 0 } else { i++ } } if i != j { lines = append(lines, s[j:i]) } return lines } // MultiCell supports printing text with line breaks. They can be automatic (as // soon as the text reaches the right border of the cell) or explicit (via the // \n character). As many cells as necessary are output, one below the other. // // Text can be aligned, centered or justified. The cell block can be framed and // the background painted. See CellFormat() for more details. // // The current position after calling MultiCell() is the beginning of the next // line, equivalent to calling CellFormat with ln equal to 1. // // w is the width of the cells. A value of zero indicates cells that reach to // the right margin. // // h indicates the line height of each cell in the unit of measure specified in New(). // // Note: this method has a known bug that treats UTF-8 fonts differently than // non-UTF-8 fonts. With UTF-8 fonts, all trailing newlines in txtStr are // removed. With a non-UTF-8 font, if txtStr has one or more trailing newlines, // only the last is removed. In the next major module version, the UTF-8 logic // will be changed to match the non-UTF-8 logic. To prepare for that change, // applications that use UTF-8 fonts and depend on having all trailing newlines // removed should call strings.TrimRight(txtStr, "\r\n") before calling this // method. func (f *Fpdf) MultiCell(w, h float64, txtStr, borderStr, alignStr string, fill bool) { if f.err != nil { return } // dbg("MultiCell") if alignStr == "" { alignStr = "J" } cw := f.currentFont.Cw if w == 0 { w = f.w - f.rMargin - f.x } wmax := int(math.Ceil((w - 2*f.cMargin) * 1000 / f.fontSize)) s := strings.Replace(txtStr, "\r", "", -1) srune := []rune(s) // remove extra line breaks var nb int if f.isCurrentUTF8 { nb = len(srune) for nb > 0 && srune[nb-1] == '\n' { nb-- } srune = srune[0:nb] } else { nb = len(s) bytes2 := []byte(s) // for nb > 0 && bytes2[nb-1] == '\n' { // Prior to August 2019, if s ended with a newline, this code stripped it. // After that date, to be compatible with the UTF-8 code above, *all* // trailing newlines were removed. Because this regression caused at least // one application to break (see issue #333), the original behavior has been // reinstated with a caveat included in the documentation. if nb > 0 && bytes2[nb-1] == '\n' { nb-- } s = s[0:nb] } // dbg("[%s]\n", s) var b, b2 string b = "0" if len(borderStr) > 0 { if borderStr == "1" { borderStr = "LTRB" b = "LRT" b2 = "LR" } else { b2 = "" if strings.Contains(borderStr, "L") { b2 += "L" } if strings.Contains(borderStr, "R") { b2 += "R" } if strings.Contains(borderStr, "T") { b = b2 + "T" } else { b = b2 } } } sep := -1 i := 0 j := 0 l := 0 ls := 0 ns := 0 nl := 1 for i < nb { // Get next character var c rune if f.isCurrentUTF8 { c = srune[i] } else { c = rune(s[i]) } if c == '\n' { // Explicit line break if f.ws > 0 { f.ws = 0 f.out("0 Tw") } if f.isCurrentUTF8 { newAlignStr := alignStr if newAlignStr == "J" { if f.isRTL { newAlignStr = "R" } else { newAlignStr = "L" } } f.CellFormat(w, h, string(srune[j:i]), b, 2, newAlignStr, fill, 0, "") } else { f.CellFormat(w, h, s[j:i], b, 2, alignStr, fill, 0, "") } i++ sep = -1 j = i l = 0 ns = 0 nl++ if len(borderStr) > 0 && nl == 2 { b = b2 } continue } if c == ' ' || isChinese(c) { sep = i ls = l ns++ } if int(c) >= len(cw) { f.err = fmt.Errorf("character outside the supported range: %s", string(c)) return } if cw[int(c)] == 0 { //Marker width 0 used for missing symbols l += f.currentFont.Desc.MissingWidth } else if cw[int(c)] != 65535 { //Marker width 65535 used for zero width symbols l += cw[int(c)] } if l > wmax { // Automatic line break if sep == -1 { if i == j { i++ } if f.ws > 0 { f.ws = 0 f.out("0 Tw") } if f.isCurrentUTF8 { f.CellFormat(w, h, string(srune[j:i]), b, 2, alignStr, fill, 0, "") } else { f.CellFormat(w, h, s[j:i], b, 2, alignStr, fill, 0, "") } } else { if alignStr == "J" { if ns > 1 { f.ws = float64((wmax-ls)/1000) * f.fontSize / float64(ns-1) } else { f.ws = 0 } f.outf("%.3f Tw", f.ws*f.k) } if f.isCurrentUTF8 { f.CellFormat(w, h, string(srune[j:sep]), b, 2, alignStr, fill, 0, "") } else { f.CellFormat(w, h, s[j:sep], b, 2, alignStr, fill, 0, "") } i = sep + 1 } sep = -1 j = i l = 0 ns = 0 nl++ if len(borderStr) > 0 && nl == 2 { b = b2 } } else { i++ } } // Last chunk if f.ws > 0 { f.ws = 0 f.out("0 Tw") } if len(borderStr) > 0 && strings.Contains(borderStr, "B") { b += "B" } if f.isCurrentUTF8 { if alignStr == "J" { if f.isRTL { alignStr = "R" } else { alignStr = "" } } f.CellFormat(w, h, string(srune[j:i]), b, 2, alignStr, fill, 0, "") } else { f.CellFormat(w, h, s[j:i], b, 2, alignStr, fill, 0, "") } f.x = f.lMargin } // write outputs text in flowing mode func (f *Fpdf) write(h float64, txtStr string, link int, linkStr string) { // dbg("Write") cw := f.currentFont.Cw w := f.w - f.rMargin - f.x wmax := (w - 2*f.cMargin) * 1000 / f.fontSize s := strings.Replace(txtStr, "\r", "", -1) var nb int if f.isCurrentUTF8 { nb = len([]rune(s)) if nb == 1 && s == " " { f.x += f.GetStringWidth(s) return } } else { nb = len(s) } sep := -1 i := 0 j := 0 l := 0.0 nl := 1 for i < nb { // Get next character var c rune if f.isCurrentUTF8 { c = []rune(s)[i] } else { c = rune(byte(s[i])) } if c == '\n' { // Explicit line break if f.isCurrentUTF8 { f.CellFormat(w, h, string([]rune(s)[j:i]), "", 2, "", false, link, linkStr) } else { f.CellFormat(w, h, s[j:i], "", 2, "", false, link, linkStr) } i++ sep = -1 j = i l = 0.0 if nl == 1 { f.x = f.lMargin w = f.w - f.rMargin - f.x wmax = (w - 2*f.cMargin) * 1000 / f.fontSize } nl++ continue } if c == ' ' { sep = i } l += float64(cw[int(c)]) if l > wmax { // Automatic line break if sep == -1 { if f.x > f.lMargin { // Move to next line f.x = f.lMargin f.y += h w = f.w - f.rMargin - f.x wmax = (w - 2*f.cMargin) * 1000 / f.fontSize i++ nl++ continue } if i == j { i++ } if f.isCurrentUTF8 { f.CellFormat(w, h, string([]rune(s)[j:i]), "", 2, "", false, link, linkStr) } else { f.CellFormat(w, h, s[j:i], "", 2, "", false, link, linkStr) } } else { if f.isCurrentUTF8 { f.CellFormat(w, h, string([]rune(s)[j:sep]), "", 2, "", false, link, linkStr) } else { f.CellFormat(w, h, s[j:sep], "", 2, "", false, link, linkStr) } i = sep + 1 } sep = -1 j = i l = 0.0 if nl == 1 { f.x = f.lMargin w = f.w - f.rMargin - f.x wmax = (w - 2*f.cMargin) * 1000 / f.fontSize } nl++ } else { i++ } } // Last chunk if i != j { if f.isCurrentUTF8 { f.CellFormat(l/1000*f.fontSize, h, string([]rune(s)[j:]), "", 0, "", false, link, linkStr) } else { f.CellFormat(l/1000*f.fontSize, h, s[j:], "", 0, "", false, link, linkStr) } } } // Write prints text from the current position. When the right margin is // reached (or the \n character is met) a line break occurs and text continues // from the left margin. Upon method exit, the current position is left just at // the end of the text. // // It is possible to put a link on the text. // // h indicates the line height in the unit of measure specified in New(). func (f *Fpdf) Write(h float64, txtStr string) { f.write(h, txtStr, 0, "") } // Writef is like Write but uses printf-style formatting. See the documentation // for package fmt for more details on fmtStr and args. func (f *Fpdf) Writef(h float64, fmtStr string, args ...interface{}) { f.write(h, sprintf(fmtStr, args...), 0, "") } // WriteLinkString writes text that when clicked launches an external URL. See // Write() for argument details. func (f *Fpdf) WriteLinkString(h float64, displayStr, targetStr string) { f.write(h, displayStr, 0, targetStr) } // WriteLinkID writes text that when clicked jumps to another location in the // PDF. linkID is an identifier returned by AddLink(). See Write() for argument // details. func (f *Fpdf) WriteLinkID(h float64, displayStr string, linkID int) { f.write(h, displayStr, linkID, "") } // WriteAligned is an implementation of Write that makes it possible to align // text. // // width indicates the width of the box the text will be drawn in. This is in // the unit of measure specified in New(). If it is set to 0, the bounding box //of the page will be taken (pageWidth - leftMargin - rightMargin). // // lineHeight indicates the line height in the unit of measure specified in // New(). // // alignStr sees to horizontal alignment of the given textStr. The options are // "L", "C" and "R" (Left, Center, Right). The default is "L". func (f *Fpdf) WriteAligned(width, lineHeight float64, textStr, alignStr string) { lMargin, _, rMargin, _ := f.GetMargins() pageWidth, _ := f.GetPageSize() if width == 0 { width = pageWidth - (lMargin + rMargin) } var lines []string if f.isCurrentUTF8 { lines = f.SplitText(textStr, width) } else { for _, line := range f.SplitLines([]byte(textStr), width) { lines = append(lines, string(line)) } } for _, lineBt := range lines { lineStr := string(lineBt) lineWidth := f.GetStringWidth(lineStr) switch alignStr { case "C": f.SetLeftMargin(lMargin + ((width - lineWidth) / 2)) f.Write(lineHeight, lineStr) f.SetLeftMargin(lMargin) case "R": f.SetLeftMargin(lMargin + (width - lineWidth) - 2.01*f.cMargin) f.Write(lineHeight, lineStr) f.SetLeftMargin(lMargin) default: f.SetRightMargin(pageWidth - lMargin - width) f.Write(lineHeight, lineStr) f.SetRightMargin(rMargin) } } } // Ln performs a line break. The current abscissa goes back to the left margin // and the ordinate increases by the amount passed in parameter. A negative // value of h indicates the height of the last printed cell. // // This method is demonstrated in the example for MultiCell. func (f *Fpdf) Ln(h float64) { f.x = f.lMargin if h < 0 { f.y += f.lasth } else { f.y += h } } // ImageTypeFromMime returns the image type used in various image-related // functions (for example, Image()) that is associated with the specified MIME // type. For example, "jpg" is returned if mimeStr is "image/jpeg". An error is // set if the specified MIME type is not supported. func (f *Fpdf) ImageTypeFromMime(mimeStr string) (tp string) { switch mimeStr { case "image/png": tp = "png" case "image/jpg": tp = "jpg" case "image/jpeg": tp = "jpg" case "image/gif": tp = "gif" default: f.SetErrorf("unsupported image type: %s", mimeStr) } return } func (f *Fpdf) imageOut(info *ImageInfoType, x, y, w, h float64, allowNegativeX, flow bool, link int, linkStr string, density float64) { /* // density in dpi if density == 0.0 { density = 72.0 } */ // Automatic width and height calculation if needed if w == 0 && h == 0 { // Put image at 96 dpi w = -96 h = -96 } if w == -1 { // Set image width to whatever value for dpi we read // from the image or that was set manually w = -info.dpi } if h == -1 { // Set image height to whatever value for dpi we read // from the image or that was set manually h = -info.dpi } if w < 0 { w = -info.w * density / w / f.k } if h < 0 { h = -info.h * density / h / f.k } if w == 0 { w = h * info.w / info.h } if h == 0 { h = w * info.h / info.w } // Flowing mode if flow { if f.y+h > f.pageBreakTrigger && !f.inHeader && !f.inFooter && f.acceptPageBreak() { // Automatic page break x2 := f.x f.AddPageFormat(f.curOrientation, f.curPageSize) if f.err != nil { return } f.x = x2 } y = f.y f.y += h } if !allowNegativeX { if x < 0 { x = f.x } } // dbg("h %.2f", h) // q 85.04 0 0 NaN 28.35 NaN cm /I2 Do Q f.outf("q %.5f 0 0 %.5f %.5f %.5f cm /I%s Do Q", w*f.k, h*f.k, x*f.k, (f.h-(y+h))*f.k, info.i) if link > 0 || len(linkStr) > 0 { f.newLink(x, y, w, h, link, linkStr) } } // Image puts a JPEG, PNG or GIF image in the current page. // // Deprecated in favor of ImageOptions -- see that function for // details on the behavior of arguments func (f *Fpdf) Image(imageNameStr string, x, y, w, h float64, flow bool, tp string, link int, linkStr string, density float64) { options := ImageOptions{ ReadDpi: false, ImageType: tp, } f.ImageOptions(imageNameStr, x, y, w, h, flow, options, link, linkStr, density) } // ImageOptions puts a JPEG, PNG or GIF image in the current page. The size it // will take on the page can be specified in different ways. If both w and h // are 0, the image is rendered at 96 dpi. If either w or h is zero, it will be // calculated from the other dimension so that the aspect ratio is maintained. // If w and/or h are -1, the dpi for that dimension will be read from the // ImageInfoType object. PNG files can contain dpi information, and if present, // this information will be populated in the ImageInfoType object and used in // Width, Height, and Extent calculations. Otherwise, the SetDpi function can // be used to change the dpi from the default of 72. // // If w and h are any other negative value, their absolute values // indicate their dpi extents. // // Supported JPEG formats are 24 bit, 32 bit and gray scale. Supported PNG // formats are 24 bit, indexed color, and 8 bit indexed gray scale. If a GIF // image is animated, only the first frame is rendered. Transparency is // supported. It is possible to put a link on the image. // // imageNameStr may be the name of an image as registered with a call to either // RegisterImageReader() or RegisterImage(). In the first case, the image is // loaded using an io.Reader. This is generally useful when the image is // obtained from some other means than as a disk-based file. In the second // case, the image is loaded as a file. Alternatively, imageNameStr may // directly specify a sufficiently qualified filename. // // However the image is loaded, if it is used more than once only one copy is // embedded in the file. // // If x is negative, the current abscissa is used. // // If flow is true, the current y value is advanced after placing the image and // a page break may be made if necessary. // // If link refers to an internal page anchor (that is, it is non-zero; see // AddLink()), the image will be a clickable internal link. Otherwise, if // linkStr specifies a URL, the image will be a clickable external link. func (f *Fpdf) ImageOptions(imageNameStr string, x, y, w, h float64, flow bool, options ImageOptions, link int, linkStr string, density float64) { if f.err != nil { return } info := f.RegisterImageOptions(imageNameStr, options) if f.err != nil { return } f.imageOut(info, x, y, w, h, options.AllowNegativePosition, flow, link, linkStr, density) return } // RegisterImageReader registers an image, reading it from Reader r, adding it // to the PDF file but not adding it to the page. // // This function is now deprecated in favor of RegisterImageOptionsReader func (f *Fpdf) RegisterImageReader(imgName, tp string, r io.Reader) (info *ImageInfoType) { options := ImageOptions{ ReadDpi: false, ImageType: tp, } return f.RegisterImageOptionsReader(imgName, options, r) } // ImageOptions provides a place to hang any options we want to use while // parsing an image. // // ImageType's possible values are (case insensitive): // "JPG", "JPEG", "PNG" and "GIF". If empty, the type is inferred from // the file extension. // // ReadDpi defines whether to attempt to automatically read the image // dpi information from the image file. Normally, this should be set // to true (understanding that not all images will have this info // available). However, for backwards compatibility with previous // versions of the API, it defaults to false. // // AllowNegativePosition can be set to true in order to prevent the default // coercion of negative x values to the current x position. type ImageOptions struct { ImageType string ReadDpi bool AllowNegativePosition bool } // RegisterImageOptionsReader registers an image, reading it from Reader r, adding it // to the PDF file but not adding it to the page. Use Image() with the same // name to add the image to the page. Note that tp should be specified in this // case. // // See Image() for restrictions on the image and the options parameters. func (f *Fpdf) RegisterImageOptionsReader(imgName string, options ImageOptions, r io.Reader) (info *ImageInfoType) { // Thanks, Ivan Daniluk, for generalizing this code to use the Reader interface. if f.err != nil { return } info, ok := f.images[imgName] if ok { return } // First use of this image, get info if options.ImageType == "" { f.err = fmt.Errorf("image type should be specified if reading from custom reader") return } options.ImageType = strings.ToLower(options.ImageType) if options.ImageType == "jpeg" { options.ImageType = "jpg" } switch options.ImageType { case "jpg": info = f.parsejpg(r) case "png": info = f.parsepng(r, options.ReadDpi) case "gif": info = f.parsegif(r) default: f.err = fmt.Errorf("unsupported image type: %s", options.ImageType) } if f.err != nil { return } if info.i, f.err = generateImageID(info); f.err != nil { return } f.images[imgName] = info return } // RegisterImage registers an image, adding it to the PDF file but not adding // it to the page. Use Image() with the same filename to add the image to the // page. Note that Image() calls this function, so this function is only // necessary if you need information about the image before placing it. // // This function is now deprecated in favor of RegisterImageOptions. // See Image() for restrictions on the image and the "tp" parameters. func (f *Fpdf) RegisterImage(fileStr, tp string) (info *ImageInfoType) { options := ImageOptions{ ReadDpi: false, ImageType: tp, } return f.RegisterImageOptions(fileStr, options) } // RegisterImageOptions registers an image, adding it to the PDF file but not // adding it to the page. Use Image() with the same filename to add the image // to the page. Note that Image() calls this function, so this function is only // necessary if you need information about the image before placing it. See // Image() for restrictions on the image and the "tp" parameters. func (f *Fpdf) RegisterImageOptions(fileStr string, options ImageOptions) (info *ImageInfoType) { info, ok := f.images[fileStr] if ok { return } file, err := os.Open(fileStr) if err != nil { f.err = err return } defer file.Close() // First use of this image, get info if options.ImageType == "" { pos := strings.LastIndex(fileStr, ".") if pos < 0 { f.err = fmt.Errorf("image file has no extension and no type was specified: %s", fileStr) return } options.ImageType = fileStr[pos+1:] } return f.RegisterImageOptionsReader(fileStr, options, file) } // GetImageInfo returns information about the registered image specified by // imageStr. If the image has not been registered, nil is returned. The // internal error is not modified by this method. func (f *Fpdf) GetImageInfo(imageStr string) (info *ImageInfoType) { return f.images[imageStr] } // ImportObjects imports objects from gofpdi into current document func (f *Fpdf) ImportObjects(objs map[string][]byte) { for k, v := range objs { f.importedObjs[k] = v } } // ImportObjPos imports object hash positions from gofpdi func (f *Fpdf) ImportObjPos(objPos map[string]map[int]string) { for k, v := range objPos { f.importedObjPos[k] = v } } // putImportedTemplates writes the imported template objects to the PDF func (f *Fpdf) putImportedTemplates() { nOffset := f.n + 1 // keep track of list of sha1 hashes (to be replaced with integers) objsIDHash := make([]string, len(f.importedObjs)) // actual object data with new id objsIDData := make([][]byte, len(f.importedObjs)) // Populate hash slice and data slice i := 0 for k, v := range f.importedObjs { objsIDHash[i] = k objsIDData[i] = v i++ } // Populate a lookup table to get an object id from a hash hashToObjID := make(map[string]int, len(f.importedObjs)) for i = 0; i < len(objsIDHash); i++ { hashToObjID[objsIDHash[i]] = i + nOffset } // Now, replace hashes inside data with %040d object id for i = 0; i < len(objsIDData); i++ { // get hash hash := objsIDHash[i] for pos, h := range f.importedObjPos[hash] { // Convert object id into a 40 character string padded with spaces objIDPadded := fmt.Sprintf("%40s", fmt.Sprintf("%d", hashToObjID[h])) // Convert objIDPadded into []byte objIDBytes := []byte(objIDPadded) // Replace sha1 hash with object id padded for j := pos; j < pos+40; j++ { objsIDData[i][j] = objIDBytes[j-pos] } } // Save objsIDHash so that procset dictionary has the correct object ids f.importedTplIDs[hash] = i + nOffset } // Now, put objects for i = 0; i < len(objsIDData); i++ { f.newobj() f.out(string(objsIDData[i])) } } // UseImportedTemplate uses imported template from gofpdi. It draws imported // PDF page onto page. func (f *Fpdf) UseImportedTemplate(tplName string, scaleX float64, scaleY float64, tX float64, tY float64) { f.outf("q 0 J 1 w 0 j 0 G 0 g q %.4F 0 0 %.4F %.4F %.4F cm %s Do Q Q\n", scaleX*f.k, scaleY*f.k, tX*f.k, (tY+f.h)*f.k, tplName) } // ImportTemplates imports gofpdi template names into importedTplObjs for // inclusion in the procset dictionary func (f *Fpdf) ImportTemplates(tpls map[string]string) { for tplName, tplID := range tpls { f.importedTplObjs[tplName] = tplID } } // GetConversionRatio returns the conversion ratio based on the unit given when // creating the PDF. func (f *Fpdf) GetConversionRatio() float64 { return f.k } // GetXY returns the abscissa and ordinate of the current position. // // Note: the value returned for the abscissa will be affected by the current // cell margin. To account for this, you may need to either add the value // returned by GetCellMargin() to it or call SetCellMargin(0) to remove the // cell margin. func (f *Fpdf) GetXY() (float64, float64) { return f.x, f.y } // GetX returns the abscissa of the current position. // // Note: the value returned will be affected by the current cell margin. To // account for this, you may need to either add the value returned by // GetCellMargin() to it or call SetCellMargin(0) to remove the cell margin. func (f *Fpdf) GetX() float64 { return f.x } // SetX defines the abscissa of the current position. If the passed value is // negative, it is relative to the right of the page. func (f *Fpdf) SetX(x float64) { if x >= 0 { f.x = x } else { f.x = f.w + x } } // GetY returns the ordinate of the current position. func (f *Fpdf) GetY() float64 { return f.y } // SetY moves the current abscissa back to the left margin and sets the // ordinate. If the passed value is negative, it is relative to the bottom of // the page. func (f *Fpdf) SetY(y float64) { // dbg("SetY x %.2f, lMargin %.2f", f.x, f.lMargin) f.x = f.lMargin if y >= 0 { f.y = y } else { f.y = f.h + y } } // SetHomeXY is a convenience method that sets the current position to the left // and top margins. func (f *Fpdf) SetHomeXY() { f.SetY(f.tMargin) f.SetX(f.lMargin) } // SetXY defines the abscissa and ordinate of the current position. If the // passed values are negative, they are relative respectively to the right and // bottom of the page. func (f *Fpdf) SetXY(x, y float64) { f.SetY(y) f.SetX(x) } // SetProtection applies certain constraints on the finished PDF document. // // actionFlag is a bitflag that controls various document operations. // CnProtectPrint allows the document to be printed. CnProtectModify allows a // document to be modified by a PDF editor. CnProtectCopy allows text and // images to be copied into the system clipboard. CnProtectAnnotForms allows // annotations and forms to be added by a PDF editor. These values can be // combined by or-ing them together, for example, // CnProtectCopy|CnProtectModify. This flag is advisory; not all PDF readers // implement the constraints that this argument attempts to control. // // userPassStr specifies the password that will need to be provided to view the // contents of the PDF. The permissions specified by actionFlag will apply. // // ownerPassStr specifies the password that will need to be provided to gain // full access to the document regardless of the actionFlag value. An empty // string for this argument will be replaced with a random value, effectively // prohibiting full access to the document. func (f *Fpdf) SetProtection(actionFlag byte, userPassStr, ownerPassStr string) { if f.err != nil { return } f.protect.setProtection(actionFlag, userPassStr, ownerPassStr) } // OutputAndClose sends the PDF document to the writer specified by w. This // method will close both f and w, even if an error is detected and no document // is produced. func (f *Fpdf) OutputAndClose(w io.WriteCloser) error { f.Output(w) w.Close() return f.err } // OutputFileAndClose creates or truncates the file specified by fileStr and // writes the PDF document to it. This method will close f and the newly // written file, even if an error is detected and no document is produced. // // Most examples demonstrate the use of this method. func (f *Fpdf) OutputFileAndClose(fileStr string) error { if f.err == nil { pdfFile, err := os.Create(fileStr) if err == nil { f.Output(pdfFile) pdfFile.Close() } else { f.err = err } } return f.err } // Output sends the PDF document to the writer specified by w. No output will // take place if an error has occurred in the document generation process. w // remains open after this function returns. After returning, f is in a closed // state and its methods should not be called. func (f *Fpdf) Output(w io.Writer) error { if f.err != nil { return f.err } // dbg("Output") if f.state < 3 { f.Close() } _, err := f.buffer.WriteTo(w) if err != nil { f.err = err } return f.err } func (f *Fpdf) getpagesizestr(sizeStr string) (size SizeType) { if f.err != nil { return } sizeStr = strings.ToLower(sizeStr) // dbg("Size [%s]", sizeStr) var ok bool //20210302 archium addendum begin //enable custom page formats if strings.Contains(sizeStr, "x") { var ( aString []string aFloat [2]float64 aErr error ) aString = strings.SplitN(sizeStr, "x", 2) //fmt.Println(aString[0], aString[1]) for i := 0; i <= 1; i++ { aFloat[i], aErr = strconv.ParseFloat(aString[i], 64) if aErr != nil { f.err = fmt.Errorf("unknown page size %s", sizeStr) } } size, ok = SizeType{(aFloat[0] * f.k), (aFloat[1] * f.k)}, true } else { //20210302 archium addendum end size, ok = f.stdPageSizes[sizeStr] //20210302 archium addendum begin } //20210302 archium addendum end if ok { // dbg("found %s", sizeStr) size.Wd /= f.k size.Ht /= f.k } else { f.err = fmt.Errorf("unknown page size %s", sizeStr) } return } // GetPageSizeStr returns the SizeType for the given sizeStr (that is A4, A3, etc..) func (f *Fpdf) GetPageSizeStr(sizeStr string) (size SizeType) { return f.getpagesizestr(sizeStr) } func (f *Fpdf) _getpagesize(size SizeType) SizeType { if size.Wd > size.Ht { size.Wd, size.Ht = size.Ht, size.Wd } return size } func (f *Fpdf) beginpage(orientationStr string, size SizeType) { if f.err != nil { return } f.page++ // add the default page boxes, if any exist, to the page f.pageBoxes[f.page] = make(map[string]PageBox) for box, pb := range f.defPageBoxes { f.pageBoxes[f.page][box] = pb } f.pages = append(f.pages, bytes.NewBufferString("")) f.pageLinks = append(f.pageLinks, make([]linkType, 0, 0)) f.pageAttachments = append(f.pageAttachments, []annotationAttach{}) f.state = 2 f.x = f.lMargin f.y = f.tMargin f.fontFamily = "" // Check page size and orientation if orientationStr == "" { orientationStr = f.defOrientation } else { orientationStr = strings.ToUpper(orientationStr[0:1]) } if orientationStr != f.curOrientation || size.Wd != f.curPageSize.Wd || size.Ht != f.curPageSize.Ht { // New size or orientation if orientationStr == "P" { f.w = size.Wd f.h = size.Ht } else { f.w = size.Ht f.h = size.Wd } f.wPt = f.w * f.k f.hPt = f.h * f.k f.pageBreakTrigger = f.h - f.bMargin f.curOrientation = orientationStr f.curPageSize = size } if orientationStr != f.defOrientation || size.Wd != f.defPageSize.Wd || size.Ht != f.defPageSize.Ht { f.pageSizes[f.page] = SizeType{f.wPt, f.hPt} } return } func (f *Fpdf) endpage() { f.EndLayer() f.state = 1 } // Load a font definition file from the given Reader func (f *Fpdf) loadfont(r io.Reader) (def fontDefType) { if f.err != nil { return } // dbg("Loading font [%s]", fontStr) var buf bytes.Buffer _, err := buf.ReadFrom(r) if err != nil { f.err = err return } err = json.Unmarshal(buf.Bytes(), &def) if err != nil { f.err = err return } if def.i, err = generateFontID(def); err != nil { f.err = err } // dump(def) return } // Escape special characters in strings func (f *Fpdf) escape(s string) string { s = strings.Replace(s, "\\", "\\\\", -1) s = strings.Replace(s, "(", "\\(", -1) s = strings.Replace(s, ")", "\\)", -1) s = strings.Replace(s, "\r", "\\r", -1) return s } // textstring formats a text string func (f *Fpdf) textstring(s string) string { if f.protect.encrypted { b := []byte(s) f.protect.rc4(uint32(f.n), &b) s = string(b) } return "(" + f.escape(s) + ")" } func blankCount(str string) (count int) { l := len(str) for j := 0; j < l; j++ { if byte(' ') == str[j] { count++ } } return } // SetUnderlineThickness accepts a multiplier for adjusting the text underline // thickness, defaulting to 1. See SetUnderlineThickness example. func (f *Fpdf) SetUnderlineThickness(thickness float64) { f.userUnderlineThickness = thickness } // Underline text func (f *Fpdf) dounderline(x, y float64, txt string) string { up := float64(f.currentFont.Up) ut := float64(f.currentFont.Ut) * f.userUnderlineThickness w := f.GetStringWidth(txt) + f.ws*float64(blankCount(txt)) return sprintf("%.2f %.2f %.2f %.2f re f", x*f.k, (f.h-(y-up/1000*f.fontSize))*f.k, w*f.k, -ut/1000*f.fontSizePt) } func (f *Fpdf) dostrikeout(x, y float64, txt string) string { up := float64(f.currentFont.Up) ut := float64(f.currentFont.Ut) w := f.GetStringWidth(txt) + f.ws*float64(blankCount(txt)) return sprintf("%.2f %.2f %.2f %.2f re f", x*f.k, (f.h-(y+4*up/1000*f.fontSize))*f.k, w*f.k, -ut/1000*f.fontSizePt) } func bufEqual(buf []byte, str string) bool { return string(buf[0:len(str)]) == str } func be16(buf []byte) int { return 256*int(buf[0]) + int(buf[1]) } func (f *Fpdf) newImageInfo() *ImageInfoType { // default dpi to 72 unless told otherwise return &ImageInfoType{scale: f.k, dpi: f.density} } // parsejpg extracts info from io.Reader with JPEG data // Thank you, Bruno Michel, for providing this code. func (f *Fpdf) parsejpg(r io.Reader) (info *ImageInfoType) { info = f.newImageInfo() var ( data bytes.Buffer err error ) _, err = data.ReadFrom(r) if err != nil { f.err = err return } info.data = data.Bytes() config, err := jpeg.DecodeConfig(bytes.NewReader(info.data)) if err != nil { f.err = err return } info.w = float64(config.Width) info.h = float64(config.Height) info.f = "DCTDecode" info.bpc = 8 switch config.ColorModel { case color.GrayModel: info.cs = "DeviceGray" case color.YCbCrModel: info.cs = "DeviceRGB" case color.CMYKModel: info.cs = "DeviceCMYK" default: f.err = fmt.Errorf("image JPEG buffer has unsupported color space (%v)", config.ColorModel) return } return } // parsepng extracts info from a PNG data func (f *Fpdf) parsepng(r io.Reader, readdpi bool) (info *ImageInfoType) { buf, err := bufferFromReader(r) if err != nil { f.err = err return } return f.parsepngstream(buf, readdpi) } func (f *Fpdf) readBeInt32(r io.Reader) (val int32) { err := binary.Read(r, binary.BigEndian, &val) if err != nil && err != io.EOF { f.err = err } return } func (f *Fpdf) readByte(r io.Reader) (val byte) { err := binary.Read(r, binary.BigEndian, &val) if err != nil { f.err = err } return } // parsegif extracts info from a GIF data (via PNG conversion) func (f *Fpdf) parsegif(r io.Reader) (info *ImageInfoType) { data, err := bufferFromReader(r) if err != nil { f.err = err return } var img image.Image img, err = gif.Decode(data) if err != nil { f.err = err return } pngBuf := new(bytes.Buffer) err = png.Encode(pngBuf, img) if err != nil { f.err = err return } return f.parsepngstream(pngBuf, false) } // newobj begins a new object func (f *Fpdf) newobj() { // dbg("newobj") f.n++ for j := len(f.offsets); j <= f.n; j++ { f.offsets = append(f.offsets, 0) } f.offsets[f.n] = f.buffer.Len() f.outf("%d 0 obj", f.n) } func (f *Fpdf) putstream(b []byte) { // dbg("putstream") if f.protect.encrypted { f.protect.rc4(uint32(f.n), &b) } f.out("stream") f.out(string(b)) f.out("endstream") } // out; Add a line to the document func (f *Fpdf) out(s string) { if f.state == 2 { f.pages[f.page].WriteString(s) f.pages[f.page].WriteString("\n") } else { f.buffer.WriteString(s) f.buffer.WriteString("\n") } } // outbuf adds a buffered line to the document func (f *Fpdf) outbuf(r io.Reader) { if f.state == 2 { f.pages[f.page].ReadFrom(r) f.pages[f.page].WriteString("\n") } else { f.buffer.ReadFrom(r) f.buffer.WriteString("\n") } } // RawWriteStr writes a string directly to the PDF generation buffer. This is a // low-level function that is not required for normal PDF construction. An // understanding of the PDF specification is needed to use this method // correctly. func (f *Fpdf) RawWriteStr(str string) { f.out(str) } // RawWriteBuf writes the contents of the specified buffer directly to the PDF // generation buffer. This is a low-level function that is not required for // normal PDF construction. An understanding of the PDF specification is needed // to use this method correctly. func (f *Fpdf) RawWriteBuf(r io.Reader) { f.outbuf(r) } // outf adds a formatted line to the document func (f *Fpdf) outf(fmtStr string, args ...interface{}) { f.out(sprintf(fmtStr, args...)) } // SetDefaultCatalogSort sets the default value of the catalog sort flag that // will be used when initializing a new Fpdf instance. See SetCatalogSort() for // more details. func SetDefaultCatalogSort(flag bool) { gl.catalogSort = flag } // SetCatalogSort sets a flag that will be used, if true, to consistently order // the document's internal resource catalogs. This method is typically only // used for test purposes to facilitate PDF comparison. func (f *Fpdf) SetCatalogSort(flag bool) { f.catalogSort = flag } // SetDefaultCreationDate sets the default value of the document creation date // that will be used when initializing a new Fpdf instance. See // SetCreationDate() for more details. func SetDefaultCreationDate(tm time.Time) { gl.creationDate = tm } // SetDefaultModificationDate sets the default value of the document modification date // that will be used when initializing a new Fpdf instance. See // SetCreationDate() for more details. func SetDefaultModificationDate(tm time.Time) { gl.modDate = tm } // SetCreationDate fixes the document's internal CreationDate value. By // default, the time when the document is generated is used for this value. // This method is typically only used for testing purposes to facilitate PDF // comparison. Specify a zero-value time to revert to the default behavior. func (f *Fpdf) SetCreationDate(tm time.Time) { f.creationDate = tm } // SetModificationDate fixes the document's internal ModDate value. // See `SetCreationDate` for more details. func (f *Fpdf) SetModificationDate(tm time.Time) { f.modDate = tm } // SetJavascript adds Adobe JavaScript to the document. func (f *Fpdf) SetJavascript(script string) { f.javascript = &script } // RegisterAlias adds an (alias, replacement) pair to the document so we can // replace all occurrences of that alias after writing but before the document // is closed. Functions ExampleFpdf_RegisterAlias() and // ExampleFpdf_RegisterAlias_utf8() in fpdf_test.go demonstrate this method. func (f *Fpdf) RegisterAlias(alias, replacement string) { // Note: map[string]string assignments embed literal escape ("\00") sequences // into utf16 key and value strings. Consequently, subsequent search/replace // operations will fail unexpectedly if utf8toutf16() conversions take place // here. Instead, conversions are deferred until the actual search/replace // operation takes place when the PDF output is generated. f.aliasMap[alias] = replacement } func (f *Fpdf) replaceAliases() { for mode := 0; mode < 2; mode++ { for alias, replacement := range f.aliasMap { if mode == 1 { alias = utf8toutf16(alias, false) replacement = utf8toutf16(replacement, false) } for n := 1; n <= f.page; n++ { s := f.pages[n].String() if strings.Contains(s, alias) { s = strings.Replace(s, alias, replacement, -1) f.pages[n].Truncate(0) f.pages[n].WriteString(s) } } } } } func (f *Fpdf) putpages() { var wPt, hPt float64 var pageSize SizeType var ok bool nb := f.page if len(f.aliasNbPagesStr) > 0 { // Replace number of pages f.RegisterAlias(f.aliasNbPagesStr, sprintf("%d", nb)) } f.replaceAliases() if f.defOrientation == "P" { wPt = f.defPageSize.Wd * f.k hPt = f.defPageSize.Ht * f.k } else { wPt = f.defPageSize.Ht * f.k hPt = f.defPageSize.Wd * f.k } pagesObjectNumbers := make([]int, nb+1) // 1-based for n := 1; n <= nb; n++ { // Page f.newobj() pagesObjectNumbers[n] = f.n // save for /Kids f.out("< 0 { var annots fmtBuffer annots.printf("/Annots [") for _, pl := range f.pageLinks[n] { annots.printf("<>>>", f.textstring(pl.linkStr)) } else { l := f.links[pl.link] var sz SizeType var h float64 sz, ok = f.pageSizes[l.page] if ok { h = sz.Ht } else { h = hPt } // dbg("h [%.2f], l.y [%.2f] f.k [%.2f]\n", h, l.y, f.k) annots.printf("/Dest [%d 0 R /XYZ 0 %.2f null]>>", 1+2*l.page, h-l.y*f.k) } } f.putAttachmentAnnotationLinks(&annots, n) annots.printf("]") f.out(annots.String()) } if f.pdfVersion > "1.3" { f.out("/Group <>") } f.outf("/Contents %d 0 R>>", f.n+1) f.out("endobj") // Page content f.newobj() if f.compress { data := sliceCompress(f.pages[n].Bytes()) f.outf("<>", len(data)) f.putstream(data) } else { f.outf("<>", f.pages[n].Len()) f.putstream(f.pages[n].Bytes()) } f.out("endobj") } // Pages root f.offsets[1] = f.buffer.Len() f.out("1 0 obj") f.out("<>") f.out("endobj") } func (f *Fpdf) putfonts() { if f.err != nil { return } nf := f.n for _, diff := range f.diffs { // Encodings f.newobj() f.outf("<>", diff) f.out("endobj") } { var fileList []string var info fontFileType var file string for file = range f.fontFiles { fileList = append(fileList, file) } if f.catalogSort { sort.SliceStable(fileList, func(i, j int) bool { return fileList[i] < fileList[j] }) } for _, file = range fileList { info = f.fontFiles[file] if info.fontType != "UTF8" { f.newobj() info.n = f.n f.fontFiles[file] = info var font []byte if info.embedded { font = info.content } else { var err error font, err = f.loadFontFile(file) if err != nil { f.err = err return } } compressed := file[len(file)-2:] == ".z" if !compressed && info.length2 > 0 { buf := font[6:info.length1] buf = append(buf, font[6+info.length1+6:info.length2]...) font = buf } f.outf("< 0 { f.outf("/Length2 %d /Length3 0", info.length2) } f.out(">>") f.putstream(font) f.out("endobj") } } } { var keyList []string var font fontDefType var key string for key = range f.fonts { keyList = append(keyList, key) } if f.catalogSort { sort.SliceStable(keyList, func(i, j int) bool { return keyList[i] < keyList[j] }) } for _, key = range keyList { font = f.fonts[key] // Font objects font.N = f.n + 1 f.fonts[key] = font tp := font.Tp name := font.Name switch tp { case "Core": // Core font f.newobj() f.out("<>") f.out("endobj") case "Type1": fallthrough case "TrueType": // Additional Type1 or TrueType/OpenType font f.newobj() f.out("< 0 { f.outf("/Encoding %d 0 R", nf+font.DiffN) } else { f.out("/Encoding /WinAnsiEncoding") } f.out(">>") f.out("endobj") // Widths f.newobj() var s fmtBuffer s.WriteString("[") for j := 32; j < 256; j++ { s.printf("%d ", font.Cw[j]) } s.WriteString("]") f.out(s.String()) f.out("endobj") // Descriptor f.newobj() s.Truncate(0) s.printf("<>", suffix, f.fontFiles[font.File].n) f.out(s.String()) f.out("endobj") case "UTF8": fontName := "utf8" + font.Name usedRunes := font.usedRunes delete(usedRunes, 0) utf8FontStream := font.utf8File.GenerateCutFont(usedRunes) utf8FontSize := len(utf8FontStream) compressedFontStream := sliceCompress(utf8FontStream) CodeSignDictionary := font.utf8File.CodeSymbolDictionary delete(CodeSignDictionary, 0) f.newobj() f.out(fmt.Sprintf("<>\n"+"endobj", fontName, f.n+1, f.n+2)) f.newobj() f.out("<>") f.out("endobj") f.newobj() f.out("<>") f.putstream([]byte(toUnicode)) f.out("endobj") // CIDInfo f.newobj() f.out("<>") f.out("endobj") // Font descriptor f.newobj() var s fmtBuffer s.printf("<>") f.out(s.String()) f.out("endobj") // Embed CIDToGIDMap cidToGidMap := make([]byte, 256*256*2) for cc, glyph := range CodeSignDictionary { cidToGidMap[cc*2] = byte(glyph >> 8) cidToGidMap[cc*2+1] = byte(glyph & 0xFF) } cidToGidMap = sliceCompress(cidToGidMap) f.newobj() f.out("<>") f.putstream(cidToGidMap) f.out("endobj") //Font file f.newobj() f.out("<>") f.putstream(compressedFontStream) f.out("endobj") default: f.err = fmt.Errorf("unsupported font type: %s", tp) return } } } return } func (f *Fpdf) generateCIDFontMap(font *fontDefType, LastRune int) { rangeID := 0 cidArray := make(map[int]*untypedKeyMap) cidArrayKeys := make([]int, 0) prevCid := -2 prevWidth := -1 interval := false startCid := 1 cwLen := LastRune + 1 // for each character for cid := startCid; cid < cwLen; cid++ { if font.Cw[cid] == 0x00 { continue } width := font.Cw[cid] if width == 65535 { width = 0 } if numb, OK := font.usedRunes[cid]; cid > 255 && (!OK || numb == 0) { continue } if cid == prevCid+1 { if width == prevWidth { if width == cidArray[rangeID].get(0) { cidArray[rangeID].put(nil, width) } else { cidArray[rangeID].pop() rangeID = prevCid r := untypedKeyMap{ valueSet: make([]int, 0), keySet: make([]interface{}, 0), } cidArray[rangeID] = &r cidArrayKeys = append(cidArrayKeys, rangeID) cidArray[rangeID].put(nil, prevWidth) cidArray[rangeID].put(nil, width) } interval = true cidArray[rangeID].put("interval", 1) } else { if interval { // new range rangeID = cid r := untypedKeyMap{ valueSet: make([]int, 0), keySet: make([]interface{}, 0), } cidArray[rangeID] = &r cidArrayKeys = append(cidArrayKeys, rangeID) cidArray[rangeID].put(nil, width) } else { cidArray[rangeID].put(nil, width) } interval = false } } else { rangeID = cid r := untypedKeyMap{ valueSet: make([]int, 0), keySet: make([]interface{}, 0), } cidArray[rangeID] = &r cidArrayKeys = append(cidArrayKeys, rangeID) cidArray[rangeID].put(nil, width) interval = false } prevCid = cid prevWidth = width } previousKey := -1 nextKey := -1 isInterval := false for g := 0; g < len(cidArrayKeys); { key := cidArrayKeys[g] ws := *cidArray[key] cws := len(ws.keySet) if (key == nextKey) && (!isInterval) && (ws.getIndex("interval") < 0 || cws < 4) { if cidArray[key].getIndex("interval") >= 0 { cidArray[key].delete("interval") } cidArray[previousKey] = arrayMerge(cidArray[previousKey], cidArray[key]) cidArrayKeys = remove(cidArrayKeys, key) } else { g++ previousKey = key } nextKey = key + cws // ui := ws.getIndex("interval") // ui = ui + 1 if ws.getIndex("interval") >= 0 { if cws > 3 { isInterval = true } else { isInterval = false } cidArray[key].delete("interval") nextKey-- } else { isInterval = false } } var w fmtBuffer for _, k := range cidArrayKeys { ws := cidArray[k] if len(arrayCountValues(ws.valueSet)) == 1 { w.printf(" %d %d %d", k, k+len(ws.valueSet)-1, ws.get(0)) } else { w.printf(" %d [ %s ]\n", k, implode(" ", ws.valueSet)) } } f.out("/W [" + w.String() + " ]") } func implode(sep string, arr []int) string { var s fmtBuffer for i := 0; i < len(arr)-1; i++ { s.printf("%v", arr[i]) s.printf(sep) } if len(arr) > 0 { s.printf("%v", arr[len(arr)-1]) } return s.String() } // arrayCountValues counts the occurrences of each item in the $mp array. func arrayCountValues(mp []int) map[int]int { answer := make(map[int]int) for _, v := range mp { answer[v] = answer[v] + 1 } return answer } func (f *Fpdf) loadFontFile(name string) ([]byte, error) { if f.fontLoader != nil { reader, err := f.fontLoader.Open(name) if err == nil { data, err := ioutil.ReadAll(reader) if closer, ok := reader.(io.Closer); ok { closer.Close() } return data, err } } return ioutil.ReadFile(path.Join(f.fontpath, name)) } func (f *Fpdf) putimages() { var keyList []string var key string for key = range f.images { keyList = append(keyList, key) } // Sort the keyList []string by the corresponding image's width. if f.catalogSort { sort.SliceStable(keyList, func(i, j int) bool { return f.images[keyList[i]].w < f.images[keyList[j]].w }) } // Maintain a list of inserted image SHA-1 hashes, with their // corresponding object ID number. insertedImages := map[string]int{} for _, key = range keyList { image := f.images[key] // Check if this image has already been inserted using it's SHA-1 hash. insertedImageObjN, isFound := insertedImages[image.i] // If found, skip inserting the image as a new object, and // use the object ID from the insertedImages map. // If not, insert the image into the PDF and store the object ID. if isFound { image.n = insertedImageObjN } else { f.putimage(image) insertedImages[image.i] = image.n } } } func (f *Fpdf) putimage(info *ImageInfoType) { f.newobj() info.n = f.n f.out("< 0 { f.outf("/Filter /%s", info.f) } if len(info.dp) > 0 { f.outf("/DecodeParms <<%s>>", info.dp) } if len(info.trns) > 0 { var trns fmtBuffer for _, v := range info.trns { trns.printf("%d %d ", v, v) } f.outf("/Mask [%s]", trns.String()) } if info.smask != nil { f.outf("/SMask %d 0 R", f.n+1) } f.outf("/Length %d>>", len(info.data)) f.putstream(info.data) f.out("endobj") // Soft mask if len(info.smask) > 0 { smask := &ImageInfoType{ w: info.w, h: info.h, cs: "DeviceGray", bpc: 8, f: info.f, dp: sprintf("/Predictor 15 /Colors 1 /BitsPerComponent 8 /Columns %d", int(info.w)), data: info.smask, scale: f.k, } f.putimage(smask) } // Palette if info.cs == "Indexed" { f.newobj() if f.compress { pal := sliceCompress(info.pal) f.outf("<>", len(pal)) f.putstream(pal) } else { f.outf("<>", len(info.pal)) f.putstream(info.pal) } f.out("endobj") } } func (f *Fpdf) putxobjectdict() { { var image *ImageInfoType var key string var keyList []string for key = range f.images { keyList = append(keyList, key) } if f.catalogSort { sort.SliceStable(keyList, func(i, j int) bool { return f.images[keyList[i]].i < f.images[keyList[j]].i }) } for _, key = range keyList { image = f.images[key] f.outf("/I%s %d 0 R", image.i, image.n) } } { var keyList []string var key string var tpl Template keyList = templateKeyList(f.templates, f.catalogSort) for _, key = range keyList { tpl = f.templates[key] // for _, tpl := range f.templates { id := tpl.ID() if objID, ok := f.templateObjects[id]; ok { f.outf("/TPL%s %d 0 R", id, objID) } } } { for tplName, objID := range f.importedTplObjs { // here replace obj id hash with n f.outf("%s %d 0 R", tplName, f.importedTplIDs[objID]) } } } func (f *Fpdf) putresourcedict() { f.out("/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]") f.out("/Font <<") { var keyList []string var font fontDefType var key string for key = range f.fonts { keyList = append(keyList, key) } if f.catalogSort { sort.SliceStable(keyList, func(i, j int) bool { return f.fonts[keyList[i]].i < f.fonts[keyList[j]].i }) } for _, key = range keyList { font = f.fonts[key] f.outf("/F%s %d 0 R", font.i, font.N) } } f.out(">>") f.out("/XObject <<") f.putxobjectdict() f.out(">>") count := len(f.blendList) if count > 1 { f.out("/ExtGState <<") for j := 1; j < count; j++ { f.outf("/GS%d %d 0 R", j, f.blendList[j].objNum) } f.out(">>") } count = len(f.gradientList) if count > 1 { f.out("/Shading <<") for j := 1; j < count; j++ { f.outf("/Sh%d %d 0 R", j, f.gradientList[j].objNum) } f.out(">>") } // Layers f.layerPutResourceDict() f.spotColorPutResourceDict() } func (f *Fpdf) putBlendModes() { count := len(f.blendList) for j := 1; j < count; j++ { bl := f.blendList[j] f.newobj() f.blendList[j].objNum = f.n f.outf("<>", bl.fillStr, bl.strokeStr, bl.modeStr) f.out("endobj") } } func (f *Fpdf) putGradients() { count := len(f.gradientList) for j := 1; j < count; j++ { var f1 int gr := f.gradientList[j] if gr.tp == 2 || gr.tp == 3 { f.newobj() f.outf("<>", gr.clr1Str, gr.clr2Str) f.out("endobj") f1 = f.n } f.newobj() f.outf("<>", gr.x1, gr.y1, gr.x2, gr.y2, f1) } else if gr.tp == 3 { f.outf("/Coords [%.5f %.5f 0 %.5f %.5f %.5f] /Function %d 0 R /Extend [true true]>>", gr.x1, gr.y1, gr.x2, gr.y2, gr.r, f1) } f.out("endobj") f.gradientList[j].objNum = f.n } } func (f *Fpdf) putjavascript() { if f.javascript == nil { return } f.newobj() f.nJs = f.n f.out("<<") f.outf("/Names [(EmbeddedJS) %d 0 R]", f.n+1) f.out(">>") f.out("endobj") f.newobj() f.out("<<") f.out("/S /JavaScript") f.outf("/JS %s", f.textstring(*f.javascript)) f.out(">>") f.out("endobj") } func (f *Fpdf) putresources() { if f.err != nil { return } f.layerPutLayers() f.putBlendModes() f.putGradients() f.putSpotColors() f.putfonts() if f.err != nil { return } f.putimages() f.putTemplates() f.putImportedTemplates() // gofpdi // Resource dictionary f.offsets[2] = f.buffer.Len() f.out("2 0 obj") f.out("<<") f.putresourcedict() f.out(">>") f.out("endobj") f.putjavascript() if f.protect.encrypted { f.newobj() f.protect.objNum = f.n f.out("<<") f.out("/Filter /Standard") f.out("/V 1") f.out("/R 2") f.outf("/O (%s)", f.escape(string(f.protect.oValue))) f.outf("/U (%s)", f.escape(string(f.protect.uValue))) f.outf("/P %d", f.protect.pValue) f.out(">>") f.out("endobj") } return } // returns Now() if tm is zero func timeOrNow(tm time.Time) time.Time { if tm.IsZero() { return time.Now() } return tm } func (f *Fpdf) putinfo() { if len(f.producer) > 0 { f.outf("/Producer %s", f.textstring(f.producer)) } if len(f.title) > 0 { f.outf("/Title %s", f.textstring(f.title)) } if len(f.subject) > 0 { f.outf("/Subject %s", f.textstring(f.subject)) } if len(f.author) > 0 { f.outf("/Author %s", f.textstring(f.author)) } if len(f.keywords) > 0 { f.outf("/Keywords %s", f.textstring(f.keywords)) } if len(f.creator) > 0 { f.outf("/Creator %s", f.textstring(f.creator)) } creation := timeOrNow(f.creationDate) f.outf("/CreationDate %s", f.textstring("D:"+creation.Format("20060102150405"))) mod := timeOrNow(f.modDate) f.outf("/ModDate %s", f.textstring("D:"+mod.Format("20060102150405"))) } func (f *Fpdf) putcatalog() { f.out("/Type /Catalog") f.out("/Pages 1 0 R") switch f.zoomMode { case "fullpage": f.out("/OpenAction [3 0 R /Fit]") case "fullwidth": f.out("/OpenAction [3 0 R /FitH null]") case "real": f.out("/OpenAction [3 0 R /XYZ null null 1]") } // } else if !is_string($this->zoomMode)) // $this->out('/OpenAction [3 0 R /XYZ null null '.sprintf('%.2f',$this->zoomMode/100).']'); switch f.layoutMode { case "single", "SinglePage": f.out("/PageLayout /SinglePage") case "continuous", "OneColumn": f.out("/PageLayout /OneColumn") case "two", "TwoColumnLeft": f.out("/PageLayout /TwoColumnLeft") case "TwoColumnRight": f.out("/PageLayout /TwoColumnRight") case "TwoPageLeft", "TwoPageRight": if f.pdfVersion < "1.5" { f.pdfVersion = "1.5" } f.out("/PageLayout /" + f.layoutMode) } // Bookmarks if len(f.outlines) > 0 { f.outf("/Outlines %d 0 R", f.outlineRoot) f.out("/PageMode /UseOutlines") } // Layers f.layerPutCatalog() // Name dictionary : // -> Javascript // -> Embedded files f.out("/Names <<") // JavaScript if f.javascript != nil { f.outf("/JavaScript %d 0 R", f.nJs) } // Embedded files f.outf("/EmbeddedFiles %s", f.getEmbeddedFiles()) f.out(">>") } func (f *Fpdf) putheader() { if len(f.blendMap) > 0 && f.pdfVersion < "1.4" { f.pdfVersion = "1.4" } f.outf("%%PDF-%s", f.pdfVersion) } func (f *Fpdf) puttrailer() { f.outf("/Size %d", f.n+1) f.outf("/Root %d 0 R", f.n) f.outf("/Info %d 0 R", f.n-1) if f.protect.encrypted { f.outf("/Encrypt %d 0 R", f.protect.objNum) f.out("/ID [()()]") } } func (f *Fpdf) putxmp() { if len(f.xmp) == 0 { return } f.newobj() f.outf("<< /Type /Metadata /Subtype /XML /Length %d >>", len(f.xmp)) f.putstream(f.xmp) f.out("endobj") } func (f *Fpdf) putbookmarks() { nb := len(f.outlines) if nb > 0 { lru := make(map[int]int) level := 0 for i, o := range f.outlines { if o.level > 0 { parent := lru[o.level-1] f.outlines[i].parent = parent f.outlines[parent].last = i if o.level > level { f.outlines[parent].first = i } } else { f.outlines[i].parent = nb } if o.level <= level && i > 0 { prev := lru[o.level] f.outlines[prev].next = i f.outlines[i].prev = prev } lru[o.level] = i level = o.level } n := f.n + 1 for _, o := range f.outlines { f.newobj() f.outf("<>") f.out("endobj") } f.newobj() f.outlineRoot = f.n f.outf("<>", n+lru[0]) f.out("endobj") } } func (f *Fpdf) enddoc() { if f.err != nil { return } f.layerEndDoc() f.putheader() // Embedded files f.putAttachments() f.putAnnotationsAttachments() f.putpages() f.putresources() if f.err != nil { return } // Bookmarks f.putbookmarks() // Metadata f.putxmp() // Info f.newobj() f.out("<<") f.putinfo() f.out(">>") f.out("endobj") // Catalog f.newobj() f.out("<<") f.putcatalog() f.out(">>") f.out("endobj") // Cross-ref o := f.buffer.Len() f.out("xref") f.outf("0 %d", f.n+1) f.out("0000000000 65535 f ") for j := 1; j <= f.n; j++ { f.outf("%010d 00000 n ", f.offsets[j]) } // Trailer f.out("trailer") f.out("<<") f.puttrailer() f.out(">>") f.out("startxref") f.outf("%d", o) f.out("%%EOF") f.state = 3 return } // Path Drawing // MoveTo moves the stylus to (x, y) without drawing the path from the // previous point. Paths must start with a MoveTo to set the original // stylus location or the result is undefined. // // Create a "path" by moving a virtual stylus around the page (with // MoveTo, LineTo, CurveTo, CurveBezierCubicTo, ArcTo & ClosePath) // then draw it or fill it in (with DrawPath). The main advantage of // using the path drawing routines rather than multiple Fpdf.Line is // that PDF creates nice line joins at the angles, rather than just // overlaying the lines. func (f *Fpdf) MoveTo(x, y float64) { f.point(x, y) f.x, f.y = x, y } // LineTo creates a line from the current stylus location to (x, y), which // becomes the new stylus location. Note that this only creates the line in // the path; it does not actually draw the line on the page. // // The MoveTo() example demonstrates this method. func (f *Fpdf) LineTo(x, y float64) { f.outf("%.2f %.2f l", x*f.k, (f.h-y)*f.k) f.x, f.y = x, y } // CurveTo creates a single-segment quadratic Bézier curve. The curve starts at // the current stylus location and ends at the point (x, y). The control point // (cx, cy) specifies the curvature. At the start point, the curve is tangent // to the straight line between the current stylus location and the control // point. At the end point, the curve is tangent to the straight line between // the end point and the control point. // // The MoveTo() example demonstrates this method. func (f *Fpdf) CurveTo(cx, cy, x, y float64) { f.outf("%.5f %.5f %.5f %.5f v", cx*f.k, (f.h-cy)*f.k, x*f.k, (f.h-y)*f.k) f.x, f.y = x, y } // CurveBezierCubicTo creates a single-segment cubic Bézier curve. The curve // starts at the current stylus location and ends at the point (x, y). The // control points (cx0, cy0) and (cx1, cy1) specify the curvature. At the // current stylus, the curve is tangent to the straight line between the // current stylus location and the control point (cx0, cy0). At the end point, // the curve is tangent to the straight line between the end point and the // control point (cx1, cy1). // // The MoveTo() example demonstrates this method. func (f *Fpdf) CurveBezierCubicTo(cx0, cy0, cx1, cy1, x, y float64) { f.curve(cx0, cy0, cx1, cy1, x, y) f.x, f.y = x, y } // ClosePath creates a line from the current location to the last MoveTo point // (if not the same) and mark the path as closed so the first and last lines // join nicely. // // The MoveTo() example demonstrates this method. func (f *Fpdf) ClosePath() { f.outf("h") } // DrawPath actually draws the path on the page. // // styleStr can be "F" for filled, "D" for outlined only, or "DF" or "FD" for // outlined and filled. An empty string will be replaced with "D". // Path-painting operators as defined in the PDF specification are also // allowed: "S" (Stroke the path), "s" (Close and stroke the path), // "f" (fill the path, using the nonzero winding number), "f*" // (Fill the path, using the even-odd rule), "B" (Fill and then stroke // the path, using the nonzero winding number rule), "B*" (Fill and // then stroke the path, using the even-odd rule), "b" (Close, fill, // and then stroke the path, using the nonzero winding number rule) and // "b*" (Close, fill, and then stroke the path, using the even-odd // rule). // Drawing uses the current draw color, line width, and cap style // centered on the // path. Filling uses the current fill color. // // The MoveTo() example demonstrates this method. func (f *Fpdf) DrawPath(styleStr string) { f.outf(fillDrawOp(styleStr)) } // ArcTo draws an elliptical arc centered at point (x, y). rx and ry specify its // horizontal and vertical radii. If the start of the arc is not at // the current position, a connecting line will be drawn. // // degRotate specifies the angle that the arc will be rotated. degStart and // degEnd specify the starting and ending angle of the arc. All angles are // specified in degrees and measured counter-clockwise from the 3 o'clock // position. // // styleStr can be "F" for filled, "D" for outlined only, or "DF" or "FD" for // outlined and filled. An empty string will be replaced with "D". Drawing uses // the current draw color, line width, and cap style centered on the arc's // path. Filling uses the current fill color. // // The MoveTo() example demonstrates this method. func (f *Fpdf) ArcTo(x, y, rx, ry, degRotate, degStart, degEnd float64) { f.arc(x, y, rx, ry, degRotate, degStart, degEnd, "", true) } func (f *Fpdf) arc(x, y, rx, ry, degRotate, degStart, degEnd float64, styleStr string, path bool) { x *= f.k y = (f.h - y) * f.k rx *= f.k ry *= f.k segments := int(degEnd-degStart) / 60 if segments < 2 { segments = 2 } angleStart := degStart * math.Pi / 180 angleEnd := degEnd * math.Pi / 180 angleTotal := angleEnd - angleStart dt := angleTotal / float64(segments) dtm := dt / 3 if degRotate != 0 { a := -degRotate * math.Pi / 180 f.outf("q %.5f %.5f %.5f %.5f %.5f %.5f cm", math.Cos(a), -1*math.Sin(a), math.Sin(a), math.Cos(a), x, y) x = 0 y = 0 } t := angleStart a0 := x + rx*math.Cos(t) b0 := y + ry*math.Sin(t) c0 := -rx * math.Sin(t) d0 := ry * math.Cos(t) sx := a0 / f.k // start point of arc sy := f.h - (b0 / f.k) if path { if f.x != sx || f.y != sy { // Draw connecting line to start point f.LineTo(sx, sy) } } else { f.point(sx, sy) } for j := 1; j <= segments; j++ { // Draw this bit of the total curve t = (float64(j) * dt) + angleStart a1 := x + rx*math.Cos(t) b1 := y + ry*math.Sin(t) c1 := -rx * math.Sin(t) d1 := ry * math.Cos(t) f.curve((a0+(c0*dtm))/f.k, f.h-((b0+(d0*dtm))/f.k), (a1-(c1*dtm))/f.k, f.h-((b1-(d1*dtm))/f.k), a1/f.k, f.h-(b1/f.k)) a0 = a1 b0 = b1 c0 = c1 d0 = d1 if path { f.x = a1 / f.k f.y = f.h - (b1 / f.k) } } if !path { f.out(fillDrawOp(styleStr)) } if degRotate != 0 { f.out("Q") } }