-
Notifications
You must be signed in to change notification settings - Fork 17
/
pdf_core.go
1989 lines (1868 loc) · 85.9 KB
/
pdf_core.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// -----------------------------------------------------------------------------
// github.com/balacode/one-file-pdf one-file-pdf/[pdf_core.go]
// (c) [email protected] License: MIT
// -----------------------------------------------------------------------------
// Package pdf provides a PDF writer type to generate PDF files.
// Create a new PDF writer by assigning pdf.NewPDF(paperSize) to a variable.
// Then call property setters and methods to render the document.
// Finally, call WriteFile(filename) to save the file,
// or use Bytes() to get the PDF document as an array of bytes.
package pdf
// # Main Structure and Constructor
// PDF struct
// NewPDF(paperSize string) PDF
//
// # Plugins
// pdfNewFontHandler func ()pdfFontHandler
//
// # Read-Only Properties (p *PDF)
// PageCount() int
// PageHeight() float64
// PageWidth() float64
//
// # Properties
// Color() color.RGBA SetColor(nameOrHTMLColor string) *PDF
// SetColorRGB(r, g, b byte) *PDF
// Compression() bool SetCompression(val bool) *PDF
// CurrentPage() int SetCurrentPage(pageNo int) *PDF
// DocAuthor() string SetDocAuthor(s string) *PDF
// DocCreator() string SetDocCreator(s string) *PDF
// DocKeywords() string SetDocKeywords(s string) *PDF
// DocSubject() string SetDocSubject(s string) *PDF
// DocTitle() string SetDocTitle(s string) *PDF
// FontName() string SetFontName(name string) *PDF
// FontSize() float64 SetFontSize(points float64) *PDF
// SetFont(name string, points float64) *PDF
// HorizontalScaling() uint16 SetHorizontalScaling(percent uint16) *PDF
// LineWidth() float64 SetLineWidth(points float64) *PDF
// Units() string SetUnits(units string) *PDF
// X() float64 SetX(x float64) *PDF
// Y() float64 SetY(y float64) *PDF
// SetXY(x, y float64) *PDF
// # Methods (p *PDF)
// AddPage() *PDF
// Bytes() []byte
// DrawBox(x, y, width, height float64, optFill ...bool) *PDF
// DrawCircle(x, y, radius float64, optFill ...bool) *PDF
// DrawEllipse(x, y, xRadius, yRadius float64,
// optFill ...bool) *PDF
// DrawImage(x, y, height float64, fileNameOrBytes interface{},
// backColor ...string) *PDF
// DrawLine(x1, y1, x2, y2 float64) *PDF
// DrawText(s string) *PDF
// DrawTextAlignedToBox(
// x, y, width, height float64, align, text string) *PDF
// DrawTextAt(x, y float64, text string) *PDF
// DrawTextInBox(
// x, y, width, height float64, align, text string) *PDF
// DrawUnitGrid() *PDF
// FillBox(x, y, width, height float64) *PDF
// FillCircle(x, y, radius float64) *PDF
// FillEllipse(x, y, xRadius, yRadius float64) *PDF
// NextLine() *PDF
// Reset() *PDF
// SaveFile(filename string) error
// SetColumnWidths(widths ...float64) *PDF
//
// # Metrics Methods (p *PDF)
// TextWidth(s string) float64
// ToColor(nameOrHTMLColor string) (color.RGBA, error)
// ToPoints(numberAndUnit string) (float64, error)
// ToUnits(points float64) float64
// WrapTextLines(width float64, text string) (ret []string)
//
// # Error Handling Methods (p *PDF)
// Clean() *PDF
// Errors() []error
// PullError() error
// (*PDF) ErrorInfo(err error) (ret struct {
// ID int
// Msg, Src, Val string
// })
//
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// # Internal Structures
// pdfError struct
// (err pdfError) Error() string
// pdfFont struct
// pdfImage struct
// pdfPage struct
// pdfPaperSize struct
//
// # Internal Methods (p *PDF)
// applyFont() (handler pdfFontHandler, err error)
// drawTextLine(s string) *PDF
// drawTextBox(x, y, width, height float64,
// wrapText bool, align, text string) *PDF
// init() *PDF
// loadImage(fileNameOrBytes interface{}, back color.RGBA,
// ) (img pdfImage, idx int, err error)
// makeImage(source image.Image, back color.RGBA,
// ) (widthPx, heightPx int, isGray bool, ar []byte)
// reservePage() *PDF
// textWidthPt(s string) float64
//
// # Internal Generation Methods (p *PDF)
// nextObj() int
// write(a ...interface{}) *PDF
// writeCurve(x1, y1, x2, y2, x3, y3 float64) *PDF
// writeMode(optFill ...bool) (mode string)
// writeObj(objType string) *PDF
// writePages(pagesIndex, fontsIndex, imagesIndex int) *PDF
// writeStreamData(ar []byte) *PDF
// writeStreamObj(ar []byte) *PDF
//
// # Internal Functions (*PDF) - just attached to PDF, but not using its data
// escape(s string) string
// isWhiteSpace(s string) bool
// splitLines(s string) []string
// toUpperLettersDigits(s, extras string) string
// (p *PDF):
// getPaperSize(name string) (pdfPaperSize, error)
// getPointsPerUnit(units string) (ret float64, err error)
// putError(id int, msg, val string) *PDF
// writeTo(wr io.Writer, args ...interface{}) (count int, err error)
//
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// # Constants
// PDFColorNames = map[string]color.RGBA
//
// # Internal Constants
// pdfBlack = color.RGBA{A: 255}
// pdfFontNames = []string
// pdfFontWidths = [][]int
// pdfStandardPaperSizes = map[string][2]int
import (
"bytes"
"compress/zlib"
"crypto/sha512"
"fmt"
"image"
"image/color"
_ "image/gif"
_ "image/jpeg"
_ "image/png" // init image decoders
"io"
"os"
"reflect"
"runtime"
"strconv"
"strings"
"unicode" // only uses IsDigit(), IsLetter(), IsSpace()
)
// -----------------------------------------------------------------------------
// # Main Structure and Constructor
// PDF is the main structure representing a PDF document.
type PDF struct {
paperSize pdfPaperSize // paper size used in this PDF
pageNo int // current page number
page *pdfPage // pointer to the current page
pages []pdfPage // all the pages added to this PDF
fonts []pdfFont // all the fonts used in this PDF
images []pdfImage // all the images used in this PDF
columnWidths []float64 // user-set column widths (like tab stops)
columnNo int // number of the current column
units string // name of active measurement unit
ptPerUnit float64 // number of points per measurement unit
color color.RGBA // current drawing color
lineWidth float64 // current line width (in points)
font *pdfFont // currently selected font
fontName string // current font's name
fontSizePt float64 // current font's size (in points)
horzScaling uint16 // horizontal scaling factor (in %)
compression bool // enable stream compression?
content bytes.Buffer // content buffer where PDF is written
writer io.Writer // writer to PDF buffer or current page's buffer
objOffsets []int // object offsets used by Bytes() and write..()
objIndex int // object index used by Bytes() and write..()
errors []error // errors that occurred during method calls
isInit bool // has the PDF been initialized?
//
// document metadata fields
docAuthor, docCreator, docKeywords, docSubject, docTitle string
} // PDF
// NewPDF creates and initializes a new PDF object. Specify paperSize as:
// A, B, C series (e.g. "A4") or "LETTER", "LEGAL", "LEDGER", or "TABLOID"
// To specify a landscape orientation, add "-L" suffix e.g. "A4-L".
// You can also specify custom paper sizes using "width unit x height unit",
// for example "20 cm x 20 cm" or even "15cm x 10inch", etc.
func NewPDF(paperSize string) PDF {
var p PDF
size, err := p.init().getPaperSize(paperSize)
if err, isT := err.(pdfError); isT {
p.putError(0xE52F92, err.msg, paperSize)
p.paperSize, _ = p.getPaperSize("A4")
}
p.paperSize = size
return p
} // NewPDF
// -----------------------------------------------------------------------------
// # Plugins
// plugin to instantiate a font handler
var pdfNewFontHandler func() pdfFontHandler
// pdfFontHandler interface provides methods to parse and embed TrueType fonts.
type pdfFontHandler interface {
//
// reads and parses a font from a file name, slice of bytes, or io.Reader
readFont(owner *PDF, font interface{}) bool
//
// returns the width of text 's' in points
textWidthPt(s string) float64
//
// writes text in the string 's' and returns its width in points
writeText(s string)
//
// writes the PDF objects that define the subset font (i.e. embeds font)
writeFontObjects(font *pdfFont)
} // pdfFontHandler
// -----------------------------------------------------------------------------
// # Read-Only Properties (p *PDF)
// PageCount returns the total number of pages in the document.
func (p *PDF) PageCount() int { p.reservePage(); return len(p.pages) }
// PageHeight returns the height of the current page in selected units.
func (p *PDF) PageHeight() float64 { return p.ToUnits(p.paperSize.heightPt) }
// PageWidth returns the width of the current page in selected units.
func (p *PDF) PageWidth() float64 { return p.ToUnits(p.paperSize.widthPt) }
// -----------------------------------------------------------------------------
// # Properties (p *PDF)
// Color returns the current color, which is used for text, lines and fills.
func (p *PDF) Color() color.RGBA { p.init(); return p.color }
// SetColor sets the current color using a web/X11 color name
// (e.g. "HONEY DEW") or HTML color value such as "#191970"
// for midnight blue (#RRGGBB). The current color is used
// for subsequent text and line drawing and fills.
// If the name is unknown or invalid, sets color to black.
func (p *PDF) SetColor(nameOrHTMLColor string) *PDF {
color, err := p.init().ToColor(nameOrHTMLColor)
if err, isT := err.(pdfError); isT {
p.putError(0xE5B3A5, err.msg, nameOrHTMLColor)
}
p.color = color
return p
} // SetColor
// SetColorRGB sets the current color using red, green and blue values.
// The current color is used for subsequent text/line drawing and fills.
func (p *PDF) SetColorRGB(r, g, b byte) *PDF {
p.init()
p.color = color.RGBA{r, g, b, 255}
return p
} // SetColorRGB
// Compression returns the current compression mode. If it is true,
// all PDF content will be compressed when the PDF is generated. If
// false, most PDF content (excluding images) will be in plain text,
// which is useful for debugging or to study PDF commands.
func (p *PDF) Compression() bool { p.init(); return p.compression }
// SetCompression sets the compression mode used to generate the PDF.
// If set to true, all PDF steams will be compressed when the PDF is
// generated. If false, most content (excluding images) will be in
// plain text, which is useful for debugging or to study PDF commands.
func (p *PDF) SetCompression(val bool) *PDF {
p.init()
p.compression = val
return p
} // SetCompression
// CurrentPage returns the current page's number, starting from 1.
func (p *PDF) CurrentPage() int { return p.pageNo + 1 }
// SetCurrentPage opens the specified page. Page numbers start from 1.
func (p *PDF) SetCurrentPage(pageNo int) *PDF {
if pageNo < 1 || pageNo > len(p.pages) {
p.putError(0xE65AF0, "pageNo out of range",
fmt.Sprint("pageNo:", pageNo, " range:1..", len(p.pages)))
return p
}
p.pageNo = pageNo - 1
return p
} // SetCurrentPage
// DocAuthor returns the optional 'document author' metadata property.
func (p *PDF) DocAuthor() string { p.init(); return p.docAuthor }
// SetDocAuthor sets the optional 'document author' metadata property.
func (p *PDF) SetDocAuthor(s string) *PDF { p.docAuthor = s; return p }
// DocCreator returns the optional 'document creator' metadata property.
func (p *PDF) DocCreator() string { p.init(); return p.docCreator }
// SetDocCreator sets the optional 'document creator' metadata property.
func (p *PDF) SetDocCreator(s string) *PDF { p.docCreator = s; return p }
// DocKeywords returns the optional 'document keywords' metadata property.
func (p *PDF) DocKeywords() string { p.init(); return p.docKeywords }
// SetDocKeywords sets the optional 'document keywords' metadata property.
func (p *PDF) SetDocKeywords(s string) *PDF { p.docKeywords = s; return p }
// DocSubject returns the optional 'document subject' metadata property.
func (p *PDF) DocSubject() string { p.init(); return p.docSubject }
// SetDocSubject sets the optional 'document subject' metadata property.
func (p *PDF) SetDocSubject(s string) *PDF { p.docSubject = s; return p }
// DocTitle returns the optional 'document subject' metadata property.
func (p *PDF) DocTitle() string { p.init(); return p.docTitle }
// SetDocTitle sets the optional 'document title' metadata property.
func (p *PDF) SetDocTitle(s string) *PDF { p.docTitle = s; return p }
// FontName returns the name of the currently-active typeface.
func (p *PDF) FontName() string { p.init(); return p.fontName }
// SetFontName changes the current font, while using the
// same font size as the previous font. Use one of the
// standard font names, such as 'Helvetica'.
func (p *PDF) SetFontName(name string) *PDF {
p.init()
p.fontName = name
return p
} // SetFontName
// FontSize returns the current font size in points.
func (p *PDF) FontSize() float64 { p.init(); return p.fontSizePt }
// SetFontSize changes the current font size in points,
// without changing the currently-selected font typeface.
func (p *PDF) SetFontSize(points float64) *PDF {
p.init()
p.fontSizePt = points
return p
} // SetFontSize
// SetFont changes the current font name and size in points.
// For the font name, use one of the standard font names, e.g. 'Helvetica'.
// This font will be used for subsequent text drawing.
func (p *PDF) SetFont(name string, points float64) *PDF {
return p.SetFontName(name).SetFontSize(points)
} // SetFont
// HorizontalScaling returns the current horizontal scaling in percent.
func (p *PDF) HorizontalScaling() uint16 { p.init(); return p.horzScaling }
// SetHorizontalScaling changes the horizontal scaling in percent.
// For example, 200 will stretch text to double its normal width.
func (p *PDF) SetHorizontalScaling(percent uint16) *PDF {
p.init()
p.horzScaling = percent
return p
} // SetHorizontalScaling
// LineWidth returns the current line width in points.
func (p *PDF) LineWidth() float64 { p.init(); return p.lineWidth }
// SetLineWidth changes the line width in points.
func (p *PDF) SetLineWidth(points float64) *PDF {
p.init()
p.lineWidth = points
return p
} // SetLineWidth
// Units returns the currently selected measurement units.
// E.g.: mm cm " in inch inches tw twip twips pt point points
func (p *PDF) Units() string { p.init(); return p.units }
// SetUnits changes the current measurement units:
// mm cm " in inch inches tw twip twips pt point points (can be in any case)
func (p *PDF) SetUnits(units string) *PDF {
ppu, err := p.init().getPointsPerUnit(units)
if err, isT := err.(pdfError); isT {
return p.putError(0xEB4AAA, err.msg, units)
}
p.ptPerUnit, p.units = ppu, p.toUpperLettersDigits(units, "")
return p
} // SetUnits
// X returns the X-coordinate of the current drawing position.
func (p *PDF) X() float64 { return p.reservePage().ToUnits(p.page.x) }
// SetX changes the X-coordinate of the current drawing position.
func (p *PDF) SetX(x float64) *PDF {
p.init().reservePage()
p.page.x = x * p.ptPerUnit
return p
} // SetX
// Y returns the Y-coordinate of the current drawing position.
func (p *PDF) Y() float64 {
return p.reservePage().ToUnits(p.paperSize.heightPt - p.page.y)
} // Y
// SetY changes the Y-coordinate of the current drawing position.
func (p *PDF) SetY(y float64) *PDF {
p.init().reservePage()
p.page.y = p.paperSize.heightPt - y*p.ptPerUnit
return p
} // SetY
// SetXY changes both X- and Y-coordinates of the current drawing position.
func (p *PDF) SetXY(x, y float64) *PDF { return p.SetX(x).SetY(y) }
// -----------------------------------------------------------------------------
// # Methods (p *PDF)
// AddPage appends a new blank page to the PDF and makes it the current page.
func (p *PDF) AddPage() *PDF {
COLOR := color.RGBA{1, 0, 1, 0x01} // unlikely default color
p.pages = append(p.pages, pdfPage{
x: -1, y: p.paperSize.heightPt + 1, lineWidth: 1,
strokeColor: COLOR, nonStrokeColor: COLOR,
fontSizePt: 10, horzScaling: 100,
})
p.pageNo = len(p.pages) - 1
p.page = &p.pages[p.pageNo]
p.writer = &p.page.content
return p
} // AddPage
// Bytes generates the PDF document from various page and
// auxiliary objects and returns it in an array of bytes,
// identical to the content of a PDF file. This method is where
// you'll find the core structure of a PDF document.
func (p *PDF) Bytes() []byte {
// free any existing generated content and write PDF header
p.reservePage()
const pagesIndex = 3
var (
fontsIndex = pagesIndex + len(p.pages)*2
imagesIndex = fontsIndex + len(p.fonts)
infoIndex int // set when metadata found
prevWriter = p.writer
)
p.content.Reset()
p.writer = &p.content
p.objOffsets = []int{}
p.objIndex = 0
p.write("%PDF-1.4\n\n").
writeObj("/Catalog").write("/Pages 2 0 R>>\n" + "endobj\n\n")
//
// write /Pages object (2 0 obj), page count, page size and the pages
p.writePages(pagesIndex, fontsIndex, imagesIndex)
//
// write fonts
for _, font := range p.fonts {
if font.handler == nil {
p.writeObj("/Font").write("/Subtype/Type1/Name/FNT", font.id, "\n",
"/BaseFont/", font.name, "\n",
"/Encoding/StandardEncoding>>\n"+"endobj\n")
} else {
font.handler.writeFontObjects(&font)
}
}
// write images
for _, img := range p.images {
colorSpace := "DeviceRGB"
if img.isGray {
colorSpace = "DeviceGray"
}
old := p.compression
p.compression = true
p.writeObj("/XObject").
write("/Subtype/Image\n",
"/Width ", img.widthPx, "/Height ", img.heightPx,
"/ColorSpace/", colorSpace, "/BitsPerComponent 8\n").
writeStreamData(img.data).write("\n" + "endobj\n\n")
p.compression = old
}
// write info object
if p.docTitle != "" || p.docSubject != "" ||
p.docKeywords != "" || p.docAuthor != "" || p.docCreator != "" {
//
infoIndex = imagesIndex + len(p.images)
p.writeObj("/Info")
for _, tuple := range [][]string{
{"/Title ", p.docTitle}, {"/Subject ", p.docSubject},
{"/Keywords ", p.docKeywords}, {"/Author ", p.docAuthor},
{"/Creator ", p.docCreator},
} {
if tuple[1] != "" {
p.write(tuple[0], "(", p.escape(tuple[1]), ")")
}
}
p.write(">>\n" + "endobj\n\n")
}
// write cross-reference table at end of document
start := p.content.Len()
p.write("xref\n"+
"0 ", len(p.objOffsets), "\n"+"0000000000 65535 f \n")
for _, offset := range p.objOffsets[1:] {
p.write(fmt.Sprintf("%010d 00000 n \n", offset))
}
// write the trailer
p.write("trailer\n"+"<</Size ", len(p.objOffsets), "/Root 1 0 R")
if infoIndex > 0 {
p.write("/Info ", infoIndex, " 0 R") // optional reference to info
}
p.write(">>\n"+"startxref\n", start, "\n", "%%EOF\n")
p.writer = prevWriter
return p.content.Bytes()
} // Bytes
// DrawBox draws a rectangle of the specified width and height,
// with the top-left corner starting at point (x, y).
// To fill the rectangle, pass true in the optional optFill.
func (p *PDF) DrawBox(x, y, width, height float64, optFill ...bool) *PDF {
width, height = width*p.ptPerUnit, height*p.ptPerUnit
x, y = x*p.ptPerUnit, p.paperSize.heightPt-y*p.ptPerUnit-height
mode := p.writeMode(optFill...)
return p.write(x, " ", y, " ", width, " ", height, " re ", mode, "\n")
// re: construct a rectangular path
} // DrawBox
// DrawCircle draws a circle of radius r centered on (x, y),
// by drawing 4 Bézier curves (PDF has no circle primitive)
// To fill the circle, pass true in the optional optFill.
func (p *PDF) DrawCircle(x, y, radius float64, optFill ...bool) *PDF {
return p.DrawEllipse(x, y, radius, radius, optFill...)
} // DrawCircle
// DrawEllipse draws an ellipse centered on (x, y),
// with horizontal radius xRadius and vertical radius yRadius
// by drawing 4 Bézier curves (PDF has no ellipse primitive).
// To fill the ellipse, pass true in the optional optFill.
func (p *PDF) DrawEllipse(x, y, xRadius, yRadius float64,
optFill ...bool) *PDF {
x, y = x*p.ptPerUnit, p.paperSize.heightPt-y*p.ptPerUnit
const ratio = 0.552284749830794 // (4/3) * tan(PI/8)
var (
r = xRadius * p.ptPerUnit // horizontal radius
v = yRadius * p.ptPerUnit // vertical radius
m, n = r * ratio, v * ratio // ratios for control points
mode = p.writeMode(optFill...) // prepare colors/line width
)
return p.write(x-r, " ", y, " m\n"). // x0 y0 m: move to point (x0, y0)
// control-1 control-2 endpoint
writeCurve(x-r, y+n, x-m, y+v, x+0, y+v). // top left arc
writeCurve(x+m, y+v, x+r, y+n, x+r, y+0). // top right
writeCurve(x+r, y-n, x+m, y-v, x+0, y-v). // bottom right
writeCurve(x-m, y-v, x-r, y-n, x-r, y+0). // bottom left
write(mode, "\n") // b: fill or S: stroke
} // DrawEllipse
// DrawImage draws a PNG image. x, y, height specify the position and height
// of the image. Width is scaled to match the image's aspect ratio.
// fileNameOrBytes is either a string specifying a file name,
// or a byte slice with PNG image data.
func (p *PDF) DrawImage(x, y, height float64, fileNameOrBytes interface{},
backColor ...string) *PDF {
//
back := color.RGBA{R: 255, G: 255, B: 255, A: 255} // white by default
if len(backColor) > 0 {
back, _ = p.ToColor(backColor[0])
}
// add the image to the current page, if not already referenced
p.reservePage()
img, idx, err := p.loadImage(fileNameOrBytes, back)
if err, isT := err.(pdfError); isT {
return p.putError(0xE8F375, err.msg, err.val)
}
var found bool
for _, id := range p.page.imageIDs {
if id == idx {
found = true
break
}
}
if !found {
p.page.imageIDs = append(p.page.imageIDs, idx)
}
// draw the image
h := height * p.ptPerUnit
w := float64(img.widthPx) / float64(img.heightPx) * h
x, y = x*p.ptPerUnit, p.paperSize.heightPt-y*p.ptPerUnit-h
return p.write("q\n", w, " 0 0 ", h, " ", x, " ", y, " cm\n"+
"/IMG", idx, " Do\n"+"Q\n")
// q: save graphics state
// cm: concatenate matrix to current transform matrix
// Do: invoke named XObject (/IMGn)
// Q: restore graphics state
} // DrawImage
// DrawLine draws a straight line from point (x1, y1) to point (x2, y2).
func (p *PDF) DrawLine(x1, y1, x2, y2 float64) *PDF {
x1, y1 = x1*p.ptPerUnit, p.paperSize.heightPt-y1*p.ptPerUnit
x2, y2 = x2*p.ptPerUnit, p.paperSize.heightPt-y2*p.ptPerUnit
p.writeMode(true) // prepare color/line width
return p.write(x1, " ", y1, " m ", x2, " ", y2, " l S\n")
// m: move l:line S: stroke path (for lines)
} // DrawLine
// DrawText draws a text string at the current position (X, Y).
func (p *PDF) DrawText(s string) *PDF {
if len(p.columnWidths) == 0 {
return p.drawTextLine(s)
}
x := 0.0
for i := 0; i < p.columnNo; i, x = i+1, x+p.columnWidths[i] {
}
p.SetX(x).drawTextLine(s)
if p.columnNo < len(p.columnWidths)-1 {
p.columnNo++
return p
}
return p.NextLine()
} // DrawText
// DrawTextAlignedToBox draws 'text' within a rectangle specified
// by 'x', 'y', 'width' and 'height'. If 'align' is blank, the
// text is center-aligned both vertically and horizontally.
// Specify 'L' or 'R' to align the text left or right, and 'T' or
// 'B' to align the text to the top or bottom of the box.
func (p *PDF) DrawTextAlignedToBox(
x, y, width, height float64, align, text string) *PDF {
return p.drawTextBox(x, y, width, height, false, align, text)
} // DrawTextAlignedToBox
// DrawTextAt draws text at the specified point (x, y).
func (p *PDF) DrawTextAt(x, y float64, text string) *PDF {
return p.SetXY(x, y).DrawText(text)
} // DrawTextAt
// DrawTextInBox draws word-wrapped text within a rectangle
// specified by 'x', 'y', 'width' and 'height'. If 'align' is blank,
// the text is center-aligned both vertically and horizontally.
// Specify 'L' or 'R' to align the text left or right, and 'T' or
// 'B' to align the text to the top or bottom of the box.
func (p *PDF) DrawTextInBox(
x, y, width, height float64, align, text string) *PDF {
return p.drawTextBox(x, y, width, height, true, align, text)
} // DrawTextInBox
// DrawUnitGrid draws a light-gray grid demarcated in the
// current measurement unit. The grid fills the entire page.
// It helps with item positioning.
func (p *PDF) DrawUnitGrid() *PDF {
pw, ph := p.PageWidth(), p.PageHeight()
p.SetLineWidth(0.1).SetFont("Helvetica", 8)
for i, x := 0, 0.0; x < pw; i, x = i+1, x+1 { // vertical lines |
p.SetColorRGB(200, 200, 200).DrawLine(x, 0, x, ph).
SetColor("Indigo").SetXY(x+0.1, 0.3).DrawText(strconv.Itoa(i))
}
for i, y := 0, 0.0; y < ph; i, y = i+1, y+1 { // horizontal lines -
p.SetColorRGB(200, 200, 200).DrawLine(0, y, pw, y).
SetColor("Indigo").SetXY(0.1, y+0.3).DrawText(strconv.Itoa(i))
}
return p
} // DrawUnitGrid
// FillBox fills a rectangle with the current color.
func (p *PDF) FillBox(x, y, width, height float64) *PDF {
return p.DrawBox(x, y, width, height, true)
} // FillBox
// FillCircle fills a circle of radius r centered on (x, y),
// by drawing 4 Bézier curves (PDF has no circle primitive)
func (p *PDF) FillCircle(x, y, radius float64) *PDF {
return p.DrawEllipse(x, y, radius, radius, true)
} // FillCircle
// FillEllipse fills a Ellipse of radius r centered on (x, y),
// by drawing 4 Bézier curves (PDF has no ellipse primitive)
func (p *PDF) FillEllipse(x, y, xRadius, yRadius float64) *PDF {
return p.DrawEllipse(x, y, xRadius, yRadius, true)
} // FillEllipse
// NextLine advances the text writing position to the next line.
// I.e. the Y increases by the height of the font and
// the X-coordinate is reset to zero.
func (p *PDF) NextLine() *PDF {
x, y := 0.0, p.Y()+p.FontSize()*p.ptPerUnit
if len(p.columnWidths) > 0 {
x = p.columnWidths[0]
}
if y > p.paperSize.heightPt*p.ptPerUnit {
p.AddPage()
y = 0
}
p.columnNo = 0
return p.SetXY(x, y)
} // NextLine
// Reset releases all resources and resets all variables, except paper size.
func (p *PDF) Reset() *PDF {
p.page, p.writer = nil, nil
*p = NewPDF(p.paperSize.name)
return p
} // Reset
// SaveFile generates and saves the PDF document to a file.
func (p *PDF) SaveFile(filename string) error {
err := os.WriteFile(filename, p.Bytes(), 0644)
if err != nil {
p.putError(0xED3F6D, "Failed writing file", err.Error())
}
return err
} // SaveFile
// SetColumnWidths creates column positions (tab stops) along the X-axis.
// To remove all column positions, call this method without any argument.
func (p *PDF) SetColumnWidths(widths ...float64) *PDF {
p.init()
p.columnWidths = widths
return p
} // SetColumnWidths
// -----------------------------------------------------------------------------
// # Metrics Methods (p *PDF)
// TextWidth returns the width of the text in current units.
func (p *PDF) TextWidth(s string) float64 {
return p.ToUnits(p.textWidthPt(s))
} // TextWidth
// ToColor returns an RGBA color value from a web/X11 color name
// (e.g. "HONEY DEW") or HTML color value such as "#191970"
// If the name or code is unknown or invalid, returns zero value (black).
func (p *PDF) ToColor(nameOrHTMLColor string) (color.RGBA, error) {
//
// if name starts with '#' treat it as HTML color code (#RRGGBB)
s := p.toUpperLettersDigits(nameOrHTMLColor, "#")
if len(s) >= 7 && s[0] == '#' {
var hex [6]byte
for i, r := range s[1:7] {
switch {
case r >= '0' && r <= '9':
hex[i] = byte(r - '0')
case r >= 'A' && r <= 'F':
hex[i] = byte(r - 'A' + 10)
default:
return pdfBlack, pdfError{id: 0xEED50B, src: "ToColor",
msg: "Bad color code", val: nameOrHTMLColor}
}
}
return color.RGBA{
hex[0]*16 + hex[1],
hex[2]*16 + hex[3],
hex[4]*16 + hex[5], 255}, nil
}
if cl, found := PDFColorNames[s]; found { // search for color name (quick)
return color.RGBA{cl.R, cl.G, cl.B, 255}, nil
}
for k, v := range PDFColorNames { // (slower search)
if p.toUpperLettersDigits(k, "") == s {
return v, nil
}
}
return pdfBlack, pdfError{id: 0xE00982, src: "ToColor",
msg: "Unknown color name", val: nameOrHTMLColor}
} // ToColor
// ToPoints converts a string composed of a number and unit to points.
// For example '1 cm' or '1cm' becomes 28.346 points.
// Recognised units: mm cm " in inch inches tw twip twips pt point points
func (p *PDF) ToPoints(numberAndUnit string) (float64, error) {
var num, unit string // extract number and unit
for _, r := range numberAndUnit {
switch {
case r == '-', r == '.', unicode.IsDigit(r):
num += string(r)
case r == '"', unicode.IsLetter(r):
unit += string(r)
}
}
ppu := 1.0
if unit != "" {
var err error
ppu, err = p.getPointsPerUnit(unit)
if err, isT := err.(pdfError); isT {
return 0, err
}
}
n, err := strconv.ParseFloat(num, 64)
if err != nil {
return 0, pdfError{id: 0xE154AC, msg: "Invalid number", val: num}
}
return n * ppu, nil
} // ToPoints
// ToUnits converts points to the currently selected unit of measurement.
func (p *PDF) ToUnits(points float64) float64 {
if int(p.ptPerUnit*100) == 0 {
return points
}
return points / p.ptPerUnit
} // ToUnits
// WrapTextLines splits a string into multiple lines so that the text
// fits in the specified width. The text is wrapped on word boundaries.
// Newline characters ("\r" and "\n") also cause text to be split.
// You can find out the number of lines needed to wrap some
// text by checking the length of the returned array.
func (p *PDF) WrapTextLines(width float64, text string) (ret []string) {
fit := func(s string, step, n int, width float64) int {
for max := len(s); n > 0 && n <= max; {
w := p.TextWidth(s[:n])
switch step {
case 1, 3: // keep halving (or - 1) until n chars fit in width
if w <= width {
return n
}
n--
if step == 1 {
n /= 2
}
case 2: // increase n until n chars won't fit in width
if w > width {
return n
}
n = 1 + int((float64(n) * 1.2)) // increase n by 1 + 20% of n
}
}
return 0
}
// split text into lines. then break lines based on text width
for _, line := range p.splitLines(text) {
for p.TextWidth(line) > width {
n := len(line) // reduce, increase, then reduce n to get best fit
for i := 1; i <= 3; i++ {
n = fit(line, i, n, width)
}
// move to the last word (if white-space is found)
found, max := false, n
for n > 0 {
if p.isWhiteSpace(line[n-1 : n]) {
found = true
break
}
n--
}
if !found {
n = max
}
if n <= 0 {
break
}
ret = append(ret, line[:n])
line = line[n:]
}
ret = append(ret, line)
}
return ret
} // WrapTextLines
// -----------------------------------------------------------------------------
// # Error Handling Methods (p *PDF)
// Clean clears all accumulated errors.
func (p *PDF) Clean() *PDF { p.errors = nil; return p }
// ErrorInfo extracts and returns additional error details from PDF errors
func (*PDF) ErrorInfo(err error) (ret struct {
ID int
Msg, Src, Val string
}) {
if err, isT := err.(pdfError); isT {
ret.ID, ret.Msg, ret.Src, ret.Val = err.id, err.msg, err.src, err.val
}
return ret
} // ErrorInfo
// Errors returns a slice of all accumulated errors.
func (p *PDF) Errors() []error { return p.errors }
// PullError removes and returns the first error from the errors collection.
func (p *PDF) PullError() error {
if len(p.errors) == 0 {
return nil
}
ret := p.errors[0]
p.errors = p.errors[1:]
return ret
} // PullError
// -----------------------------------------------------------------------------
// # Internal Structures
// pdfError stores extended error details for errors in this package.
type pdfError struct {
id int // unique ID of the error (only within package)
msg, src, val string // the error message, source method and invalid value
} // pdfError
// Error creates and returns an error message from pdfError details
func (err pdfError) Error() string {
ret := fmt.Sprintf("%s %q", err.msg, err.val)
if err.src != "" {
ret += " @" + err.src
}
return ret
} // Error
// pdfFont represents a font name and its appearance
type pdfFont struct {
id int
name string
builtInIndex int
isBold, isItalic bool
handler pdfFontHandler
} // pdfFont
// pdfImage represents an image
type pdfImage struct {
filename string // name of file from which image was read
widthPx, heightPx int // width and height in pixels
data []byte // image data
hash [64]byte // hash of data (used to compare images)
backColor color.RGBA // background color (used to compare images)
isGray bool // image is grayscale? (if false, color image)
} // pdfImage
// pdfPage holds references, state and the stream buffer for each page
type pdfPage struct {
fontIDs, imageIDs []int // references to fonts and images
x, y, lineWidth, fontSizePt float64 // current drawing state
strokeColor, nonStrokeColor color.RGBA // "
fontID int // "
horzScaling uint16 // "
content bytes.Buffer // write..() calls send output here
} // pdfPage
// pdfPaperSize represents a page size name and its dimensions in points
type pdfPaperSize struct {
name string // paper size: e.g. 'Letter', 'A4', etc.
widthPt, heightPt float64 // width and height in points
} // pdfPaperSize
// -----------------------------------------------------------------------------
// # Internal Methods (p *PDF)
// applyFont writes a font change command, provided the font has
// been changed since the last operation that uses fonts.
//
// This should be called just before a font needs to be used.
// This way, if a font is picked with SetFontName() property, but
// never used to draw text, no font selection command is output.
//
// Before calling this method, the font name must be already
// set by SetFontName(), which is stored in p.font.fontName
//
// What this method does:
// - Validates the current font name and determines if it is a
// standard (built-in) font like Helvetica or a TrueType font.
// - Fills the document-wide list of fonts (p.fonts).
// - Adds items to the list of font ID's used on the current page.
func (p *PDF) applyFont() (handler pdfFontHandler, err error) {
var (
font pdfFont
name = p.toUpperLettersDigits(p.fontName, "")
valid = name != ""
)
if valid {
valid = false
for i, fname := range pdfFontNames {
fname = p.toUpperLettersDigits(fname, "")
if fname != name {
continue
}
has := strings.Contains
font = pdfFont{
name: pdfFontNames[i],
builtInIndex: i,
isBold: has(fname, "BOLD"),
isItalic: has(fname, "OBLIQUE") || has(fname, "ITALIC"),
}
valid = true
break
}
}
if !valid && pdfNewFontHandler != nil {
handler = pdfNewFontHandler()
valid = handler.readFont(p, p.fontName)
font.handler = handler
}
// if there is no selected font or it's invalid, use Helvetica
if !valid {
err = pdfError{id: 0xE86819, msg: "Invalid font", val: p.fontName}
p.fontName = "Helvetica"
p.applyFont()
return nil, err
}
// has the font been added to the global list? if not, add it:
for _, it := range p.fonts {