├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── _config.yml ├── demo ├── .gitignore ├── demo.go ├── demo_test.go ├── filler_text.go ├── png_images.png └── samples │ ├── corporate.pdf │ ├── corporate.png │ ├── hello.pdf │ ├── hello.png │ └── zapf_dingbats_table.pdf ├── doc └── changelog.md ├── go.mod ├── image ├── alter_api.png ├── dice.png ├── gophers.png ├── rgbt64.png └── rgbw64.png ├── pdf_core.go ├── pdf_test.go └── pdf_ttfont.go /.gitignore: -------------------------------------------------------------------------------- 1 | ## ----------------------------------------------------------------------------- 2 | ## github.com/balacode/one-file-pdf one-file-pdf/[.gitignore] 3 | ## (c) balarabe@protonmail.com License: MIT 4 | ## ----------------------------------------------------------------------------- 5 | 6 | ## binary files 7 | *.exe 8 | *.ttf 9 | demo/demo 10 | 11 | ## test binary, build with `go test -c` 12 | *.test 13 | 14 | ## output of the go coverage tool 15 | *.out 16 | 17 | ## drafts, etc. 18 | *.repl 19 | *`* 20 | __* 21 | ~~* 22 | 23 | ## logfiles and batch files 24 | *.log 25 | *.bat 26 | 27 | ## ignore generated PDF files 28 | ## (reference samples are in demo/sample) 29 | demo/corporate.pdf 30 | demo/hello.pdf 31 | demo/png_images.pdf 32 | 33 | ## end 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | sudo: false 4 | 5 | go: 6 | - 1.9.2 7 | - tip 8 | 9 | before_install: 10 | - go get github.com/mattn/goveralls 11 | 12 | script: 13 | - $GOPATH/bin/goveralls -service=travis-ci 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at balarabe@protonmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Balarabe (balacode@protonmail.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## one-file-pdf - A minimalist PDF generator in <2K lines and 1 file 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/balacode/one-file-pdf)](https://goreportcard.com/report/github.com/balacode/one-file-pdf) 3 | [![Build Status](https://travis-ci.org/balacode/one-file-pdf.svg?branch=master)](https://travis-ci.org/balacode/one-file-pdf) 4 | [![Test Coverage](https://coveralls.io/repos/github/balacode/one-file-pdf/badge.svg?branch=master&service=github)](https://coveralls.io/github/balacode/one-file-pdf?branch=master) 5 | [![Gitter chat](https://badges.gitter.im/balacode/one-file-pdf.png)](https://gitter.im/one-file-pdf/Lobby) 6 | [![godoc](https://godoc.org/github.com/balacode/one-file-pdf?status.svg)](https://godoc.org/github.com/balacode/one-file-pdf) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 8 | 9 | The main idea behind this project was: 10 | *"How small can I make a PDF generator for it to still be useful for 80% of common PDF generation needs?"* 11 | 12 | The result is a single .go file with less than 1999 lines of code, about 400 of which are color and glyph-size constants, and ~350 are comments. 13 | 14 | - It's easier to learn about the internals of the PDF format with a small, concise library. 15 | - The current version of the file is indicated in the header (the timestamp). 16 | 17 | ## Features: 18 | - The essentials for generating PDF documents, sufficient for common business reports. 19 | - Use all built-in PDF fonts: Courier, Helvetica, Symbol, Times, ZapfDingbats, and their variants 20 | - Specify colo(u)rs by name (144 web colors), HTML codes (#RRGGBB) or RGB value 21 | - Set columns for text (like tab stops on the page) 22 | - Built-in grid option to help measurement and positioning 23 | - Metadata properties: author, creator, keywords, subject and title 24 | - Set the measurement units you want: mm, cm, inches, twips or points 25 | - Draw lines with different thickness 26 | - Filled or outline rectangles, circles and ellipses 27 | - JPEG, GIF and transparent PNG images (filled with specified background color) 28 | - Stream compression can be turned on or off (PDF files normally compress streams to reduce file size, but turning it off helps in debugging or learning about PDF commands) 29 | 30 | ## Not Yet Supported: 31 | - Unicode (requires font embedding) 32 | - Font embedding 33 | - PDF encryption 34 | - Paths, curves and complex graphics 35 | 36 | ## Installation: 37 | 38 | ```bash 39 |    go get github.com/balacode/one-file-pdf 40 | ``` 41 | 42 | ## Naming Convention: 43 | All types in are prefixed with PDF for public, and 'pdf' for private types. 44 | The only type you need to use is PDF, while PDFColorNames are left public for reference. 45 | 46 | ## Hello World: 47 | 48 | ```go 49 | package main 50 | 51 | import ( 52 | "fmt" 53 | "github.com/balacode/one-file-pdf" 54 | ) 55 | 56 | func main() { 57 | fmt.Println(`Generating a "Hello World" PDF...`) 58 | 59 | // create a new PDF using 'A4' page size 60 | var pdf = pdf.NewPDF("A4") 61 | 62 | // set the measurement units to centimeters 63 | pdf.SetUnits("cm") 64 | 65 | // draw a grid to help us align stuff (just a guide, not necessary) 66 | pdf.DrawUnitGrid() 67 | 68 | // draw the word 'HELLO' in orange, using 100pt bold Helvetica font 69 | // - text is placed on top of, not below the Y-coordinate 70 | // - you can use method chaining 71 | pdf.SetFont("Helvetica-Bold", 100). 72 | SetXY(5, 5). 73 | SetColor("Orange"). 74 | DrawText("HELLO") 75 | 76 | // draw the word 'WORLD' in blue-violet, using 100pt Helvetica font 77 | // note that here we use the colo(u)r hex code instead 78 | // of its name, using the CSS/HTML format: #RRGGBB 79 | pdf.SetXY(5, 9). 80 | SetColor("#8A2BE2"). 81 | SetFont("Helvetica", 100). 82 | DrawText("WORLD!") 83 | 84 | // draw a flower icon using 300pt Zapf-Dingbats font 85 | pdf.SetX(7).SetY(17). 86 | SetColorRGB(255, 0, 0). 87 | SetFont("ZapfDingbats", 300). 88 | DrawText("a") 89 | 90 | // save the file: 91 | // if the file exists, it will be overwritten 92 | // if the file is in use, prints an error message 93 | pdf.SaveFile("hello.pdf") 94 | } // main 95 | ``` 96 | 97 | ## Samples: 98 | Click on a sample to see the PDF in more detail. 99 | 100 | [!["Hello World!" sample image](demo/samples/hello.png)](demo/samples/hello.pdf) 101 | 102 | [!["Synergy Ipsum" sample image](demo/samples/corporate.png)](demo/samples/corporate.pdf) 103 | 104 | ## Changelog: 105 | 106 | These are the most recent changes in the functionality of the package, 107 | not including internal changes which are best seen in the commits history. 108 | 109 | **2018-04-14** 110 | - Changed CurrentPage from read-only to read/write property: added SetCurrentPage() 111 | - Created PageCount() read-only property 112 | - Created dingbats() demo to generate `zapf_dingbats_table.pdf`. 113 | You can use this table to look up the hex code for each icon. 114 | - Changed text encoding from /WinAnsiEncoding to /StandardEncoding 115 | 116 | See [changelog.md](./doc/changelog.md) for changes made earlier. 117 | 118 | ## Roadmap: 119 | 120 | - Achieve 100% test coverage 121 | - Create a unit test for every method 122 | - Unicode support 123 | - Partial font embedding 124 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | ## ----------------------------------------------------------------------------- 2 | ## github.com/balacode/one-file-pdf one-file-pdf/demo/[.gitignore] 3 | ## (c) balarabe@protonmail.com License: MIT 4 | ## ----------------------------------------------------------------------------- 5 | 6 | ## ignore this file till it can be made smaller 7 | ## it is about 6MB now! 8 | png_images.pdf 9 | 10 | ## end 11 | -------------------------------------------------------------------------------- /demo/demo.go: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // github.com/balacode/one-file-pdf one-file-pdf/demo/[demo.go] 3 | // (c) balarabe@protonmail.com License: MIT 4 | // ----------------------------------------------------------------------------- 5 | 6 | package main 7 | 8 | // This demo generates the sample PDF files 9 | // and demonstrates the API of PDF 10 | 11 | import ( 12 | "fmt" 13 | "strings" 14 | 15 | "github.com/balacode/one-file-pdf" 16 | ) 17 | 18 | func main() { 19 | /*1*/ helloWorld() 20 | /*2*/ corporateIpsum() 21 | /*3*/ pngImages() 22 | /*4*/ dingbats() 23 | } // main 24 | 25 | func helloWorld() { 26 | fmt.Println(`Generating a "Hello World" PDF...`) 27 | // 28 | // create a new PDF using 'A4' page size 29 | doc := pdf.NewPDF("A4") 30 | // 31 | // you can call AddPage() to add the first page, but it is 32 | // not required: the first page is added automatically 33 | // doc.AddPage() 34 | // 35 | // set the measurement units to centimeters 36 | doc.SetUnits("cm") 37 | // 38 | // draw a grid to help us align stuff (just a guide, not necessary) 39 | doc.DrawUnitGrid() 40 | // 41 | // draw the word 'HELLO' in orange, using 100pt bold Helvetica font 42 | // - text is placed on top of, not below the Y-coordinate 43 | // - you can use method chaining 44 | doc.SetFont("Helvetica-Bold", 100). 45 | SetXY(5, 5). 46 | SetColor("Orange"). 47 | DrawText("HELLO") 48 | // 49 | // draw the word 'WORLD' in blue-violet, using 100pt Helvetica font 50 | // note that here we use the colo(u)r hex code instead 51 | // of its name, using the CSS/HTML format: #RRGGBB 52 | doc.SetXY(5, 9). 53 | SetColor("#8A2BE2"). 54 | SetFont("Helvetica", 100). 55 | DrawText("WORLD!") 56 | // 57 | // draw a flower icon using 300pt Zapf-Dingbats font 58 | doc.SetXY(7, 17). 59 | SetColor("Red"). 60 | SetFont("ZapfDingbats", 300). 61 | DrawText("a") 62 | // 63 | // save the file: 64 | // if the file exists, it will be overwritten 65 | // if the file is in use, prints an error message 66 | doc.SaveFile("hello.pdf") 67 | } // helloWorld 68 | 69 | func corporateIpsum() { 70 | const FILENAME = "corporate.pdf" 71 | fmt.Println("Generating sample PDF:", FILENAME, "...") 72 | doc := pdf.NewPDF("A4") // create a new PDF using 'A4' page size 73 | doc.SetUnits("cm") 74 | // 75 | // draw the heading 76 | doc.SetColor("#002FA7 InternationalKleinBlue"). 77 | FillBox(0, 1.5, 21, 1.5). 78 | SetFont("Helvetica-Bold", 50). 79 | SetColor("White"). 80 | SetXY(3.5, 2.7).DrawText("Synergy Ipsum") 81 | // 82 | // draw the green circle 83 | doc.SetColor("#74C365 Mantis").FillCircle(21, 21, 10) // x, y, radius 84 | // 85 | // draw the left column of text (in a box) 86 | col1 := strings.ReplaceAll(CorporateFiller1, "\n", " ") 87 | doc.SetColor("#73C2FB MayaBlue"). 88 | FillBox(0, 4, 10, 15). // xywh 89 | SetColor("black"). 90 | SetFont("times-roman", 11). 91 | DrawTextInBox(0.5, 4.5, 9, 15, "LT", col1) 92 | // 93 | // draw the right column of text 94 | col2 := strings.ReplaceAll(CorporateFiller2, "\n", " ") 95 | doc.SetColor("black"). 96 | SetFont("Times-Italic", 11). 97 | DrawTextInBox(10.5, 4, 9, 28, "LT", col2) 98 | // 99 | // draw the bottom-left box with a checkmark 100 | doc.SetColor("#EAA221 Marigold"). 101 | FillBox(0, 25, 5, 5). // xywh 102 | SetFont("zapfdingbats", 50). 103 | SetColor("white"). 104 | DrawTextInBox(0, 25, 5, 5, "C", string(rune(063))) 105 | // 106 | // save the file 107 | doc.SaveFile(FILENAME) 108 | } // corporateIpsum 109 | 110 | func pngImages() { 111 | const FILENAME = "png_images.pdf" 112 | fmt.Println("Generating sample PDF:", FILENAME, "...") 113 | doc := pdf.NewPDF("A4") 114 | doc.SetUnits("cm") 115 | // 116 | // draw background pattern 117 | for x := 0.0; x < doc.PageWidth(); x += 6 { 118 | for y := 0.0; y < doc.PageHeight(); y += 5 { 119 | doc.DrawImage(x, y, 5, "../image/gophers.png", "cyan") 120 | } 121 | } 122 | // draw dice 123 | doc.SetColor("WHITE").FillBox(3.5, 4.5, 14.7, 17). 124 | // 125 | DrawImage(4, 5, 5, "../image/dice.png", "WHITE"). 126 | DrawImage(11, 5, 5, "../image/dice.png", "RED"). 127 | // 128 | DrawImage(4, 10.5, 5, "../image/dice.png", "GREEN"). 129 | DrawImage(11, 10.5, 5, "../image/dice.png", "BLUE"). 130 | // 131 | DrawImage(4, 16, 5, "../image/dice.png", "BLACK"). 132 | SetFont("Helvetica-Bold", 50). 133 | SetXY(3, 3).SetColor("#009150"). 134 | DrawText("PNG Image Demo") 135 | // 136 | doc.SaveFile(FILENAME) 137 | } // pngImages 138 | 139 | // dingbats generates a useful table of icon codes for Zapf-Dingbats 140 | func dingbats() { 141 | const filename = "zapf_dingbats_table.pdf" 142 | fmt.Println("Generating", filename, "...") 143 | // 144 | // create a new PDF using 'A4' page size 145 | doc := pdf.NewPDF("A4") 146 | doc.SetUnits("cm") 147 | // 148 | const boxSize = 1.2 // cm 149 | x, y := 1.0, 1.0 // cm 150 | // 151 | doc.SetFont("Helvetica-Bold", 100) 152 | doc.SetLineWidth(0.02) 153 | // 154 | for row := 0; row < 16; row++ { 155 | x = 1.0 156 | for col := 0; col < 16; col++ { 157 | // 158 | // draw border around each icon 159 | doc.SetColor("gray") 160 | doc.DrawBox(x, y, boxSize, boxSize) 161 | // 162 | // draw hex code 163 | doc.SetColor("dark green") 164 | doc.SetFont("Helvetica", 7) 165 | doc.DrawTextInBox(x+0.1, y, boxSize, boxSize, "TL", 166 | fmt.Sprintf("%02Xh", row*16+col)) 167 | // 168 | // draw decimmal code 169 | doc.SetColor("dark violet") 170 | doc.SetFont("Helvetica", 7) 171 | doc.DrawTextInBox(x, y, boxSize, boxSize, "TR", 172 | fmt.Sprintf("%d", row*16+col)) 173 | // 174 | // this is the right way to use a dingbat code (0-255): 175 | // (casting int to rune to string won't work as expected) 176 | code := row*16 + col 177 | s := string([]byte{byte(code)}) 178 | // 179 | // draw the dingbat icon 180 | doc.SetColor("black") 181 | doc.SetFont("ZapfDingbats", 20) 182 | doc.DrawTextInBox(x, y+0.1, boxSize, boxSize, "C", s) 183 | // 184 | x += boxSize 185 | } 186 | y += boxSize 187 | } 188 | doc.SaveFile(filename) 189 | } // dingbats 190 | 191 | // end 192 | -------------------------------------------------------------------------------- /demo/demo_test.go: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // github.com/balacode/one-file-pdf one-file-pdf/demo/[demo_test.go] 3 | // (c) balarabe@protonmail.com License: MIT 4 | // ----------------------------------------------------------------------------- 5 | 6 | package main 7 | 8 | import ( 9 | "testing" 10 | ) 11 | 12 | func TestDemos(t *testing.T) { 13 | main() 14 | } // TestDemos 15 | 16 | // end 17 | -------------------------------------------------------------------------------- /demo/filler_text.go: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // github.com/balacode/one-file-pdf one-file-pdf/demo/[filler_text.go] 3 | // (c) balarabe@protonmail.com License: MIT 4 | // ----------------------------------------------------------------------------- 5 | 6 | package main 7 | 8 | // CorporateFiller1 is the filler in the first column of "Synergy Ipsum" demo 9 | const CorporateFiller1 = ` 10 | CORPORATE SYNERGY: WIN-WIN FOR ALL STAKEHOLDERS 11 | Dramatically mesh low-risk high-yield alignments 12 | before transparent e-tailers. Completely pursue 13 | scalable customer service through sustainable 14 | potentialities. Enthusiastically mesh long-term high- 15 | impact infrastructures vis-a-vis efficient customer 16 | service. Distinctively re-engineer revolutionary meta- 17 | services and premium architectures. Continually 18 | reintermediate integrated processes through 19 | technically sound intellectual capital. Credibly 20 | reintermediate backend ideas for cross-platform 21 | models. Efficiently unleash cross-media information 22 | without cross-media value. Credibly pontificate highly 23 | efficient manufactured products and enabled data. 24 | Holisticly predominate extensible testing procedures 25 | for reliable supply chains. Quickly maximize timely 26 | deliverables for real-time schemas. Energistically 27 | microcoordinate clicks- and-mortar testing procedures 28 | via next-generation manufactured products. Uniquely 29 | matrix economically sound value through cooperative 30 | technology. Seamlessly underwhelm optimal testing 31 | procedures via bricks-and- clicks processes. 32 | Collaboratively unleash market-driven "outside the 33 | box" thinking for long-term high-impact solutions. 34 | Dynamically target high-payoff intellectual capital 35 | for customized technologies. Collaboratively 36 | administrate empowered markets via plug-and- play 37 | networks. Objectively innovate empowered manufactured 38 | products via parallel platforms. Compellingly embrace 39 | empowered e-business after user friendly intellectual 40 | capital. Dramatically engage top- line web services 41 | vis-a-vis cutting-edge deliverables.` 42 | 43 | // CorporateFiller2 is the filler in the second column of "Synergy Ipsum" demo 44 | const CorporateFiller2 = ` 45 | Seamlessly empower fully researched growth strategies and 46 | interoperable internal or "organic" sources. Dramatically 47 | maintain clicks-and-mortar solutions without functional 48 | solutions Collaboratively build backward-compatible 49 | relationships via tactical paradigms. Assertively iterate 50 | resource maximizing products after leading-edge intellectual 51 | capital. Efficiently enable enabled sources and cost effective 52 | products. Continually whiteboard superior opportunities via 53 | covalent scenarios. Dramatically synthesize integrated schemas 54 | with optimal networks. Compellingly reconceptualize compelling 55 | outsourcing via optimal customer service. Progressively maintain 56 | extensive infomediaries via extensible niches. Dramatically 57 | engage high-payoff infomediaries rather than client-centric 58 | imperatives. Rapaciously seize adaptive infomediaries and user- 59 | centric intellectual capital. Quickly disseminate superior 60 | deliverables via web-enabled applications. Objectively pursue 61 | diverse catalysts for change for interoperable meta-services. 62 | Interactively actualize front-end processes with effective 63 | convergence. Compellingly supply just in time catalysts for 64 | change through top-line potentialities. Completely iterate 65 | covalent strategic theme areas via accurate e-markets. 66 | Proactively fabricate one-to-one materials via effective e- 67 | business. Uniquely deploy cross-unit benefits with wireless 68 | testing procedures. Interactively productize premium 69 | technologies via interdependent quality vectors. Professionally 70 | cultivate one-to-one customer service with robust ideas. Quickly 71 | cultivate optimal processes and tactical architectures. 72 | Dramatically visualize customer directed convergence without 73 | revolutionary ROI. Efficiently innovate open-source 74 | infrastructures via inexpensive materials. Globally incubate 75 | standards compliant channels before scalable benefits. 76 | Energistically scale future-proof core competencies vis-a-vis 77 | impactful experiences. Utilize bleeding-edge technologies rather 78 | than just in time initiatives. Proactively envisioned multimedia 79 | based expertise and cross-media growth strategies. Completely 80 | synergize scalable e-commerce rather than high standards in e- 81 | services. Monotonically engage market-driven intellectual 82 | capital through wireless opportunities. Globally microcoordinate 83 | interactive supply chains with distinctive quality vectors. 84 | Phosfluorescently expedite impactful supply chains via focused 85 | results. Collaboratively administrate turnkey channels via 86 | virtual e-tailers. Competently parallel task fully researched 87 | data and enterprise process improvements. Credibly innovate 88 | granular internal or "organic" sources via high standards in 89 | web-readiness. Quickly aggregate B2B users and worldwide 90 | potentialities. Interactively coordinate proactive e-commerce 91 | via process-centric "outside the box" thinking.` 92 | 93 | // end 94 | -------------------------------------------------------------------------------- /demo/png_images.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balacode/one-file-pdf/6d3753730cb9184a9c80d710f7b8784b8539733e/demo/png_images.png -------------------------------------------------------------------------------- /demo/samples/corporate.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balacode/one-file-pdf/6d3753730cb9184a9c80d710f7b8784b8539733e/demo/samples/corporate.pdf -------------------------------------------------------------------------------- /demo/samples/corporate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balacode/one-file-pdf/6d3753730cb9184a9c80d710f7b8784b8539733e/demo/samples/corporate.png -------------------------------------------------------------------------------- /demo/samples/hello.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balacode/one-file-pdf/6d3753730cb9184a9c80d710f7b8784b8539733e/demo/samples/hello.pdf -------------------------------------------------------------------------------- /demo/samples/hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balacode/one-file-pdf/6d3753730cb9184a9c80d710f7b8784b8539733e/demo/samples/hello.png -------------------------------------------------------------------------------- /demo/samples/zapf_dingbats_table.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balacode/one-file-pdf/6d3753730cb9184a9c80d710f7b8784b8539733e/demo/samples/zapf_dingbats_table.pdf -------------------------------------------------------------------------------- /doc/changelog.md: -------------------------------------------------------------------------------- 1 | ## Detailed Changelog 2 | 3 | These are the changes in the functionality of the package, 4 | not including internal changes. Internal changes are are 5 | best seen in the commits history. 6 | 7 | **2018-MAR-30** 8 | - **ALTERED API: Removed SetErrorLogger() method** 9 | - **ALTERED API: ToPoints(): added error return value** 10 | - Initialize PDF automatically, even when NewPDF() wasn't called. The paper size 11 | is A4, and the units CM by default. To specify a different paper size, use NewPDF(). 12 | - No need to add the first page with AddPage(). It is inserted automatically. 13 | - New error handling methods Clean(), Errors(), ErrorInfo() and PullError(). 14 | - SetColumnWidths(): can be called without arguments when you need to reset all columns. 15 | - Added various unit tests, for 95% code coverage. 16 | - Fixed text wrapping bug that could cause PDF to freeze. 17 | 18 | **2018-MAR-14** 19 | - Added support for color JPEG, GIF and PNG images with transparency blending 20 | - Added all standard A, B, and C paper sizes, and US Tabloid and Ledger 21 | - DrawImage(): added backColor optional parameter (using ...) so you can specify the background color for transparent PNGs 22 | - DrawImage(): changed to draw images down from the Y-coordinate position (below, not above Y) 23 | - Created "PNG Images Demo", which outputs to png_images.pdf 24 | - Created ToColor() function to convert named colors and HTML color codes to RGBA color values 25 | - Created ToUnits() method to convert points to the currently-active units of measurement 26 | - Removed PDFNoPage constant 27 | - Created basic unit tests and test helper functions 28 | - Various internal changes, reducing file length by about 60 lines 29 | 30 | **2018-MAR-08** 31 | - New methods DrawCircle(), DrawEllipse(), FillCircle(), FillEllipse() 32 | - New demo demonstrating circles and text wrapping: corporate.pdf ("Synergy Ipsum") 33 | - SetColor(): now allows HTML color values like `"#4C9141 MayGreen"`, ignores the extra chars. 34 | - Log an error when the name of a selected font is unknown. 35 | - Log the specified measurement unit's name when its name is not valid. 36 | 37 | **2018-MAR-09** 38 | - Replaced PDFColor type with standard lib's color.RGBA (import "image/color") 39 | - SetColorRGB(): changed parameters from int to uint8 40 | - Changed PDFPageSize and PDFStandardPageSizes to private structures 41 | - SaveFile() now returns `error` instead of `*PDF`, to allow caller to check for IO errors 42 | - SetColumnWidths() is no longer limited to 100 columns 43 | - Font names and color names can be specified with spaces, underscores or '-' delimiting words 44 | - Removed module-global PDFErrorHandler, created SetErrorLogger() to set the handler for each PDF instance 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // github.com/balacode/one-file-pdf one-file-pdf/[go.mod] 3 | // (c) balarabe@protonmail.com License: MIT 4 | // ----------------------------------------------------------------------------- 5 | 6 | module github.com/balacode/one-file-pdf 7 | 8 | go 1.16 9 | 10 | // end 11 | -------------------------------------------------------------------------------- /image/alter_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balacode/one-file-pdf/6d3753730cb9184a9c80d710f7b8784b8539733e/image/alter_api.png -------------------------------------------------------------------------------- /image/dice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balacode/one-file-pdf/6d3753730cb9184a9c80d710f7b8784b8539733e/image/dice.png -------------------------------------------------------------------------------- /image/gophers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balacode/one-file-pdf/6d3753730cb9184a9c80d710f7b8784b8539733e/image/gophers.png -------------------------------------------------------------------------------- /image/rgbt64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balacode/one-file-pdf/6d3753730cb9184a9c80d710f7b8784b8539733e/image/rgbt64.png -------------------------------------------------------------------------------- /image/rgbw64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balacode/one-file-pdf/6d3753730cb9184a9c80d710f7b8784b8539733e/image/rgbw64.png -------------------------------------------------------------------------------- /pdf_core.go: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // github.com/balacode/one-file-pdf one-file-pdf/[pdf_core.go] 3 | // (c) balarabe@protonmail.com License: MIT 4 | // ----------------------------------------------------------------------------- 5 | 6 | // Package pdf provides a PDF writer type to generate PDF files. 7 | // Create a new PDF writer by assigning pdf.NewPDF(paperSize) to a variable. 8 | // Then call property setters and methods to render the document. 9 | // Finally, call WriteFile(filename) to save the file, 10 | // or use Bytes() to get the PDF document as an array of bytes. 11 | package pdf 12 | 13 | // # Main Structure and Constructor 14 | // PDF struct 15 | // NewPDF(paperSize string) PDF 16 | // 17 | // # Plugins 18 | // pdfNewFontHandler func ()pdfFontHandler 19 | // 20 | // # Read-Only Properties (p *PDF) 21 | // PageCount() int 22 | // PageHeight() float64 23 | // PageWidth() float64 24 | // 25 | // # Properties 26 | // Color() color.RGBA SetColor(nameOrHTMLColor string) *PDF 27 | // SetColorRGB(r, g, b byte) *PDF 28 | // Compression() bool SetCompression(val bool) *PDF 29 | // CurrentPage() int SetCurrentPage(pageNo int) *PDF 30 | // DocAuthor() string SetDocAuthor(s string) *PDF 31 | // DocCreator() string SetDocCreator(s string) *PDF 32 | // DocKeywords() string SetDocKeywords(s string) *PDF 33 | // DocSubject() string SetDocSubject(s string) *PDF 34 | // DocTitle() string SetDocTitle(s string) *PDF 35 | // FontName() string SetFontName(name string) *PDF 36 | // FontSize() float64 SetFontSize(points float64) *PDF 37 | // SetFont(name string, points float64) *PDF 38 | // HorizontalScaling() uint16 SetHorizontalScaling(percent uint16) *PDF 39 | // LineWidth() float64 SetLineWidth(points float64) *PDF 40 | // Units() string SetUnits(units string) *PDF 41 | // X() float64 SetX(x float64) *PDF 42 | // Y() float64 SetY(y float64) *PDF 43 | // SetXY(x, y float64) *PDF 44 | // # Methods (p *PDF) 45 | // AddPage() *PDF 46 | // Bytes() []byte 47 | // DrawBox(x, y, width, height float64, optFill ...bool) *PDF 48 | // DrawCircle(x, y, radius float64, optFill ...bool) *PDF 49 | // DrawEllipse(x, y, xRadius, yRadius float64, 50 | // optFill ...bool) *PDF 51 | // DrawImage(x, y, height float64, fileNameOrBytes interface{}, 52 | // backColor ...string) *PDF 53 | // DrawLine(x1, y1, x2, y2 float64) *PDF 54 | // DrawText(s string) *PDF 55 | // DrawTextAlignedToBox( 56 | // x, y, width, height float64, align, text string) *PDF 57 | // DrawTextAt(x, y float64, text string) *PDF 58 | // DrawTextInBox( 59 | // x, y, width, height float64, align, text string) *PDF 60 | // DrawUnitGrid() *PDF 61 | // FillBox(x, y, width, height float64) *PDF 62 | // FillCircle(x, y, radius float64) *PDF 63 | // FillEllipse(x, y, xRadius, yRadius float64) *PDF 64 | // NextLine() *PDF 65 | // Reset() *PDF 66 | // SaveFile(filename string) error 67 | // SetColumnWidths(widths ...float64) *PDF 68 | // 69 | // # Metrics Methods (p *PDF) 70 | // TextWidth(s string) float64 71 | // ToColor(nameOrHTMLColor string) (color.RGBA, error) 72 | // ToPoints(numberAndUnit string) (float64, error) 73 | // ToUnits(points float64) float64 74 | // WrapTextLines(width float64, text string) (ret []string) 75 | // 76 | // # Error Handling Methods (p *PDF) 77 | // Clean() *PDF 78 | // Errors() []error 79 | // PullError() error 80 | // (*PDF) ErrorInfo(err error) (ret struct { 81 | // ID int 82 | // Msg, Src, Val string 83 | // }) 84 | // 85 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 86 | // # Internal Structures 87 | // pdfError struct 88 | // (err pdfError) Error() string 89 | // pdfFont struct 90 | // pdfImage struct 91 | // pdfPage struct 92 | // pdfPaperSize struct 93 | // 94 | // # Internal Methods (p *PDF) 95 | // applyFont() (handler pdfFontHandler, err error) 96 | // drawTextLine(s string) *PDF 97 | // drawTextBox(x, y, width, height float64, 98 | // wrapText bool, align, text string) *PDF 99 | // init() *PDF 100 | // loadImage(fileNameOrBytes interface{}, back color.RGBA, 101 | // ) (img pdfImage, idx int, err error) 102 | // makeImage(source image.Image, back color.RGBA, 103 | // ) (widthPx, heightPx int, isGray bool, ar []byte) 104 | // reservePage() *PDF 105 | // textWidthPt(s string) float64 106 | // 107 | // # Internal Generation Methods (p *PDF) 108 | // nextObj() int 109 | // write(a ...interface{}) *PDF 110 | // writeCurve(x1, y1, x2, y2, x3, y3 float64) *PDF 111 | // writeMode(optFill ...bool) (mode string) 112 | // writeObj(objType string) *PDF 113 | // writePages(pagesIndex, fontsIndex, imagesIndex int) *PDF 114 | // writeStreamData(ar []byte) *PDF 115 | // writeStreamObj(ar []byte) *PDF 116 | // 117 | // # Internal Functions (*PDF) - just attached to PDF, but not using its data 118 | // escape(s string) string 119 | // isWhiteSpace(s string) bool 120 | // splitLines(s string) []string 121 | // toUpperLettersDigits(s, extras string) string 122 | // (p *PDF): 123 | // getPaperSize(name string) (pdfPaperSize, error) 124 | // getPointsPerUnit(units string) (ret float64, err error) 125 | // putError(id int, msg, val string) *PDF 126 | // writeTo(wr io.Writer, args ...interface{}) (count int, err error) 127 | // 128 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 129 | // # Constants 130 | // PDFColorNames = map[string]color.RGBA 131 | // 132 | // # Internal Constants 133 | // pdfBlack = color.RGBA{A: 255} 134 | // pdfFontNames = []string 135 | // pdfFontWidths = [][]int 136 | // pdfStandardPaperSizes = map[string][2]int 137 | 138 | import ( 139 | "bytes" 140 | "compress/zlib" 141 | "crypto/sha512" 142 | "fmt" 143 | "image" 144 | "image/color" 145 | _ "image/gif" 146 | _ "image/jpeg" 147 | _ "image/png" // init image decoders 148 | "io" 149 | "os" 150 | "reflect" 151 | "runtime" 152 | "strconv" 153 | "strings" 154 | "unicode" // only uses IsDigit(), IsLetter(), IsSpace() 155 | ) 156 | 157 | // ----------------------------------------------------------------------------- 158 | // # Main Structure and Constructor 159 | 160 | // PDF is the main structure representing a PDF document. 161 | type PDF struct { 162 | paperSize pdfPaperSize // paper size used in this PDF 163 | pageNo int // current page number 164 | page *pdfPage // pointer to the current page 165 | pages []pdfPage // all the pages added to this PDF 166 | fonts []pdfFont // all the fonts used in this PDF 167 | images []pdfImage // all the images used in this PDF 168 | columnWidths []float64 // user-set column widths (like tab stops) 169 | columnNo int // number of the current column 170 | units string // name of active measurement unit 171 | ptPerUnit float64 // number of points per measurement unit 172 | color color.RGBA // current drawing color 173 | lineWidth float64 // current line width (in points) 174 | font *pdfFont // currently selected font 175 | fontName string // current font's name 176 | fontSizePt float64 // current font's size (in points) 177 | horzScaling uint16 // horizontal scaling factor (in %) 178 | compression bool // enable stream compression? 179 | content bytes.Buffer // content buffer where PDF is written 180 | writer io.Writer // writer to PDF buffer or current page's buffer 181 | objOffsets []int // object offsets used by Bytes() and write..() 182 | objIndex int // object index used by Bytes() and write..() 183 | errors []error // errors that occurred during method calls 184 | isInit bool // has the PDF been initialized? 185 | // 186 | // document metadata fields 187 | docAuthor, docCreator, docKeywords, docSubject, docTitle string 188 | } // PDF 189 | 190 | // NewPDF creates and initializes a new PDF object. Specify paperSize as: 191 | // A, B, C series (e.g. "A4") or "LETTER", "LEGAL", "LEDGER", or "TABLOID" 192 | // To specify a landscape orientation, add "-L" suffix e.g. "A4-L". 193 | // You can also specify custom paper sizes using "width unit x height unit", 194 | // for example "20 cm x 20 cm" or even "15cm x 10inch", etc. 195 | func NewPDF(paperSize string) PDF { 196 | var p PDF 197 | size, err := p.init().getPaperSize(paperSize) 198 | if err, isT := err.(pdfError); isT { 199 | p.putError(0xE52F92, err.msg, paperSize) 200 | p.paperSize, _ = p.getPaperSize("A4") 201 | } 202 | p.paperSize = size 203 | return p 204 | } // NewPDF 205 | 206 | // ----------------------------------------------------------------------------- 207 | // # Plugins 208 | 209 | // plugin to instantiate a font handler 210 | var pdfNewFontHandler func() pdfFontHandler 211 | 212 | // pdfFontHandler interface provides methods to parse and embed TrueType fonts. 213 | type pdfFontHandler interface { 214 | // 215 | // reads and parses a font from a file name, slice of bytes, or io.Reader 216 | readFont(owner *PDF, font interface{}) bool 217 | // 218 | // returns the width of text 's' in points 219 | textWidthPt(s string) float64 220 | // 221 | // writes text in the string 's' and returns its width in points 222 | writeText(s string) 223 | // 224 | // writes the PDF objects that define the subset font (i.e. embeds font) 225 | writeFontObjects(font *pdfFont) 226 | } // pdfFontHandler 227 | 228 | // ----------------------------------------------------------------------------- 229 | // # Read-Only Properties (p *PDF) 230 | 231 | // PageCount returns the total number of pages in the document. 232 | func (p *PDF) PageCount() int { p.reservePage(); return len(p.pages) } 233 | 234 | // PageHeight returns the height of the current page in selected units. 235 | func (p *PDF) PageHeight() float64 { return p.ToUnits(p.paperSize.heightPt) } 236 | 237 | // PageWidth returns the width of the current page in selected units. 238 | func (p *PDF) PageWidth() float64 { return p.ToUnits(p.paperSize.widthPt) } 239 | 240 | // ----------------------------------------------------------------------------- 241 | // # Properties (p *PDF) 242 | 243 | // Color returns the current color, which is used for text, lines and fills. 244 | func (p *PDF) Color() color.RGBA { p.init(); return p.color } 245 | 246 | // SetColor sets the current color using a web/X11 color name 247 | // (e.g. "HONEY DEW") or HTML color value such as "#191970" 248 | // for midnight blue (#RRGGBB). The current color is used 249 | // for subsequent text and line drawing and fills. 250 | // If the name is unknown or invalid, sets color to black. 251 | func (p *PDF) SetColor(nameOrHTMLColor string) *PDF { 252 | color, err := p.init().ToColor(nameOrHTMLColor) 253 | if err, isT := err.(pdfError); isT { 254 | p.putError(0xE5B3A5, err.msg, nameOrHTMLColor) 255 | } 256 | p.color = color 257 | return p 258 | } // SetColor 259 | 260 | // SetColorRGB sets the current color using red, green and blue values. 261 | // The current color is used for subsequent text/line drawing and fills. 262 | func (p *PDF) SetColorRGB(r, g, b byte) *PDF { 263 | p.init() 264 | p.color = color.RGBA{r, g, b, 255} 265 | return p 266 | } // SetColorRGB 267 | 268 | // Compression returns the current compression mode. If it is true, 269 | // all PDF content will be compressed when the PDF is generated. If 270 | // false, most PDF content (excluding images) will be in plain text, 271 | // which is useful for debugging or to study PDF commands. 272 | func (p *PDF) Compression() bool { p.init(); return p.compression } 273 | 274 | // SetCompression sets the compression mode used to generate the PDF. 275 | // If set to true, all PDF steams will be compressed when the PDF is 276 | // generated. If false, most content (excluding images) will be in 277 | // plain text, which is useful for debugging or to study PDF commands. 278 | func (p *PDF) SetCompression(val bool) *PDF { 279 | p.init() 280 | p.compression = val 281 | return p 282 | } // SetCompression 283 | 284 | // CurrentPage returns the current page's number, starting from 1. 285 | func (p *PDF) CurrentPage() int { return p.pageNo + 1 } 286 | 287 | // SetCurrentPage opens the specified page. Page numbers start from 1. 288 | func (p *PDF) SetCurrentPage(pageNo int) *PDF { 289 | if pageNo < 1 || pageNo > len(p.pages) { 290 | p.putError(0xE65AF0, "pageNo out of range", 291 | fmt.Sprint("pageNo:", pageNo, " range:1..", len(p.pages))) 292 | return p 293 | } 294 | p.pageNo = pageNo - 1 295 | return p 296 | } // SetCurrentPage 297 | 298 | // DocAuthor returns the optional 'document author' metadata property. 299 | func (p *PDF) DocAuthor() string { p.init(); return p.docAuthor } 300 | 301 | // SetDocAuthor sets the optional 'document author' metadata property. 302 | func (p *PDF) SetDocAuthor(s string) *PDF { p.docAuthor = s; return p } 303 | 304 | // DocCreator returns the optional 'document creator' metadata property. 305 | func (p *PDF) DocCreator() string { p.init(); return p.docCreator } 306 | 307 | // SetDocCreator sets the optional 'document creator' metadata property. 308 | func (p *PDF) SetDocCreator(s string) *PDF { p.docCreator = s; return p } 309 | 310 | // DocKeywords returns the optional 'document keywords' metadata property. 311 | func (p *PDF) DocKeywords() string { p.init(); return p.docKeywords } 312 | 313 | // SetDocKeywords sets the optional 'document keywords' metadata property. 314 | func (p *PDF) SetDocKeywords(s string) *PDF { p.docKeywords = s; return p } 315 | 316 | // DocSubject returns the optional 'document subject' metadata property. 317 | func (p *PDF) DocSubject() string { p.init(); return p.docSubject } 318 | 319 | // SetDocSubject sets the optional 'document subject' metadata property. 320 | func (p *PDF) SetDocSubject(s string) *PDF { p.docSubject = s; return p } 321 | 322 | // DocTitle returns the optional 'document subject' metadata property. 323 | func (p *PDF) DocTitle() string { p.init(); return p.docTitle } 324 | 325 | // SetDocTitle sets the optional 'document title' metadata property. 326 | func (p *PDF) SetDocTitle(s string) *PDF { p.docTitle = s; return p } 327 | 328 | // FontName returns the name of the currently-active typeface. 329 | func (p *PDF) FontName() string { p.init(); return p.fontName } 330 | 331 | // SetFontName changes the current font, while using the 332 | // same font size as the previous font. Use one of the 333 | // standard font names, such as 'Helvetica'. 334 | func (p *PDF) SetFontName(name string) *PDF { 335 | p.init() 336 | p.fontName = name 337 | return p 338 | } // SetFontName 339 | 340 | // FontSize returns the current font size in points. 341 | func (p *PDF) FontSize() float64 { p.init(); return p.fontSizePt } 342 | 343 | // SetFontSize changes the current font size in points, 344 | // without changing the currently-selected font typeface. 345 | func (p *PDF) SetFontSize(points float64) *PDF { 346 | p.init() 347 | p.fontSizePt = points 348 | return p 349 | } // SetFontSize 350 | 351 | // SetFont changes the current font name and size in points. 352 | // For the font name, use one of the standard font names, e.g. 'Helvetica'. 353 | // This font will be used for subsequent text drawing. 354 | func (p *PDF) SetFont(name string, points float64) *PDF { 355 | return p.SetFontName(name).SetFontSize(points) 356 | } // SetFont 357 | 358 | // HorizontalScaling returns the current horizontal scaling in percent. 359 | func (p *PDF) HorizontalScaling() uint16 { p.init(); return p.horzScaling } 360 | 361 | // SetHorizontalScaling changes the horizontal scaling in percent. 362 | // For example, 200 will stretch text to double its normal width. 363 | func (p *PDF) SetHorizontalScaling(percent uint16) *PDF { 364 | p.init() 365 | p.horzScaling = percent 366 | return p 367 | } // SetHorizontalScaling 368 | 369 | // LineWidth returns the current line width in points. 370 | func (p *PDF) LineWidth() float64 { p.init(); return p.lineWidth } 371 | 372 | // SetLineWidth changes the line width in points. 373 | func (p *PDF) SetLineWidth(points float64) *PDF { 374 | p.init() 375 | p.lineWidth = points 376 | return p 377 | } // SetLineWidth 378 | 379 | // Units returns the currently selected measurement units. 380 | // E.g.: mm cm " in inch inches tw twip twips pt point points 381 | func (p *PDF) Units() string { p.init(); return p.units } 382 | 383 | // SetUnits changes the current measurement units: 384 | // mm cm " in inch inches tw twip twips pt point points (can be in any case) 385 | func (p *PDF) SetUnits(units string) *PDF { 386 | ppu, err := p.init().getPointsPerUnit(units) 387 | if err, isT := err.(pdfError); isT { 388 | return p.putError(0xEB4AAA, err.msg, units) 389 | } 390 | p.ptPerUnit, p.units = ppu, p.toUpperLettersDigits(units, "") 391 | return p 392 | } // SetUnits 393 | 394 | // X returns the X-coordinate of the current drawing position. 395 | func (p *PDF) X() float64 { return p.reservePage().ToUnits(p.page.x) } 396 | 397 | // SetX changes the X-coordinate of the current drawing position. 398 | func (p *PDF) SetX(x float64) *PDF { 399 | p.init().reservePage() 400 | p.page.x = x * p.ptPerUnit 401 | return p 402 | } // SetX 403 | 404 | // Y returns the Y-coordinate of the current drawing position. 405 | func (p *PDF) Y() float64 { 406 | return p.reservePage().ToUnits(p.paperSize.heightPt - p.page.y) 407 | } // Y 408 | 409 | // SetY changes the Y-coordinate of the current drawing position. 410 | func (p *PDF) SetY(y float64) *PDF { 411 | p.init().reservePage() 412 | p.page.y = p.paperSize.heightPt - y*p.ptPerUnit 413 | return p 414 | } // SetY 415 | 416 | // SetXY changes both X- and Y-coordinates of the current drawing position. 417 | func (p *PDF) SetXY(x, y float64) *PDF { return p.SetX(x).SetY(y) } 418 | 419 | // ----------------------------------------------------------------------------- 420 | // # Methods (p *PDF) 421 | 422 | // AddPage appends a new blank page to the PDF and makes it the current page. 423 | func (p *PDF) AddPage() *PDF { 424 | COLOR := color.RGBA{1, 0, 1, 0x01} // unlikely default color 425 | p.pages = append(p.pages, pdfPage{ 426 | x: -1, y: p.paperSize.heightPt + 1, lineWidth: 1, 427 | strokeColor: COLOR, nonStrokeColor: COLOR, 428 | fontSizePt: 10, horzScaling: 100, 429 | }) 430 | p.pageNo = len(p.pages) - 1 431 | p.page = &p.pages[p.pageNo] 432 | p.writer = &p.page.content 433 | return p 434 | } // AddPage 435 | 436 | // Bytes generates the PDF document from various page and 437 | // auxiliary objects and returns it in an array of bytes, 438 | // identical to the content of a PDF file. This method is where 439 | // you'll find the core structure of a PDF document. 440 | func (p *PDF) Bytes() []byte { 441 | // free any existing generated content and write PDF header 442 | p.reservePage() 443 | const pagesIndex = 3 444 | var ( 445 | fontsIndex = pagesIndex + len(p.pages)*2 446 | imagesIndex = fontsIndex + len(p.fonts) 447 | infoIndex int // set when metadata found 448 | prevWriter = p.writer 449 | ) 450 | p.content.Reset() 451 | p.writer = &p.content 452 | p.objOffsets = []int{} 453 | p.objIndex = 0 454 | p.write("%PDF-1.4\n\n"). 455 | writeObj("/Catalog").write("/Pages 2 0 R>>\n" + "endobj\n\n") 456 | 457 | // 458 | // write /Pages object (2 0 obj), page count, page size and the pages 459 | p.writePages(pagesIndex, fontsIndex, imagesIndex) 460 | // 461 | // write fonts 462 | for _, font := range p.fonts { 463 | if font.handler == nil { 464 | p.writeObj("/Font").write("/Subtype/Type1/Name/FNT", font.id, "\n", 465 | "/BaseFont/", font.name, "\n", 466 | "/Encoding/StandardEncoding>>\n"+"endobj\n") 467 | } else { 468 | font.handler.writeFontObjects(&font) 469 | } 470 | } 471 | // write images 472 | for _, img := range p.images { 473 | colorSpace := "DeviceRGB" 474 | if img.isGray { 475 | colorSpace = "DeviceGray" 476 | } 477 | old := p.compression 478 | p.compression = true 479 | p.writeObj("/XObject"). 480 | write("/Subtype/Image\n", 481 | "/Width ", img.widthPx, "/Height ", img.heightPx, 482 | "/ColorSpace/", colorSpace, "/BitsPerComponent 8\n"). 483 | writeStreamData(img.data).write("\n" + "endobj\n\n") 484 | p.compression = old 485 | } 486 | // write info object 487 | if p.docTitle != "" || p.docSubject != "" || 488 | p.docKeywords != "" || p.docAuthor != "" || p.docCreator != "" { 489 | // 490 | infoIndex = imagesIndex + len(p.images) 491 | p.writeObj("/Info") 492 | for _, tuple := range [][]string{ 493 | {"/Title ", p.docTitle}, {"/Subject ", p.docSubject}, 494 | {"/Keywords ", p.docKeywords}, {"/Author ", p.docAuthor}, 495 | {"/Creator ", p.docCreator}, 496 | } { 497 | if tuple[1] != "" { 498 | p.write(tuple[0], "(", p.escape(tuple[1]), ")") 499 | } 500 | } 501 | p.write(">>\n" + "endobj\n\n") 502 | } 503 | // write cross-reference table at end of document 504 | start := p.content.Len() 505 | p.write("xref\n"+ 506 | "0 ", len(p.objOffsets), "\n"+"0000000000 65535 f \n") 507 | for _, offset := range p.objOffsets[1:] { 508 | p.write(fmt.Sprintf("%010d 00000 n \n", offset)) 509 | } 510 | // write the trailer 511 | p.write("trailer\n"+"< 0 { 513 | p.write("/Info ", infoIndex, " 0 R") // optional reference to info 514 | } 515 | p.write(">>\n"+"startxref\n", start, "\n", "%%EOF\n") 516 | p.writer = prevWriter 517 | return p.content.Bytes() 518 | } // Bytes 519 | 520 | // DrawBox draws a rectangle of the specified width and height, 521 | // with the top-left corner starting at point (x, y). 522 | // To fill the rectangle, pass true in the optional optFill. 523 | func (p *PDF) DrawBox(x, y, width, height float64, optFill ...bool) *PDF { 524 | width, height = width*p.ptPerUnit, height*p.ptPerUnit 525 | x, y = x*p.ptPerUnit, p.paperSize.heightPt-y*p.ptPerUnit-height 526 | mode := p.writeMode(optFill...) 527 | return p.write(x, " ", y, " ", width, " ", height, " re ", mode, "\n") 528 | // re: construct a rectangular path 529 | } // DrawBox 530 | 531 | // DrawCircle draws a circle of radius r centered on (x, y), 532 | // by drawing 4 Bézier curves (PDF has no circle primitive) 533 | // To fill the circle, pass true in the optional optFill. 534 | func (p *PDF) DrawCircle(x, y, radius float64, optFill ...bool) *PDF { 535 | return p.DrawEllipse(x, y, radius, radius, optFill...) 536 | } // DrawCircle 537 | 538 | // DrawEllipse draws an ellipse centered on (x, y), 539 | // with horizontal radius xRadius and vertical radius yRadius 540 | // by drawing 4 Bézier curves (PDF has no ellipse primitive). 541 | // To fill the ellipse, pass true in the optional optFill. 542 | func (p *PDF) DrawEllipse(x, y, xRadius, yRadius float64, 543 | optFill ...bool) *PDF { 544 | x, y = x*p.ptPerUnit, p.paperSize.heightPt-y*p.ptPerUnit 545 | const ratio = 0.552284749830794 // (4/3) * tan(PI/8) 546 | var ( 547 | r = xRadius * p.ptPerUnit // horizontal radius 548 | v = yRadius * p.ptPerUnit // vertical radius 549 | m, n = r * ratio, v * ratio // ratios for control points 550 | mode = p.writeMode(optFill...) // prepare colors/line width 551 | ) 552 | return p.write(x-r, " ", y, " m\n"). // x0 y0 m: move to point (x0, y0) 553 | // control-1 control-2 endpoint 554 | writeCurve(x-r, y+n, x-m, y+v, x+0, y+v). // top left arc 555 | writeCurve(x+m, y+v, x+r, y+n, x+r, y+0). // top right 556 | writeCurve(x+r, y-n, x+m, y-v, x+0, y-v). // bottom right 557 | writeCurve(x-m, y-v, x-r, y-n, x-r, y+0). // bottom left 558 | write(mode, "\n") // b: fill or S: stroke 559 | } // DrawEllipse 560 | 561 | // DrawImage draws a PNG image. x, y, height specify the position and height 562 | // of the image. Width is scaled to match the image's aspect ratio. 563 | // fileNameOrBytes is either a string specifying a file name, 564 | // or a byte slice with PNG image data. 565 | func (p *PDF) DrawImage(x, y, height float64, fileNameOrBytes interface{}, 566 | backColor ...string) *PDF { 567 | // 568 | back := color.RGBA{R: 255, G: 255, B: 255, A: 255} // white by default 569 | if len(backColor) > 0 { 570 | back, _ = p.ToColor(backColor[0]) 571 | } 572 | // add the image to the current page, if not already referenced 573 | p.reservePage() 574 | img, idx, err := p.loadImage(fileNameOrBytes, back) 575 | if err, isT := err.(pdfError); isT { 576 | return p.putError(0xE8F375, err.msg, err.val) 577 | } 578 | var found bool 579 | for _, id := range p.page.imageIDs { 580 | if id == idx { 581 | found = true 582 | break 583 | } 584 | } 585 | if !found { 586 | p.page.imageIDs = append(p.page.imageIDs, idx) 587 | } 588 | // draw the image 589 | h := height * p.ptPerUnit 590 | w := float64(img.widthPx) / float64(img.heightPx) * h 591 | x, y = x*p.ptPerUnit, p.paperSize.heightPt-y*p.ptPerUnit-h 592 | return p.write("q\n", w, " 0 0 ", h, " ", x, " ", y, " cm\n"+ 593 | "/IMG", idx, " Do\n"+"Q\n") 594 | // q: save graphics state 595 | // cm: concatenate matrix to current transform matrix 596 | // Do: invoke named XObject (/IMGn) 597 | // Q: restore graphics state 598 | } // DrawImage 599 | 600 | // DrawLine draws a straight line from point (x1, y1) to point (x2, y2). 601 | func (p *PDF) DrawLine(x1, y1, x2, y2 float64) *PDF { 602 | x1, y1 = x1*p.ptPerUnit, p.paperSize.heightPt-y1*p.ptPerUnit 603 | x2, y2 = x2*p.ptPerUnit, p.paperSize.heightPt-y2*p.ptPerUnit 604 | p.writeMode(true) // prepare color/line width 605 | return p.write(x1, " ", y1, " m ", x2, " ", y2, " l S\n") 606 | // m: move l:line S: stroke path (for lines) 607 | } // DrawLine 608 | 609 | // DrawText draws a text string at the current position (X, Y). 610 | func (p *PDF) DrawText(s string) *PDF { 611 | if len(p.columnWidths) == 0 { 612 | return p.drawTextLine(s) 613 | } 614 | x := 0.0 615 | for i := 0; i < p.columnNo; i, x = i+1, x+p.columnWidths[i] { 616 | } 617 | p.SetX(x).drawTextLine(s) 618 | if p.columnNo < len(p.columnWidths)-1 { 619 | p.columnNo++ 620 | return p 621 | } 622 | return p.NextLine() 623 | } // DrawText 624 | 625 | // DrawTextAlignedToBox draws 'text' within a rectangle specified 626 | // by 'x', 'y', 'width' and 'height'. If 'align' is blank, the 627 | // text is center-aligned both vertically and horizontally. 628 | // Specify 'L' or 'R' to align the text left or right, and 'T' or 629 | // 'B' to align the text to the top or bottom of the box. 630 | func (p *PDF) DrawTextAlignedToBox( 631 | x, y, width, height float64, align, text string) *PDF { 632 | return p.drawTextBox(x, y, width, height, false, align, text) 633 | } // DrawTextAlignedToBox 634 | 635 | // DrawTextAt draws text at the specified point (x, y). 636 | func (p *PDF) DrawTextAt(x, y float64, text string) *PDF { 637 | return p.SetXY(x, y).DrawText(text) 638 | } // DrawTextAt 639 | 640 | // DrawTextInBox draws word-wrapped text within a rectangle 641 | // specified by 'x', 'y', 'width' and 'height'. If 'align' is blank, 642 | // the text is center-aligned both vertically and horizontally. 643 | // Specify 'L' or 'R' to align the text left or right, and 'T' or 644 | // 'B' to align the text to the top or bottom of the box. 645 | func (p *PDF) DrawTextInBox( 646 | x, y, width, height float64, align, text string) *PDF { 647 | return p.drawTextBox(x, y, width, height, true, align, text) 648 | } // DrawTextInBox 649 | 650 | // DrawUnitGrid draws a light-gray grid demarcated in the 651 | // current measurement unit. The grid fills the entire page. 652 | // It helps with item positioning. 653 | func (p *PDF) DrawUnitGrid() *PDF { 654 | pw, ph := p.PageWidth(), p.PageHeight() 655 | p.SetLineWidth(0.1).SetFont("Helvetica", 8) 656 | for i, x := 0, 0.0; x < pw; i, x = i+1, x+1 { // vertical lines | 657 | p.SetColorRGB(200, 200, 200).DrawLine(x, 0, x, ph). 658 | SetColor("Indigo").SetXY(x+0.1, 0.3).DrawText(strconv.Itoa(i)) 659 | } 660 | for i, y := 0, 0.0; y < ph; i, y = i+1, y+1 { // horizontal lines - 661 | p.SetColorRGB(200, 200, 200).DrawLine(0, y, pw, y). 662 | SetColor("Indigo").SetXY(0.1, y+0.3).DrawText(strconv.Itoa(i)) 663 | } 664 | return p 665 | } // DrawUnitGrid 666 | 667 | // FillBox fills a rectangle with the current color. 668 | func (p *PDF) FillBox(x, y, width, height float64) *PDF { 669 | return p.DrawBox(x, y, width, height, true) 670 | } // FillBox 671 | 672 | // FillCircle fills a circle of radius r centered on (x, y), 673 | // by drawing 4 Bézier curves (PDF has no circle primitive) 674 | func (p *PDF) FillCircle(x, y, radius float64) *PDF { 675 | return p.DrawEllipse(x, y, radius, radius, true) 676 | } // FillCircle 677 | 678 | // FillEllipse fills a Ellipse of radius r centered on (x, y), 679 | // by drawing 4 Bézier curves (PDF has no ellipse primitive) 680 | func (p *PDF) FillEllipse(x, y, xRadius, yRadius float64) *PDF { 681 | return p.DrawEllipse(x, y, xRadius, yRadius, true) 682 | } // FillEllipse 683 | 684 | // NextLine advances the text writing position to the next line. 685 | // I.e. the Y increases by the height of the font and 686 | // the X-coordinate is reset to zero. 687 | func (p *PDF) NextLine() *PDF { 688 | x, y := 0.0, p.Y()+p.FontSize()*p.ptPerUnit 689 | if len(p.columnWidths) > 0 { 690 | x = p.columnWidths[0] 691 | } 692 | if y > p.paperSize.heightPt*p.ptPerUnit { 693 | p.AddPage() 694 | y = 0 695 | } 696 | p.columnNo = 0 697 | return p.SetXY(x, y) 698 | } // NextLine 699 | 700 | // Reset releases all resources and resets all variables, except paper size. 701 | func (p *PDF) Reset() *PDF { 702 | p.page, p.writer = nil, nil 703 | *p = NewPDF(p.paperSize.name) 704 | return p 705 | } // Reset 706 | 707 | // SaveFile generates and saves the PDF document to a file. 708 | func (p *PDF) SaveFile(filename string) error { 709 | err := os.WriteFile(filename, p.Bytes(), 0644) 710 | if err != nil { 711 | p.putError(0xED3F6D, "Failed writing file", err.Error()) 712 | } 713 | return err 714 | } // SaveFile 715 | 716 | // SetColumnWidths creates column positions (tab stops) along the X-axis. 717 | // To remove all column positions, call this method without any argument. 718 | func (p *PDF) SetColumnWidths(widths ...float64) *PDF { 719 | p.init() 720 | p.columnWidths = widths 721 | return p 722 | } // SetColumnWidths 723 | 724 | // ----------------------------------------------------------------------------- 725 | // # Metrics Methods (p *PDF) 726 | 727 | // TextWidth returns the width of the text in current units. 728 | func (p *PDF) TextWidth(s string) float64 { 729 | return p.ToUnits(p.textWidthPt(s)) 730 | } // TextWidth 731 | 732 | // ToColor returns an RGBA color value from a web/X11 color name 733 | // (e.g. "HONEY DEW") or HTML color value such as "#191970" 734 | // If the name or code is unknown or invalid, returns zero value (black). 735 | func (p *PDF) ToColor(nameOrHTMLColor string) (color.RGBA, error) { 736 | // 737 | // if name starts with '#' treat it as HTML color code (#RRGGBB) 738 | s := p.toUpperLettersDigits(nameOrHTMLColor, "#") 739 | if len(s) >= 7 && s[0] == '#' { 740 | var hex [6]byte 741 | for i, r := range s[1:7] { 742 | switch { 743 | case r >= '0' && r <= '9': 744 | hex[i] = byte(r - '0') 745 | case r >= 'A' && r <= 'F': 746 | hex[i] = byte(r - 'A' + 10) 747 | default: 748 | return pdfBlack, pdfError{id: 0xEED50B, src: "ToColor", 749 | msg: "Bad color code", val: nameOrHTMLColor} 750 | } 751 | } 752 | return color.RGBA{ 753 | hex[0]*16 + hex[1], 754 | hex[2]*16 + hex[3], 755 | hex[4]*16 + hex[5], 255}, nil 756 | } 757 | if cl, found := PDFColorNames[s]; found { // search for color name (quick) 758 | return color.RGBA{cl.R, cl.G, cl.B, 255}, nil 759 | } 760 | for k, v := range PDFColorNames { // (slower search) 761 | if p.toUpperLettersDigits(k, "") == s { 762 | return v, nil 763 | } 764 | } 765 | return pdfBlack, pdfError{id: 0xE00982, src: "ToColor", 766 | msg: "Unknown color name", val: nameOrHTMLColor} 767 | } // ToColor 768 | 769 | // ToPoints converts a string composed of a number and unit to points. 770 | // For example '1 cm' or '1cm' becomes 28.346 points. 771 | // Recognised units: mm cm " in inch inches tw twip twips pt point points 772 | func (p *PDF) ToPoints(numberAndUnit string) (float64, error) { 773 | var num, unit string // extract number and unit 774 | for _, r := range numberAndUnit { 775 | switch { 776 | case r == '-', r == '.', unicode.IsDigit(r): 777 | num += string(r) 778 | case r == '"', unicode.IsLetter(r): 779 | unit += string(r) 780 | } 781 | } 782 | ppu := 1.0 783 | if unit != "" { 784 | var err error 785 | ppu, err = p.getPointsPerUnit(unit) 786 | if err, isT := err.(pdfError); isT { 787 | return 0, err 788 | } 789 | } 790 | n, err := strconv.ParseFloat(num, 64) 791 | if err != nil { 792 | return 0, pdfError{id: 0xE154AC, msg: "Invalid number", val: num} 793 | } 794 | return n * ppu, nil 795 | } // ToPoints 796 | 797 | // ToUnits converts points to the currently selected unit of measurement. 798 | func (p *PDF) ToUnits(points float64) float64 { 799 | if int(p.ptPerUnit*100) == 0 { 800 | return points 801 | } 802 | return points / p.ptPerUnit 803 | } // ToUnits 804 | 805 | // WrapTextLines splits a string into multiple lines so that the text 806 | // fits in the specified width. The text is wrapped on word boundaries. 807 | // Newline characters ("\r" and "\n") also cause text to be split. 808 | // You can find out the number of lines needed to wrap some 809 | // text by checking the length of the returned array. 810 | func (p *PDF) WrapTextLines(width float64, text string) (ret []string) { 811 | fit := func(s string, step, n int, width float64) int { 812 | for max := len(s); n > 0 && n <= max; { 813 | w := p.TextWidth(s[:n]) 814 | switch step { 815 | case 1, 3: // keep halving (or - 1) until n chars fit in width 816 | if w <= width { 817 | return n 818 | } 819 | n-- 820 | if step == 1 { 821 | n /= 2 822 | } 823 | case 2: // increase n until n chars won't fit in width 824 | if w > width { 825 | return n 826 | } 827 | n = 1 + int((float64(n) * 1.2)) // increase n by 1 + 20% of n 828 | } 829 | } 830 | return 0 831 | } 832 | // split text into lines. then break lines based on text width 833 | for _, line := range p.splitLines(text) { 834 | for p.TextWidth(line) > width { 835 | n := len(line) // reduce, increase, then reduce n to get best fit 836 | for i := 1; i <= 3; i++ { 837 | n = fit(line, i, n, width) 838 | } 839 | // move to the last word (if white-space is found) 840 | found, max := false, n 841 | for n > 0 { 842 | if p.isWhiteSpace(line[n-1 : n]) { 843 | found = true 844 | break 845 | } 846 | n-- 847 | } 848 | if !found { 849 | n = max 850 | } 851 | if n <= 0 { 852 | break 853 | } 854 | ret = append(ret, line[:n]) 855 | line = line[n:] 856 | } 857 | ret = append(ret, line) 858 | } 859 | return ret 860 | } // WrapTextLines 861 | 862 | // ----------------------------------------------------------------------------- 863 | // # Error Handling Methods (p *PDF) 864 | 865 | // Clean clears all accumulated errors. 866 | func (p *PDF) Clean() *PDF { p.errors = nil; return p } 867 | 868 | // ErrorInfo extracts and returns additional error details from PDF errors 869 | func (*PDF) ErrorInfo(err error) (ret struct { 870 | ID int 871 | Msg, Src, Val string 872 | }) { 873 | if err, isT := err.(pdfError); isT { 874 | ret.ID, ret.Msg, ret.Src, ret.Val = err.id, err.msg, err.src, err.val 875 | } 876 | return ret 877 | } // ErrorInfo 878 | 879 | // Errors returns a slice of all accumulated errors. 880 | func (p *PDF) Errors() []error { return p.errors } 881 | 882 | // PullError removes and returns the first error from the errors collection. 883 | func (p *PDF) PullError() error { 884 | if len(p.errors) == 0 { 885 | return nil 886 | } 887 | ret := p.errors[0] 888 | p.errors = p.errors[1:] 889 | return ret 890 | } // PullError 891 | 892 | // ----------------------------------------------------------------------------- 893 | // # Internal Structures 894 | 895 | // pdfError stores extended error details for errors in this package. 896 | type pdfError struct { 897 | id int // unique ID of the error (only within package) 898 | msg, src, val string // the error message, source method and invalid value 899 | } // pdfError 900 | 901 | // Error creates and returns an error message from pdfError details 902 | func (err pdfError) Error() string { 903 | ret := fmt.Sprintf("%s %q", err.msg, err.val) 904 | if err.src != "" { 905 | ret += " @" + err.src 906 | } 907 | return ret 908 | } // Error 909 | 910 | // pdfFont represents a font name and its appearance 911 | type pdfFont struct { 912 | id int 913 | name string 914 | builtInIndex int 915 | isBold, isItalic bool 916 | handler pdfFontHandler 917 | } // pdfFont 918 | 919 | // pdfImage represents an image 920 | type pdfImage struct { 921 | filename string // name of file from which image was read 922 | widthPx, heightPx int // width and height in pixels 923 | data []byte // image data 924 | hash [64]byte // hash of data (used to compare images) 925 | backColor color.RGBA // background color (used to compare images) 926 | isGray bool // image is grayscale? (if false, color image) 927 | } // pdfImage 928 | 929 | // pdfPage holds references, state and the stream buffer for each page 930 | type pdfPage struct { 931 | fontIDs, imageIDs []int // references to fonts and images 932 | x, y, lineWidth, fontSizePt float64 // current drawing state 933 | strokeColor, nonStrokeColor color.RGBA // " 934 | fontID int // " 935 | horzScaling uint16 // " 936 | content bytes.Buffer // write..() calls send output here 937 | } // pdfPage 938 | 939 | // pdfPaperSize represents a page size name and its dimensions in points 940 | type pdfPaperSize struct { 941 | name string // paper size: e.g. 'Letter', 'A4', etc. 942 | widthPt, heightPt float64 // width and height in points 943 | } // pdfPaperSize 944 | 945 | // ----------------------------------------------------------------------------- 946 | // # Internal Methods (p *PDF) 947 | 948 | // applyFont writes a font change command, provided the font has 949 | // been changed since the last operation that uses fonts. 950 | // 951 | // This should be called just before a font needs to be used. 952 | // This way, if a font is picked with SetFontName() property, but 953 | // never used to draw text, no font selection command is output. 954 | // 955 | // Before calling this method, the font name must be already 956 | // set by SetFontName(), which is stored in p.font.fontName 957 | // 958 | // What this method does: 959 | // - Validates the current font name and determines if it is a 960 | // standard (built-in) font like Helvetica or a TrueType font. 961 | // - Fills the document-wide list of fonts (p.fonts). 962 | // - Adds items to the list of font ID's used on the current page. 963 | func (p *PDF) applyFont() (handler pdfFontHandler, err error) { 964 | var ( 965 | font pdfFont 966 | name = p.toUpperLettersDigits(p.fontName, "") 967 | valid = name != "" 968 | ) 969 | if valid { 970 | valid = false 971 | for i, fname := range pdfFontNames { 972 | fname = p.toUpperLettersDigits(fname, "") 973 | if fname != name { 974 | continue 975 | } 976 | has := strings.Contains 977 | font = pdfFont{ 978 | name: pdfFontNames[i], 979 | builtInIndex: i, 980 | isBold: has(fname, "BOLD"), 981 | isItalic: has(fname, "OBLIQUE") || has(fname, "ITALIC"), 982 | } 983 | valid = true 984 | break 985 | } 986 | } 987 | if !valid && pdfNewFontHandler != nil { 988 | handler = pdfNewFontHandler() 989 | valid = handler.readFont(p, p.fontName) 990 | font.handler = handler 991 | } 992 | // if there is no selected font or it's invalid, use Helvetica 993 | if !valid { 994 | err = pdfError{id: 0xE86819, msg: "Invalid font", val: p.fontName} 995 | p.fontName = "Helvetica" 996 | p.applyFont() 997 | return nil, err 998 | } 999 | // has the font been added to the global list? if not, add it: 1000 | for _, it := range p.fonts { 1001 | if font.name == it.name { 1002 | font.id = it.id 1003 | break 1004 | } 1005 | } 1006 | if font.id == 0 { 1007 | font.id = 1 + len(p.fonts) 1008 | p.fonts = append(p.fonts, font) 1009 | } 1010 | if p.page.fontID == font.id && 1011 | int(p.page.fontSizePt*100) == int(p.fontSizePt)*100 { 1012 | return handler, err 1013 | } 1014 | // add the font ID to the current page, if not already referenced 1015 | var alreadyUsedOnPage bool 1016 | for _, id := range p.page.fontIDs { 1017 | if id == font.id { 1018 | alreadyUsedOnPage = true 1019 | break 1020 | } 1021 | } 1022 | if !alreadyUsedOnPage { 1023 | p.page.fontIDs = append(p.page.fontIDs, 0) 1024 | p.page.fontIDs[len(p.page.fontIDs)-1] = font.id 1025 | } 1026 | p.page.fontID = font.id 1027 | p.page.fontSizePt = p.fontSizePt 1028 | p.write("BT /FNT", p.page.fontID, " ", int(p.page.fontSizePt), 1029 | " Tf ET\n") 1030 | // BT: begin text /FNT0 i0 Tf: set font to FNT0 index i0 ET: end text 1031 | return handler, err 1032 | } // applyFont 1033 | 1034 | // drawTextLine writes a line of text at the current coordinates to the 1035 | // current page's content stream, using a sequence of raw PDF commands 1036 | func (p *PDF) drawTextLine(s string) *PDF { 1037 | if s == "" { 1038 | return p 1039 | } 1040 | // draw the text 1041 | handler, err := p.applyFont() 1042 | if err, isT := err.(pdfError); isT { 1043 | p.putError(0xEAEAC4, err.msg, err.val) 1044 | } 1045 | if p.page.horzScaling != p.horzScaling { 1046 | p.page.horzScaling = p.horzScaling 1047 | p.write("BT ", p.page.horzScaling, " Tz ET\n") 1048 | // BT: begin text n0 Tz: set horiz. text scaling to n0% ET: end text 1049 | } 1050 | p.writeMode(true) // fill / non-stroke 1051 | if handler == nil { 1052 | p.write("BT ", int(p.page.x), " ", int(p.page.y), 1053 | " Td (", p.escape(s), ") Tj ET\n") 1054 | // BT: begin text Td: move text position Tj: show text ET: end text 1055 | } else { 1056 | handler.writeText(s) 1057 | } 1058 | p.page.x += p.textWidthPt(s) 1059 | return p 1060 | } // drawTextLine 1061 | 1062 | // drawTextBox draws a line of text, or a word-wrapped block of text. 1063 | // align: specify up to 2 flags: L R T B to align left, right, top or bottom 1064 | // the default (blank) is C center, both vertically and horizontally 1065 | func (p *PDF) drawTextBox(x, y, width, height float64, 1066 | wrapText bool, align, text string) *PDF { 1067 | if text == "" { 1068 | return p 1069 | } 1070 | p.reservePage() 1071 | handler, err := p.applyFont() 1072 | if err, isT := err.(pdfError); isT { 1073 | p.putError(0xE0737C, err.msg, err.val) 1074 | } 1075 | var lines []string 1076 | if wrapText { 1077 | lines = p.WrapTextLines(width, text) 1078 | _ = handler // TODO: ^needs to interact with font handler to get width 1079 | } else { 1080 | lines = []string{text} 1081 | } 1082 | align = strings.ToUpper(align) 1083 | lineHeight := p.FontSize() 1084 | allLinesHeight := lineHeight * float64(len(lines)) 1085 | // 1086 | // calculate aligned y-axis position of text (top, bottom, center) 1087 | y, height = y*p.ptPerUnit+p.fontSizePt, height*p.ptPerUnit 1088 | if strings.Contains(align, "B") { // bottom 1089 | y += height - allLinesHeight - 4 // 4pt margin 1090 | } else if !strings.Contains(align, "T") { 1091 | y += height/2 - allLinesHeight/2 - p.fontSizePt*0.15 // center 1092 | } 1093 | y = p.paperSize.heightPt - y 1094 | // 1095 | // calculate x-axis position of text (left, right, center) 1096 | x, width = x*p.ptPerUnit, width*p.ptPerUnit 1097 | for _, line := range lines { 1098 | off := 0.0 // x-offset to align in box 1099 | if strings.Contains(align, "L") { 1100 | off = p.fontSizePt / 6 // left margin 1101 | } else if strings.Contains(align, "R") { 1102 | off = width - p.textWidthPt(line) - p.fontSizePt/6 1103 | } else { 1104 | off = width/2 - p.textWidthPt(line)/2 // center 1105 | } 1106 | p.page.x, p.page.y = x+off, y 1107 | p.drawTextLine(line) 1108 | y -= lineHeight 1109 | } 1110 | return p 1111 | } // drawTextBox 1112 | 1113 | // init initializes the PDF object, if not initialized already 1114 | func (p *PDF) init() *PDF { 1115 | if p.isInit { 1116 | return p 1117 | } 1118 | p.units = "POINT" 1119 | p.paperSize, _ = p.getPaperSize("A4") 1120 | p.ptPerUnit, _ = p.getPointsPerUnit(p.units) 1121 | p.color, p.lineWidth = pdfBlack, 1 // point 1122 | p.fontName, p.fontSizePt = "Helvetica", 10 1123 | p.horzScaling, p.compression = 100, true 1124 | p.isInit = true 1125 | return p 1126 | } // init 1127 | 1128 | // loadImage reads an image from a file or byte array, stores its data in 1129 | // the PDF's images array, and returns a pdfImage and its reference index 1130 | func (p *PDF) loadImage(fileNameOrBytes interface{}, back color.RGBA, 1131 | ) (img pdfImage, idx int, err error) { 1132 | var buf *bytes.Buffer 1133 | switch val := fileNameOrBytes.(type) { 1134 | case string: 1135 | for i, it := range p.images { 1136 | if it.filename == val && it.backColor == back { 1137 | return it, i, nil 1138 | } 1139 | } 1140 | img.filename = val 1141 | data, err := os.ReadFile(val) 1142 | if err != nil { 1143 | return pdfImage{}, -1, pdfError{id: 0xE9F387, 1144 | msg: "Failed reading file", val: err.Error()} 1145 | } 1146 | buf = bytes.NewBuffer(data) 1147 | img.hash = sha512.Sum512(data) 1148 | case []byte: 1149 | buf = bytes.NewBuffer(val) 1150 | img.hash = sha512.Sum512(val) 1151 | default: 1152 | return pdfImage{}, -1, 1153 | pdfError{id: 0xEE3E42, msg: "Invalid type in fileNameOrBytes", 1154 | val: fmt.Sprintf("%s = %v", 1155 | reflect.TypeOf(fileNameOrBytes), fileNameOrBytes)} 1156 | } 1157 | for i, it := range p.images { 1158 | if bytes.Equal(it.hash[:], img.hash[:]) && it.backColor == back { 1159 | return it, i, nil 1160 | } 1161 | } 1162 | decoded, _, err2 := image.Decode(buf) 1163 | if err2 != nil { 1164 | return pdfImage{}, -1, 1165 | pdfError{id: 0xE64335, msg: "Image not decoded", val: err2.Error()} 1166 | } 1167 | img.backColor = back 1168 | img.widthPx, img.heightPx, img.isGray, img.data = makeImage(decoded, back) 1169 | p.images = append(p.images, img) 1170 | return img, len(p.images) - 1, nil 1171 | } // loadImage 1172 | 1173 | // makeImage encodes the source image in a PDF image data stream 1174 | func makeImage(source image.Image, back color.RGBA, 1175 | ) (widthPx, heightPx int, isGray bool, ar []byte) { 1176 | // 1177 | // blends color into the background (back), using opacity (alpha) value 1178 | blend := func(color, alpha uint32, back byte) byte { 1179 | c, a := float64(color), 65535-float64(alpha) // range 0-65535 1180 | return byte((c + (float64(back)*255-c)/65536*a) / 65536 * 255) 1181 | } 1182 | widthPx, heightPx = source.Bounds().Max.X, source.Bounds().Max.Y 1183 | model := source.ColorModel() 1184 | isGray = model == color.GrayModel || model == color.Gray16Model 1185 | for y := 0; y < heightPx; y++ { 1186 | for x := 0; x < widthPx; x++ { 1187 | r, g, b, a := source.At(x, y).RGBA() // value range: 0-65535 1188 | switch { 1189 | case isGray: 1190 | ar = append(ar, byte(float64(r))) 1191 | case a == 65535: // if fully opaque: 1192 | ar = append(ar, byte(r), byte(g), byte(b)) // use pixel color 1193 | case a == 0: // if transparent: 1194 | ar = append(ar, back.R, back.G, back.B) // use background color 1195 | default: // if semi-transparent: 1196 | ar = append(ar, 1197 | blend(r, a, back.R), // blend pixel and back colors 1198 | blend(g, a, back.G), // based on the alpha value 1199 | blend(b, a, back.B)) 1200 | } 1201 | } 1202 | } 1203 | return widthPx, heightPx, isGray, ar 1204 | } // makeImage 1205 | 1206 | // reservePage ensures there is at least one page in the PDF 1207 | func (p *PDF) reservePage() *PDF { 1208 | if len(p.pages) == 0 { 1209 | p.AddPage() 1210 | } 1211 | return p 1212 | } // reservePage 1213 | 1214 | // textWidthPt returns the width of text in points 1215 | func (p *PDF) textWidthPt(s string) float64 { 1216 | if s == "" { 1217 | return 0 1218 | } 1219 | if p.font != nil && p.font.handler != nil { 1220 | return p.font.handler.textWidthPt(s) 1221 | } 1222 | w := 0.0 1223 | for i, r := range s { 1224 | if r < 0 || r > 255 { 1225 | p.putError(0xE31046, "Rune out of range", 1226 | fmt.Sprintf("at %d = '%s'", i, string(r))) 1227 | break 1228 | } 1229 | id := p.fonts[p.page.fontID-1].builtInIndex 1230 | if id >= 0 && id <= 9 { 1231 | w += float64(pdfFontWidths[r][id]) 1232 | } else { 1233 | w += 600 // for Courier font 1234 | } 1235 | } 1236 | return w * p.fontSizePt / 1000.0 * float64(p.horzScaling) / 100.0 1237 | } // textWidthPt 1238 | 1239 | // ----------------------------------------------------------------------------- 1240 | // # Internal Generation Methods (p *PDF) 1241 | 1242 | // nextObj increases the object serial no. and stores its offset in array 1243 | func (p *PDF) nextObj() int { 1244 | p.objIndex++ 1245 | for len(p.objOffsets) <= p.objIndex { 1246 | p.objOffsets = append(p.objOffsets, p.content.Len()) 1247 | } 1248 | return p.objIndex 1249 | } // nextObj 1250 | 1251 | // write writes strings and numbers to the current page's content 1252 | // stream or to the final generated PDF, if there is no active page 1253 | func (p *PDF) write(a ...interface{}) *PDF { 1254 | p.reservePage() 1255 | p.writeTo(p.writer, a...) 1256 | return p 1257 | } // write 1258 | 1259 | // writeCurve writes a Bézier curve using the 'c' PDF primitive. 1260 | // The starting point is the current (x, y) position. 1261 | // (x1, y1) and (x2, y2) are the two control points, (x3, y3) the end point. 1262 | func (p *PDF) writeCurve(x1, y1, x2, y2, x3, y3 float64) *PDF { 1263 | return p.write(" ", x1, " ", y1, " ", x2, " ", y2, 1264 | " ", x3, " ", y3, " c\n") 1265 | } // writeCurve 1266 | 1267 | // writeMode sets the stroking or non-stroking color and line width. 1268 | // 'fill' arg specifies non-stroking (true) or stroking mode (none/false) 1269 | func (p *PDF) writeMode(optFill ...bool) (mode string) { 1270 | p.reservePage() 1271 | mode = "S" // S: stroke path (for lines) 1272 | if len(optFill) > 0 && optFill[0] { 1273 | mode = "b" // b: fill / text 1274 | if pv := &p.page.nonStrokeColor; *pv != p.color { 1275 | *pv = p.color 1276 | p.write(" ", float64(pv.R)/255, " ", float64(pv.G)/255, " ", 1277 | float64(pv.B)/255, " rg\n") // rg: set non-stroking/text color 1278 | } 1279 | } 1280 | if pv := &p.page.strokeColor; *pv != p.color { 1281 | *pv = p.color 1282 | p.write(float64(pv.R)/255, " ", float64(pv.G)/255, 1283 | " ", float64(pv.B)/255, " RG\n") // RG: set stroke (line) color 1284 | } 1285 | if pv := &p.page.lineWidth; int(*pv*100) != int(p.lineWidth*100) { 1286 | *pv = p.lineWidth 1287 | p.write(float64(*pv), " w\n") // n0 w: set line width to n0 1288 | } 1289 | return mode 1290 | } // writeMode 1291 | 1292 | // writeObj writes an object header. objType must start with '/', e.g. /Catalog 1293 | func (p *PDF) writeObj(objType string) *PDF { 1294 | return p.write(p.nextObj(), " 0 obj < 0 { 1303 | pageIndex := pagesIndex 1304 | p.write("/Kids[") 1305 | for i := range p.pages { 1306 | if i > 0 { 1307 | p.write(" ") 1308 | } 1309 | p.write(pageIndex, " 0 R") 1310 | pageIndex += 2 // 1 for page, 1 for stream 1311 | } 1312 | p.write("]") 1313 | } 1314 | p.write(">>\n" + "endobj\n\n") 1315 | for _, pg := range p.pages { // write each page 1316 | p.writeObj("/Page"). 1317 | write("/Parent 2 0 R/Contents ", p.objIndex+1, " 0 R") 1318 | if len(pg.fontIDs) > 0 || len(pg.imageIDs) > 0 { 1319 | p.write("\n" + "/Resources <<") 1320 | } 1321 | if len(pg.fontIDs) > 0 { 1322 | p.write("/Font <<") 1323 | for fontNo := range p.fonts { 1324 | if len(pg.fontIDs) > 1 { 1325 | p.write("\n") 1326 | } 1327 | p.write("/FNT", fontNo+1, " ", fontsIndex+fontNo, " 0 R") 1328 | } 1329 | p.write(">> ") 1330 | } 1331 | if len(pg.imageIDs) > 0 { 1332 | p.write("/XObject <<") 1333 | for _, id := range pg.imageIDs { 1334 | if len(pg.imageIDs) > 1 { 1335 | p.write("\n") 1336 | } 1337 | p.write("/IMG", id, " ", imagesIndex+id, " 0 R") 1338 | } 1339 | p.write(">> ") 1340 | } 1341 | if len(pg.fontIDs) > 0 || len(pg.imageIDs) > 0 { 1342 | p.write(">> ") 1343 | } 1344 | p.write(">>\n" + "endobj\n\n") // write page object 1345 | p.writeStreamObj(pg.content.Bytes()) // write page's stream 1346 | } 1347 | return p 1348 | } // writePages 1349 | 1350 | // writeStreamData writes a stream or image stream 1351 | func (p *PDF) writeStreamData(ar []byte) *PDF { 1352 | var filter string 1353 | if p.compression { 1354 | var ( 1355 | buf bytes.Buffer 1356 | wr = zlib.NewWriter(&buf) 1357 | _, err = wr.Write(ar) 1358 | ) 1359 | if err != nil { 1360 | return p.putError(0xE782A2, "Failed compressing", err.Error()) 1361 | } 1362 | wr.Close() // don't defer, close before reading Bytes() 1363 | ar = buf.Bytes() 1364 | filter = "/Filter/FlateDecode" 1365 | } 1366 | return p.write(filter, "/Length ", len(ar), ">> stream\n", 1367 | string(ar), "\n"+"endstream\n") 1368 | } // writeStreamData 1369 | 1370 | // writeStreamObj outputs a stream object to the document's main buffer 1371 | func (p *PDF) writeStreamObj(ar []byte) *PDF { 1372 | return p.write(p.nextObj(), " 0 obj <<"). 1373 | writeStreamData(ar).write("\n" + "endobj\n\n") 1374 | } // writeStreamObj 1375 | 1376 | // ----------------------------------------------------------------------------- 1377 | // # Internal Functions (just attached to PDF, but not using it) 1378 | 1379 | // escape escapes special characters '(', '(' and '\' in strings 1380 | // in order to avoid them interfering with PDF commands 1381 | func (*PDF) escape(s string) string { 1382 | has := strings.Contains 1383 | if !has(s, "(") && !has(s, ")") && !has(s, "\\") { 1384 | return s 1385 | } 1386 | buf := bytes.NewBuffer(make([]byte, 0, len(s))) 1387 | for _, r := range s { 1388 | if r == '(' || r == ')' || r == '\\' { 1389 | buf.WriteRune('\\') 1390 | } 1391 | buf.WriteRune(r) 1392 | } 1393 | return buf.String() 1394 | } // escape 1395 | 1396 | // isWhiteSpace returns true if all the chars. in 's' are white-spaces 1397 | func (*PDF) isWhiteSpace(s string) bool { 1398 | for _, r := range s { 1399 | if !unicode.IsSpace(r) { 1400 | return false 1401 | } 1402 | } 1403 | return len(s) > 0 1404 | } // isWhiteSpace 1405 | 1406 | // splitLines splits 's' into several lines using line breaks in 's' 1407 | func (*PDF) splitLines(s string) []string { 1408 | split := func(lines []string, sep string) (ret []string) { 1409 | for _, line := range lines { 1410 | if strings.Contains(line, sep) { 1411 | ret = append(ret, strings.Split(line, sep)...) 1412 | continue 1413 | } 1414 | ret = append(ret, line) 1415 | } 1416 | return ret 1417 | } 1418 | return split(split(split([]string{s}, "\r\n"), "\r"), "\n") 1419 | } // splitLines 1420 | 1421 | // toUpperLettersDigits returns letters and digits from s, in upper case 1422 | func (*PDF) toUpperLettersDigits(s, extras string) string { 1423 | buf := bytes.NewBuffer(make([]byte, 0, len(s))) 1424 | for _, r := range strings.ToUpper(s) { 1425 | if unicode.IsLetter(r) || unicode.IsDigit(r) || 1426 | strings.ContainsRune(extras, r) { 1427 | buf.WriteRune(r) 1428 | } 1429 | } 1430 | return buf.String() 1431 | } // toUpperLettersDigits 1432 | 1433 | // getPaperSize returns a pdfPaperSize based on the specified paper name. 1434 | // Specify custom paper sizes using "width x height", e.g. "9cm x 20cm" 1435 | // If the paper size is not found, returns a zero-initialized structure 1436 | func (p *PDF) getPaperSize(name string) (pdfPaperSize, error) { 1437 | s := strings.ToUpper(name) 1438 | if strings.Contains(s, " X ") { 1439 | wh := strings.Split(s, " X ") 1440 | w, err := p.ToPoints(wh[0]) 1441 | if err, isT := err.(pdfError); isT { 1442 | return pdfPaperSize{}, err 1443 | } 1444 | var h float64 1445 | h, err = p.ToPoints(wh[1]) 1446 | if err, isT := err.(pdfError); isT { 1447 | return pdfPaperSize{}, err 1448 | } 1449 | return pdfPaperSize{s, w, h}, nil 1450 | } 1451 | s = p.toUpperLettersDigits(s, "-") 1452 | landscape := strings.HasSuffix(s, "-L") 1453 | s = p.toUpperLettersDigits(s, "") 1454 | if landscape { 1455 | s = s[:len(s)-1] // "-" is already removed above. now remove the "L" 1456 | } 1457 | wh, found := pdfStandardPaperSizes[s] 1458 | if !found { 1459 | return pdfPaperSize{}, 1460 | pdfError{id: 0xEE42FB, msg: "Unknown paper size", val: name} 1461 | } 1462 | // convert mm to points: div by 25.4mm/inch; mul by 72 points/inch 1463 | w, h := float64(wh[0])/25.4*72, float64(wh[1])/25.4*72 1464 | if landscape { 1465 | return pdfPaperSize{s + "-L", h, w}, nil 1466 | } 1467 | return pdfPaperSize{s, w, h}, nil 1468 | } // getPaperSize 1469 | 1470 | // getPointsPerUnit returns number of points per named measurement unit 1471 | func (p *PDF) getPointsPerUnit(units string) (ret float64, err error) { 1472 | switch p.toUpperLettersDigits(units, `"`) { 1473 | case "CM": 1474 | ret = 28.3464566929134 // " / 2.54cm per " * 72 points per inch 1475 | case "IN", "INCH", "INCHES", `"`: 1476 | ret = 72.0 // 72 points per inch 1477 | case "MM": 1478 | ret = 2.83464566929134 // 1 inch / 25.4mm per " * 72 points per inch 1479 | case "PT", "POINT", "POINTS": 1480 | ret = 1.0 // 1 point 1481 | case "TW", "TWIP", "TWIPS": 1482 | ret = 0.05 // 1 point / 20 twips per point 1483 | default: 1484 | err = pdfError{id: 0xEE34DA, msg: "Unknown measurement units", 1485 | val: units} 1486 | } 1487 | return ret, err 1488 | } // getPointsPerUnit 1489 | 1490 | // putError appends an error to the errors collection 1491 | func (p *PDF) putError(id int, msg, val string) *PDF { 1492 | var fn string // get the public method name 1493 | for i := 0; i < 10; i++ { 1494 | programCounter, _, _, _ := runtime.Caller(i) 1495 | fn = runtime.FuncForPC(programCounter).Name() 1496 | fn = fn[strings.LastIndex(fn, ".")+1:] 1497 | if unicode.IsLower(rune(fn[0])) { 1498 | continue 1499 | } 1500 | break 1501 | } 1502 | p.errors = append(p.errors, pdfError{id: id, src: fn, msg: msg, val: val}) 1503 | return p 1504 | } // putError 1505 | 1506 | // writeTo writes multiple strings and numbers specified in 'args' using 1507 | // writer 'wr'. Returns total bytes written and the first error if any. 1508 | func (*PDF) writeTo(wr io.Writer, args ...interface{}) (count int, err error) { 1509 | for _, any := range args { 1510 | n, err := 0, error(nil) 1511 | switch val := any.(type) { 1512 | case string: 1513 | n, err = io.WriteString(wr, val) 1514 | case float64: 1515 | n, err = io.WriteString(wr, strconv.FormatFloat(val, 'f', 3, 64)) 1516 | case int: 1517 | n, err = io.WriteString(wr, strconv.FormatInt(int64(val), 10)) 1518 | case int16: 1519 | n, err = io.WriteString(wr, strconv.FormatInt(int64(val), 10)) 1520 | case uint: 1521 | n, err = io.WriteString(wr, strconv.FormatInt(int64(val), 10)) 1522 | case uint16: 1523 | n, err = io.WriteString(wr, strconv.FormatInt(int64(val), 10)) 1524 | case *bytes.Buffer: 1525 | if val != nil { 1526 | n, err = wr.Write(val.Bytes()) 1527 | } 1528 | case []byte: 1529 | n, err = wr.Write(val) 1530 | default: 1531 | n, err = 0, 1532 | fmt.Errorf("Invalid type %s = %v", reflect.TypeOf(val), val) 1533 | } 1534 | count += n 1535 | if err != nil { 1536 | return count, err 1537 | } 1538 | } 1539 | return count, nil 1540 | } // writeTo 1541 | 1542 | // ----------------------------------------------------------------------------- 1543 | // # Constants 1544 | 1545 | // PDFColorNames maps web (X11) color names to values. 1546 | // From https://en.wikipedia.org/wiki/X11_color_names 1547 | var PDFColorNames = map[string]color.RGBA{ 1548 | "ALICE BLUE": {R: 240, G: 248, B: 255, A: 255}, // #F0F8FF 1549 | "ANTIQUE WHITE": {R: 250, G: 235, B: 215, A: 255}, // #FAEBD7 1550 | "AQUA": {R: 000, G: 255, B: 255, A: 255}, // #00FFFF 1551 | "AQUAMARINE": {R: 127, G: 255, B: 212, A: 255}, // #7FFFD4 1552 | "AZURE": {R: 240, G: 255, B: 255, A: 255}, // #F0FFFF 1553 | "BEIGE": {R: 245, G: 245, B: 220, A: 255}, // #F5F5DC 1554 | "BISQUE": {R: 255, G: 228, B: 196, A: 255}, // #FFE4C4 1555 | "BLACK": {R: 000, G: 000, B: 000, A: 255}, // #000000 1556 | "BLANCHED ALMOND": {R: 255, G: 235, B: 205, A: 255}, // #FFEBCD 1557 | "BLUE": {R: 000, G: 000, B: 255, A: 255}, // #0000Ff 1558 | "BLUE VIOLET": {R: 138, G: 43, B: 226, A: 255}, // #8A2Be2 1559 | "BROWN": {R: 165, G: 42, B: 42, A: 255}, // #A52A2A 1560 | "BURLYWOOD": {R: 222, G: 184, B: 135, A: 255}, // #Deb887 1561 | "CADET BLUE": {R: 95, G: 158, B: 160, A: 255}, // #5F9EA0 1562 | "CHARTREUSE": {R: 127, G: 255, B: 000, A: 255}, // #7FFF00 1563 | "CHOCOLATE": {R: 210, G: 105, B: 30, A: 255}, // #D2691E 1564 | "CORAL": {R: 255, G: 127, B: 80, A: 255}, // #FF7F50 1565 | "CORNFLOWER BLUE": {R: 100, G: 149, B: 237, A: 255}, // #6495ED 1566 | "CORNSILK": {R: 255, G: 248, B: 220, A: 255}, // #FFF8DC 1567 | "CRIMSON": {R: 220, G: 20, B: 60, A: 255}, // #DC143C 1568 | "CYAN": {R: 000, G: 255, B: 255, A: 255}, // #00FFFF 1569 | "DARK BLUE": {R: 000, G: 000, B: 139, A: 255}, // #00008B 1570 | "DARK CYAN": {R: 000, G: 139, B: 139, A: 255}, // #008B8B 1571 | "DARK GOLDEN ROD": {R: 184, G: 134, B: 11, A: 255}, // #B8860B 1572 | "DARK GRAY": {R: 169, G: 169, B: 169, A: 255}, // #A9A9A9 1573 | "DARK GREEN": {R: 000, G: 100, B: 000, A: 255}, // #006400 1574 | "DARK KHAKI": {R: 189, G: 183, B: 107, A: 255}, // #BDB76B 1575 | "DARK MAGENTA": {R: 139, G: 000, B: 139, A: 255}, // #8B008B 1576 | "DARK OLIVE GREEN": {R: 85, G: 107, B: 47, A: 255}, // #556B2F 1577 | "DARK ORANGE": {R: 255, G: 140, B: 000, A: 255}, // #FF8C00 1578 | "DARK ORCHID": {R: 153, G: 50, B: 204, A: 255}, // #9932CC 1579 | "DARK RED": {R: 139, G: 000, B: 000, A: 255}, // #8B0000 1580 | "DARK SALMON": {R: 233, G: 150, B: 122, A: 255}, // #E9967A 1581 | "DARK SEA GREEN": {R: 143, G: 188, B: 143, A: 255}, // #8FBC8F 1582 | "DARK SLATE BLUE": {R: 72, G: 61, B: 139, A: 255}, // #483D8B 1583 | "DARK SLATE GRAY": {R: 47, G: 79, B: 79, A: 255}, // #2F4F4F 1584 | "DARK TURQUOISE": {R: 000, G: 206, B: 209, A: 255}, // #00CED1 1585 | "DARK VIOLET": {R: 148, G: 000, B: 211, A: 255}, // #9400D3 1586 | "DEEP PINK": {R: 255, G: 20, B: 147, A: 255}, // #FF1493 1587 | "DEEP SKY BLUE": {R: 000, G: 191, B: 255, A: 255}, // #00BFFF 1588 | "DIM GRAY": {R: 105, G: 105, B: 105, A: 255}, // #696969 1589 | "DODGER BLUE": {R: 30, G: 144, B: 255, A: 255}, // #1E90FF 1590 | "FIRE BRICK": {R: 178, G: 34, B: 34, A: 255}, // #B22222 1591 | "FLORAL WHITE": {R: 255, G: 250, B: 240, A: 255}, // #FFFAF0 1592 | "FOREST GREEN": {R: 34, G: 139, B: 34, A: 255}, // #228B22 1593 | "FUCHSIA": {R: 255, G: 000, B: 255, A: 255}, // #FF00FF 1594 | "GAINSBORO": {R: 220, G: 220, B: 220, A: 255}, // #DCDCDC 1595 | "GHOST WHITE": {R: 248, G: 248, B: 255, A: 255}, // #F8F8FF 1596 | "GOLD": {R: 255, G: 215, B: 000, A: 255}, // #FFD700 1597 | "GOLDEN ROD": {R: 218, G: 165, B: 32, A: 255}, // #DAA520 1598 | "GRAY": {R: 190, G: 190, B: 190, A: 255}, // #BEBEBE (X11) 1599 | "GREEN": {R: 000, G: 255, B: 000, A: 255}, // #00FF00 (X11) 1600 | "GREEN YELLOW": {R: 173, G: 255, B: 47, A: 255}, // #ADFF2F 1601 | "HONEY DEW": {R: 240, G: 255, B: 240, A: 255}, // #F0FFF0 1602 | "HOT PINK": {R: 255, G: 105, B: 180, A: 255}, // #FF69B4 1603 | "INDIAN RED": {R: 205, G: 92, B: 92, A: 255}, // #CD5C5C 1604 | "INDIGO": {R: 75, G: 000, B: 130, A: 255}, // #4B0082 1605 | "IVORY": {R: 255, G: 255, B: 240, A: 255}, // #FFFFF0 1606 | "KHAKI": {R: 240, G: 230, B: 140, A: 255}, // #F0E68C 1607 | "LAVENDER": {R: 230, G: 230, B: 250, A: 255}, // #E6E6FA 1608 | "LAVENDER BLUSH": {R: 255, G: 240, B: 245, A: 255}, // #FFF0F5 1609 | "LAWN GREEN": {R: 124, G: 252, B: 000, A: 255}, // #7CFC00 1610 | "LEMON CHIFFON": {R: 255, G: 250, B: 205, A: 255}, // #FFFACD 1611 | "LIGHT BLUE": {R: 173, G: 216, B: 230, A: 255}, // #ADD8E6 1612 | "LIGHT CORAL": {R: 240, G: 128, B: 128, A: 255}, // #F08080 1613 | "LIGHT CYAN": {R: 224, G: 255, B: 255, A: 255}, // #E0FFFF 1614 | "LIGHT GOLDENROD YELLOW": {R: 250, G: 250, B: 210, A: 255}, // #FAFAD2 1615 | "LIGHT GRAY": {R: 211, G: 211, B: 211, A: 255}, // #D3D3D3 1616 | "LIGHT GREEN": {R: 144, G: 238, B: 144, A: 255}, // #90EE90 1617 | "LIGHT PINK": {R: 255, G: 182, B: 193, A: 255}, // #FFB6C1 1618 | "LIGHT SALMON": {R: 255, G: 160, B: 122, A: 255}, // #FFA07A 1619 | "LIGHT SEA GREEN": {R: 32, G: 178, B: 170, A: 255}, // #20B2AA 1620 | "LIGHT SKY BLUE": {R: 135, G: 206, B: 250, A: 255}, // #87CEFA 1621 | "LIGHT SLATE GRAY": {R: 119, G: 136, B: 153, A: 255}, // #778899 1622 | "LIGHT STEEL BLUE": {R: 176, G: 196, B: 222, A: 255}, // #B0C4DE 1623 | "LIGHT YELLOW": {R: 255, G: 255, B: 224, A: 255}, // #FFFFE0 1624 | "LIME": {R: 000, G: 255, B: 000, A: 255}, // #00FF00 1625 | "LIME GREEN": {R: 50, G: 205, B: 50, A: 255}, // #32CD32 1626 | "LINEN": {R: 250, G: 240, B: 230, A: 255}, // #FAF0E6 1627 | "MAGENTA": {R: 255, G: 000, B: 255, A: 255}, // #FF00FF 1628 | "MAROON": {R: 176, G: 48, B: 96, A: 255}, // #B03060 (X11) 1629 | "MEDIUM AQUAMARINE": {R: 102, G: 205, B: 170, A: 255}, // #66CDAA 1630 | "MEDIUM BLUE": {R: 000, G: 000, B: 205, A: 255}, // #0000CD 1631 | "MEDIUM ORCHID": {R: 186, G: 85, B: 211, A: 255}, // #BA55D3 1632 | "MEDIUM PURPLE": {R: 147, G: 112, B: 219, A: 255}, // #9370DB 1633 | "MEDIUM SEA GREEN": {R: 60, G: 179, B: 113, A: 255}, // #3CB371 1634 | "MEDIUM SLATE BLUE": {R: 123, G: 104, B: 238, A: 255}, // #7B68EE 1635 | "MEDIUM SPRING GREEN": {R: 000, G: 250, B: 154, A: 255}, // #00FA9A 1636 | "MEDIUM TURQUOISE": {R: 72, G: 209, B: 204, A: 255}, // #48D1CC 1637 | "MEDIUM VIOLET RED": {R: 199, G: 21, B: 133, A: 255}, // #C71585 1638 | "MIDNIGHT BLUE": {R: 25, G: 25, B: 112, A: 255}, // #191970 1639 | "MINT CREAM": {R: 245, G: 255, B: 250, A: 255}, // #F5FFFA 1640 | "MISTY ROSE": {R: 255, G: 228, B: 225, A: 255}, // #FFE4E1 1641 | "MOCCASIN": {R: 255, G: 228, B: 181, A: 255}, // #FFE4B5 1642 | "NAVAJO WHITE": {R: 255, G: 222, B: 173, A: 255}, // #FFDEAD 1643 | "NAVY": {R: 000, G: 000, B: 128, A: 255}, // #000080 1644 | "OLD LACE": {R: 253, G: 245, B: 230, A: 255}, // #FDF5E6 1645 | "OLIVE": {R: 128, G: 128, B: 000, A: 255}, // #808000 1646 | "OLIVE DRAB": {R: 107, G: 142, B: 35, A: 255}, // #6B8E23 1647 | "ORANGE": {R: 255, G: 165, B: 000, A: 255}, // #FFA500 1648 | "ORANGE RED": {R: 255, G: 69, B: 000, A: 255}, // #FF4500 1649 | "ORCHID": {R: 218, G: 112, B: 214, A: 255}, // #DA70D6 1650 | "PALE GOLDEN ROD": {R: 238, G: 232, B: 170, A: 255}, // #EEE8AA 1651 | "PALE GREEN": {R: 152, G: 251, B: 152, A: 255}, // #98FB98 1652 | "PALE TURQUOISE": {R: 175, G: 238, B: 238, A: 255}, // #AFEEEE 1653 | "PALE VIOLET RED": {R: 219, G: 112, B: 147, A: 255}, // #DB7093 1654 | "PAPAYA WHIP": {R: 255, G: 239, B: 213, A: 255}, // #FFEFD5 1655 | "PEACH PUFF": {R: 255, G: 218, B: 185, A: 255}, // #FFDAB9 1656 | "PERU": {R: 205, G: 133, B: 63, A: 255}, // #CD853F 1657 | "PINK": {R: 255, G: 192, B: 203, A: 255}, // #FFC0CB 1658 | "PLUM": {R: 221, G: 160, B: 221, A: 255}, // #DDA0DD 1659 | "POWDER BLUE": {R: 176, G: 224, B: 230, A: 255}, // #B0E0E6 1660 | "PURPLE": {R: 160, G: 32, B: 240, A: 255}, // #A020F0 (X11) 1661 | "REBECCA PURPLE": {R: 102, G: 51, B: 153, A: 255}, // #663399 1662 | "RED": {R: 255, G: 000, B: 000, A: 255}, // #FF0000 1663 | "ROSY BROWN": {R: 188, G: 143, B: 143, A: 255}, // #BC8F8F 1664 | "ROYAL BLUE": {R: 65, G: 105, B: 225, A: 255}, // #4169E1 1665 | "SADDLE BROWN": {R: 139, G: 69, B: 19, A: 255}, // #8B4513 1666 | "SALMON": {R: 250, G: 128, B: 114, A: 255}, // #FA8072 1667 | "SANDY BROWN": {R: 244, G: 164, B: 96, A: 255}, // #F4A460 1668 | "SEA GREEN": {R: 46, G: 139, B: 87, A: 255}, // #2E8B57 1669 | "SEASHELL": {R: 255, G: 245, B: 238, A: 255}, // #FFF5EE 1670 | "SIENNA": {R: 160, G: 82, B: 45, A: 255}, // #A0522D 1671 | "SILVER": {R: 192, G: 192, B: 192, A: 255}, // #C0C0C0 1672 | "SKY BLUE": {R: 135, G: 206, B: 235, A: 255}, // #87CEEB 1673 | "SLATE BLUE": {R: 106, G: 90, B: 205, A: 255}, // #6A5ACD 1674 | "SLATE GRAY": {R: 112, G: 128, B: 144, A: 255}, // #708090 1675 | "SNOW": {R: 255, G: 250, B: 250, A: 255}, // #FFFAFA 1676 | "SPRING GREEN": {R: 000, G: 255, B: 127, A: 255}, // #00FF7F 1677 | "STEEL BLUE": {R: 70, G: 130, B: 180, A: 255}, // #4682B4 1678 | "TAN": {R: 210, G: 180, B: 140, A: 255}, // #D2B48C 1679 | "TEAL": {R: 000, G: 128, B: 128, A: 255}, // #008080 1680 | "THISTLE": {R: 216, G: 191, B: 216, A: 255}, // #D8BFD8 1681 | "TOMATO": {R: 255, G: 99, B: 71, A: 255}, // #FF6347 1682 | "TURQUOISE": {R: 64, G: 224, B: 208, A: 255}, // #40E0D0 1683 | "VIOLET": {R: 238, G: 130, B: 238, A: 255}, // #EE82EE 1684 | "WEB GRAY": {R: 128, G: 128, B: 128, A: 255}, // #808080 (Web) 1685 | "WEB GREEN": {R: 000, G: 128, B: 000, A: 255}, // #008000 (Web) 1686 | "WEB MAROON": {R: 127, G: 000, B: 000, A: 255}, // #7F0000 (Web) 1687 | "WEB PURPLE": {R: 127, G: 000, B: 127, A: 255}, // #7F007F (Web) 1688 | "WHEAT": {R: 245, G: 222, B: 179, A: 255}, // #F5DEB3 1689 | "WHITE": {R: 255, G: 255, B: 255, A: 255}, // #FFFFFF 1690 | "WHITE SMOKE": {R: 245, G: 245, B: 245, A: 255}, // #F5F5F5 1691 | "YELLOW": {R: 255, G: 255, B: 000, A: 255}, // #FFFF00 1692 | "YELLOW GREEN": {R: 154, G: 205, B: 50, A: 255}, // #9ACD32 1693 | } // PDFColorNames 1694 | 1695 | // ----------------------------------------------------------------------------- 1696 | // # Internal Constants 1697 | 1698 | var pdfBlack = color.RGBA{A: 255} 1699 | 1700 | // pdfFontNames contains font names available on all PDF implementations 1701 | var pdfFontNames = []string{ 1702 | "Helvetica", "Helvetica-Bold", // 0 1 1703 | "Helvetica-BoldOblique", "Helvetica-Oblique", // 2 3 1704 | "Symbol", // 4 1705 | "Times-Bold", "Times-BoldItalic", "Times-Italic", "Times-Roman", // 5 6 7 8 1706 | "ZapfDingbats", // 9 1707 | // keep fixed-width Courier font at the end of the list: 1708 | "Courier", "Courier-Bold", "Courier-BoldOblique", "Courier-Oblique", 1709 | } // pdfFontNames 1710 | 1711 | // pdfFontWidths specifies widths of built-in fonts, 1712 | // in thousandths of a point per point of height 1713 | var pdfFontWidths = [][]int{ 1714 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 000 1715 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 001 1716 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 002 1717 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 003 1718 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 004 1719 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 005 1720 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 006 1721 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 007 1722 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 008 1723 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 009 1724 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 010 1725 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 011 1726 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 012 1727 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 013 1728 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 014 1729 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 015 1730 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 016 1731 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 017 1732 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 018 1733 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 019 1734 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 020 1735 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 021 1736 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 022 1737 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 023 1738 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 024 1739 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 025 1740 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 026 1741 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 027 1742 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 028 1743 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 029 1744 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 030 1745 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 000}, // 031 1746 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 278}, // 032 1747 | {278, 333, 333, 278, 333, 333, 389, 333, 333, 974}, // 033 ! 1748 | {355, 474, 474, 355, 713, 555, 555, 420, 408, 961}, // 034 " 1749 | {556, 556, 556, 556, 500, 500, 500, 500, 500, 974}, // 035 # 1750 | {556, 556, 556, 556, 549, 500, 500, 500, 500, 980}, // 036 $ 1751 | {889, 889, 889, 889, 833, 000, 833, 833, 833, 719}, // 037 % 1752 | {667, 722, 722, 667, 778, 833, 778, 778, 778, 789}, // 038 & 1753 | {191, 238, 238, 191, 439, 278, 278, 214, 180, 790}, // 039 ' 1754 | {333, 333, 333, 333, 333, 333, 333, 333, 333, 791}, // 040 ( 1755 | {333, 333, 333, 333, 333, 333, 333, 333, 333, 690}, // 041 ) 1756 | {389, 389, 389, 389, 500, 500, 500, 500, 500, 960}, // 042 * 1757 | {584, 584, 584, 584, 549, 570, 570, 675, 564, 939}, // 043 + 1758 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 549}, // 044 , 1759 | {333, 333, 333, 333, 549, 333, 333, 333, 333, 855}, // 045 - 1760 | {278, 278, 278, 278, 250, 250, 250, 250, 250, 911}, // 046 . 1761 | {278, 278, 278, 278, 278, 278, 278, 278, 278, 933}, // 047 / 1762 | {556, 556, 556, 556, 500, 500, 500, 500, 500, 911}, // 048 000 1763 | {556, 556, 556, 556, 500, 500, 500, 500, 500, 945}, // 049 1 1764 | {556, 556, 556, 556, 500, 500, 500, 500, 500, 974}, // 050 2 1765 | {556, 556, 556, 556, 500, 500, 500, 500, 500, 755}, // 051 3 1766 | {556, 556, 556, 556, 500, 500, 500, 500, 500, 846}, // 052 4 1767 | {556, 556, 556, 556, 500, 500, 500, 500, 500, 762}, // 053 5 1768 | {556, 556, 556, 556, 500, 500, 500, 500, 500, 761}, // 054 6 1769 | {556, 556, 556, 556, 500, 500, 500, 500, 500, 571}, // 055 7 1770 | {556, 556, 556, 556, 500, 500, 500, 500, 500, 677}, // 056 8 1771 | {556, 556, 556, 556, 500, 500, 500, 500, 500, 763}, // 057 9 1772 | {278, 333, 333, 278, 278, 333, 333, 333, 278, 760}, // 058 : 1773 | {278, 333, 333, 278, 278, 333, 333, 333, 278, 759}, // 059 ; 1774 | {584, 584, 584, 584, 549, 570, 570, 675, 564, 754}, // 060 < 1775 | {584, 584, 584, 584, 549, 570, 570, 675, 564, 494}, // 061 = 1776 | {584, 584, 584, 584, 549, 570, 570, 675, 564, 552}, // 062 > 1777 | {556, 611, 611, 556, 444, 500, 500, 500, 444, 537}, // 063 ? 1778 | {1015, 975, 975, 1015, 549, 930, 832, 920, 921, 577}, // 064 @ 1779 | {667, 722, 722, 667, 722, 722, 667, 611, 722, 692}, // 065 A 1780 | {667, 722, 722, 667, 667, 667, 667, 611, 667, 786}, // 066 B 1781 | {722, 722, 722, 722, 722, 722, 667, 667, 667, 788}, // 067 C 1782 | {722, 722, 722, 722, 612, 722, 722, 722, 722, 788}, // 068 D 1783 | {667, 667, 667, 667, 611, 667, 667, 611, 611, 790}, // 069 E 1784 | {611, 611, 611, 611, 763, 611, 667, 611, 556, 793}, // 070 F 1785 | {778, 778, 778, 778, 603, 778, 722, 722, 722, 794}, // 071 G 1786 | {722, 722, 722, 722, 722, 778, 778, 722, 722, 816}, // 072 H 1787 | {278, 278, 278, 278, 333, 389, 389, 333, 333, 823}, // 073 I 1788 | {500, 556, 556, 500, 631, 500, 500, 444, 389, 789}, // 074 J 1789 | {667, 722, 722, 667, 722, 778, 667, 667, 722, 841}, // 075 K 1790 | {556, 611, 611, 556, 686, 667, 611, 556, 611, 823}, // 076 L 1791 | {833, 833, 833, 833, 889, 944, 889, 833, 889, 833}, // 077 M 1792 | {722, 722, 722, 722, 722, 722, 722, 667, 722, 816}, // 078 N 1793 | {778, 778, 778, 778, 722, 778, 722, 722, 722, 831}, // 079 O 1794 | {667, 667, 667, 667, 768, 611, 611, 611, 556, 923}, // 080 P 1795 | {778, 778, 778, 778, 741, 778, 722, 722, 722, 744}, // 081 Q 1796 | {722, 722, 722, 722, 556, 722, 667, 611, 667, 723}, // 082 R 1797 | {667, 667, 667, 667, 592, 556, 556, 500, 556, 749}, // 083 S 1798 | {611, 611, 611, 611, 611, 667, 611, 556, 611, 790}, // 084 T 1799 | {722, 722, 722, 722, 690, 722, 722, 722, 722, 792}, // 085 U 1800 | {667, 667, 667, 667, 439, 722, 667, 611, 722, 695}, // 086 V 1801 | {944, 944, 944, 944, 768, 1000, 889, 833, 944, 776}, // 087 W 1802 | {667, 667, 667, 667, 645, 722, 667, 611, 722, 768}, // 088 X 1803 | {667, 667, 667, 667, 795, 722, 611, 556, 722, 792}, // 089 Y 1804 | {611, 611, 611, 611, 611, 667, 611, 556, 611, 759}, // 090 Z 1805 | {278, 333, 333, 278, 333, 333, 333, 389, 333, 707}, // 091 [ 1806 | {278, 278, 278, 278, 863, 278, 278, 278, 278, 708}, // 092 \ 1807 | {278, 333, 333, 278, 333, 333, 333, 389, 333, 682}, // 093 ] 1808 | {469, 584, 584, 469, 658, 581, 570, 422, 469, 701}, // 094 ^ 1809 | {556, 556, 556, 556, 500, 500, 500, 500, 500, 826}, // 095 _ 1810 | {333, 333, 333, 333, 500, 333, 333, 333, 333, 815}, // 096 \x60 1811 | {556, 556, 556, 556, 631, 500, 500, 500, 444, 789}, // 097 a 1812 | {556, 611, 611, 556, 549, 556, 500, 500, 500, 789}, // 098 b 1813 | {500, 556, 556, 500, 549, 444, 444, 444, 444, 707}, // 099 c 1814 | {556, 611, 611, 556, 494, 556, 500, 500, 500, 687}, // 100 d 1815 | {556, 556, 556, 556, 439, 444, 444, 444, 444, 696}, // 101 e 1816 | {278, 333, 333, 278, 521, 333, 333, 278, 333, 689}, // 102 f 1817 | {556, 611, 611, 556, 411, 500, 500, 500, 500, 786}, // 103 g 1818 | {556, 611, 611, 556, 603, 556, 556, 500, 500, 787}, // 104 h 1819 | {222, 278, 278, 222, 329, 278, 278, 278, 278, 713}, // 105 i 1820 | {222, 278, 278, 222, 603, 333, 278, 278, 278, 791}, // 106 j 1821 | {500, 556, 556, 500, 549, 556, 500, 444, 500, 785}, // 107 k 1822 | {222, 278, 278, 222, 549, 278, 278, 278, 278, 791}, // 108 l 1823 | {833, 889, 889, 833, 576, 833, 778, 722, 778, 873}, // 109 m 1824 | {556, 611, 611, 556, 521, 556, 556, 500, 500, 761}, // 110 n 1825 | {556, 611, 611, 556, 549, 500, 500, 500, 500, 762}, // 111 o 1826 | {556, 611, 611, 556, 549, 556, 500, 500, 500, 762}, // 112 p 1827 | {556, 611, 611, 556, 521, 556, 500, 500, 500, 759}, // 113 q 1828 | {333, 389, 389, 333, 549, 444, 389, 389, 333, 759}, // 114 r 1829 | {500, 556, 556, 500, 603, 389, 389, 389, 389, 892}, // 115 s 1830 | {278, 333, 333, 278, 439, 333, 278, 278, 278, 892}, // 116 t 1831 | {556, 611, 611, 556, 576, 556, 556, 500, 500, 788}, // 117 u 1832 | {500, 556, 556, 500, 713, 500, 444, 444, 500, 784}, // 118 v 1833 | {722, 778, 778, 722, 686, 722, 667, 667, 722, 438}, // 119 w 1834 | {500, 556, 556, 500, 493, 500, 500, 444, 500, 138}, // 120 x 1835 | {500, 556, 556, 500, 686, 500, 444, 444, 500, 277}, // 121 y 1836 | {500, 500, 500, 500, 494, 444, 389, 389, 444, 415}, // 122 z 1837 | {334, 389, 389, 334, 480, 394, 348, 400, 480, 392}, // 123 { 1838 | {260, 280, 280, 260, 200, 220, 220, 275, 200, 392}, // 124 | 1839 | {334, 389, 389, 334, 480, 394, 348, 400, 480, 668}, // 125 } 1840 | {584, 584, 584, 584, 549, 520, 570, 541, 541, 668}, // 126 ~ 1841 | {350, 350, 350, 350, 000, 350, 350, 350, 350, 000}, // 127 1842 | {556, 556, 556, 556, 000, 500, 500, 500, 500, 390}, // 128 1843 | {350, 350, 350, 350, 000, 350, 350, 350, 350, 390}, // 129 1844 | {222, 278, 278, 222, 000, 333, 333, 333, 333, 317}, // 130 1845 | {556, 556, 556, 556, 000, 500, 500, 500, 500, 317}, // 131 1846 | {333, 500, 500, 333, 000, 500, 500, 556, 444, 276}, // 132 1847 | {1000, 1000, 1000, 1000, 000, 1000, 1000, 889, 1000, 276}, // 133 1848 | {556, 556, 556, 556, 000, 500, 500, 500, 500, 509}, // 134 1849 | {556, 556, 556, 556, 000, 500, 500, 500, 500, 509}, // 135 1850 | {333, 333, 333, 333, 000, 333, 333, 333, 333, 410}, // 136 1851 | {1000, 1000, 1000, 1000, 000, 1000, 1000, 1000, 1000, 410}, // 137 1852 | {667, 667, 667, 667, 000, 556, 556, 500, 556, 234}, // 138 1853 | {333, 333, 333, 333, 000, 333, 333, 333, 333, 234}, // 139 1854 | {1000, 1000, 1000, 1000, 000, 1000, 944, 944, 889, 334}, // 140 1855 | {350, 350, 350, 350, 000, 350, 350, 350, 350, 334}, // 141 1856 | {611, 611, 611, 611, 000, 667, 611, 556, 611, 000}, // 142 1857 | {350, 350, 350, 350, 000, 350, 350, 350, 350, 000}, // 143 1858 | {350, 350, 350, 350, 000, 350, 350, 350, 350, 000}, // 144 1859 | {222, 278, 278, 222, 000, 333, 333, 333, 333, 000}, // 145 1860 | {222, 278, 278, 222, 000, 333, 333, 333, 333, 000}, // 146 1861 | {333, 500, 500, 333, 000, 500, 500, 556, 444, 000}, // 147 1862 | {333, 500, 500, 333, 000, 500, 500, 556, 444, 000}, // 148 1863 | {350, 350, 350, 350, 000, 350, 350, 350, 350, 000}, // 149 1864 | {556, 556, 556, 556, 000, 500, 500, 500, 500, 000}, // 150 1865 | {1000, 1000, 1000, 1000, 000, 1000, 1000, 889, 1000, 000}, // 151 1866 | {333, 333, 333, 333, 000, 333, 333, 333, 333, 000}, // 152 1867 | {1000, 1000, 1000, 1000, 000, 1000, 1000, 980, 980, 000}, // 153 1868 | {500, 556, 556, 500, 000, 389, 389, 389, 389, 000}, // 154 1869 | {333, 333, 333, 333, 000, 333, 333, 333, 333, 000}, // 155 1870 | {944, 944, 944, 944, 000, 722, 722, 667, 722, 000}, // 156 1871 | {350, 350, 350, 350, 000, 350, 350, 350, 350, 000}, // 157 1872 | {500, 500, 500, 500, 000, 444, 389, 389, 444, 000}, // 158 1873 | {667, 667, 667, 667, 000, 722, 611, 556, 722, 000}, // 159 1874 | {278, 278, 278, 278, 750, 250, 250, 250, 250, 000}, // 160 1875 | {333, 333, 333, 333, 620, 333, 389, 389, 333, 732}, // 161 1876 | {556, 556, 556, 556, 247, 500, 500, 500, 500, 544}, // 162 1877 | {556, 556, 556, 556, 549, 500, 500, 500, 500, 544}, // 163 1878 | {556, 556, 556, 556, 167, 500, 500, 500, 500, 910}, // 164 1879 | {556, 556, 556, 556, 713, 500, 500, 500, 500, 667}, // 165 1880 | {260, 280, 280, 260, 500, 220, 220, 275, 200, 760}, // 166 1881 | {556, 556, 556, 556, 753, 500, 500, 500, 500, 760}, // 167 1882 | {333, 333, 333, 333, 753, 333, 333, 333, 333, 776}, // 168 1883 | {737, 737, 737, 737, 753, 747, 747, 760, 760, 595}, // 169 1884 | {370, 370, 370, 370, 753, 300, 266, 276, 276, 694}, // 170 1885 | {556, 556, 556, 556, 1042, 500, 500, 500, 500, 626}, // 171 1886 | {584, 584, 584, 584, 987, 570, 606, 675, 564, 788}, // 172 1887 | {333, 333, 333, 333, 603, 333, 333, 333, 333, 788}, // 173 1888 | {737, 737, 737, 737, 987, 747, 747, 760, 760, 788}, // 174 1889 | {333, 333, 333, 333, 603, 333, 333, 333, 333, 788}, // 175 1890 | {400, 400, 400, 400, 400, 400, 400, 400, 400, 788}, // 176 1891 | {584, 584, 584, 584, 549, 570, 570, 675, 564, 788}, // 177 1892 | {333, 333, 333, 333, 411, 300, 300, 300, 300, 788}, // 178 1893 | {333, 333, 333, 333, 549, 300, 300, 300, 300, 788}, // 179 1894 | {333, 333, 333, 333, 549, 333, 333, 333, 333, 788}, // 180 1895 | {556, 611, 611, 556, 713, 556, 576, 500, 500, 788}, // 181 1896 | {537, 556, 556, 537, 494, 540, 500, 523, 453, 788}, // 182 1897 | {278, 278, 278, 278, 460, 250, 250, 250, 250, 788}, // 183 1898 | {333, 333, 333, 333, 549, 333, 333, 333, 333, 788}, // 184 1899 | {333, 333, 333, 333, 549, 300, 300, 300, 300, 788}, // 185 1900 | {365, 365, 365, 365, 549, 330, 300, 310, 310, 788}, // 186 1901 | {556, 556, 556, 556, 549, 500, 500, 500, 500, 788}, // 187 1902 | {834, 834, 834, 834, 1000, 750, 750, 750, 750, 788}, // 188 1903 | {834, 834, 834, 834, 603, 750, 750, 750, 750, 788}, // 189 1904 | {834, 834, 834, 834, 1000, 750, 750, 750, 750, 788}, // 190 1905 | {611, 611, 611, 611, 658, 500, 500, 500, 444, 788}, // 191 1906 | {667, 722, 722, 667, 823, 722, 667, 611, 722, 788}, // 192 1907 | {667, 722, 722, 667, 686, 722, 667, 611, 722, 788}, // 193 1908 | {667, 722, 722, 667, 795, 722, 667, 611, 722, 788}, // 194 1909 | {667, 722, 722, 667, 987, 722, 667, 611, 722, 788}, // 195 1910 | {667, 722, 722, 667, 768, 722, 667, 611, 722, 788}, // 196 1911 | {667, 722, 722, 667, 768, 722, 667, 611, 722, 788}, // 197 1912 | {1000, 1000, 1000, 1000, 823, 1000, 944, 889, 889, 788}, // 198 1913 | {722, 722, 722, 722, 768, 722, 667, 667, 667, 788}, // 199 1914 | {667, 667, 667, 667, 768, 667, 667, 611, 611, 788}, // 200 1915 | {667, 667, 667, 667, 713, 667, 667, 611, 611, 788}, // 201 1916 | {667, 667, 667, 667, 713, 667, 667, 611, 611, 788}, // 202 1917 | {667, 667, 667, 667, 713, 667, 667, 611, 611, 788}, // 203 1918 | {278, 278, 278, 278, 713, 389, 389, 333, 333, 788}, // 204 1919 | {278, 278, 278, 278, 713, 389, 389, 333, 333, 788}, // 205 1920 | {278, 278, 278, 278, 713, 389, 389, 333, 333, 788}, // 206 1921 | {278, 278, 278, 278, 713, 389, 389, 333, 333, 788}, // 207 1922 | {722, 722, 722, 722, 768, 722, 722, 722, 722, 788}, // 208 1923 | {722, 722, 722, 722, 713, 722, 722, 667, 722, 788}, // 209 1924 | {778, 778, 778, 778, 790, 778, 722, 722, 722, 788}, // 210 1925 | {778, 778, 778, 778, 790, 778, 722, 722, 722, 788}, // 211 1926 | {778, 778, 778, 778, 890, 778, 722, 722, 722, 894}, // 212 1927 | {778, 778, 778, 778, 823, 778, 722, 722, 722, 838}, // 213 1928 | {778, 778, 778, 778, 549, 778, 722, 722, 722, 1016}, // 214 1929 | {584, 584, 584, 584, 250, 570, 570, 675, 564, 458}, // 215 1930 | {778, 778, 778, 778, 713, 778, 722, 722, 722, 748}, // 216 1931 | {722, 722, 722, 722, 603, 722, 722, 722, 722, 924}, // 217 1932 | {722, 722, 722, 722, 603, 722, 722, 722, 722, 748}, // 218 1933 | {722, 722, 722, 722, 1042, 722, 722, 722, 722, 918}, // 219 1934 | {722, 722, 722, 722, 987, 722, 722, 722, 722, 927}, // 220 1935 | {667, 667, 667, 667, 603, 722, 611, 556, 722, 928}, // 221 1936 | {667, 667, 667, 667, 987, 611, 611, 611, 556, 928}, // 222 1937 | {611, 611, 611, 611, 603, 556, 500, 500, 500, 834}, // 223 1938 | {556, 556, 556, 556, 494, 500, 500, 500, 444, 873}, // 224 1939 | {556, 556, 556, 556, 329, 500, 500, 500, 444, 828}, // 225 1940 | {556, 556, 556, 556, 790, 500, 500, 500, 444, 924}, // 226 1941 | {556, 556, 556, 556, 790, 500, 500, 500, 444, 924}, // 227 1942 | {556, 556, 556, 556, 786, 500, 500, 500, 444, 917}, // 228 1943 | {556, 556, 556, 556, 713, 500, 500, 500, 444, 930}, // 229 1944 | {889, 889, 889, 889, 384, 722, 722, 667, 667, 931}, // 230 1945 | {500, 556, 556, 500, 384, 444, 444, 444, 444, 463}, // 231 1946 | {556, 556, 556, 556, 384, 444, 444, 444, 444, 883}, // 232 1947 | {556, 556, 556, 556, 384, 444, 444, 444, 444, 836}, // 233 1948 | {556, 556, 556, 556, 384, 444, 444, 444, 444, 836}, // 234 1949 | {556, 556, 556, 556, 384, 444, 444, 444, 444, 867}, // 235 1950 | {278, 278, 278, 278, 494, 278, 278, 278, 278, 867}, // 236 1951 | {278, 278, 278, 278, 494, 278, 278, 278, 278, 696}, // 237 1952 | {278, 278, 278, 278, 494, 278, 278, 278, 278, 696}, // 238 1953 | {278, 278, 278, 278, 494, 278, 278, 278, 278, 874}, // 239 1954 | {556, 611, 611, 556, 000, 500, 500, 500, 500, 000}, // 240 1955 | {556, 611, 611, 556, 329, 556, 556, 500, 500, 874}, // 241 1956 | {556, 611, 611, 556, 274, 500, 500, 500, 500, 760}, // 242 1957 | {556, 611, 611, 556, 686, 500, 500, 500, 500, 946}, // 243 1958 | {556, 611, 611, 556, 686, 500, 500, 500, 500, 771}, // 244 1959 | {556, 611, 611, 556, 686, 500, 500, 500, 500, 865}, // 245 1960 | {556, 611, 611, 556, 384, 500, 500, 500, 500, 771}, // 246 1961 | {584, 584, 584, 584, 384, 570, 570, 675, 564, 888}, // 247 1962 | {611, 611, 611, 611, 384, 500, 500, 500, 500, 967}, // 248 1963 | {556, 611, 611, 556, 384, 556, 556, 500, 500, 888}, // 249 1964 | {556, 611, 611, 556, 384, 556, 556, 500, 500, 831}, // 250 1965 | {556, 611, 611, 556, 384, 556, 556, 500, 500, 873}, // 251 1966 | {556, 611, 611, 556, 494, 556, 556, 500, 500, 927}, // 252 1967 | {500, 556, 556, 500, 494, 500, 444, 444, 500, 970}, // 253 1968 | {556, 611, 611, 556, 494, 556, 500, 500, 500, 918}, // 254 1969 | {500, 556, 556, 500, 000, 500, 444, 444, 500, 000}, // 255 1970 | } // pdfFontWidths 1971 | 1972 | // pdfStandardPaperSizes contains standard paper sizes in mm (width x height) 1973 | var pdfStandardPaperSizes = map[string][2]int{ 1974 | "A0": {841, 1189}, "B0": {1000, 1414}, "C0": {917, 1297}, // ISO-216 1975 | "A1": {594, 841}, "B1": {707, 1000}, "C1": {648, 917}, 1976 | "A2": {420, 594}, "B2": {500, 707}, "C2": {458, 648}, 1977 | "A3": {297, 420}, "B3": {353, 500}, "C3": {324, 458}, 1978 | "A4": {210, 297}, "B4": {250, 353}, "C4": {229, 324}, 1979 | "A5": {148, 210}, "B5": {176, 250}, "C5": {162, 229}, 1980 | "A6": {105, 148}, "B6": {125, 176}, "C6": {114, 162}, 1981 | "A7": {74, 105}, "B7": {88, 125}, "C7": {81, 114}, 1982 | "A8": {52, 74}, "B8": {62, 88}, "C8": {57, 81}, 1983 | "A9": {37, 52}, "B9": {44, 62}, "C9": {40, 57}, 1984 | "A10": {26, 37}, "B10": {31, 44}, "C10": {28, 40}, 1985 | "LEGAL": {216, 356}, "TABLOID": {279, 432}, // US paper sizes 1986 | "LETTER": {216, 279}, "LEDGER": {432, 279}, 1987 | } // pdfStandardPaperSizes 1988 | 1989 | // end 1990 | -------------------------------------------------------------------------------- /pdf_ttfont.go: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // github.com/balacode/one-file-pdf one-file-pdf/[pdf_ttfont.go] 3 | // (c) balarabe@protonmail.com License: MIT 4 | // ----------------------------------------------------------------------------- 5 | 6 | // THIS FILE IS A WORK IN PROGRESS 7 | 8 | // This file contains a TTF font parser and PDF font-related functionality. 9 | // It augments PDF in pdf_core.go to support Unicode and font embedding, 10 | // but is not required for basic PDF functionality. 11 | 12 | // # Module Initialization 13 | // init() 14 | // 15 | // # pdfFontHandler Interface (f *pdfTTFont) 16 | // readFont(owner *PDF, font interface{}) bool 17 | // textWidthPt(s string) float64 18 | // writeText(s string) 19 | // writeFontObjects(font *pdfFont) 20 | 21 | package pdf 22 | 23 | import ( 24 | "bytes" 25 | "fmt" 26 | "io" 27 | "os" 28 | "reflect" 29 | ) 30 | 31 | // pdfTTFont __ 32 | type pdfTTFont struct { 33 | Name string 34 | Data []byte 35 | Err error 36 | HEAD struct { // font header table: general info about the font 37 | UnitsPerEm uint16 38 | XMin int16 39 | YMin int16 40 | XMax int16 41 | YMax int16 42 | } 43 | HHEA struct { // horizontal header: layout of horizontally-written fonts 44 | HMetricCount uint16 45 | Ascent int16 46 | Descent int16 47 | } 48 | MAXP struct { // maximum profile table: specifies memory requirements 49 | NumGlyphs uint16 50 | } 51 | HMTX struct { // horizontal metrics table: width of each glyph 52 | Widths []uint16 53 | } 54 | CMAP struct { // maps character codes to glyph indices 55 | Chars map[int]uint16 56 | } 57 | NAME struct { // font names table 58 | PostScriptName string 59 | } 60 | OS2 struct { // OS/2 compatibility table 61 | Version uint16 62 | STypoAscender int16 63 | STypoDescender int16 64 | STypoLineGap int16 65 | } 66 | POST struct { // glyph name and PostScript font table 67 | ItalicAngle int16 68 | } 69 | LOCA []uint32 // glyph data location table 70 | pdf *PDF 71 | } // pdfTTFont 72 | 73 | // ----------------------------------------------------------------------------- 74 | // # Module Initialization 75 | 76 | // init __ 77 | func init() { 78 | // TODO: uncomment this line when font handler is implemented 79 | // pdfNewFontHandler = func() pdfFontHandler { return &pdfTTFont{} } 80 | } // init 81 | 82 | // ----------------------------------------------------------------------------- 83 | // # pdfFontHandler Interface (f *pdfTTFont) 84 | 85 | // readFont loads a font from a file name, slice of bytes, or io.Reader 86 | func (f *pdfTTFont) readFont(owner *PDF, font interface{}) bool { 87 | f.Err = nil 88 | f.pdf = owner 89 | var ( 90 | src string 91 | rd io.Reader 92 | ) 93 | switch arg := font.(type) { 94 | case string: 95 | { 96 | src = arg 97 | data, err := os.ReadFile(arg) 98 | if err != nil { 99 | f.pdf.putError(0xE5445B, "Failed reading font file", src) 100 | return false 101 | } 102 | rd = bytes.NewReader(data) 103 | } 104 | case []byte: 105 | { 106 | src = fmt.Sprintf("[]byte len(%d)", len(arg)) 107 | rd = bytes.NewReader(arg) 108 | } 109 | case io.Reader: 110 | { 111 | src = "io.Reader" 112 | rd = arg 113 | } 114 | default: 115 | f.pdf.putError(0xECEB7B, "Invalid type in arg", 116 | reflect.TypeOf(font).String()) 117 | return false 118 | } 119 | _ = rd // TODO: remove when reader is used 120 | return f.Err == nil 121 | } // readFont 122 | 123 | // textWidthPt returns the width of text 's' in points 124 | func (f *pdfTTFont) textWidthPt(s string) float64 { 125 | f.Err = nil 126 | var ret float64 127 | for _, r := range s { 128 | _ = r 129 | glyph, found := 0, false // TODO: find out glyph entry 130 | if !found { 131 | // TODO: 0xE0074A: error 132 | continue 133 | } 134 | w := float64(f.HMTX.Widths[glyph]) 135 | if f.HEAD.UnitsPerEm != 1000 { 136 | w = w * 1000.0 / float64(f.HEAD.UnitsPerEm) 137 | } 138 | ret += w / 1000.0 * f.pdf.fontSizePt 139 | } 140 | return ret 141 | } // textWidthPt 142 | 143 | // writeText encodes text in the string 's' 144 | func (f *pdfTTFont) writeText(s string) { 145 | f.Err = nil 146 | f.pdf.write("BT ", f.pdf.page.x, " ", f.pdf.page.y, " Td ") 147 | // 148 | // TODO: add each rune of s, to determine glyphs to embed 149 | // 150 | // write hex encoded text to PDF 151 | f.pdf.write("[<") 152 | for _, r := range s { 153 | _ = r 154 | glyph, found := 0, false // TODO: find out glyph entry 155 | if !found { 156 | // TODO: 0xE1DC96: error 157 | return 158 | } 159 | f.pdf.write(fmt.Sprintf("%04X", glyph)) 160 | } 161 | f.pdf.write(">] TJ ET\n") 162 | } // writeText 163 | 164 | // writeFontObjects writes the PDF objects that define the embedded font 165 | func (f *pdfTTFont) writeFontObjects(font *pdfFont) { 166 | f.Err = nil 167 | // TODO: write font-related objects here (call writer methods) 168 | if f.Err != nil { 169 | f.pdf.putError(0xED2CDF, f.Err.Error(), "") 170 | } 171 | } // writeFontObjects 172 | 173 | // ----------------------------------------------------------------------------- 174 | // # TTF Parsing Methods (f *pdfTTFont) 175 | 176 | // readTTF __ 177 | func (f *pdfTTFont) readTTF(reader io.Reader) { 178 | if f.Err != nil { 179 | return 180 | } 181 | var err error 182 | f.Data, err = io.ReadAll(reader) 183 | if err != nil { 184 | f.Err = err 185 | return 186 | } 187 | rd := bytes.NewReader(f.Data) 188 | ver := f.read(rd, 4) 189 | if !bytes.Equal(ver, []byte{0, 1, 0, 0}) { 190 | // TODO: 0xE0E9AE: error 191 | return 192 | } 193 | for _, fn := range []func(*bytes.Reader){ 194 | f.readHEAD, f.readHHEA, f.readMAXP, f.readHMTX, f.readCMAP, 195 | f.readNAME, f.readOS2, f.readPOST, f.readLOCA} { 196 | if f.Err != nil { 197 | /// 0xE2257E: log error 198 | break 199 | } 200 | fn(rd) 201 | } 202 | } // readTTF 203 | 204 | // readHEAD __ 205 | func (f *pdfTTFont) readHEAD(rd *bytes.Reader) { 206 | if f.Err != nil { 207 | return 208 | } 209 | // TODO: implement 210 | } // readHEAD 211 | 212 | // readHHEA __ 213 | func (f *pdfTTFont) readHHEA(rd *bytes.Reader) { 214 | if f.Err != nil { 215 | return 216 | } 217 | // TODO: implement 218 | } // readHHEA 219 | 220 | // readMAXP __ 221 | func (f *pdfTTFont) readMAXP(rd *bytes.Reader) { 222 | if f.Err != nil { 223 | return 224 | } 225 | // TODO: implement 226 | } // readMAXP 227 | 228 | // readHMTX __ 229 | func (f *pdfTTFont) readHMTX(rd *bytes.Reader) { 230 | if f.Err != nil { 231 | return 232 | } 233 | // TODO: implement 234 | } // readHMTX 235 | 236 | // readCMAP __ 237 | func (f *pdfTTFont) readCMAP(rd *bytes.Reader) { 238 | if f.Err != nil { 239 | return 240 | } 241 | // TODO: implement 242 | } // readCMAP 243 | 244 | // readNAME __ 245 | func (f *pdfTTFont) readNAME(rd *bytes.Reader) { 246 | if f.Err != nil { 247 | return 248 | } 249 | // TODO: implement 250 | } // readNAME 251 | 252 | // readOS2 __ 253 | func (f *pdfTTFont) readOS2(rd *bytes.Reader) { 254 | if f.Err != nil { 255 | return 256 | } 257 | // TODO: implement 258 | } // readOS2 259 | 260 | // readPOST __ 261 | func (f *pdfTTFont) readPOST(rd *bytes.Reader) { 262 | if f.Err != nil { 263 | return 264 | } 265 | // TODO: implement 266 | } // readPOST 267 | 268 | // readLOCA __ 269 | func (f *pdfTTFont) readLOCA(rd *bytes.Reader) { 270 | if f.Err != nil { 271 | return 272 | } 273 | // TODO: implement 274 | } // readLOCA 275 | 276 | // read __ 277 | func (f *pdfTTFont) read(rd *bytes.Reader, size int, useData ...bool) []byte { 278 | if f.Err != nil { 279 | return nil 280 | } 281 | if len(useData) > 0 && useData[0] == false { 282 | _, err := rd.Seek(int64(size), 1) 283 | if err != nil { 284 | f.Err = err 285 | } 286 | return nil 287 | } 288 | ret := make([]byte, size) 289 | n, err := rd.Read(ret) 290 | if err != nil { 291 | f.Err = err 292 | return nil 293 | } 294 | if n != size { 295 | /// 0xE9B50D: error "END OF FILE DURING READING" 296 | return nil 297 | } 298 | return ret 299 | } // read 300 | 301 | // readI16 __ 302 | func (f *pdfTTFont) readI16(rd *bytes.Reader) int16 { 303 | ar := f.read(rd, 2) 304 | if f.Err != nil { 305 | return 0 306 | } 307 | ret := int(uint16(ar[0])<<8 | uint16(ar[1])) 308 | if ret >= 32768 { 309 | ret -= 65536 310 | } 311 | return int16(ret) 312 | } // readI16 313 | 314 | // readUI16 __ 315 | func (f *pdfTTFont) readUI16(rd *bytes.Reader) uint16 { 316 | ar := f.read(rd, 2) 317 | if f.Err != nil { 318 | return 0 319 | } 320 | return uint16(ar[0])<<8 | uint16(ar[1]) 321 | } // readUI16 322 | 323 | // readUI32 __ 324 | func (f *pdfTTFont) readUI32(rd *bytes.Reader) uint32 { 325 | ar := f.read(rd, 4) 326 | if f.Err != nil { 327 | return 0 328 | } 329 | return uint32(ar[0])<<24 | uint32(ar[1])<<16 | 330 | uint32(ar[2])<<8 | uint32(ar[3]) 331 | } // readUI32 332 | 333 | // end 334 | --------------------------------------------------------------------------------