├── LICENSE ├── README.md ├── const.go ├── go.mod ├── go.sum ├── helper.go ├── importer.go ├── reader.go └── writer.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2020 David Barnes 4 | Copyright (c) 2017 Setasign - Jan Slabon, https://www.setasign.com 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gofpdi 2 | [![MIT 3 | licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/phpdave11/gofpdi/master/LICENSE) 4 | [![Report](https://goreportcard.com/badge/github.com/phpdave11/gofpdi)](https://goreportcard.com/report/github.com/phpdave11/gofpdi) 5 | [![GoDoc](https://img.shields.io/badge/godoc-gofpdi-blue.svg)](https://godoc.org/github.com/phpdave11/gofpdi) 6 | 7 | ## Go Free PDF Document Importer 8 | 9 | gofpdi allows you to import an existing PDF into a new PDF. The following PDF generation libraries are supported: 10 | 11 | - [gopdf](https://github.com/signintech/gopdf) 12 | 13 | - [gofpdf](https://github.com/phpdave11/gofpdf) 14 | 15 | ## Acknowledgments 16 | This package’s code is derived from the [fpdi](https://github.com/Setasign/FPDI/tree/1.6.x-legacy) library created by [Jan Slabon](https://github.com/JanSlabon). 17 | [mrtsbt](https://github.com/mrtsbt) added support for reading a PDF from an `io.ReadSeeker` stream and also added support for using gofpdi concurrently. [Asher Tuggle](https://github.com/awesomeunleashed) added support for reading PDFs that have split xref tables. 18 | 19 | ## Examples 20 | 21 | ### gopdf example 22 | 23 | ```go 24 | package main 25 | 26 | import ( 27 | "github.com/signintech/gopdf" 28 | "io" 29 | "net/http" 30 | "os" 31 | ) 32 | 33 | func main() { 34 | var err error 35 | 36 | // Download a Font 37 | fontUrl := "https://github.com/google/fonts/raw/master/ofl/daysone/DaysOne-Regular.ttf" 38 | if err = DownloadFile("example-font.ttf", fontUrl); err != nil { 39 | panic(err) 40 | } 41 | 42 | // Download a PDF 43 | fileUrl := "https://tcpdf.org/files/examples/example_012.pdf" 44 | if err = DownloadFile("example-pdf.pdf", fileUrl); err != nil { 45 | panic(err) 46 | } 47 | 48 | pdf := gopdf.GoPdf{} 49 | pdf.Start(gopdf.Config{PageSize: gopdf.Rect{W: 595.28, H: 841.89}}) //595.28, 841.89 = A4 50 | 51 | pdf.AddPage() 52 | 53 | err = pdf.AddTTFFont("daysone", "example-font.ttf") 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | err = pdf.SetFont("daysone", "", 20) 59 | if err != nil { 60 | panic(err) 61 | } 62 | 63 | // Color the page 64 | pdf.SetLineWidth(0.1) 65 | pdf.SetFillColor(124, 252, 0) //setup fill color 66 | pdf.RectFromUpperLeftWithStyle(50, 100, 400, 600, "FD") 67 | pdf.SetFillColor(0, 0, 0) 68 | 69 | pdf.SetX(50) 70 | pdf.SetY(50) 71 | pdf.Cell(nil, "Import existing PDF into GoPDF Document") 72 | 73 | // Import page 1 74 | tpl1 := pdf.ImportPage("example-pdf.pdf", 1, "/MediaBox") 75 | 76 | // Draw pdf onto page 77 | pdf.UseImportedTemplate(tpl1, 50, 100, 400, 0) 78 | 79 | pdf.WritePdf("example.pdf") 80 | 81 | } 82 | 83 | // DownloadFile will download a url to a local file. It's efficient because it will 84 | // write as it downloads and not load the whole file into memory. 85 | func DownloadFile(filepath string, url string) error { 86 | // Get the data 87 | resp, err := http.Get(url) 88 | if err != nil { 89 | return err 90 | } 91 | defer resp.Body.Close() 92 | 93 | // Create the file 94 | out, err := os.Create(filepath) 95 | if err != nil { 96 | return err 97 | } 98 | defer out.Close() 99 | 100 | // Write the body to file 101 | _, err = io.Copy(out, resp.Body) 102 | return err 103 | } 104 | ``` 105 | 106 | Generated PDF: [example.pdf](https://github.com/signintech/gopdf/files/3144466/example.pdf) 107 | 108 | Screenshot of PDF: 109 | 110 | ![example](https://user-images.githubusercontent.com/9421180/57180557-4c1dbd80-6e4f-11e9-8f47-9d40217805be.jpg) 111 | 112 | ### gofpdf example #1 - import PDF from file 113 | 114 | ```go 115 | package main 116 | 117 | import ( 118 | "github.com/phpdave11/gofpdf" 119 | "github.com/phpdave11/gofpdf/contrib/gofpdi" 120 | "io" 121 | "net/http" 122 | "os" 123 | ) 124 | 125 | func main() { 126 | var err error 127 | 128 | pdf := gofpdf.New("P", "mm", "A4", "") 129 | 130 | // Download a PDF 131 | fileUrl := "https://tcpdf.org/files/examples/example_026.pdf" 132 | if err = DownloadFile("example-pdf.pdf", fileUrl); err != nil { 133 | panic(err) 134 | } 135 | 136 | // Import example-pdf.pdf with gofpdi free pdf document importer 137 | tpl1 := gofpdi.ImportPage(pdf, "example-pdf.pdf", 1, "/MediaBox") 138 | 139 | pdf.AddPage() 140 | 141 | pdf.SetFillColor(200, 700, 220) 142 | pdf.Rect(20, 50, 150, 215, "F") 143 | 144 | // Draw imported template onto page 145 | gofpdi.UseImportedTemplate(pdf, tpl1, 20, 50, 150, 0) 146 | 147 | pdf.SetFont("Helvetica", "", 20) 148 | pdf.Cell(0, 0, "Import existing PDF into gofpdf document with gofpdi") 149 | 150 | err = pdf.OutputFileAndClose("example.pdf") 151 | if err != nil { 152 | panic(err) 153 | } 154 | } 155 | 156 | // DownloadFile will download a url to a local file. It's efficient because it will 157 | // write as it downloads and not load the whole file into memory. 158 | func DownloadFile(filepath string, url string) error { 159 | // Get the data 160 | resp, err := http.Get(url) 161 | if err != nil { 162 | return err 163 | } 164 | defer resp.Body.Close() 165 | 166 | // Create the file 167 | out, err := os.Create(filepath) 168 | if err != nil { 169 | return err 170 | } 171 | defer out.Close() 172 | 173 | // Write the body to file 174 | _, err = io.Copy(out, resp.Body) 175 | return err 176 | } 177 | ``` 178 | 179 | Generated PDF: [example.pdf](https://github.com/phpdave11/gofpdf/files/3178770/example.pdf) 180 | 181 | Screenshot of PDF: 182 | ![example](https://user-images.githubusercontent.com/9421180/57713804-ca8d1300-7638-11e9-9f8e-e3f803374803.jpg) 183 | 184 | 185 | 186 | ### gofpdf example #2 - import PDF from stream 187 | 188 | ```go 189 | package main 190 | 191 | import ( 192 | "bytes" 193 | "github.com/phpdave11/gofpdf" 194 | "github.com/phpdave11/gofpdf/contrib/gofpdi" 195 | "io" 196 | "io/ioutil" 197 | "net/http" 198 | ) 199 | 200 | func main() { 201 | var err error 202 | 203 | pdf := gofpdf.New("P", "mm", "A4", "") 204 | 205 | // Download a PDF into memory 206 | res, err := http.Get("https://tcpdf.org/files/examples/example_038.pdf") 207 | if err != nil { 208 | panic(err) 209 | } 210 | pdfBytes, err := ioutil.ReadAll(res.Body) 211 | res.Body.Close() 212 | if err != nil { 213 | panic(err) 214 | } 215 | 216 | // convert []byte to io.ReadSeeker 217 | rs := io.ReadSeeker(bytes.NewReader(pdfBytes)) 218 | 219 | // Import in-memory PDF stream with gofpdi free pdf document importer 220 | tpl1 := gofpdi.ImportPageFromStream(pdf, &rs, 1, "/TrimBox") 221 | 222 | pdf.AddPage() 223 | 224 | pdf.SetFillColor(200, 700, 220) 225 | pdf.Rect(20, 50, 150, 215, "F") 226 | 227 | // Draw imported template onto page 228 | gofpdi.UseImportedTemplate(pdf, tpl1, 20, 50, 150, 0) 229 | 230 | pdf.SetFont("Helvetica", "", 20) 231 | pdf.Cell(0, 0, "Import PDF stream into gofpdf document with gofpdi") 232 | 233 | err = pdf.OutputFileAndClose("example.pdf") 234 | if err != nil { 235 | panic(err) 236 | } 237 | } 238 | ``` 239 | 240 | Generated PDF: 241 | 242 | [example.pdf](https://github.com/phpdave11/gofpdi/files/3483219/example.pdf) 243 | 244 | Screenshot of PDF: 245 | 246 | ![example.jpg](https://user-images.githubusercontent.com/9421180/62728726-18b87500-b9e2-11e9-885c-7c68b7ac6222.jpg) 247 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | package gofpdi 2 | 3 | const ( 4 | PDF_TYPE_NULL = iota 5 | PDF_TYPE_NUMERIC 6 | PDF_TYPE_TOKEN 7 | PDF_TYPE_HEX 8 | PDF_TYPE_STRING 9 | PDF_TYPE_DICTIONARY 10 | PDF_TYPE_ARRAY 11 | PDF_TYPE_OBJDEC 12 | PDF_TYPE_OBJREF 13 | PDF_TYPE_OBJECT 14 | PDF_TYPE_STREAM 15 | PDF_TYPE_BOOLEAN 16 | PDF_TYPE_REAL 17 | ) 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/phpdave11/gofpdi 2 | 3 | go 1.12 4 | 5 | require github.com/pkg/errors v0.8.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 2 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 3 | -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | package gofpdi 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Determine if a value is numeric 8 | // Courtesy of https://github.com/syyongx/php2go/blob/master/php.go 9 | func is_numeric(val interface{}) bool { 10 | switch val.(type) { 11 | case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: 12 | case float32, float64, complex64, complex128: 13 | return true 14 | case string: 15 | str := val.(string) 16 | if str == "" { 17 | return false 18 | } 19 | // Trim any whitespace 20 | str = strings.TrimSpace(str) 21 | //fmt.Println(str) 22 | if str[0] == '-' || str[0] == '+' { 23 | if len(str) == 1 { 24 | return false 25 | } 26 | str = str[1:] 27 | } 28 | // hex 29 | if len(str) > 2 && str[0] == '0' && (str[1] == 'x' || str[1] == 'X') { 30 | for _, h := range str[2:] { 31 | if !((h >= '0' && h <= '9') || (h >= 'a' && h <= 'f') || (h >= 'A' && h <= 'F')) { 32 | return false 33 | } 34 | } 35 | return true 36 | } 37 | // 0-9,Point,Scientific 38 | p, s, l := 0, 0, len(str) 39 | for i, v := range str { 40 | if v == '.' { // Point 41 | if p > 0 || s > 0 || i+1 == l { 42 | return false 43 | } 44 | p = i 45 | } else if v == 'e' || v == 'E' { // Scientific 46 | if i == 0 || s > 0 || i+1 == l { 47 | return false 48 | } 49 | s = i 50 | } else if v < '0' || v > '9' { 51 | return false 52 | } 53 | } 54 | return true 55 | } 56 | 57 | return false 58 | } 59 | 60 | func in_array(needle interface{}, hystack interface{}) bool { 61 | switch key := needle.(type) { 62 | case string: 63 | for _, item := range hystack.([]string) { 64 | if key == item { 65 | return true 66 | } 67 | } 68 | case int: 69 | for _, item := range hystack.([]int) { 70 | if key == item { 71 | return true 72 | } 73 | } 74 | case int64: 75 | for _, item := range hystack.([]int64) { 76 | if key == item { 77 | return true 78 | } 79 | } 80 | default: 81 | return false 82 | } 83 | return false 84 | } 85 | 86 | // Taken from png library 87 | 88 | // intSize is either 32 or 64. 89 | const intSize = 32 << (^uint(0) >> 63) 90 | 91 | func abs(x int) int { 92 | // m := -1 if x < 0. m := 0 otherwise. 93 | m := x >> (intSize - 1) 94 | 95 | // In two's complement representation, the negative number 96 | // of any number (except the smallest one) can be computed 97 | // by flipping all the bits and add 1. This is faster than 98 | // code with a branch. 99 | // See Hacker's Delight, section 2-4. 100 | return (x ^ m) - m 101 | } 102 | 103 | // filterPaeth applies the Paeth filter to the cdat slice. 104 | // cdat is the current row's data, pdat is the previous row's data. 105 | func filterPaeth(cdat, pdat []byte, bytesPerPixel int) { 106 | var a, b, c, pa, pb, pc int 107 | for i := 0; i < bytesPerPixel; i++ { 108 | a, c = 0, 0 109 | for j := i; j < len(cdat); j += bytesPerPixel { 110 | b = int(pdat[j]) 111 | pa = b - c 112 | pb = a - c 113 | pc = abs(pa + pb) 114 | pa = abs(pa) 115 | pb = abs(pb) 116 | if pa <= pb && pa <= pc { 117 | // No-op. 118 | } else if pb <= pc { 119 | a = b 120 | } else { 121 | a = c 122 | } 123 | a += int(cdat[j]) 124 | a &= 0xff 125 | cdat[j] = uint8(a) 126 | c = b 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /importer.go: -------------------------------------------------------------------------------- 1 | package gofpdi 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // The Importer class to be used by a pdf generation library 9 | type Importer struct { 10 | sourceFile string 11 | readers map[string]*PdfReader 12 | writers map[string]*PdfWriter 13 | tplMap map[int]*TplInfo 14 | tplN int 15 | writer *PdfWriter 16 | importedPages map[string]int 17 | } 18 | 19 | type TplInfo struct { 20 | SourceFile string 21 | Writer *PdfWriter 22 | TemplateId int 23 | } 24 | 25 | func (this *Importer) GetReader() *PdfReader { 26 | return this.GetReaderForFile(this.sourceFile) 27 | } 28 | 29 | func (this *Importer) GetWriter() *PdfWriter { 30 | return this.GetWriterForFile(this.sourceFile) 31 | } 32 | 33 | func (this *Importer) GetReaderForFile(file string) *PdfReader { 34 | if _, ok := this.readers[file]; ok { 35 | return this.readers[file] 36 | } 37 | 38 | return nil 39 | } 40 | 41 | func (this *Importer) GetWriterForFile(file string) *PdfWriter { 42 | if _, ok := this.writers[file]; ok { 43 | return this.writers[file] 44 | } 45 | 46 | return nil 47 | } 48 | 49 | func NewImporter() *Importer { 50 | importer := &Importer{} 51 | importer.init() 52 | 53 | return importer 54 | } 55 | 56 | func (this *Importer) init() { 57 | this.readers = make(map[string]*PdfReader, 0) 58 | this.writers = make(map[string]*PdfWriter, 0) 59 | this.tplMap = make(map[int]*TplInfo, 0) 60 | this.writer, _ = NewPdfWriter("") 61 | this.importedPages = make(map[string]int, 0) 62 | } 63 | 64 | func (this *Importer) SetSourceFile(f string) { 65 | this.sourceFile = f 66 | 67 | // If reader hasn't been instantiated, do that now 68 | if _, ok := this.readers[this.sourceFile]; !ok { 69 | reader, err := NewPdfReader(this.sourceFile) 70 | if err != nil { 71 | panic(err) 72 | } 73 | this.readers[this.sourceFile] = reader 74 | } 75 | 76 | // If writer hasn't been instantiated, do that now 77 | if _, ok := this.writers[this.sourceFile]; !ok { 78 | writer, err := NewPdfWriter("") 79 | if err != nil { 80 | panic(err) 81 | } 82 | 83 | // Make the next writer start template numbers at this.tplN 84 | writer.SetTplIdOffset(this.tplN) 85 | this.writers[this.sourceFile] = writer 86 | } 87 | } 88 | 89 | func (this *Importer) SetSourceStream(rs *io.ReadSeeker) { 90 | this.sourceFile = fmt.Sprintf("%v", rs) 91 | 92 | if _, ok := this.readers[this.sourceFile]; !ok { 93 | reader, err := NewPdfReaderFromStream(this.sourceFile, *rs) 94 | if err != nil { 95 | panic(err) 96 | } 97 | this.readers[this.sourceFile] = reader 98 | } 99 | 100 | // If writer hasn't been instantiated, do that now 101 | if _, ok := this.writers[this.sourceFile]; !ok { 102 | writer, err := NewPdfWriter("") 103 | if err != nil { 104 | panic(err) 105 | } 106 | 107 | // Make the next writer start template numbers at this.tplN 108 | writer.SetTplIdOffset(this.tplN) 109 | this.writers[this.sourceFile] = writer 110 | } 111 | } 112 | 113 | func (this *Importer) GetNumPages() int { 114 | result, err := this.GetReader().getNumPages() 115 | 116 | if err != nil { 117 | panic(err) 118 | } 119 | 120 | return result 121 | } 122 | 123 | func (this *Importer) GetPageSizes() map[int]map[string]map[string]float64 { 124 | result, err := this.GetReader().getAllPageBoxes(1.0) 125 | 126 | if err != nil { 127 | panic(err) 128 | } 129 | 130 | return result 131 | } 132 | 133 | func (this *Importer) ImportPage(pageno int, box string) int { 134 | // If page has already been imported, return existing tplN 135 | pageNameNumber := fmt.Sprintf("%s-%04d", this.sourceFile, pageno) 136 | if _, ok := this.importedPages[pageNameNumber]; ok { 137 | return this.importedPages[pageNameNumber] 138 | } 139 | 140 | res, err := this.GetWriter().ImportPage(this.GetReader(), pageno, box) 141 | if err != nil { 142 | panic(err) 143 | } 144 | 145 | // Get current template id 146 | tplN := this.tplN 147 | 148 | // Set tpl info 149 | this.tplMap[tplN] = &TplInfo{SourceFile: this.sourceFile, TemplateId: res, Writer: this.GetWriter()} 150 | 151 | // Increment template id 152 | this.tplN++ 153 | 154 | // Cache imported page tplN 155 | this.importedPages[pageNameNumber] = tplN 156 | 157 | return tplN 158 | } 159 | 160 | func (this *Importer) SetNextObjectID(objId int) { 161 | this.GetWriter().SetNextObjectID(objId) 162 | } 163 | 164 | // Put form xobjects and get back a map of template names (e.g. /GOFPDITPL1) and their object ids (int) 165 | func (this *Importer) PutFormXobjects() map[string]int { 166 | res := make(map[string]int, 0) 167 | tplNamesIds, err := this.GetWriter().PutFormXobjects(this.GetReader()) 168 | if err != nil { 169 | panic(err) 170 | } 171 | for tplName, pdfObjId := range tplNamesIds { 172 | res[tplName] = pdfObjId.id 173 | } 174 | return res 175 | } 176 | 177 | // Put form xobjects and get back a map of template names (e.g. /GOFPDITPL1) and their object ids (sha1 hash) 178 | func (this *Importer) PutFormXobjectsUnordered() map[string]string { 179 | this.GetWriter().SetUseHash(true) 180 | res := make(map[string]string, 0) 181 | tplNamesIds, err := this.GetWriter().PutFormXobjects(this.GetReader()) 182 | if err != nil { 183 | panic(err) 184 | } 185 | for tplName, pdfObjId := range tplNamesIds { 186 | res[tplName] = pdfObjId.hash 187 | } 188 | return res 189 | } 190 | 191 | // Get object ids (int) and their contents (string) 192 | func (this *Importer) GetImportedObjects() map[int]string { 193 | res := make(map[int]string, 0) 194 | pdfObjIdBytes := this.GetWriter().GetImportedObjects() 195 | for pdfObjId, bytes := range pdfObjIdBytes { 196 | res[pdfObjId.id] = string(bytes) 197 | } 198 | return res 199 | } 200 | 201 | // Get object ids (sha1 hash) and their contents ([]byte) 202 | // The contents may have references to other object hashes which will need to be replaced by the pdf generator library 203 | // The positions of the hashes (sha1 - 40 characters) can be obtained by calling GetImportedObjHashPos() 204 | func (this *Importer) GetImportedObjectsUnordered() map[string][]byte { 205 | res := make(map[string][]byte, 0) 206 | pdfObjIdBytes := this.GetWriter().GetImportedObjects() 207 | for pdfObjId, bytes := range pdfObjIdBytes { 208 | res[pdfObjId.hash] = bytes 209 | } 210 | return res 211 | } 212 | 213 | // Get the positions of the hashes (sha1 - 40 characters) within each object, to be replaced with 214 | // actual objects ids by the pdf generator library 215 | func (this *Importer) GetImportedObjHashPos() map[string]map[int]string { 216 | res := make(map[string]map[int]string, 0) 217 | pdfObjIdPosHash := this.GetWriter().GetImportedObjHashPos() 218 | for pdfObjId, posHashMap := range pdfObjIdPosHash { 219 | res[pdfObjId.hash] = posHashMap 220 | } 221 | return res 222 | } 223 | 224 | // For a given template id (returned from ImportPage), get the template name (e.g. /GOFPDITPL1) and 225 | // the 4 float64 values necessary to draw the template a x,y for a given width and height. 226 | func (this *Importer) UseTemplate(tplid int, _x float64, _y float64, _w float64, _h float64) (string, float64, float64, float64, float64) { 227 | // Look up template id in importer tpl map 228 | tplInfo := this.tplMap[tplid] 229 | return tplInfo.Writer.UseTemplate(tplInfo.TemplateId, _x, _y, _w, _h) 230 | } 231 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | package gofpdi 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "compress/zlib" 7 | "encoding/binary" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "math" 12 | "os" 13 | "strconv" 14 | 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | type PdfReader struct { 19 | availableBoxes []string 20 | stack []string 21 | trailer *PdfValue 22 | catalog *PdfValue 23 | pages []*PdfValue 24 | xrefPos int 25 | xref map[int]map[int]int 26 | xrefStream map[int][2]int 27 | f io.ReadSeeker 28 | nBytes int64 29 | sourceFile string 30 | curPage int 31 | alreadyRead bool 32 | pageCount int 33 | } 34 | 35 | func NewPdfReaderFromStream(sourceFile string, rs io.ReadSeeker) (*PdfReader, error) { 36 | length, err := rs.Seek(0, 2) 37 | if err != nil { 38 | return nil, errors.Wrapf(err, "Failed to determine stream length") 39 | } 40 | parser := &PdfReader{f: rs, sourceFile: sourceFile, nBytes: length} 41 | if err := parser.init(); err != nil { 42 | return nil, errors.Wrap(err, "Failed to initialize parser") 43 | } 44 | if err := parser.read(); err != nil { 45 | return nil, errors.Wrap(err, "Failed to read pdf from stream") 46 | } 47 | return parser, nil 48 | } 49 | 50 | func NewPdfReader(filename string) (*PdfReader, error) { 51 | var err error 52 | f, err := os.Open(filename) 53 | if err != nil { 54 | return nil, errors.Wrap(err, "Failed to open file") 55 | } 56 | info, err := f.Stat() 57 | if err != nil { 58 | return nil, errors.Wrap(err, "Failed to obtain file information") 59 | } 60 | 61 | parser := &PdfReader{f: f, sourceFile: filename, nBytes: info.Size()} 62 | if err = parser.init(); err != nil { 63 | return nil, errors.Wrap(err, "Failed to initialize parser") 64 | } 65 | if err = parser.read(); err != nil { 66 | return nil, errors.Wrap(err, "Failed to read pdf") 67 | } 68 | 69 | return parser, nil 70 | } 71 | 72 | func (this *PdfReader) init() error { 73 | this.availableBoxes = []string{"/MediaBox", "/CropBox", "/BleedBox", "/TrimBox", "/ArtBox"} 74 | this.xref = make(map[int]map[int]int, 0) 75 | this.xrefStream = make(map[int][2]int, 0) 76 | err := this.read() 77 | if err != nil { 78 | return errors.Wrap(err, "Failed to read pdf") 79 | } 80 | return nil 81 | } 82 | 83 | type PdfValue struct { 84 | Type int 85 | String string 86 | Token string 87 | Int int 88 | Real float64 89 | Bool bool 90 | Dictionary map[string]*PdfValue 91 | Array []*PdfValue 92 | Id int 93 | NewId int 94 | Gen int 95 | Value *PdfValue 96 | Stream *PdfValue 97 | Bytes []byte 98 | } 99 | 100 | // Jump over comments 101 | func (this *PdfReader) skipComments(r *bufio.Reader) error { 102 | var err error 103 | var b byte 104 | 105 | for { 106 | b, err = r.ReadByte() 107 | if err != nil { 108 | return errors.Wrap(err, "Failed to ReadByte while skipping comments") 109 | } 110 | 111 | if b == '\n' || b == '\r' { 112 | if b == '\r' { 113 | // Peek and see if next char is \n 114 | b2, err := r.ReadByte() 115 | if err != nil { 116 | return errors.Wrap(err, "Failed to read byte") 117 | } 118 | if b2 != '\n' { 119 | r.UnreadByte() 120 | } 121 | } 122 | break 123 | } 124 | } 125 | 126 | return nil 127 | } 128 | 129 | // Advance reader so that whitespace is ignored 130 | func (this *PdfReader) skipWhitespace(r *bufio.Reader) error { 131 | var err error 132 | var b byte 133 | 134 | for { 135 | b, err = r.ReadByte() 136 | if err != nil { 137 | if err == io.EOF { 138 | break 139 | } 140 | return errors.Wrap(err, "Failed to read byte") 141 | } 142 | 143 | if b == ' ' || b == '\n' || b == '\r' || b == '\t' { 144 | continue 145 | } else { 146 | r.UnreadByte() 147 | break 148 | } 149 | } 150 | 151 | return nil 152 | } 153 | 154 | // Read a token 155 | func (this *PdfReader) readToken(r *bufio.Reader) (string, error) { 156 | var err error 157 | 158 | // If there is a token available on the stack, pop it out and return it. 159 | if len(this.stack) > 0 { 160 | var popped string 161 | popped, this.stack = this.stack[len(this.stack)-1], this.stack[:len(this.stack)-1] 162 | return popped, nil 163 | } 164 | 165 | err = this.skipWhitespace(r) 166 | if err != nil { 167 | return "", errors.Wrap(err, "Failed to skip whitespace") 168 | } 169 | 170 | b, err := r.ReadByte() 171 | if err != nil { 172 | if err == io.EOF { 173 | return "", nil 174 | } 175 | return "", errors.Wrap(err, "Failed to read byte") 176 | } 177 | 178 | switch b { 179 | case '[', ']', '(', ')': 180 | // This is either an array or literal string delimeter, return it. 181 | return string(b), nil 182 | 183 | case '<', '>': 184 | // This could either be a hex string or a dictionary delimiter. 185 | // Determine the appropriate case and return the token. 186 | nb, err := r.ReadByte() 187 | if err != nil { 188 | return "", errors.Wrap(err, "Failed to read byte") 189 | } 190 | if nb == b { 191 | return string(b) + string(nb), nil 192 | } else { 193 | r.UnreadByte() 194 | return string(b), nil 195 | } 196 | 197 | case '%': 198 | err = this.skipComments(r) 199 | if err != nil { 200 | return "", errors.Wrap(err, "Failed to skip comments") 201 | } 202 | return this.readToken(r) 203 | 204 | default: 205 | // FIXME this may not be performant to create new strings for each byte 206 | // Is it probably better to create a buffer and then convert to a string at the end. 207 | str := string(b) 208 | 209 | loop: 210 | for { 211 | b, err := r.ReadByte() 212 | if err != nil { 213 | return "", errors.Wrap(err, "Failed to read byte") 214 | } 215 | switch b { 216 | case ' ', '%', '[', ']', '<', '>', '(', ')', '\r', '\n', '\t', '/': 217 | r.UnreadByte() 218 | break loop 219 | default: 220 | str += string(b) 221 | } 222 | } 223 | return str, nil 224 | } 225 | 226 | return "", nil 227 | } 228 | 229 | // Read a value based on a token 230 | func (this *PdfReader) readValue(r *bufio.Reader, t string) (*PdfValue, error) { 231 | var err error 232 | var b byte 233 | 234 | result := &PdfValue{} 235 | result.Type = -1 236 | result.Token = t 237 | result.Dictionary = make(map[string]*PdfValue, 0) 238 | result.Array = make([]*PdfValue, 0) 239 | 240 | switch t { 241 | case "<": 242 | // This is a hex string 243 | 244 | // Read bytes until '>' is found 245 | var s string 246 | for { 247 | b, err = r.ReadByte() 248 | if err != nil { 249 | return nil, errors.Wrap(err, "Failed to read byte") 250 | } 251 | if b != '>' { 252 | s += string(b) 253 | } else { 254 | break 255 | } 256 | } 257 | 258 | result.Type = PDF_TYPE_HEX 259 | result.String = s 260 | 261 | case "<<": 262 | // This is a dictionary 263 | 264 | // Recurse into this function until we reach the end of the dictionary. 265 | for { 266 | key, err := this.readToken(r) 267 | if err != nil { 268 | return nil, errors.Wrap(err, "Failed to read token") 269 | } 270 | if key == "" { 271 | return nil, errors.New("Token is empty") 272 | } 273 | 274 | if key == ">>" { 275 | break 276 | } 277 | 278 | // read next token 279 | newKey, err := this.readToken(r) 280 | if err != nil { 281 | return nil, errors.Wrap(err, "Failed to read token") 282 | } 283 | 284 | value, err := this.readValue(r, newKey) 285 | if err != nil { 286 | return nil, errors.Wrap(err, "Failed to read value for token: "+newKey) 287 | } 288 | 289 | if value.Type == -1 { 290 | return result, nil 291 | } 292 | 293 | // Catch missing value 294 | if value.Type == PDF_TYPE_TOKEN && value.String == ">>" { 295 | result.Type = PDF_TYPE_NULL 296 | result.Dictionary[key] = value 297 | break 298 | } 299 | 300 | // Set value in dictionary 301 | result.Dictionary[key] = value 302 | } 303 | 304 | result.Type = PDF_TYPE_DICTIONARY 305 | return result, nil 306 | 307 | case "[": 308 | // This is an array 309 | 310 | tmpResult := make([]*PdfValue, 0) 311 | 312 | // Recurse into this function until we reach the end of the array 313 | for { 314 | key, err := this.readToken(r) 315 | if err != nil { 316 | return nil, errors.Wrap(err, "Failed to read token") 317 | } 318 | if key == "" { 319 | return nil, errors.New("Token is empty") 320 | } 321 | 322 | if key == "]" { 323 | break 324 | } 325 | 326 | value, err := this.readValue(r, key) 327 | if err != nil { 328 | return nil, errors.Wrap(err, "Failed to read value for token: "+key) 329 | } 330 | 331 | if value.Type == -1 { 332 | return result, nil 333 | } 334 | 335 | tmpResult = append(tmpResult, value) 336 | } 337 | 338 | result.Type = PDF_TYPE_ARRAY 339 | result.Array = tmpResult 340 | 341 | case "(": 342 | // This is a string 343 | 344 | openBrackets := 1 345 | 346 | // Create new buffer 347 | var buf bytes.Buffer 348 | 349 | // Read bytes until brackets are balanced 350 | for openBrackets > 0 { 351 | b, err := r.ReadByte() 352 | 353 | if err != nil { 354 | return nil, errors.Wrap(err, "Failed to read byte") 355 | } 356 | 357 | switch b { 358 | case '(': 359 | openBrackets++ 360 | 361 | case ')': 362 | openBrackets-- 363 | 364 | case '\\': 365 | nb, err := r.ReadByte() 366 | if err != nil { 367 | return nil, errors.Wrap(err, "Failed to read byte") 368 | } 369 | 370 | buf.WriteByte(b) 371 | buf.WriteByte(nb) 372 | 373 | continue 374 | } 375 | 376 | if openBrackets > 0 { 377 | buf.WriteByte(b) 378 | } 379 | } 380 | 381 | result.Type = PDF_TYPE_STRING 382 | result.String = buf.String() 383 | 384 | case "stream": 385 | return nil, errors.New("Stream not implemented") 386 | 387 | default: 388 | result.Type = PDF_TYPE_TOKEN 389 | result.Token = t 390 | 391 | if is_numeric(t) { 392 | // A numeric token. Make sure that it is not part of something else 393 | t2, err := this.readToken(r) 394 | if err != nil { 395 | return nil, errors.Wrap(err, "Failed to read token") 396 | } 397 | if t2 != "" { 398 | if is_numeric(t2) { 399 | // Two numeric tokens in a row. 400 | // In this case, we're probably in front of either an object reference 401 | // or an object specification. 402 | // Determine the case and return the data. 403 | t3, err := this.readToken(r) 404 | if err != nil { 405 | return nil, errors.Wrap(err, "Failed to read token") 406 | } 407 | 408 | if t3 != "" { 409 | switch t3 { 410 | case "obj": 411 | result.Type = PDF_TYPE_OBJDEC 412 | result.Id, _ = strconv.Atoi(t) 413 | result.Gen, _ = strconv.Atoi(t2) 414 | return result, nil 415 | 416 | case "R": 417 | result.Type = PDF_TYPE_OBJREF 418 | result.Id, _ = strconv.Atoi(t) 419 | result.Gen, _ = strconv.Atoi(t2) 420 | return result, nil 421 | } 422 | 423 | // If we get to this point, that numeric value up there was just a numeric value. 424 | // Push the extra tokens back into the stack and return the value. 425 | this.stack = append(this.stack, t3) 426 | } 427 | } 428 | 429 | this.stack = append(this.stack, t2) 430 | } 431 | 432 | if n, err := strconv.Atoi(t); err == nil { 433 | result.Type = PDF_TYPE_NUMERIC 434 | result.Int = n 435 | result.Real = float64(n) // Also assign Real value here to fix page box parsing bugs 436 | } else { 437 | result.Type = PDF_TYPE_REAL 438 | result.Real, _ = strconv.ParseFloat(t, 64) 439 | } 440 | } else if t == "true" || t == "false" { 441 | result.Type = PDF_TYPE_BOOLEAN 442 | result.Bool = t == "true" 443 | } else if t == "null" { 444 | result.Type = PDF_TYPE_NULL 445 | } else { 446 | result.Type = PDF_TYPE_TOKEN 447 | result.Token = t 448 | } 449 | } 450 | 451 | return result, nil 452 | } 453 | 454 | // Resolve a compressed object (PDF 1.5) 455 | func (this *PdfReader) resolveCompressedObject(objSpec *PdfValue) (*PdfValue, error) { 456 | var err error 457 | 458 | // Make sure object reference exists in xrefStream 459 | if _, ok := this.xrefStream[objSpec.Id]; !ok { 460 | return nil, errors.New(fmt.Sprintf("Could not find object ID %d in xref stream or xref table.", objSpec.Id)) 461 | } 462 | 463 | // Get object id and index 464 | objectId := this.xrefStream[objSpec.Id][0] 465 | objectIndex := this.xrefStream[objSpec.Id][1] 466 | 467 | // Read compressed object 468 | compressedObjSpec := &PdfValue{Type: PDF_TYPE_OBJREF, Id: objectId, Gen: 0} 469 | 470 | // Resolve compressed object 471 | compressedObj, err := this.resolveObject(compressedObjSpec) 472 | if err != nil { 473 | return nil, errors.Wrap(err, "Failed to resolve compressed object") 474 | } 475 | 476 | // Verify object type is /ObjStm 477 | if _, ok := compressedObj.Value.Dictionary["/Type"]; ok { 478 | if compressedObj.Value.Dictionary["/Type"].Token != "/ObjStm" { 479 | return nil, errors.New("Expected compressed object type to be /ObjStm") 480 | } 481 | } else { 482 | return nil, errors.New("Could not determine compressed object type.") 483 | } 484 | 485 | // Get number of sub-objects in compressed object 486 | n := compressedObj.Value.Dictionary["/N"].Int 487 | if n <= 0 { 488 | return nil, errors.New("No sub objects in compressed object") 489 | } 490 | 491 | // Get offset of first object 492 | first := compressedObj.Value.Dictionary["/First"].Int 493 | 494 | // Get length 495 | //length := compressedObj.Value.Dictionary["/Length"].Int 496 | 497 | // Check for filter 498 | filter := "" 499 | if _, ok := compressedObj.Value.Dictionary["/Filter"]; ok { 500 | filter = compressedObj.Value.Dictionary["/Filter"].Token 501 | if filter != "/FlateDecode" { 502 | return nil, errors.New("Unsupported filter - expected /FlateDecode, got: " + filter) 503 | } 504 | } 505 | 506 | if filter == "/FlateDecode" { 507 | // Decompress if filter is /FlateDecode 508 | // Uncompress zlib compressed data 509 | var out bytes.Buffer 510 | zlibReader, _ := zlib.NewReader(bytes.NewBuffer(compressedObj.Stream.Bytes)) 511 | defer zlibReader.Close() 512 | io.Copy(&out, zlibReader) 513 | 514 | // Set stream to uncompressed data 515 | compressedObj.Stream.Bytes = out.Bytes() 516 | } 517 | 518 | // Get io.Reader for bytes 519 | r := bufio.NewReader(bytes.NewBuffer(compressedObj.Stream.Bytes)) 520 | 521 | subObjId := 0 522 | subObjPos := 0 523 | 524 | // Read sub-object indeces and their positions within the (un)compressed object 525 | for i := 0; i < n; i++ { 526 | var token string 527 | var _objidx int 528 | var _objpos int 529 | 530 | // Read first token (object index) 531 | token, err = this.readToken(r) 532 | if err != nil { 533 | return nil, errors.Wrap(err, "Failed to read token") 534 | } 535 | 536 | // Convert line (string) into int 537 | _objidx, err = strconv.Atoi(token) 538 | if err != nil { 539 | return nil, errors.Wrap(err, "Failed to convert token into integer: "+token) 540 | } 541 | 542 | // Read first token (object index) 543 | token, err = this.readToken(r) 544 | if err != nil { 545 | return nil, errors.Wrap(err, "Failed to read token") 546 | } 547 | 548 | // Convert line (string) into int 549 | _objpos, err = strconv.Atoi(token) 550 | if err != nil { 551 | return nil, errors.Wrap(err, "Failed to convert token into integer: "+token) 552 | } 553 | 554 | if i == objectIndex { 555 | subObjId = _objidx 556 | subObjPos = _objpos 557 | } 558 | } 559 | 560 | // Now create an io.ReadSeeker 561 | rs := io.ReadSeeker(bytes.NewReader(compressedObj.Stream.Bytes)) 562 | 563 | // Determine where to seek to (sub-object position + /First) 564 | seekTo := int64(subObjPos + first) 565 | 566 | // Fast forward to the object 567 | rs.Seek(seekTo, 0) 568 | 569 | // Create a new io.Reader 570 | r = bufio.NewReader(rs) 571 | 572 | // Read token 573 | token, err := this.readToken(r) 574 | if err != nil { 575 | return nil, errors.Wrap(err, "Failed to read token") 576 | } 577 | 578 | // Read object 579 | obj, err := this.readValue(r, token) 580 | if err != nil { 581 | return nil, errors.Wrap(err, "Failed to read value for token: "+token) 582 | } 583 | 584 | result := &PdfValue{} 585 | result.Id = subObjId 586 | result.Gen = 0 587 | result.Type = PDF_TYPE_OBJECT 588 | result.Value = obj 589 | 590 | return result, nil 591 | } 592 | 593 | func (this *PdfReader) resolveObject(objSpec *PdfValue) (*PdfValue, error) { 594 | var err error 595 | var old_pos int64 596 | 597 | // Create new bufio.Reader 598 | r := bufio.NewReader(this.f) 599 | 600 | if objSpec.Type == PDF_TYPE_OBJREF { 601 | // This is a reference, resolve it. 602 | offset := this.xref[objSpec.Id][objSpec.Gen] 603 | 604 | if _, ok := this.xref[objSpec.Id]; !ok { 605 | // This may be a compressed object 606 | return this.resolveCompressedObject(objSpec) 607 | } 608 | 609 | // Save current file position 610 | // This is needed if you want to resolve reference while you're reading another object. 611 | // (e.g.: if you need to determine the length of a stream) 612 | old_pos, err = this.f.Seek(0, os.SEEK_CUR) 613 | if err != nil { 614 | return nil, errors.Wrap(err, "Failed to get current position of file") 615 | } 616 | 617 | // Reposition the file pointer and load the object header 618 | _, err = this.f.Seek(int64(offset), 0) 619 | if err != nil { 620 | return nil, errors.Wrap(err, "Failed to set position of file") 621 | } 622 | 623 | token, err := this.readToken(r) 624 | if err != nil { 625 | return nil, errors.Wrap(err, "Failed to read token") 626 | } 627 | 628 | obj, err := this.readValue(r, token) 629 | if err != nil { 630 | return nil, errors.Wrap(err, "Failed to read value for token: "+token) 631 | } 632 | 633 | if obj.Type != PDF_TYPE_OBJDEC { 634 | return nil, errors.New(fmt.Sprintf("Expected type to be PDF_TYPE_OBJDEC, got: %d", obj.Type)) 635 | } 636 | 637 | if obj.Id != objSpec.Id { 638 | return nil, errors.New(fmt.Sprintf("Object ID (%d) does not match ObjSpec ID (%d)", obj.Id, objSpec.Id)) 639 | } 640 | 641 | if obj.Gen != objSpec.Gen { 642 | return nil, errors.New("Object Gen does not match ObjSpec Gen") 643 | } 644 | 645 | // Read next token 646 | token, err = this.readToken(r) 647 | if err != nil { 648 | return nil, errors.Wrap(err, "Failed to read token") 649 | } 650 | 651 | // Read actual object value 652 | value, err := this.readValue(r, token) 653 | if err != nil { 654 | return nil, errors.Wrap(err, "Failed to read value for token: "+token) 655 | } 656 | 657 | // Read next token 658 | token, err = this.readToken(r) 659 | if err != nil { 660 | return nil, errors.Wrap(err, "Failed to read token") 661 | } 662 | 663 | result := &PdfValue{} 664 | result.Id = obj.Id 665 | result.Gen = obj.Gen 666 | result.Type = PDF_TYPE_OBJECT 667 | result.Value = value 668 | 669 | if token == "stream" { 670 | result.Type = PDF_TYPE_STREAM 671 | 672 | err = this.skipWhitespace(r) 673 | if err != nil { 674 | return nil, errors.Wrap(err, "Failed to skip whitespace") 675 | } 676 | 677 | // Get stream length dictionary 678 | lengthDict := value.Dictionary["/Length"] 679 | 680 | // Get number of bytes of stream 681 | length := lengthDict.Int 682 | 683 | // If lengthDict is an object reference, resolve the object and set length 684 | if lengthDict.Type == PDF_TYPE_OBJREF { 685 | lengthDict, err = this.resolveObject(lengthDict) 686 | 687 | if err != nil { 688 | return nil, errors.Wrap(err, "Failed to resolve length object of stream") 689 | } 690 | 691 | // Set length to resolved object value 692 | length = lengthDict.Value.Int 693 | } 694 | 695 | // Read length bytes 696 | bytes := make([]byte, length) 697 | 698 | // Cannot use reader.Read() because that may not read all the bytes 699 | _, err := io.ReadFull(r, bytes) 700 | if err != nil { 701 | return nil, errors.Wrap(err, "Failed to read bytes from buffer") 702 | } 703 | 704 | token, err = this.readToken(r) 705 | if err != nil { 706 | return nil, errors.Wrap(err, "Failed to read token") 707 | } 708 | if token != "endstream" { 709 | return nil, errors.New("Expected next token to be: endstream, got: " + token) 710 | } 711 | 712 | token, err = this.readToken(r) 713 | if err != nil { 714 | return nil, errors.Wrap(err, "Failed to read token") 715 | } 716 | 717 | streamObj := &PdfValue{} 718 | streamObj.Type = PDF_TYPE_STREAM 719 | streamObj.Bytes = bytes 720 | 721 | result.Stream = streamObj 722 | } 723 | 724 | if token != "endobj" { 725 | return nil, errors.New("Expected next token to be: endobj, got: " + token) 726 | } 727 | 728 | // Reposition the file pointer to previous position 729 | _, err = this.f.Seek(old_pos, 0) 730 | if err != nil { 731 | return nil, errors.Wrap(err, "Failed to set position of file") 732 | } 733 | 734 | return result, nil 735 | 736 | } else { 737 | return objSpec, nil 738 | } 739 | 740 | return &PdfValue{}, nil 741 | } 742 | 743 | // Find the xref offset (should be at the end of the PDF) 744 | func (this *PdfReader) findXref() error { 745 | var result int 746 | var err error 747 | var toRead int64 748 | 749 | toRead = 1500 750 | 751 | // If PDF is smaller than 1500 bytes, be sure to only read the number of bytes that are in the file 752 | fileSize := this.nBytes 753 | if fileSize < toRead { 754 | toRead = fileSize 755 | } 756 | 757 | // 0 means relative to the origin of the file, 758 | // 1 means relative to the current offset, 759 | // and 2 means relative to the end. 760 | whence := 2 761 | 762 | // Perform seek operation 763 | _, err = this.f.Seek(-toRead, whence) 764 | if err != nil { 765 | return errors.Wrap(err, "Failed to set position of file") 766 | } 767 | 768 | // Create new bufio.Reader 769 | r := bufio.NewReader(this.f) 770 | for { 771 | // Read all tokens until "startxref" is found 772 | token, err := this.readToken(r) 773 | if err != nil { 774 | return errors.Wrap(err, "Failed to read token") 775 | } 776 | 777 | if token == "startxref" { 778 | token, err = this.readToken(r) 779 | // Probably EOF before finding startxref 780 | if err != nil { 781 | return errors.Wrap(err, "Failed to find startxref token") 782 | } 783 | 784 | // Convert line (string) into int 785 | result, err = strconv.Atoi(token) 786 | if err != nil { 787 | return errors.Wrap(err, "Failed to convert xref position into integer: "+token) 788 | } 789 | 790 | // Successfully read the xref position 791 | this.xrefPos = result 792 | break 793 | } 794 | } 795 | 796 | // Rewind file pointer 797 | whence = 0 798 | _, err = this.f.Seek(0, whence) 799 | if err != nil { 800 | return errors.Wrap(err, "Failed to set position of file") 801 | } 802 | 803 | this.xrefPos = result 804 | 805 | return nil 806 | } 807 | 808 | // Read and parse the xref table 809 | func (this *PdfReader) readXref() error { 810 | var err error 811 | 812 | // Create new bufio.Reader 813 | r := bufio.NewReader(this.f) 814 | 815 | // Set file pointer to xref start 816 | _, err = this.f.Seek(int64(this.xrefPos), 0) 817 | if err != nil { 818 | return errors.Wrap(err, "Failed to set position of file") 819 | } 820 | 821 | // Xref should start with 'xref' 822 | t, err := this.readToken(r) 823 | if err != nil { 824 | return errors.Wrap(err, "Failed to read token") 825 | } 826 | if t != "xref" { 827 | // Maybe this is an XRef stream ... 828 | v, err := this.readValue(r, t) 829 | if err != nil { 830 | return errors.Wrap(err, "Failed to read XRef stream") 831 | } 832 | 833 | if v.Type == PDF_TYPE_OBJDEC { 834 | // Read next token 835 | t, err = this.readToken(r) 836 | if err != nil { 837 | return errors.Wrap(err, "Failed to read token") 838 | } 839 | 840 | // Read actual object value 841 | v, err := this.readValue(r, t) 842 | if err != nil { 843 | return errors.Wrap(err, "Failed to read value for token: "+t) 844 | } 845 | 846 | // If /Type is set, check to see if it is XRef 847 | if _, ok := v.Dictionary["/Type"]; ok { 848 | if v.Dictionary["/Type"].Token == "/XRef" { 849 | // Continue reading xref stream data now that it is confirmed that it is an xref stream 850 | 851 | // Check for /DecodeParms 852 | paethDecode := false 853 | if _, ok := v.Dictionary["/DecodeParms"]; ok { 854 | columns := 0 855 | predictor := 0 856 | 857 | if _, ok2 := v.Dictionary["/DecodeParms"].Dictionary["/Columns"]; ok2 { 858 | columns = v.Dictionary["/DecodeParms"].Dictionary["/Columns"].Int 859 | } 860 | if _, ok2 := v.Dictionary["/DecodeParms"].Dictionary["/Predictor"]; ok2 { 861 | predictor = v.Dictionary["/DecodeParms"].Dictionary["/Predictor"].Int 862 | } 863 | 864 | if columns > 4 || predictor > 12 { 865 | return errors.New("Unsupported /DecodeParms - only tested with /Columns <= 4 and /Predictor <= 12") 866 | } 867 | paethDecode = true 868 | } 869 | 870 | /* 871 | // Check to make sure field size is [1 2 1] - not yet tested with other field sizes 872 | if v.Dictionary["/W"].Array[0].Int != 1 || v.Dictionary["/W"].Array[1].Int > 4 || v.Dictionary["/W"].Array[2].Int != 1 { 873 | return errors.New(fmt.Sprintf("Unsupported field sizes in cross-reference stream dictionary: /W [%d %d %d]", 874 | v.Dictionary["/W"].Array[0].Int, 875 | v.Dictionary["/W"].Array[1].Int, 876 | v.Dictionary["/W"].Array[2].Int)) 877 | } 878 | */ 879 | 880 | index := make([]int, 2) 881 | 882 | // If /Index is not set, this is an error 883 | if _, ok := v.Dictionary["/Index"]; ok { 884 | if len(v.Dictionary["/Index"].Array) < 2 { 885 | return errors.Wrap(err, "Index array does not contain 2 elements") 886 | } 887 | 888 | index[0] = v.Dictionary["/Index"].Array[0].Int 889 | index[1] = v.Dictionary["/Index"].Array[1].Int 890 | } else { 891 | index[0] = 0 892 | } 893 | 894 | prevXref := 0 895 | 896 | // Check for previous xref stream 897 | if _, ok := v.Dictionary["/Prev"]; ok { 898 | prevXref = v.Dictionary["/Prev"].Int 899 | } 900 | 901 | // Set root object 902 | if _, ok := v.Dictionary["/Root"]; ok { 903 | // Just set the whole dictionary with /Root key to keep compatibiltiy with existing code 904 | this.trailer = v 905 | } else { 906 | // Don't return an error here. The trailer could be in another XRef stream. 907 | //return errors.New("Did not set root object") 908 | } 909 | 910 | startObject := index[0] 911 | 912 | err = this.skipWhitespace(r) 913 | if err != nil { 914 | return errors.Wrap(err, "Failed to skip whitespace") 915 | } 916 | 917 | // Get stream length dictionary 918 | lengthDict := v.Dictionary["/Length"] 919 | 920 | // Get number of bytes of stream 921 | length := lengthDict.Int 922 | 923 | // If lengthDict is an object reference, resolve the object and set length 924 | if lengthDict.Type == PDF_TYPE_OBJREF { 925 | lengthDict, err = this.resolveObject(lengthDict) 926 | 927 | if err != nil { 928 | return errors.Wrap(err, "Failed to resolve length object of stream") 929 | } 930 | 931 | // Set length to resolved object value 932 | length = lengthDict.Value.Int 933 | } 934 | 935 | t, err = this.readToken(r) 936 | if err != nil { 937 | return errors.Wrap(err, "Failed to read token") 938 | } 939 | if t != "stream" { 940 | return errors.New("Expected next token to be: stream, got: " + t) 941 | } 942 | 943 | err = this.skipWhitespace(r) 944 | if err != nil { 945 | return errors.Wrap(err, "Failed to skip whitespace") 946 | } 947 | 948 | // Read length bytes 949 | data := make([]byte, length) 950 | 951 | // Cannot use reader.Read() because that may not read all the bytes 952 | _, err := io.ReadFull(r, data) 953 | if err != nil { 954 | return errors.Wrap(err, "Failed to read bytes from buffer") 955 | } 956 | 957 | // Look for endstream token 958 | t, err = this.readToken(r) 959 | if err != nil { 960 | return errors.Wrap(err, "Failed to read token") 961 | } 962 | if t != "endstream" { 963 | return errors.New("Expected next token to be: endstream, got: " + t) 964 | } 965 | 966 | // Look for endobj token 967 | t, err = this.readToken(r) 968 | if err != nil { 969 | return errors.Wrap(err, "Failed to read token") 970 | } 971 | if t != "endobj" { 972 | return errors.New("Expected next token to be: endobj, got: " + t) 973 | } 974 | 975 | // Now decode zlib data 976 | b := bytes.NewReader(data) 977 | 978 | z, err := zlib.NewReader(b) 979 | if err != nil { 980 | return errors.Wrap(err, "zlib.NewReader error") 981 | } 982 | defer z.Close() 983 | 984 | p, err := ioutil.ReadAll(z) 985 | if err != nil { 986 | return errors.Wrap(err, "ioutil.ReadAll error") 987 | } 988 | 989 | objPos := 0 990 | objGen := 0 991 | i := startObject 992 | 993 | // Decode result with paeth algorithm 994 | var result []byte 995 | b = bytes.NewReader(p) 996 | 997 | firstFieldSize := v.Dictionary["/W"].Array[0].Int 998 | middleFieldSize := v.Dictionary["/W"].Array[1].Int 999 | lastFieldSize := v.Dictionary["/W"].Array[2].Int 1000 | 1001 | fieldSize := firstFieldSize + middleFieldSize + lastFieldSize 1002 | if paethDecode { 1003 | fieldSize++ 1004 | } 1005 | 1006 | prevRow := make([]byte, fieldSize) 1007 | for { 1008 | result = make([]byte, fieldSize) 1009 | _, err := io.ReadFull(b, result) 1010 | if err != nil { 1011 | if err == io.EOF { 1012 | break 1013 | } else { 1014 | return errors.Wrap(err, "io.ReadFull error") 1015 | } 1016 | } 1017 | 1018 | if paethDecode { 1019 | filterPaeth(result, prevRow, fieldSize) 1020 | copy(prevRow, result) 1021 | } 1022 | 1023 | objectData := make([]byte, fieldSize) 1024 | if paethDecode { 1025 | copy(objectData, result[1:fieldSize]) 1026 | } else { 1027 | copy(objectData, result[0:fieldSize]) 1028 | } 1029 | 1030 | if objectData[0] == 1 { 1031 | // Regular objects 1032 | b := make([]byte, 4) 1033 | copy(b[4-middleFieldSize:], objectData[1:1+middleFieldSize]) 1034 | 1035 | objPos = int(binary.BigEndian.Uint32(b)) 1036 | objGen = int(objectData[firstFieldSize+middleFieldSize]) 1037 | 1038 | // Append map[int]int 1039 | this.xref[i] = make(map[int]int, 1) 1040 | 1041 | // Set object id, generation, and position 1042 | this.xref[i][objGen] = objPos 1043 | } else if objectData[0] == 2 { 1044 | // Compressed objects 1045 | b := make([]byte, 4) 1046 | copy(b[4-middleFieldSize:], objectData[1:1+middleFieldSize]) 1047 | 1048 | objId := int(binary.BigEndian.Uint32(b)) 1049 | objIdx := int(objectData[firstFieldSize+middleFieldSize]) 1050 | 1051 | // object id (i) is located in StmObj (objId) at index (objIdx) 1052 | this.xrefStream[i] = [2]int{objId, objIdx} 1053 | } 1054 | 1055 | i++ 1056 | } 1057 | 1058 | // Check for previous xref stream 1059 | if prevXref > 0 { 1060 | // Set xrefPos to /Prev xref 1061 | this.xrefPos = prevXref 1062 | 1063 | // Read preivous xref 1064 | xrefErr := this.readXref() 1065 | if xrefErr != nil { 1066 | return errors.Wrap(xrefErr, "Failed to read prev xref") 1067 | } 1068 | } 1069 | } 1070 | } 1071 | 1072 | return nil 1073 | } 1074 | 1075 | return errors.New("Expected xref to start with 'xref'. Got: " + t) 1076 | } 1077 | 1078 | for { 1079 | // Next value will be the starting object id (usually 0, but not always) or the trailer 1080 | t, err = this.readToken(r) 1081 | if err != nil { 1082 | return errors.Wrap(err, "Failed to read token") 1083 | } 1084 | 1085 | // Check for trailer 1086 | if t == "trailer" { 1087 | break 1088 | } 1089 | 1090 | // Convert token to int 1091 | startObject, err := strconv.Atoi(t) 1092 | if err != nil { 1093 | return errors.Wrap(err, "Failed to convert start object to integer: "+t) 1094 | } 1095 | 1096 | // Determine how many objects there are 1097 | t, err = this.readToken(r) 1098 | if err != nil { 1099 | return errors.Wrap(err, "Failed to read token") 1100 | } 1101 | 1102 | // Convert token to int 1103 | numObject, err := strconv.Atoi(t) 1104 | if err != nil { 1105 | return errors.Wrap(err, "Failed to convert num object to integer: "+t) 1106 | } 1107 | 1108 | // For all objects in xref, read object position, object generation, and status (free or new) 1109 | for i := startObject; i < startObject+numObject; i++ { 1110 | t, err = this.readToken(r) 1111 | if err != nil { 1112 | return errors.Wrap(err, "Failed to read token") 1113 | } 1114 | 1115 | // Get object position as int 1116 | objPos, err := strconv.Atoi(t) 1117 | if err != nil { 1118 | return errors.Wrap(err, "Failed to convert object position to integer: "+t) 1119 | } 1120 | 1121 | t, err = this.readToken(r) 1122 | if err != nil { 1123 | return errors.Wrap(err, "Failed to read token") 1124 | } 1125 | 1126 | // Get object generation as int 1127 | objGen, err := strconv.Atoi(t) 1128 | if err != nil { 1129 | return errors.Wrap(err, "Failed to convert object generation to integer: "+t) 1130 | } 1131 | 1132 | // Get object status (free or new) 1133 | objStatus, err := this.readToken(r) 1134 | if err != nil { 1135 | return errors.Wrap(err, "Failed to read token") 1136 | } 1137 | if objStatus != "f" && objStatus != "n" { 1138 | return errors.New("Expected objStatus to be 'n' or 'f', got: " + objStatus) 1139 | } 1140 | 1141 | // Since the XREF table is read from newest to oldest, make sure an xref entry does not already exist for an object. 1142 | // If it already exists, that means a newer version of the object has already been added to the table. 1143 | // Replacing it would be using the old version of the object. 1144 | // https://github.com/phpdave11/gofpdi/issues/71 1145 | _, ok := this.xref[i] 1146 | if !ok { 1147 | // Append map[int]int 1148 | this.xref[i] = make(map[int]int, 1) 1149 | 1150 | // Set object id, generation, and position 1151 | this.xref[i][objGen] = objPos 1152 | } 1153 | } 1154 | } 1155 | 1156 | // Read trailer dictionary 1157 | t, err = this.readToken(r) 1158 | if err != nil { 1159 | return errors.Wrap(err, "Failed to read token") 1160 | } 1161 | 1162 | trailer, err := this.readValue(r, t) 1163 | if err != nil { 1164 | return errors.Wrap(err, "Failed to read value for token: "+t) 1165 | } 1166 | 1167 | // If /Root is set, then set trailer object so that /Root can be read later 1168 | if _, ok := trailer.Dictionary["/Root"]; ok { 1169 | this.trailer = trailer 1170 | } 1171 | 1172 | // If a /Prev xref trailer is specified, parse that 1173 | if tr, ok := trailer.Dictionary["/Prev"]; ok { 1174 | // Resolve parent xref table 1175 | this.xrefPos = tr.Int 1176 | return this.readXref() 1177 | } 1178 | 1179 | return nil 1180 | } 1181 | 1182 | // Read root (catalog object) 1183 | func (this *PdfReader) readRoot() error { 1184 | var err error 1185 | 1186 | rootObjSpec := this.trailer.Dictionary["/Root"] 1187 | 1188 | // Read root (catalog) 1189 | this.catalog, err = this.resolveObject(rootObjSpec) 1190 | if err != nil { 1191 | return errors.Wrap(err, "Failed to resolve root object") 1192 | } 1193 | 1194 | return nil 1195 | } 1196 | 1197 | // Read kids (pages inside a page tree) 1198 | func (this *PdfReader) readKids(kids *PdfValue, r int) error { 1199 | // Loop through pages and add to result 1200 | for i := 0; i < len(kids.Array); i++ { 1201 | page, err := this.resolveObject(kids.Array[i]) 1202 | if err != nil { 1203 | return errors.Wrap(err, "Failed to resolve page/pages object") 1204 | } 1205 | 1206 | objType := page.Value.Dictionary["/Type"].Token 1207 | if objType == "/Page" { 1208 | // Set page and increment curPage 1209 | this.pages[this.curPage] = page 1210 | this.curPage++ 1211 | } else if objType == "/Pages" { 1212 | // Resolve kids 1213 | subKids, err := this.resolveObject(page.Value.Dictionary["/Kids"]) 1214 | if err != nil { 1215 | return errors.Wrap(err, "Failed to resolve kids") 1216 | } 1217 | 1218 | // Recurse into page tree 1219 | err = this.readKids(subKids, r+1) 1220 | if err != nil { 1221 | return errors.Wrap(err, "Failed to read kids") 1222 | } 1223 | } else { 1224 | return errors.Wrap(err, fmt.Sprintf("Unknown object type '%s'. Expected: /Pages or /Page", objType)) 1225 | } 1226 | } 1227 | 1228 | return nil 1229 | } 1230 | 1231 | // Read all pages in PDF 1232 | func (this *PdfReader) readPages() error { 1233 | var err error 1234 | 1235 | // resolve_pages_dict 1236 | pagesDict, err := this.resolveObject(this.catalog.Value.Dictionary["/Pages"]) 1237 | if err != nil { 1238 | return errors.Wrap(err, "Failed to resolve pages object") 1239 | } 1240 | 1241 | // This will normally return itself 1242 | kids, err := this.resolveObject(pagesDict.Value.Dictionary["/Kids"]) 1243 | if err != nil { 1244 | return errors.Wrap(err, "Failed to resolve kids object") 1245 | } 1246 | 1247 | // Get number of pages 1248 | pageCount, err := this.resolveObject(pagesDict.Value.Dictionary["/Count"]) 1249 | if err != nil { 1250 | return errors.Wrap(err, "Failed to get page count") 1251 | } 1252 | this.pageCount = pageCount.Int 1253 | 1254 | // Allocate pages 1255 | this.pages = make([]*PdfValue, pageCount.Int) 1256 | 1257 | // Read kids 1258 | err = this.readKids(kids, 0) 1259 | if err != nil { 1260 | return errors.Wrap(err, "Failed to read kids") 1261 | } 1262 | 1263 | return nil 1264 | } 1265 | 1266 | // Get references to page resources for a given page number 1267 | func (this *PdfReader) getPageResources(pageno int) (*PdfValue, error) { 1268 | var err error 1269 | 1270 | // Check to make sure page exists in pages slice 1271 | if len(this.pages) < pageno { 1272 | return nil, errors.New(fmt.Sprintf("Page %d does not exist!!", pageno)) 1273 | } 1274 | 1275 | // Resolve page object 1276 | page, err := this.resolveObject(this.pages[pageno-1]) 1277 | if err != nil { 1278 | return nil, errors.Wrap(err, "Failed to resolve page object") 1279 | } 1280 | 1281 | // Check to see if /Resources exists in Dictionary 1282 | if _, ok := page.Value.Dictionary["/Resources"]; ok { 1283 | // Resolve /Resources object 1284 | res, err := this.resolveObject(page.Value.Dictionary["/Resources"]) 1285 | if err != nil { 1286 | return nil, errors.Wrap(err, "Failed to resolve resources object") 1287 | } 1288 | 1289 | // If type is PDF_TYPE_OBJECT, return its Value 1290 | if res.Type == PDF_TYPE_OBJECT { 1291 | return res.Value, nil 1292 | } 1293 | 1294 | // Otherwise, returned the resolved object 1295 | return res, nil 1296 | } else { 1297 | // If /Resources does not exist, check to see if /Parent exists and return that 1298 | if _, ok := page.Value.Dictionary["/Parent"]; ok { 1299 | // Resolve parent object 1300 | res, err := this.resolveObject(page.Value.Dictionary["/Parent"]) 1301 | if err != nil { 1302 | return nil, errors.Wrap(err, "Failed to resolve parent object") 1303 | } 1304 | 1305 | // If /Parent object type is PDF_TYPE_OBJECT, return its Value 1306 | if res.Type == PDF_TYPE_OBJECT { 1307 | return res.Value, nil 1308 | } 1309 | 1310 | // Otherwise, return the resolved parent object 1311 | return res, nil 1312 | } 1313 | } 1314 | 1315 | // Return an empty PdfValue if we got here 1316 | // TODO: Improve error handling 1317 | return &PdfValue{}, nil 1318 | } 1319 | 1320 | // Get page content and return a slice of PdfValue objects 1321 | func (this *PdfReader) getPageContent(objSpec *PdfValue) ([]*PdfValue, error) { 1322 | var err error 1323 | var content *PdfValue 1324 | 1325 | // Allocate slice 1326 | contents := make([]*PdfValue, 0) 1327 | 1328 | if objSpec.Type == PDF_TYPE_OBJREF { 1329 | // If objSpec is an object reference, resolve the object and append it to contents 1330 | content, err = this.resolveObject(objSpec) 1331 | if err != nil { 1332 | return nil, errors.Wrap(err, "Failed to resolve object") 1333 | } 1334 | contents = append(contents, content) 1335 | } else if objSpec.Type == PDF_TYPE_ARRAY { 1336 | // If objSpec is an array, loop through the array and recursively get page content and append to contents 1337 | for i := 0; i < len(objSpec.Array); i++ { 1338 | tmpContents, err := this.getPageContent(objSpec.Array[i]) 1339 | if err != nil { 1340 | return nil, errors.Wrap(err, "Failed to get page content") 1341 | } 1342 | for j := 0; j < len(tmpContents); j++ { 1343 | contents = append(contents, tmpContents[j]) 1344 | } 1345 | } 1346 | } 1347 | 1348 | return contents, nil 1349 | } 1350 | 1351 | // Get content (i.e. PDF drawing instructions) 1352 | func (this *PdfReader) getContent(pageno int) (string, error) { 1353 | var err error 1354 | var contents []*PdfValue 1355 | 1356 | // Check to make sure page exists in pages slice 1357 | if len(this.pages) < pageno { 1358 | return "", errors.New(fmt.Sprintf("Page %d does not exist.", pageno)) 1359 | } 1360 | 1361 | // Get page 1362 | page := this.pages[pageno-1] 1363 | 1364 | // FIXME: This could be slow, converting []byte to string and appending many times 1365 | buffer := "" 1366 | 1367 | // Check to make sure /Contents exists in page dictionary 1368 | if _, ok := page.Value.Dictionary["/Contents"]; ok { 1369 | // Get an array of page content 1370 | contents, err = this.getPageContent(page.Value.Dictionary["/Contents"]) 1371 | if err != nil { 1372 | return "", errors.Wrap(err, "Failed to get page content") 1373 | } 1374 | 1375 | for i := 0; i < len(contents); i++ { 1376 | // Decode content if one or more /Filter is specified. 1377 | // Most common filter is FlateDecode which can be uncompressed with zlib 1378 | tmpBuffer, err := this.rebuildContentStream(contents[i]) 1379 | if err != nil { 1380 | return "", errors.Wrap(err, "Failed to rebuild content stream") 1381 | } 1382 | 1383 | // FIXME: This is probably slow 1384 | buffer += string(tmpBuffer) 1385 | } 1386 | } 1387 | 1388 | return buffer, nil 1389 | } 1390 | 1391 | // Rebuild content stream 1392 | // This will decode content if one or more /Filter (such as FlateDecode) is specified. 1393 | // If there are multiple filters, they will be decoded in the order in which they were specified. 1394 | func (this *PdfReader) rebuildContentStream(content *PdfValue) ([]byte, error) { 1395 | var err error 1396 | var tmpFilter *PdfValue 1397 | 1398 | // Allocate slice of PdfValue 1399 | filters := make([]*PdfValue, 0) 1400 | 1401 | // If content has a /Filter, append it to filters slice 1402 | if _, ok := content.Value.Dictionary["/Filter"]; ok { 1403 | filter := content.Value.Dictionary["/Filter"] 1404 | 1405 | // If filter type is a reference, resolve it 1406 | if filter.Type == PDF_TYPE_OBJREF { 1407 | tmpFilter, err = this.resolveObject(filter) 1408 | if err != nil { 1409 | return nil, errors.Wrap(err, "Failed to resolve object") 1410 | } 1411 | filter = tmpFilter.Value 1412 | } 1413 | 1414 | if filter.Type == PDF_TYPE_TOKEN { 1415 | // If filter type is a token (e.g. FlateDecode), appent it to filters slice 1416 | filters = append(filters, filter) 1417 | } else if filter.Type == PDF_TYPE_ARRAY { 1418 | // If filter type is an array, then there are multiple filters. Set filters variable to array value. 1419 | filters = filter.Array 1420 | } 1421 | 1422 | } 1423 | 1424 | // Set stream variable to content bytes 1425 | stream := content.Stream.Bytes 1426 | 1427 | // Loop through filters and apply each filter to stream 1428 | for i := 0; i < len(filters); i++ { 1429 | switch filters[i].Token { 1430 | case "/FlateDecode": 1431 | // Uncompress zlib compressed data 1432 | var out bytes.Buffer 1433 | zlibReader, _ := zlib.NewReader(bytes.NewBuffer(stream)) 1434 | defer zlibReader.Close() 1435 | io.Copy(&out, zlibReader) 1436 | 1437 | // Set stream to uncompressed data 1438 | stream = out.Bytes() 1439 | default: 1440 | return nil, errors.New("Unspported filter: " + filters[i].Token) 1441 | } 1442 | } 1443 | 1444 | return stream, nil 1445 | } 1446 | 1447 | func (this *PdfReader) getNumPages() (int, error) { 1448 | if this.pageCount == 0 { 1449 | return 0, errors.New("Page count is 0") 1450 | } 1451 | 1452 | return this.pageCount, nil 1453 | } 1454 | 1455 | func (this *PdfReader) getAllPageBoxes(k float64) (map[int]map[string]map[string]float64, error) { 1456 | var err error 1457 | 1458 | // Allocate result with the number of available boxes 1459 | result := make(map[int]map[string]map[string]float64, len(this.pages)) 1460 | 1461 | for i := 1; i <= len(this.pages); i++ { 1462 | result[i], err = this.getPageBoxes(i, k) 1463 | if result[i] == nil { 1464 | return nil, errors.Wrap(err, "Unable to get page box") 1465 | } 1466 | } 1467 | 1468 | return result, nil 1469 | } 1470 | 1471 | // Get all page box data 1472 | func (this *PdfReader) getPageBoxes(pageno int, k float64) (map[string]map[string]float64, error) { 1473 | var err error 1474 | 1475 | // Allocate result with the number of available boxes 1476 | result := make(map[string]map[string]float64, len(this.availableBoxes)) 1477 | 1478 | // Check to make sure page exists in pages slice 1479 | if len(this.pages) < pageno { 1480 | return nil, errors.New(fmt.Sprintf("Page %d does not exist?", pageno)) 1481 | } 1482 | 1483 | // Resolve page object 1484 | page, err := this.resolveObject(this.pages[pageno-1]) 1485 | if err != nil { 1486 | return nil, errors.New("Failed to resolve page object") 1487 | } 1488 | 1489 | // Loop through available boxes and add to result 1490 | for i := 0; i < len(this.availableBoxes); i++ { 1491 | box, err := this.getPageBox(page, this.availableBoxes[i], k) 1492 | if err != nil { 1493 | return nil, errors.New("Failed to get page box") 1494 | } 1495 | 1496 | result[this.availableBoxes[i]] = box 1497 | } 1498 | 1499 | return result, nil 1500 | } 1501 | 1502 | // Get a specific page box value (e.g. MediaBox) and return its values 1503 | func (this *PdfReader) getPageBox(page *PdfValue, box_index string, k float64) (map[string]float64, error) { 1504 | var err error 1505 | var tmpBox *PdfValue 1506 | 1507 | // Allocate 8 fields in result 1508 | result := make(map[string]float64, 8) 1509 | 1510 | // Check to make sure box_index (e.g. MediaBox) exists in page dictionary 1511 | if _, ok := page.Value.Dictionary[box_index]; ok { 1512 | box := page.Value.Dictionary[box_index] 1513 | 1514 | // If the box type is a reference, resolve it 1515 | if box.Type == PDF_TYPE_OBJREF { 1516 | tmpBox, err = this.resolveObject(box) 1517 | if err != nil { 1518 | return nil, errors.New("Failed to resolve object") 1519 | } 1520 | box = tmpBox.Value 1521 | } 1522 | 1523 | if box.Type == PDF_TYPE_ARRAY { 1524 | // If the box type is an array, calculate scaled value based on k 1525 | result["x"] = box.Array[0].Real / k 1526 | result["y"] = box.Array[1].Real / k 1527 | result["w"] = math.Abs(box.Array[0].Real-box.Array[2].Real) / k 1528 | result["h"] = math.Abs(box.Array[1].Real-box.Array[3].Real) / k 1529 | result["llx"] = math.Min(box.Array[0].Real, box.Array[2].Real) 1530 | result["lly"] = math.Min(box.Array[1].Real, box.Array[3].Real) 1531 | result["urx"] = math.Max(box.Array[0].Real, box.Array[2].Real) 1532 | result["ury"] = math.Max(box.Array[1].Real, box.Array[3].Real) 1533 | } else { 1534 | // TODO: Improve error handling 1535 | return nil, errors.New("Could not get page box") 1536 | } 1537 | } else if _, ok := page.Value.Dictionary["/Parent"]; ok { 1538 | parentObj, err := this.resolveObject(page.Value.Dictionary["/Parent"]) 1539 | if err != nil { 1540 | return nil, errors.Wrap(err, "Could not resolve parent object") 1541 | } 1542 | 1543 | // If the page box is inherited from /Parent, recursively return page box of parent 1544 | return this.getPageBox(parentObj, box_index, k) 1545 | } 1546 | 1547 | return result, nil 1548 | } 1549 | 1550 | // Get page rotation for a page number 1551 | func (this *PdfReader) getPageRotation(pageno int) (*PdfValue, error) { 1552 | // Check to make sure page exists in pages slice 1553 | if len(this.pages) < pageno { 1554 | return nil, errors.New(fmt.Sprintf("Page %d does not exist!!!!", pageno)) 1555 | } 1556 | 1557 | return this._getPageRotation(this.pages[pageno-1]) 1558 | } 1559 | 1560 | // Get page rotation for a page object spec 1561 | func (this *PdfReader) _getPageRotation(page *PdfValue) (*PdfValue, error) { 1562 | var err error 1563 | 1564 | // Resolve page object 1565 | page, err = this.resolveObject(page) 1566 | if err != nil { 1567 | return nil, errors.New("Failed to resolve page object") 1568 | } 1569 | 1570 | // Check to make sure /Rotate exists in page dictionary 1571 | if _, ok := page.Value.Dictionary["/Rotate"]; ok { 1572 | res, err := this.resolveObject(page.Value.Dictionary["/Rotate"]) 1573 | if err != nil { 1574 | return nil, errors.New("Failed to resolve rotate object") 1575 | } 1576 | 1577 | // If the type is PDF_TYPE_OBJECT, return its value 1578 | if res.Type == PDF_TYPE_OBJECT { 1579 | return res.Value, nil 1580 | } 1581 | 1582 | // Otherwise, return the object 1583 | return res, nil 1584 | } else { 1585 | // Check to see if parent has a rotation 1586 | if _, ok := page.Value.Dictionary["/Parent"]; ok { 1587 | // Recursively return /Parent page rotation 1588 | res, err := this._getPageRotation(page.Value.Dictionary["/Parent"]) 1589 | if err != nil { 1590 | return nil, errors.Wrap(err, "Failed to get page rotation for parent") 1591 | } 1592 | 1593 | // If the type is PDF_TYPE_OBJECT, return its value 1594 | if res.Type == PDF_TYPE_OBJECT { 1595 | return res.Value, nil 1596 | } 1597 | 1598 | // Otherwise, return the object 1599 | return res, nil 1600 | } 1601 | } 1602 | 1603 | return &PdfValue{Int: 0}, nil 1604 | } 1605 | 1606 | func (this *PdfReader) read() error { 1607 | // Only run once 1608 | if !this.alreadyRead { 1609 | var err error 1610 | 1611 | // Find xref position 1612 | err = this.findXref() 1613 | if err != nil { 1614 | return errors.Wrap(err, "Failed to find xref position") 1615 | } 1616 | 1617 | // Parse xref table 1618 | err = this.readXref() 1619 | if err != nil { 1620 | return errors.Wrap(err, "Failed to read xref table") 1621 | } 1622 | 1623 | // Read catalog 1624 | err = this.readRoot() 1625 | if err != nil { 1626 | return errors.Wrap(err, "Failed to read root") 1627 | } 1628 | 1629 | // Read pages 1630 | err = this.readPages() 1631 | if err != nil { 1632 | return errors.Wrap(err, "Failed to to read pages") 1633 | } 1634 | 1635 | // Now that this has been read, do not read again 1636 | this.alreadyRead = true 1637 | } 1638 | 1639 | return nil 1640 | } 1641 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package gofpdi 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "compress/zlib" 7 | "crypto/sha1" 8 | "encoding/hex" 9 | "fmt" 10 | "math" 11 | "os" 12 | 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | type PdfWriter struct { 17 | f *os.File 18 | w *bufio.Writer 19 | r *PdfReader 20 | k float64 21 | tpls []*PdfTemplate 22 | m int 23 | n int 24 | offsets map[int]int 25 | offset int 26 | result map[int]string 27 | // Keep track of which objects have already been written 28 | obj_stack map[int]*PdfValue 29 | don_obj_stack map[int]*PdfValue 30 | written_objs map[*PdfObjectId][]byte 31 | written_obj_pos map[*PdfObjectId]map[int]string 32 | current_obj *PdfObject 33 | current_obj_id int 34 | tpl_id_offset int 35 | use_hash bool 36 | } 37 | 38 | type PdfObjectId struct { 39 | id int 40 | hash string 41 | } 42 | 43 | type PdfObject struct { 44 | id *PdfObjectId 45 | buffer *bytes.Buffer 46 | } 47 | 48 | func (this *PdfWriter) SetTplIdOffset(n int) { 49 | this.tpl_id_offset = n 50 | } 51 | 52 | func (this *PdfWriter) Init() { 53 | this.k = 1 54 | this.obj_stack = make(map[int]*PdfValue, 0) 55 | this.don_obj_stack = make(map[int]*PdfValue, 0) 56 | this.tpls = make([]*PdfTemplate, 0) 57 | this.written_objs = make(map[*PdfObjectId][]byte, 0) 58 | this.written_obj_pos = make(map[*PdfObjectId]map[int]string, 0) 59 | this.current_obj = new(PdfObject) 60 | } 61 | 62 | func (this *PdfWriter) SetUseHash(b bool) { 63 | this.use_hash = b 64 | } 65 | 66 | func (this *PdfWriter) SetNextObjectID(id int) { 67 | this.n = id - 1 68 | } 69 | 70 | func NewPdfWriter(filename string) (*PdfWriter, error) { 71 | writer := &PdfWriter{} 72 | writer.Init() 73 | 74 | if filename != "" { 75 | var err error 76 | f, err := os.Create(filename) 77 | if err != nil { 78 | return nil, errors.Wrap(err, "Unable to create filename: "+filename) 79 | } 80 | writer.f = f 81 | writer.w = bufio.NewWriter(f) 82 | } 83 | return writer, nil 84 | } 85 | 86 | // Done with parsing. Now, create templates. 87 | type PdfTemplate struct { 88 | Id int 89 | Reader *PdfReader 90 | Resources *PdfValue 91 | Buffer string 92 | Box map[string]float64 93 | Boxes map[string]map[string]float64 94 | X float64 95 | Y float64 96 | W float64 97 | H float64 98 | Rotation int 99 | N int 100 | } 101 | 102 | func (this *PdfWriter) GetImportedObjects() map[*PdfObjectId][]byte { 103 | return this.written_objs 104 | } 105 | 106 | // For each object (uniquely identified by a sha1 hash), return the positions 107 | // of each hash within the object, to be replaced with pdf object ids (integers) 108 | func (this *PdfWriter) GetImportedObjHashPos() map[*PdfObjectId]map[int]string { 109 | return this.written_obj_pos 110 | } 111 | 112 | func (this *PdfWriter) ClearImportedObjects() { 113 | this.written_objs = make(map[*PdfObjectId][]byte, 0) 114 | } 115 | 116 | // Create a PdfTemplate object from a page number (e.g. 1) and a boxName (e.g. MediaBox) 117 | func (this *PdfWriter) ImportPage(reader *PdfReader, pageno int, boxName string) (int, error) { 118 | var err error 119 | 120 | // Set default scale to 1 121 | this.k = 1 122 | 123 | // Get all page boxes 124 | pageBoxes, err := reader.getPageBoxes(1, this.k) 125 | if err != nil { 126 | return -1, errors.Wrap(err, "Failed to get page boxes") 127 | } 128 | 129 | // If requested box name does not exist for this page, use an alternate box 130 | if _, ok := pageBoxes[boxName]; !ok { 131 | if boxName == "/BleedBox" || boxName == "/TrimBox" || boxName == "ArtBox" { 132 | boxName = "/CropBox" 133 | } else if boxName == "/CropBox" { 134 | boxName = "/MediaBox" 135 | } 136 | } 137 | 138 | // If the requested box name or an alternate box name cannot be found, trigger an error 139 | // TODO: Improve error handling 140 | if _, ok := pageBoxes[boxName]; !ok { 141 | return -1, errors.New("Box not found: " + boxName) 142 | } 143 | 144 | pageResources, err := reader.getPageResources(pageno) 145 | if err != nil { 146 | return -1, errors.Wrap(err, "Failed to get page resources") 147 | } 148 | 149 | content, err := reader.getContent(pageno) 150 | if err != nil { 151 | return -1, errors.Wrap(err, "Failed to get content") 152 | } 153 | 154 | // Set template values 155 | tpl := &PdfTemplate{} 156 | tpl.Reader = reader 157 | tpl.Resources = pageResources 158 | tpl.Buffer = content 159 | tpl.Box = pageBoxes[boxName] 160 | tpl.Boxes = pageBoxes 161 | tpl.X = 0 162 | tpl.Y = 0 163 | tpl.W = tpl.Box["w"] 164 | tpl.H = tpl.Box["h"] 165 | 166 | // Set template rotation 167 | rotation, err := reader.getPageRotation(pageno) 168 | if err != nil { 169 | return -1, errors.Wrap(err, "Failed to get page rotation") 170 | } 171 | angle := rotation.Int % 360 172 | 173 | // Normalize angle 174 | if angle != 0 { 175 | steps := angle / 90 176 | w := tpl.W 177 | h := tpl.H 178 | 179 | if steps%2 == 0 { 180 | tpl.W = w 181 | tpl.H = h 182 | } else { 183 | tpl.W = h 184 | tpl.H = w 185 | } 186 | 187 | if angle < 0 { 188 | angle += 360 189 | } 190 | 191 | tpl.Rotation = angle * -1 192 | } 193 | 194 | this.tpls = append(this.tpls, tpl) 195 | 196 | // Return last template id 197 | return len(this.tpls) - 1, nil 198 | } 199 | 200 | // Create a new object and keep track of the offset for the xref table 201 | func (this *PdfWriter) newObj(objId int, onlyNewObj bool) { 202 | if objId < 0 { 203 | this.n++ 204 | objId = this.n 205 | } 206 | 207 | if !onlyNewObj { 208 | // set current object id integer 209 | this.current_obj_id = objId 210 | 211 | // Create new PdfObject and PdfObjectId 212 | this.current_obj = new(PdfObject) 213 | this.current_obj.buffer = new(bytes.Buffer) 214 | this.current_obj.id = new(PdfObjectId) 215 | this.current_obj.id.id = objId 216 | this.current_obj.id.hash = this.shaOfInt(objId) 217 | 218 | this.written_obj_pos[this.current_obj.id] = make(map[int]string, 0) 219 | } 220 | } 221 | 222 | func (this *PdfWriter) endObj() { 223 | this.out("endobj") 224 | 225 | this.written_objs[this.current_obj.id] = this.current_obj.buffer.Bytes() 226 | this.current_obj_id = -1 227 | } 228 | 229 | func (this *PdfWriter) shaOfInt(i int) string { 230 | hasher := sha1.New() 231 | hasher.Write([]byte(fmt.Sprintf("%d-%s", i, this.r.sourceFile))) 232 | sha := hex.EncodeToString(hasher.Sum(nil)) 233 | return sha 234 | } 235 | 236 | func (this *PdfWriter) outObjRef(objId int) { 237 | sha := this.shaOfInt(objId) 238 | 239 | // Keep track of object hash and position - to be replaced with actual object id (integer) 240 | this.written_obj_pos[this.current_obj.id][this.current_obj.buffer.Len()] = sha 241 | 242 | if this.use_hash { 243 | this.current_obj.buffer.WriteString(sha) 244 | } else { 245 | this.current_obj.buffer.WriteString(fmt.Sprintf("%d", objId)) 246 | } 247 | this.current_obj.buffer.WriteString(" 0 R ") 248 | } 249 | 250 | // Output PDF data with a newline 251 | func (this *PdfWriter) out(s string) { 252 | this.current_obj.buffer.WriteString(s) 253 | this.current_obj.buffer.WriteString("\n") 254 | } 255 | 256 | // Output PDF data 257 | func (this *PdfWriter) straightOut(s string) { 258 | this.current_obj.buffer.WriteString(s) 259 | } 260 | 261 | // Output a PdfValue 262 | func (this *PdfWriter) writeValue(value *PdfValue) { 263 | switch value.Type { 264 | case PDF_TYPE_TOKEN: 265 | this.straightOut(value.Token + " ") 266 | break 267 | 268 | case PDF_TYPE_NUMERIC: 269 | this.straightOut(fmt.Sprintf("%d", value.Int) + " ") 270 | break 271 | 272 | case PDF_TYPE_REAL: 273 | this.straightOut(fmt.Sprintf("%F", value.Real) + " ") 274 | break 275 | 276 | case PDF_TYPE_ARRAY: 277 | this.straightOut("[") 278 | for i := 0; i < len(value.Array); i++ { 279 | this.writeValue(value.Array[i]) 280 | } 281 | this.out("]") 282 | break 283 | 284 | case PDF_TYPE_DICTIONARY: 285 | this.straightOut("<<") 286 | for k, v := range value.Dictionary { 287 | this.straightOut(k + " ") 288 | this.writeValue(v) 289 | } 290 | this.straightOut(">>") 291 | break 292 | 293 | case PDF_TYPE_OBJREF: 294 | // An indirect object reference. Fill the object stack if needed. 295 | // Check to see if object already exists on the don_obj_stack. 296 | if _, ok := this.don_obj_stack[value.Id]; !ok { 297 | this.newObj(-1, true) 298 | this.obj_stack[value.Id] = &PdfValue{Type: PDF_TYPE_OBJREF, Gen: value.Gen, Id: value.Id, NewId: this.n} 299 | this.don_obj_stack[value.Id] = &PdfValue{Type: PDF_TYPE_OBJREF, Gen: value.Gen, Id: value.Id, NewId: this.n} 300 | } 301 | 302 | // Get object ID from don_obj_stack 303 | objId := this.don_obj_stack[value.Id].NewId 304 | this.outObjRef(objId) 305 | //this.out(fmt.Sprintf("%d 0 R", objId)) 306 | break 307 | 308 | case PDF_TYPE_STRING: 309 | // A string 310 | this.straightOut("(" + value.String + ")") 311 | break 312 | 313 | case PDF_TYPE_STREAM: 314 | // A stream. First, output the stream dictionary, then the stream data itself. 315 | this.writeValue(value.Value) 316 | this.out("stream") 317 | this.out(string(value.Stream.Bytes)) 318 | this.out("endstream") 319 | break 320 | 321 | case PDF_TYPE_HEX: 322 | this.straightOut("<" + value.String + ">") 323 | break 324 | 325 | case PDF_TYPE_BOOLEAN: 326 | if value.Bool { 327 | this.straightOut("true ") 328 | } else { 329 | this.straightOut("false ") 330 | } 331 | break 332 | 333 | case PDF_TYPE_NULL: 334 | // The null object 335 | this.straightOut("null ") 336 | break 337 | } 338 | } 339 | 340 | // Output Form XObjects (1 for each template) 341 | // returns a map of template names (e.g. /GOFPDITPL1) to PdfObjectId 342 | func (this *PdfWriter) PutFormXobjects(reader *PdfReader) (map[string]*PdfObjectId, error) { 343 | // Set current reader 344 | this.r = reader 345 | 346 | var err error 347 | var result = make(map[string]*PdfObjectId, 0) 348 | 349 | compress := true 350 | filter := "" 351 | if compress { 352 | filter = "/Filter /FlateDecode " 353 | } 354 | 355 | for i := 0; i < len(this.tpls); i++ { 356 | tpl := this.tpls[i] 357 | if tpl == nil { 358 | return nil, errors.New("Template is nil") 359 | } 360 | var p string 361 | if compress { 362 | var b bytes.Buffer 363 | w := zlib.NewWriter(&b) 364 | w.Write([]byte(tpl.Buffer)) 365 | w.Close() 366 | 367 | p = b.String() 368 | } else { 369 | p = tpl.Buffer 370 | } 371 | 372 | // Create new PDF object 373 | this.newObj(-1, false) 374 | 375 | cN := this.n // remember current "n" 376 | 377 | tpl.N = this.n 378 | 379 | // Return xobject form name and object position 380 | pdfObjId := new(PdfObjectId) 381 | pdfObjId.id = cN 382 | pdfObjId.hash = this.shaOfInt(cN) 383 | result[fmt.Sprintf("/GOFPDITPL%d", i+this.tpl_id_offset)] = pdfObjId 384 | 385 | this.out("<<" + filter + "/Type /XObject") 386 | this.out("/Subtype /Form") 387 | this.out("/FormType 1") 388 | 389 | this.out(fmt.Sprintf("/BBox [%.2F %.2F %.2F %.2F]", tpl.Box["llx"]*this.k, tpl.Box["lly"]*this.k, (tpl.Box["urx"]+tpl.X)*this.k, (tpl.Box["ury"]-tpl.Y)*this.k)) 390 | 391 | var c, s, tx, ty float64 392 | c = 1 393 | 394 | // Handle rotated pages 395 | if tpl.Box != nil { 396 | tx = -tpl.Box["llx"] 397 | ty = -tpl.Box["lly"] 398 | 399 | if tpl.Rotation != 0 { 400 | angle := float64(tpl.Rotation) * math.Pi / 180.0 401 | c = math.Cos(float64(angle)) 402 | s = math.Sin(float64(angle)) 403 | 404 | switch tpl.Rotation { 405 | case -90: 406 | tx = -tpl.Box["lly"] 407 | ty = tpl.Box["urx"] 408 | break 409 | 410 | case -180: 411 | tx = tpl.Box["urx"] 412 | ty = tpl.Box["ury"] 413 | break 414 | 415 | case -270: 416 | tx = tpl.Box["ury"] 417 | ty = -tpl.Box["llx"] 418 | } 419 | } 420 | } else { 421 | tx = -tpl.Box["x"] * 2 422 | ty = tpl.Box["y"] * 2 423 | } 424 | 425 | tx *= this.k 426 | ty *= this.k 427 | 428 | if c != 1 || s != 0 || tx != 0 || ty != 0 { 429 | this.out(fmt.Sprintf("/Matrix [%.5F %.5F %.5F %.5F %.5F %.5F]", c, s, -s, c, tx, ty)) 430 | } 431 | 432 | // Now write resources 433 | this.out("/Resources ") 434 | 435 | if tpl.Resources != nil { 436 | this.writeValue(tpl.Resources) // "n" will be changed 437 | } else { 438 | return nil, errors.New("Template resources are empty") 439 | } 440 | 441 | nN := this.n // remember new "n" 442 | this.n = cN // reset to current "n" 443 | 444 | this.out("/Length " + fmt.Sprintf("%d", len(p)) + " >>") 445 | 446 | this.out("stream") 447 | this.out(p) 448 | this.out("endstream") 449 | 450 | this.endObj() 451 | 452 | this.n = nN // reset to new "n" 453 | 454 | // Put imported objects, starting with the ones from the XObject's Resources, 455 | // then from dependencies of those resources). 456 | err = this.putImportedObjects(reader) 457 | if err != nil { 458 | return nil, errors.Wrap(err, "Failed to put imported objects") 459 | } 460 | } 461 | 462 | return result, nil 463 | } 464 | 465 | func (this *PdfWriter) putImportedObjects(reader *PdfReader) error { 466 | var err error 467 | var nObj *PdfValue 468 | 469 | // obj_stack will have new items added to it in the inner loop, so do another loop to check for extras 470 | // TODO make the order of this the same every time 471 | for { 472 | atLeastOne := false 473 | 474 | // FIXME: How to determine number of objects before this loop? 475 | for i := 0; i < 9999; i++ { 476 | k := i 477 | v := this.obj_stack[i] 478 | 479 | if v == nil { 480 | continue 481 | } 482 | 483 | atLeastOne = true 484 | 485 | nObj, err = reader.resolveObject(v) 486 | if err != nil { 487 | return errors.Wrap(err, "Unable to resolve object") 488 | } 489 | 490 | // New object with "NewId" field 491 | this.newObj(v.NewId, false) 492 | 493 | if nObj.Type == PDF_TYPE_STREAM { 494 | this.writeValue(nObj) 495 | } else { 496 | this.writeValue(nObj.Value) 497 | } 498 | 499 | this.endObj() 500 | 501 | // Remove from stack 502 | this.obj_stack[k] = nil 503 | } 504 | 505 | if !atLeastOne { 506 | break 507 | } 508 | } 509 | 510 | return nil 511 | } 512 | 513 | // Get the calculated size of a template 514 | // If one size is given, this method calculates the other one 515 | func (this *PdfWriter) getTemplateSize(tplid int, _w float64, _h float64) map[string]float64 { 516 | result := make(map[string]float64, 2) 517 | 518 | tpl := this.tpls[tplid] 519 | 520 | w := tpl.W 521 | h := tpl.H 522 | 523 | if _w == 0 && _h == 0 { 524 | _w = w 525 | _h = h 526 | } 527 | 528 | if _w == 0 { 529 | _w = _h * w / h 530 | } 531 | 532 | if _h == 0 { 533 | _h = _w * h / w 534 | } 535 | 536 | result["w"] = _w 537 | result["h"] = _h 538 | 539 | return result 540 | } 541 | 542 | func (this *PdfWriter) UseTemplate(tplid int, _x float64, _y float64, _w float64, _h float64) (string, float64, float64, float64, float64) { 543 | tpl := this.tpls[tplid] 544 | 545 | w := tpl.W 546 | h := tpl.H 547 | 548 | _x += tpl.X 549 | _y += tpl.Y 550 | 551 | wh := this.getTemplateSize(0, _w, _h) 552 | 553 | _w = wh["w"] 554 | _h = wh["h"] 555 | 556 | tData := make(map[string]float64, 9) 557 | tData["x"] = 0.0 558 | tData["y"] = 0.0 559 | tData["w"] = _w 560 | tData["h"] = _h 561 | tData["scaleX"] = (_w / w) 562 | tData["scaleY"] = (_h / h) 563 | tData["tx"] = _x 564 | tData["ty"] = (0 - _y - _h) 565 | tData["lty"] = (0 - _y - _h) - (0-h)*(_h/h) 566 | 567 | return fmt.Sprintf("/GOFPDITPL%d", tplid+this.tpl_id_offset), tData["scaleX"], tData["scaleY"], tData["tx"] * this.k, tData["ty"] * this.k 568 | } 569 | --------------------------------------------------------------------------------