Brush up the go code
This commit is contained in:
19
Makefile
19
Makefile
@@ -1,9 +1,18 @@
|
||||
.PHONY: help
|
||||
help: ## Lists all commands available
|
||||
# Snippet comes from: https://gist.github.com/prwhite/8168133#gistcomment-3785627
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[$$()% 0-9a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
|
||||
.PHONY: build
|
||||
build: ## Build the main covergen CLI for the local platform
|
||||
go build -o covergen ./cmd/covergen
|
||||
|
||||
.PHONY: wasm
|
||||
wasm: ## Generate the main WASM blob
|
||||
GOOS=js GOARCH=wasm go build -o covergen.wasm ./cmd/wasm/main.go
|
||||
|
||||
.PHONY: build-cross-clis
|
||||
build-cross-clis:
|
||||
build-cross-clis: ## Builds the CLIs for all supported platforms
|
||||
rm -rf ./dist && mkdir ./dist
|
||||
GOOS=linux GOARCH=amd64 go build -o dist/covergen.linux-amd64 ./cmd/covergen
|
||||
GOOS=darwin GOARCH=amd64 go build -o dist/covergen.darwin-amd64 ./cmd/covergen
|
||||
|
||||
.PHONY: wasm
|
||||
wasm:
|
||||
GOOS=js GOARCH=wasm go build -o covergen.wasm ./cmd/wasm/main.go
|
||||
|
||||
45
README.md
45
README.md
@@ -2,19 +2,46 @@
|
||||
|
||||
Just generates some magic PDF invoice covers using straight up dark magic.
|
||||
|
||||
## Just be lazy
|
||||
## Building it
|
||||
|
||||
Go see it in action and download it from [https://s3.blacknova.io/covergen/index.html](https://s3.blacknova.io/covergen/index.html)
|
||||
```shell
|
||||
# Builds the CLI
|
||||
make build
|
||||
|
||||
## Just build it
|
||||
|
||||
```
|
||||
$ go build -o covergen ./cmd/covergen
|
||||
# Builds the WASM blob
|
||||
make wasm
|
||||
```
|
||||
|
||||
## It has wasm!
|
||||
## Custom font notes
|
||||
|
||||
```
|
||||
$ make wasm
|
||||
The PDF uses the font `Mark`, which isn't a commonly available font.
|
||||
For this reason they have been vendored into the repository in `pkg/covergen/fonts` together with
|
||||
the `cp1252` character map (used in most PDFs because UTF-8 hasn't been invented yet in the PDF world).
|
||||
For usage in `fpdf` they need to be encoded, which is done throught he `makefont` tool provided by `fpdf`, see
|
||||
[https://github.com/go-pdf/fpdf#nonstandard-fonts](https://github.com/go-pdf/fpdf#nonstandard-fonts).
|
||||
The section linked will also provide instructions on how to use it, but for this specific repo, the call to regenerate
|
||||
or add more fonts is:
|
||||
```shell
|
||||
makefont --embed --enc=pkg/covergen/fonts/cp1252.map --dst=pkg/covergen/font_embed pkg/covergen/fonts/*.ttf
|
||||
```
|
||||
|
||||
To get the `makefont` binary, clone the `go-fpdf/fpdf` repo and run `go build` in the `makefont` directory.
|
||||
|
||||
## A note on WASM
|
||||
|
||||
An interesting architectural note about how WASM binaries are run in browsers is that you should not see it
|
||||
as a shell command 'exec'-ing out into a new binary, but rather as two processes living side by side.
|
||||
The calling a method exposed by WASM behaves more like doing IPC between the two processes.
|
||||
You can sort of see this happening in `cmd/wasm/main.go` where the `main` function is actually used.
|
||||
It registers the two exposed commands to the `global` scope (in browsers: `window`) before just deadlocking
|
||||
by waiting on an anonymous channel.
|
||||
|
||||
The most important note you should take from that is that the WASM binary **can crash**.
|
||||
If it crashes, it doesn't just 'boot itself' again.
|
||||
And the JS -> WASM boundary is *very*, ***very*** unsafe.
|
||||
Like 'do the smallest thing wrong and the program crashes' level of unsafe.
|
||||
No, type safety won't help you, you're literally playing with the equivalent of void pointers.
|
||||
Even panic recovery (present in the two exposed methods) doesn't protect against all of it.
|
||||
|
||||
I think where I'm going is just a strong warning to be *very* careful at that boundary.
|
||||
Don't trust any `js.Value`. Parse it thoroughly and quickly into golang-specified types.
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"covergen/pkg/covergen"
|
||||
)
|
||||
|
||||
// List of colors used for picking a (random) color in the cli
|
||||
var colors = []string{"red", "blue", "yellow", "green"}
|
||||
var colorMap = map[string]covergen.Color{
|
||||
"red": {255, 85, 0},
|
||||
@@ -69,6 +70,7 @@ func main() {
|
||||
chosenColor = colorMap[*color]
|
||||
}
|
||||
|
||||
// Newlines in CLI arguments are given escaped. Unescape them to proper newlines
|
||||
*customer = strings.ReplaceAll(*customer, "\\n", "\n")
|
||||
settings := covergen.CoverSettings{
|
||||
Number: *number,
|
||||
@@ -77,6 +79,7 @@ func main() {
|
||||
HLColor: chosenColor,
|
||||
}
|
||||
|
||||
// If output2 (back page) is not set, generate a combined PDF
|
||||
if *output2 == "" {
|
||||
pdf, err := covergen.GenerateInvoice(settings)
|
||||
|
||||
@@ -88,6 +91,7 @@ func main() {
|
||||
log.Fatal().Err(err).Msg("Failed to write invoice to disk")
|
||||
}
|
||||
} else {
|
||||
// Otherwise, generate the front and back separately
|
||||
front, err := covergen.GenerateFrontCover(settings)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to render front")
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Super simple development webserver
|
||||
func main() {
|
||||
err := http.ListenAndServe(":9090", http.FileServer(http.Dir("assets")))
|
||||
if err != nil {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"covergen/pkg/covergen"
|
||||
)
|
||||
|
||||
// jsmap is just a type alias that corresponds to the ES5 Record<string, any>, or just a plain object {}
|
||||
type jsmap = map[string]interface{}
|
||||
|
||||
type covergenArgs struct {
|
||||
@@ -21,6 +22,8 @@ type covergenArgs struct {
|
||||
HLColor string `jsref:"hlColor"`
|
||||
}
|
||||
|
||||
// parseHexColor takes a CSS hex color (#RRGGBB or the shorthand #RGB) and parses out the red, green, and blue components
|
||||
// into a covergen.Color
|
||||
func parseHexColor(s string) (c covergen.Color, err error) {
|
||||
switch len(s) {
|
||||
case 7:
|
||||
@@ -33,11 +36,12 @@ func parseHexColor(s string) (c covergen.Color, err error) {
|
||||
c.B *= 17
|
||||
default:
|
||||
err = fmt.Errorf("invalid length, must be 7 or 4")
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// settingsFromValue tries to parse the WASM boundary-crossing arguments blob into a golang-native settings object
|
||||
func settingsFromValue(arg js.Value) (*covergen.CoverSettings, error) {
|
||||
settings := covergenArgs{
|
||||
NumberPrefix: "offerte",
|
||||
@@ -71,6 +75,7 @@ func settingsFromValue(arg js.Value) (*covergen.CoverSettings, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// pdfToJs takes the pdf object and outputs it into a Uint8Array and passes that back to JS.
|
||||
func pdfToJs(pdf *fpdf.Fpdf) (js.Value, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := pdf.Output(&buf); err != nil {
|
||||
@@ -83,10 +88,11 @@ func pdfToJs(pdf *fpdf.Fpdf) (js.Value, error) {
|
||||
return ta, nil
|
||||
}
|
||||
|
||||
// generateCover is the JS exposed entrypoint to generate a 2 page pdf with both the front and back cover
|
||||
func generateCover(this js.Value, args []js.Value) interface{} {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("recovered", r)
|
||||
fmt.Printf("recovered from critical error: %+v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -119,7 +125,7 @@ func generateCover(this js.Value, args []js.Value) interface{} {
|
||||
func generateSplitCover(this js.Value, args []js.Value) interface{} {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("recovered", r)
|
||||
fmt.Printf("recovered from critical error: %+v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -2,20 +2,24 @@ package covergen
|
||||
|
||||
import "github.com/go-pdf/fpdf"
|
||||
|
||||
// Color represents a simple RGB color
|
||||
type Color struct {
|
||||
R uint8
|
||||
G uint8
|
||||
B uint8
|
||||
}
|
||||
|
||||
// Text sets the text color of the given PDF to the called-on color instance
|
||||
func (c Color) Text(pdf *fpdf.Fpdf) {
|
||||
pdf.SetTextColor(int(c.R), int(c.G), int(c.B))
|
||||
}
|
||||
|
||||
// Draw sets the draw color of the given PDF to the called-on color instance
|
||||
func (c Color) Draw(pdf *fpdf.Fpdf) {
|
||||
pdf.SetDrawColor(int(c.R), int(c.G), int(c.B))
|
||||
}
|
||||
|
||||
// Fill sets the fill color of the given PDF to the called-on color instance
|
||||
func (c Color) Fill(pdf *fpdf.Fpdf) {
|
||||
pdf.SetFillColor(int(c.R), int(c.G), int(c.B))
|
||||
}
|
||||
|
||||
@@ -8,16 +8,32 @@ import (
|
||||
"github.com/go-pdf/fpdf"
|
||||
)
|
||||
|
||||
// page margins
|
||||
const margin = 20
|
||||
const miwebbLogoName = "miwebbLogo"
|
||||
const miwebbLogoPath = "miwebb.white.png"
|
||||
const logoScale = 0.6
|
||||
|
||||
const gridXTicks = 10
|
||||
const gridYTicks = 6
|
||||
const gridMargin = 5
|
||||
const lineHeight = 1.2
|
||||
const maxFontSize = 100
|
||||
// constants for the logo
|
||||
const (
|
||||
// pdf internal name to reference the logo
|
||||
logoName = "miwebbLogo"
|
||||
// this should match
|
||||
logoPath = "miwebb.white.png"
|
||||
// Scale-up / down factor for the logo
|
||||
logoScale = 0.6
|
||||
)
|
||||
|
||||
// constants for the grid
|
||||
const (
|
||||
// Number of lines on the X axis of the grid behind the customer name
|
||||
gridXTicks = 10
|
||||
// Number of lines on the Y axis of the grid
|
||||
gridYTicks = 6
|
||||
// Internal padding for the text inside the grid, in mm
|
||||
gridPadding = 5
|
||||
// default line height used for the grid
|
||||
lineHeight = 1.2
|
||||
// maximum font size for the grid, in pt
|
||||
maxFontSize = 100
|
||||
)
|
||||
|
||||
var bgColor = Color{25, 25, 25}
|
||||
var textColor = Color{255, 255, 255}
|
||||
@@ -27,12 +43,18 @@ var gridNoduleColor = Color{255, 255, 255}
|
||||
//go:embed miwebb.white.png font_embed
|
||||
var fs embed.FS
|
||||
|
||||
const fontsEmbedPrefix = "font_embed/"
|
||||
const (
|
||||
fontsEmbedPrefix = "font_embed/"
|
||||
fontLight = "Mark Light"
|
||||
fontMedium = "Mark Medium"
|
||||
)
|
||||
|
||||
// Copied from fPDF's source, can't get it dynamically
|
||||
// constant for mm is just to ensure you don't change it without noting the scale factor that depends on it
|
||||
const scaleUnit = "mm"
|
||||
const scaleFactor = 72.0 / 25.4
|
||||
const (
|
||||
// Copied from FPDF's source, can't get it dynamically
|
||||
// constant for mm is just to ensure you don't change it without noting the scale factor that depends on it
|
||||
scaleUnit = "mm"
|
||||
scaleFactor = 72.0 / 25.4
|
||||
)
|
||||
|
||||
type CoverSettings struct {
|
||||
Number string
|
||||
@@ -42,12 +64,16 @@ type CoverSettings struct {
|
||||
HLColor Color
|
||||
}
|
||||
|
||||
// GenerateInvoice generates a fpdf.Fpdf that contains two pages, the first being the front page, (see GenerateFrontCover)
|
||||
// and the second page being the closing page (see GenerateBackCover)
|
||||
func GenerateInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
|
||||
pdf, err := GenerateFrontCover(settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generating the back cover from the front cover is just 'drawing the customer on a new page'
|
||||
// as all the other things are automatically done using the page setup handlers
|
||||
pdf.AddPage()
|
||||
drawCustomerName(pdf, settings)
|
||||
|
||||
@@ -58,6 +84,8 @@ func GenerateInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
|
||||
return pdf, nil
|
||||
}
|
||||
|
||||
// GenerateFrontCover generates a fpdf.Fpdf representing the front cover of the invoice, containing the customer name,
|
||||
// the link to the MiWebb website, and the invoice number.
|
||||
func GenerateFrontCover(settings CoverSettings) (*fpdf.Fpdf, error) {
|
||||
pdf, err := generateBaseInvoice(settings)
|
||||
if err != nil {
|
||||
@@ -74,6 +102,7 @@ func GenerateFrontCover(settings CoverSettings) (*fpdf.Fpdf, error) {
|
||||
return pdf, nil
|
||||
}
|
||||
|
||||
// GenerateBackCover generates a very plain cover containing just the customer name
|
||||
func GenerateBackCover(settings CoverSettings) (*fpdf.Fpdf, error) {
|
||||
pdf, err := generateBaseInvoice(settings)
|
||||
if err != nil {
|
||||
@@ -83,6 +112,12 @@ func GenerateBackCover(settings CoverSettings) (*fpdf.Fpdf, error) {
|
||||
return pdf, err
|
||||
}
|
||||
|
||||
// generateBaseInvoice takes care of setting up the initial fpdf.Fpdf instance by:
|
||||
// - registering all the fonts,
|
||||
// - registering the logo
|
||||
// - setting up the page settings (margins)
|
||||
// - setting up the header func that renders the logo on every page
|
||||
// After the initialization it will also render the customer name on the first page
|
||||
func generateBaseInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
|
||||
pdf := fpdf.New("P", scaleUnit, "A4", "")
|
||||
|
||||
@@ -92,7 +127,7 @@ func generateBaseInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
|
||||
if err := addEmbeddedFont(pdf, "Mark Light", "", "Mark-Light"); err != nil {
|
||||
return nil, fmt.Errorf("error adding font: %w", err)
|
||||
}
|
||||
miwebbLogo, err := addEmbeddedLogo(pdf, miwebbLogoName, "miwebb.white.png")
|
||||
logo, err := addEmbeddedLogo(pdf, logoName, logoPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error adding logo: %w", err)
|
||||
}
|
||||
@@ -102,9 +137,11 @@ func generateBaseInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
|
||||
pdf.SetAutoPageBreak(false, 0)
|
||||
|
||||
pdf.SetHeaderFunc(func() {
|
||||
// Fills the entire page with the background color
|
||||
bgColor.Fill(pdf)
|
||||
pdf.Rect(0, 0, pageWidth, pageHeight, "F")
|
||||
pdf.Image(miwebbLogoName, margin-5, margin, miwebbLogo.Width()*logoScale, miwebbLogo.Height()*logoScale, false, "", 0, "")
|
||||
// Draws the MiWebb logo
|
||||
pdf.Image(logoName, margin-5, margin, logo.Width()*logoScale, logo.Height()*logoScale, false, "", 0, "")
|
||||
})
|
||||
|
||||
pdf.AddPage()
|
||||
@@ -117,11 +154,14 @@ func generateBaseInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
|
||||
return pdf, nil
|
||||
}
|
||||
|
||||
// drawInvoiceNumber does what it says on the tin, and draws the number prefix and number on the invoice in the bottom-left corner
|
||||
// this moves the cursor to the top right corner of the text
|
||||
func drawInvoiceNumber(pdf *fpdf.Fpdf, settings CoverSettings) {
|
||||
_, pageHeight := pdf.GetPageSize()
|
||||
|
||||
// Bottom left corner of the page, given margin
|
||||
pdf.MoveTo(margin, pageHeight-margin)
|
||||
pdf.SetFont("Mark Light", "", 9)
|
||||
pdf.SetFont(fontLight, "", 9)
|
||||
textColor.Text(pdf)
|
||||
|
||||
pdf.TransformBegin()
|
||||
@@ -130,37 +170,44 @@ func drawInvoiceNumber(pdf *fpdf.Fpdf, settings CoverSettings) {
|
||||
pdf.CellFormat(0, 1, invoiceText, "", 0, "LT", false, 0, "")
|
||||
pdf.TransformEnd()
|
||||
|
||||
// Calculate where the text has gone, so we can figure out the position of the line behind it
|
||||
_, h := pdf.GetFontSize()
|
||||
// X is 'margin + half text height'
|
||||
// X is 'margin + half text height' (so centered on the line height)
|
||||
x := float64(margin) + h/2
|
||||
// Y is 'where the text ended' + some gap
|
||||
// Y is 'where the text ended' + some gap (vertically)
|
||||
y := pageHeight - margin - pdf.GetStringWidth(invoiceText) - 5
|
||||
|
||||
textColor.Draw(pdf)
|
||||
pdf.SetLineWidth(0.01)
|
||||
// the 10 here is the length of the line segment, chosen arbitrarily
|
||||
pdf.Line(x, y, x, y-10)
|
||||
}
|
||||
|
||||
// drawMiwebbLink draws a link to the MiWebb site in the bottom right corner of the page
|
||||
func drawMiwebbLink(pdf *fpdf.Fpdf) {
|
||||
pageWidth, pageHeight := pdf.GetPageSize()
|
||||
pdf.MoveTo(pageWidth-margin, pageHeight-margin)
|
||||
|
||||
pdf.SetFont("Mark Light", "", 9)
|
||||
pdf.SetFont(fontLight, "", 9)
|
||||
textColor.Text(pdf)
|
||||
pdf.CellFormat(0, 0, "WWW.MIWEBB.COM", "", 0, "RB", false, 0, "https://www.miwebb.com")
|
||||
}
|
||||
|
||||
// drawCustomerName renders the customer name and the grid underneath it, fully centered on the page
|
||||
func drawCustomerName(pdf *fpdf.Fpdf, settings CoverSettings) {
|
||||
ml, _, mr, _ := pdf.GetMargins()
|
||||
pw, ph := pdf.GetPageSize()
|
||||
|
||||
// Calculate the width and height of the grid, as we'll need it to position it
|
||||
width := pw - ml - mr
|
||||
// ticks are square, so the height is fully determined by the width of each grid segment
|
||||
height := (width/gridXTicks - 1) * gridYTicks
|
||||
text := strings.ToUpper(settings.CustomerName)
|
||||
|
||||
// Draw the grid with a weird gray color
|
||||
gridColor.Draw(pdf)
|
||||
pdf.SetLineWidth(0.01)
|
||||
// Calculates the top left corner of the grid
|
||||
gridX, gridY := ml, (ph/2)-(height/2)
|
||||
drawGrid(pdf, gridX, gridY, width, height, gridXTicks, gridYTicks)
|
||||
|
||||
@@ -172,22 +219,25 @@ func drawCustomerName(pdf *fpdf.Fpdf, settings CoverSettings) {
|
||||
1.2, gridNoduleColor,
|
||||
0.25, settings.HLColor)
|
||||
|
||||
pdf.SetFont("Mark Medium", "", 10)
|
||||
pdf.SetFont(fontMedium, "", 10)
|
||||
// Estimate font size, and calculate line height immediately after
|
||||
pdf.SetFontSize(estimateFontSize(pdf, text, width, height, gridMargin))
|
||||
_, sz := pdf.GetFontSize()
|
||||
sz *= lineHeight
|
||||
pdf.SetFontSize(estimateFontSize(pdf, text, width, height, gridPadding))
|
||||
_, fontHeight := pdf.GetFontSize()
|
||||
fontHeight *= lineHeight
|
||||
|
||||
_, textHeight := stringSize(pdf, text, sz, 0)
|
||||
// Determine the height of the full string (multiline aware)
|
||||
_, textHeight := stringSize(pdf, text, fontHeight, 0)
|
||||
// Centered vertically:
|
||||
// x = <gridX> + <margin>
|
||||
// y = <gridY> + <gridH>/2 - textHeight/2
|
||||
|
||||
pdf.MoveTo(ml+gridMargin, ph/2-(textHeight/2))
|
||||
pdf.MoveTo(ml+gridPadding, ph/2-(textHeight/2))
|
||||
textColor.Text(pdf)
|
||||
pdf.MultiCell(width-2*gridMargin, sz, text, "", "ML", false)
|
||||
// finally, we can draw the text
|
||||
pdf.MultiCell(width-2*gridPadding, fontHeight, text, "", "ML", false)
|
||||
}
|
||||
|
||||
// drawGrid ... draws a grid at the given x,y with given width and height,
|
||||
// xs ticks on the x-axis, and ys ticks on the y-axis.
|
||||
func drawGrid(pdf *fpdf.Fpdf, x, y, w, h float64, xs, ys int) {
|
||||
// Horizontal lines
|
||||
for i := 0; i < ys; i++ {
|
||||
@@ -195,13 +245,14 @@ func drawGrid(pdf *fpdf.Fpdf, x, y, w, h float64, xs, ys int) {
|
||||
pdf.Line(x, yOff, x+w, yOff)
|
||||
}
|
||||
|
||||
// Vertical line(pw/2)-(width/2)s
|
||||
// Vertical lines
|
||||
for i := 0; i < xs; i++ {
|
||||
xOff := x + (w/float64(xs-1))*float64(i)
|
||||
pdf.Line(xOff, y, xOff, y+h)
|
||||
}
|
||||
}
|
||||
|
||||
// drawNoduleLine draws the colored line on the right side of the grid, with little squares at each end
|
||||
func drawNoduleLine(pdf *fpdf.Fpdf, x1, y1, x2, y2, squareSize float64, squareColor Color, lineWidth float64, lineColor Color) {
|
||||
lineColor.Draw(pdf)
|
||||
pdf.SetLineWidth(lineWidth)
|
||||
@@ -212,14 +263,20 @@ func drawNoduleLine(pdf *fpdf.Fpdf, x1, y1, x2, y2, squareSize float64, squareCo
|
||||
pdf.Rect(x2-(squareSize/2), y2-(squareSize/2), squareSize, squareSize, "F")
|
||||
}
|
||||
|
||||
// estimateFontSize attempts to determine the optimal font size for a given piece of text in a given container (with margin)
|
||||
// this works by progressively making the font size larger and larger (up to maxFontSize) and calculating both the
|
||||
// width and height at each iteration.
|
||||
// It returns the biggest font size that still fits in the container (as in, 1pt larger will no longer fit, unless
|
||||
// the returned font size is equal to maxFontSize)
|
||||
func estimateFontSize(pdf *fpdf.Fpdf, text string, containerWidth, containerHeight, margin float64) float64 {
|
||||
// Margin is on both sides haha, but I add an extra just for 'safety'
|
||||
// Margin is on both sides haha, but I add an extra just for 'safety' on the right side
|
||||
maxWidth := containerWidth - 3*margin
|
||||
maxHeight := containerHeight - 3*margin
|
||||
|
||||
// Probably smallest we can start with
|
||||
for fontSize := 10.; fontSize <= maxFontSize; fontSize += 1 {
|
||||
width, height := stringSize(pdf, text, (fontSize/scaleFactor)*lineHeight, fontSize)
|
||||
// Return the last font size if this one overflows
|
||||
if width >= maxWidth || height >= maxHeight {
|
||||
return fontSize - 1
|
||||
}
|
||||
@@ -228,6 +285,9 @@ func estimateFontSize(pdf *fpdf.Fpdf, text string, containerWidth, containerHeig
|
||||
return maxFontSize
|
||||
}
|
||||
|
||||
// stringSize builds on fpdf's `GetStringSymbolWidth` to return both the width of the largest line (split on `\n`)
|
||||
// and the height of all lines put on top of each other.
|
||||
// this effectively calculates the bounding box of the given text fragment
|
||||
func stringSize(pdf *fpdf.Fpdf, text string, lineHeight, fontSizePt float64) (width, height float64) {
|
||||
if fontSizePt == 0 {
|
||||
fontSizePt, _ = pdf.GetFontSize()
|
||||
@@ -245,6 +305,8 @@ func stringSize(pdf *fpdf.Fpdf, text string, lineHeight, fontSizePt float64) (wi
|
||||
return
|
||||
}
|
||||
|
||||
// addEmbeddedFont takes a reference a `go:embed`ed font and embeds it into the pdf so people
|
||||
// will actually see the font if they don't magically have the specific font installed
|
||||
func addEmbeddedFont(pdf *fpdf.Fpdf, family, style, path string) error {
|
||||
jsonData, err := fs.ReadFile(fontsEmbedPrefix + path + ".json")
|
||||
if err != nil {
|
||||
@@ -263,6 +325,8 @@ func addEmbeddedFont(pdf *fpdf.Fpdf, family, style, path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// addEmbeddedLogo reads a `go:embed`ed image and adds it to the pdf, returning the handle for you to use
|
||||
// to add it to the page
|
||||
func addEmbeddedLogo(pdf *fpdf.Fpdf, name, path string) (*fpdf.ImageInfoType, error) {
|
||||
reader, err := fs.Open(path)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user