Compare commits

8 Commits

Author SHA1 Message Date
f7dd929bdd CI 1 2022-04-20 10:34:38 +02:00
30aba3c871 Fix favicon path & title
All checks were successful
continuous-integration/drone/push Build is passing
2022-04-19 22:08:42 +02:00
cb8400eea3 Cleanup frontend
Some checks reported errors
continuous-integration/drone/push Build was killed
2022-04-19 22:07:43 +02:00
63875a6f59 Brush up the go code 2022-04-19 21:22:59 +02:00
b88208ba3d ci: revert trigger
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-22 20:03:24 +01:00
5ceadd09dd Fix asset path in navbar
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-22 20:01:03 +01:00
7387f33678 fix basepath
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-22 19:57:36 +01:00
f4c704740a ci: uncommented wrong pipeline
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-22 19:49:41 +01:00
25 changed files with 4747 additions and 3816 deletions

View File

@@ -3,9 +3,9 @@ kind: pipeline
type: kubernetes type: kubernetes
name: build-clis name: build-clis
#trigger: trigger:
# branch: branch:
# - main - main
steps: steps:
- name: build - name: build
@@ -62,9 +62,9 @@ steps:
path: /wasm path: /wasm
commands: commands:
- cd frontend - cd frontend
- yarn install - npm install
- "cp /wasm/* public/" - "cp /wasm/* public/"
- yarn build - npm run build
- name: upload - name: upload
image: plugins/s3 image: plugins/s3
settings: settings:

52
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,52 @@
stages:
- build
- deploy
# cache using branch name
# https://gitlab.com/help/ci/caching/index.md
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules
build:blobs:
image: golang:1.17
stage: build
script:
- mkdir /blobs
- make wasm
- make build-cross-clis
- mv covergen.wasm dist /blobs
artifacts:
paths:
- /blobs
expire_in: 30 days
build:frontend:
image: node:16
stage: build
needs:
- build:blobs
before_script:
- corepack npm install --immutable
script:
- cd frontend
- mv /blobs/* public
- corepack npm run build
artifacts:
paths:
- dist/
expire_in: 30 days
deploy:
stage: deploy
image: alpine
needs:
- build:frontend
before_script:
- apk add lftp
script:
- lftp "$DEPLOY_USER_PASS@vps0.miwebb.com:/" -e "mirror -R dist/ .; quit"
environment:
name: live
url: https://covergen.miwebb.dev

View File

@@ -1,9 +1,29 @@
.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 .PHONY: build-cross-clis
build-cross-clis: build-cross-clis: ## Builds the CLIs for all supported platforms
rm -rf ./dist && mkdir ./dist rm -rf ./dist && mkdir ./dist
GOOS=linux GOARCH=amd64 go build -o dist/covergen.linux-amd64 ./cmd/covergen 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 GOOS=darwin GOARCH=amd64 go build -o dist/covergen.darwin-amd64 ./cmd/covergen
.PHONY: wasm .PHONY: wasm-frontend
wasm: wasm-frontend: wasm
GOOS=js GOARCH=wasm go build -o covergen.wasm ./cmd/wasm/main.go cp covergen.wasm frontend/public/covergen.wasm
.PHONY: clis-frontend
clis-frontend: build-cross-clis
cp -r dist/ frontend/public/
.PHONY: prepare-frontend
prepare-frontend: wasm-frontend clis-frontend ### Prepares all main-repo requirements for the frontend

View File

@@ -2,19 +2,46 @@
Just generates some magic PDF invoice covers using straight up dark magic. 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 # Builds the WASM blob
make wasm
```
$ go build -o covergen ./cmd/covergen
``` ```
## It has wasm! ## Custom font notes
``` The PDF uses the font `Mark`, which isn't a commonly available font.
$ make wasm 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.

View File

@@ -13,6 +13,7 @@ import (
"covergen/pkg/covergen" "covergen/pkg/covergen"
) )
// List of colors used for picking a (random) color in the cli
var colors = []string{"red", "blue", "yellow", "green"} var colors = []string{"red", "blue", "yellow", "green"}
var colorMap = map[string]covergen.Color{ var colorMap = map[string]covergen.Color{
"red": {255, 85, 0}, "red": {255, 85, 0},
@@ -69,6 +70,7 @@ func main() {
chosenColor = colorMap[*color] chosenColor = colorMap[*color]
} }
// Newlines in CLI arguments are given escaped. Unescape them to proper newlines
*customer = strings.ReplaceAll(*customer, "\\n", "\n") *customer = strings.ReplaceAll(*customer, "\\n", "\n")
settings := covergen.CoverSettings{ settings := covergen.CoverSettings{
Number: *number, Number: *number,
@@ -77,6 +79,7 @@ func main() {
HLColor: chosenColor, HLColor: chosenColor,
} }
// If output2 (back page) is not set, generate a combined PDF
if *output2 == "" { if *output2 == "" {
pdf, err := covergen.GenerateInvoice(settings) pdf, err := covergen.GenerateInvoice(settings)
@@ -88,6 +91,7 @@ func main() {
log.Fatal().Err(err).Msg("Failed to write invoice to disk") log.Fatal().Err(err).Msg("Failed to write invoice to disk")
} }
} else { } else {
// Otherwise, generate the front and back separately
front, err := covergen.GenerateFrontCover(settings) front, err := covergen.GenerateFrontCover(settings)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("Failed to render front") log.Fatal().Err(err).Msg("Failed to render front")

View File

@@ -1,14 +0,0 @@
package main
import (
"fmt"
"net/http"
)
func main() {
err := http.ListenAndServe(":9090", http.FileServer(http.Dir("assets")))
if err != nil {
fmt.Println("Failed to start server", err)
return
}
}

View File

@@ -12,6 +12,7 @@ import (
"covergen/pkg/covergen" "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 jsmap = map[string]interface{}
type covergenArgs struct { type covergenArgs struct {
@@ -21,6 +22,8 @@ type covergenArgs struct {
HLColor string `jsref:"hlColor"` 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) { func parseHexColor(s string) (c covergen.Color, err error) {
switch len(s) { switch len(s) {
case 7: case 7:
@@ -33,11 +36,12 @@ func parseHexColor(s string) (c covergen.Color, err error) {
c.B *= 17 c.B *= 17
default: default:
err = fmt.Errorf("invalid length, must be 7 or 4") err = fmt.Errorf("invalid length, must be 7 or 4")
} }
return 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) { func settingsFromValue(arg js.Value) (*covergen.CoverSettings, error) {
settings := covergenArgs{ settings := covergenArgs{
NumberPrefix: "offerte", NumberPrefix: "offerte",
@@ -71,6 +75,7 @@ func settingsFromValue(arg js.Value) (*covergen.CoverSettings, error) {
}, nil }, 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) { func pdfToJs(pdf *fpdf.Fpdf) (js.Value, error) {
var buf bytes.Buffer var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil { if err := pdf.Output(&buf); err != nil {
@@ -83,10 +88,11 @@ func pdfToJs(pdf *fpdf.Fpdf) (js.Value, error) {
return ta, nil 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{} { func generateCover(this js.Value, args []js.Value) interface{} {
defer func() { defer func() {
if r := recover(); r != nil { 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{} { func generateSplitCover(this js.Value, args []js.Value) interface{} {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
fmt.Println("recovered", r) fmt.Printf("recovered from critical error: %+v", r)
} }
}() }()

2
frontend/.gitignore vendored
View File

@@ -4,5 +4,5 @@ dist
dist-ssr dist-ssr
*.local *.local
.yarn/install-state.gz
public/*.wasm public/*.wasm
public/dist

View File

@@ -1,11 +1,30 @@
# Vue 3 + Typescript + Vite # Covergen Frontend
This template should help get you started developing with Vue 3 and Typescript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more. ## Quick startup
## Recommended IDE Setup For the frontend to function, the `covergen` wasm blob needs to be made available.
This is not present in-tree but is easily prepared by running `make prepare-frontend` in the parent directory
(i.e. the repo root).
This will also do prepare the cross-cli's, but those are optional (you can run `make wasm-frontend` to just do the wasm).
- [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) After this the usual `vite` / `vue3` commands apply:
## Type Support For `.vue` Imports in TS ```shell
npm run dev
```
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's `.vue` type support plugin by running `Volar: Switch TS Plugin on/off` from VSCode command palette. To prepare a proper final build:
```shell
npm run build
```
## A note on `wasm_exec.js`
Go WASM requires a 'helper' library, `wasm_exec.js` to help with the bridging memory from Go <-> JS.
This file is **different for each Go (1.XX) version**. The version bundled in-tree is for Go 1.16.
If you run a different version, you might need to copy it from your local golang dist folder,
usually `/usr/local/go/misc/wasm/wasm_exec.js`.
If your distribution doesn't provide this file (e.g. Fedora) you can just copy it from the go main repo by going to
[https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.js](https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.js)
and using the GitHub branch selector to select the `release-branch.go1.XX` that matches your version of go (run `go version` to find out).

View File

@@ -2,11 +2,11 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"> <link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<title>NVLS | Covergen</title> <title>MiWebb | Covergen</title>
</head> </head>
<body class="bg-gray-200"> <body class="bg-gray-200">
<div id="app"></div> <div id="app"></div>

4378
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,33 +5,33 @@
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"preview": "vite preview", "preview": "vite preview",
"lint": "yarn lint:ts && yarn lint:style", "lint": "npm run lint:ts && npm run lint:style",
"lint:ts": "vue-tsc --noEmit", "lint:ts": "vue-tsc --noEmit",
"lint:style": "eslint --ext .ts,.vue --ignore-path .gitignore .", "lint:style": "eslint --ext .ts,.vue --ignore-path .gitignore .",
"lint:fix": "yarn lint:style --fix" "lint:fix": "npm run lint:style --fix"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/forms": "^0.4.0", "@tailwindcss/forms": "^0.5.0",
"pinia": "^2.0.9", "pinia": "^2.0.13",
"tailwindcss": "^3.0.15", "tailwindcss": "^3.0.24",
"vue": "^3.2.25" "vue": "^3.2.33"
}, },
"devDependencies": { "devDependencies": {
"@types/golang-wasm-exec": "^1.15.0", "@types/golang-wasm-exec": "^1.15.0",
"@types/node": "^17.0.10", "@types/node": "^17.0.25",
"@typescript-eslint/eslint-plugin": "^5.10.0", "@typescript-eslint/eslint-plugin": "^5.20.0",
"@typescript-eslint/parser": "^5.10.0", "@typescript-eslint/parser": "^5.20.0",
"@vitejs/plugin-vue": "^2.0.0", "@vitejs/plugin-vue": "^2.3.1",
"@vue/eslint-config-typescript": "^10.0.0", "@vue/eslint-config-typescript": "^10.0.0",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.4",
"eslint": "^8.7.0", "eslint": "^8.13.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier-vue": "^3.1.0", "eslint-plugin-prettier-vue": "^3.1.0",
"eslint-plugin-vue": "^8.3.0", "eslint-plugin-vue": "^8.6.0",
"postcss": "^8.4.5", "postcss": "^8.4.12",
"prettier": "^2.5.1", "prettier": "^2.6.2",
"typescript": "^4.4.4", "typescript": "^4.6.3",
"vite": "^2.7.2", "vite": "^2.9.5",
"vue-tsc": "^0.29.8" "vue-tsc": "^0.34.7"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,3 @@
<svg xml:space="preserve" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<path d="m86.915 15.448-28.008 100.83-1.937 6.98h15.472l22.56-80.772 23.683 85.512-23.683 85.51-22.613-80.953H56.935l1.987 7.16 27.992 100.83H102.6l25.394-92.088 25.04 90.807.354 1.282h15.683l28.008-100.83 1.988-7.161h-15.47l-22.614 80.953-23.681-85.51 23.68-85.512 22.563 80.773h15.454l-1.937-6.98-27.992-100.83h-15.683l-25.394 92.088L102.95 16.73l-.353-1.282H86.913z"/>
</svg>

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -1,24 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import Logo from '@/icons/Logo.vue';
const downloads = [ const downloads = [
{ name: 'Linux', link: './assets/covergen.linux-amd64' }, { name: 'Linux', link: './dist/covergen.linux-amd64' },
{ name: 'Darwin', link: './assets/covergen.darwin-amd64' }, { name: 'Darwin', link: './dist/covergen.darwin-amd64' },
]; ];
</script> </script>
<template> <template>
<div class="w-full mx-auto px-8"> <div class="w-full mx-auto px-8">
<div class="relative flex items-center justify-between h-16"> <div class="relative flex items-center justify-between h-16">
<div class="flex-1 flex items-stretch justify-between"> <div class="flex-1 flex items-stretch justify-between">
<div class="flex-shrink-0 flex items-center"> <div class="flex-shrink-0 flex items-center">
<img <Logo class="h-12" />
src="../assets/logo.png"
alt="covergen logo"
class="h-8 w-auto mr-2"
/>
<h1>CoverGen</h1> <h1>CoverGen</h1>
</div> </div>
<div class="ml-6"> <div class="ml-6">
<div class="flex space-x-4 items-center text-sm font-medium"> <div class="flex space-x-4 items-center text-sm font-medium h-full">
<span class="text-base"> Download me: </span> <span class="text-base"> Download me: </span>
<a <a
v-for="dl in downloads" v-for="dl in downloads"

View File

@@ -5,6 +5,7 @@ import { storeToRefs } from 'pinia';
const store = useCover(); const store = useCover();
const { frontUri, backUri } = storeToRefs(store); const { frontUri, backUri } = storeToRefs(store);
</script> </script>
<template> <template>
<div class="flex flex-row grow"> <div class="flex flex-row grow">
<div class="grow p-4"> <div class="grow p-4">

View File

@@ -0,0 +1,48 @@
<template>
<svg
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 400 180"
fill="currentColor"
>
<path
d="M144.452 125.98c1.289-10.15 10.953-18.66 21.799-18.66 11.49 0 21.477 7.689 23.087 18.66Zm51.543 3.485c0-15.89-12.886-28.295-29.638-28.295-16.32 0-28.992 13.121-28.992 28.397 0 15.377 13.207 27.782 29.422 27.782 11.598 0 22.121-6.46 26.846-16.506h-7.516c-4.081 6.766-11.383 10.457-19.544 10.457-11.598 0-21.477-8.713-22.121-19.785h51.543z"
/>
<path
d="M145.748 124.918h42.28c-2.03-9.655-10.986-16.541-21.774-16.541-9.757 0-18.698 7.317-20.506 16.54m44.868 2.118h-47.412l.151-1.187c1.37-10.8 11.644-19.588 22.9-19.588 12.348 0 22.52 8.23 24.185 19.57zm-24.254-24.81c-15.376 0-27.885 12.265-27.885 27.34 0 14.735 12.702 26.723 28.314 26.723 10.57 0 20.23-5.595 25.073-14.388h-5.112c-4.265 6.654-11.581 10.458-20.176 10.458-5.906 0-11.562-2.14-15.924-6.027-4.383-3.904-6.977-9.146-7.303-14.76l-.066-1.118h51.607v-.991c0-7.415-2.917-14.29-8.213-19.36-5.307-5.079-12.522-7.877-20.315-7.877m.43 56.18c-16.836 0-30.533-12.938-30.533-28.84 0-16.243 13.505-29.457 30.103-29.457 17.24 0 30.747 12.893 30.747 29.354v3.108h-51.447c.552 4.643 2.834 8.944 6.503 12.213 3.95 3.519 9.069 5.456 14.411 5.456 8.005 0 14.779-3.618 18.583-9.927l.32-.53h9.871l-.7 1.493c-4.895 10.406-15.83 17.13-27.859 17.13M240.365 151.13c-12.705 0-23.042-9.867-23.042-21.997 0-12.13 10.337-21.999 23.042-21.999 12.705 0 23.042 9.869 23.042 21.999 0 12.13-10.337 21.998-23.042 21.998m0-49.996c-9.34 0-17.668 4.195-23.042 10.71v-29.13h-6.284v47.944h.045c.833 14.731 13.648 26.471 29.28 26.471 16.171 0 29.327-12.56 29.327-27.997 0-15.438-13.156-27.998-29.326-27.998"
/>
<path
d="M240.365 108.193c-12.094 0-21.933 9.393-21.933 20.939s9.839 20.94 21.933 20.94 21.932-9.394 21.932-20.94c0-11.546-9.838-20.939-21.932-20.939m0 43.997c-13.318 0-24.151-10.344-24.151-23.058 0-12.713 10.833-23.056 24.15-23.056 13.318 0 24.152 10.343 24.152 23.056 0 12.714-10.834 23.058-24.151 23.058m-28.217-22.36.043.771c.808 14.283 13.184 25.47 28.174 25.47 15.558 0 28.217-12.084 28.217-26.939 0-14.854-12.659-26.94-28.217-26.94-8.687 0-16.767 3.757-22.171 10.307l-1.98 2.401V83.772h-4.066zm28.217 28.359c-15.817 0-28.934-11.549-30.314-26.472h-.121V81.655h8.502v27.358c5.732-5.713 13.571-8.938 21.933-8.938 16.782 0 30.436 13.035 30.436 29.057s-13.654 29.057-30.436 29.057M61.529 21.606l-17.78 61.11-1.23 4.231h9.822l14.322-48.953L81.697 89.82l-15.034 51.824L52.308 92.58h-9.81l1.261 4.34 17.77 61.11h9.957l16.12-55.811 15.895 55.034.226.778h9.955l17.78-61.111 1.262-4.34h-9.82l-14.356 49.062L93.515 89.82l15.033-51.825 14.323 48.953h9.81l-1.23-4.23-17.77-61.111h-9.955l-16.12 55.811-15.897-55.034-.224-.777h-9.957zM169.784 86.007H163.5V40.574h6.284z"
/>
<path
d="M164.608 84.948h4.067V41.63h-4.067zm6.284 2.118h-8.502V39.514h8.502z"
/>
<path d="M169.784 46.568h-16.286v-6h16.286z" />
<path
d="M154.604 45.517h14.067v-3.882h-14.067Zm16.286 2.117h-18.505v-8.117h18.505zM169.784 28.654H163.5v-6h6.284z"
/>
<path
d="M164.608 27.595h4.067v-3.883h-4.067zm6.284 2.117h-8.502v-8.117h8.502zM356.393 157.132h-6.285v-6h6.285z"
/>
<path
d="M351.218 156.073h4.066v-3.883h-4.066zm6.285 2.117H349v-8.117h8.503zM217.325 155.916h-6.284v-29.934h6.284z"
/>
<path
d="M212.15 154.857h4.066V127.04h-4.066zm6.284 2.118h-8.502v-32.053h8.502z"
/>
<g>
<path
d="M313.766 151.13c-12.705 0-23.041-9.867-23.041-21.997 0-12.13 10.336-21.999 23.041-21.999 12.706 0 23.042 9.869 23.042 21.999 0 12.13-10.336 21.998-23.042 21.998m0-49.996c-9.34 0-17.668 4.195-23.041 10.71v-29.13h-6.285v47.944h.045c.834 14.731 13.648 26.471 29.281 26.471 16.17 0 29.326-12.56 29.326-27.997 0-15.438-13.155-27.998-29.326-27.998"
/>
</g>
<g>
<path
d="M313.766 108.193c-12.094 0-21.932 9.393-21.932 20.939s9.838 20.94 21.932 20.94 21.934-9.394 21.934-20.94c0-11.546-9.84-20.939-21.934-20.939m0 43.997c-13.316 0-24.15-10.344-24.15-23.058 0-12.713 10.834-23.056 24.15-23.056 13.318 0 24.152 10.343 24.152 23.056 0 12.714-10.834 23.058-24.152 23.058m-28.216-22.36.042.771c.808 14.283 13.184 25.47 28.174 25.47 15.56 0 28.218-12.084 28.218-26.939 0-14.854-12.658-26.94-28.218-26.94-8.686 0-16.767 3.757-22.17 10.307l-1.98 2.401V83.772h-4.066zm28.216 28.359c-15.817 0-28.934-11.549-30.314-26.472h-.12V81.655h8.502v27.358c5.731-5.713 13.57-8.938 21.932-8.938 16.782 0 30.436 13.035 30.436 29.057s-13.654 29.057-30.436 29.057"
/>
</g>
<path d="M290.716 155.918h-6.285v-29.935h6.285z" />
<path
d="M285.538 154.856h4.067v-27.818h-4.067Zm6.284 2.118h-8.503v-32.053h8.503z"
/>
</svg>
</template>

View File

@@ -19,6 +19,7 @@ declare global {
} }
} }
// Load and run the WASM blob
const go = new Go(); const go = new Go();
WebAssembly.instantiateStreaming(fetch('covergen.wasm'), go.importObject).then( WebAssembly.instantiateStreaming(fetch('covergen.wasm'), go.importObject).then(
(result) => { (result) => {
@@ -26,6 +27,13 @@ WebAssembly.instantiateStreaming(fetch('covergen.wasm'), go.importObject).then(
} }
); );
/**
* generateCover generates a pdf containing both the front and back page for the invoice.
* If you want them separately, call generateSplitCover.
*
* @see generateSplitCover
* @param args
*/
export function generateCover(args: CoverArgs): File { export function generateCover(args: CoverArgs): File {
const result = window.generateCover(args); const result = window.generateCover(args);
if ('error' in result) { if ('error' in result) {
@@ -35,6 +43,11 @@ export function generateCover(args: CoverArgs): File {
return new File([result], 'cover.pdf', { type: 'application/pdf' }); return new File([result], 'cover.pdf', { type: 'application/pdf' });
} }
/**
* generateSplitCover generates two pdfs, one for the front cover, and one for the back
* @see generateCover
* @param args
*/
export function generateSplitCover(args: CoverArgs): { export function generateSplitCover(args: CoverArgs): {
front: File; front: File;
back: File; back: File;

View File

@@ -1,5 +1,11 @@
import { readonly, ref } from 'vue'; import { readonly, Ref, ref } from 'vue';
/**
* Random labels as a library function.
* Will return a random label from the given options.
* Calling the `update` function that is returned will pick a new label.
* @param options
*/
export function randomLabel(options: string[]) { export function randomLabel(options: string[]) {
const label = ref<string>(); const label = ref<string>();
const randomLabel = () => { const randomLabel = () => {
@@ -12,7 +18,11 @@ export function randomLabel(options: string[]) {
label.value = randomLabel(); label.value = randomLabel();
return { return {
label: readonly(label), // This type refinement is proper as the `undefined` value can never happen from this point onwards
update: () => (label.value = randomLabel()), label: readonly(label as Ref<string>),
// Updates the returned `label` with a new randomly chosen one
update() {
label.value = randomLabel();
},
}; };
} }

View File

@@ -9,10 +9,16 @@
"sourceMap": true, "sourceMap": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true,
"noUnusedLocals": true,
"lib": ["esnext", "dom"], "lib": ["esnext", "dom"],
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} }
}, },
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": [
"dist",
"node_modules"
]
} }

View File

@@ -3,11 +3,12 @@ import vue from '@vitejs/plugin-vue';
import { resolve } from 'path'; import { resolve } from 'path';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig(({ mode }) => ({
plugins: [vue()], plugins: [vue()],
base: mode === 'production' ? '/covergen/' : '/',
resolve: { resolve: {
alias: { alias: {
'@': resolve(__dirname, './src'), '@': resolve(__dirname, './src'),
}, },
}, },
}); }));

File diff suppressed because it is too large Load Diff

View File

@@ -2,20 +2,24 @@ package covergen
import "github.com/go-pdf/fpdf" import "github.com/go-pdf/fpdf"
// Color represents a simple RGB color
type Color struct { type Color struct {
R uint8 R uint8
G uint8 G uint8
B 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) { func (c Color) Text(pdf *fpdf.Fpdf) {
pdf.SetTextColor(int(c.R), int(c.G), int(c.B)) 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) { func (c Color) Draw(pdf *fpdf.Fpdf) {
pdf.SetDrawColor(int(c.R), int(c.G), int(c.B)) 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) { func (c Color) Fill(pdf *fpdf.Fpdf) {
pdf.SetFillColor(int(c.R), int(c.G), int(c.B)) pdf.SetFillColor(int(c.R), int(c.G), int(c.B))
} }

View File

@@ -8,16 +8,32 @@ import (
"github.com/go-pdf/fpdf" "github.com/go-pdf/fpdf"
) )
// page margins
const margin = 20 const margin = 20
const miwebbLogoName = "miwebbLogo"
const miwebbLogoPath = "miwebb.white.png"
const logoScale = 0.6
const gridXTicks = 10 // constants for the logo
const gridYTicks = 6 const (
const gridMargin = 5 // pdf internal name to reference the logo
const lineHeight = 1.2 logoName = "miwebbLogo"
const maxFontSize = 100 // 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 bgColor = Color{25, 25, 25}
var textColor = Color{255, 255, 255} var textColor = Color{255, 255, 255}
@@ -27,12 +43,18 @@ var gridNoduleColor = Color{255, 255, 255}
//go:embed miwebb.white.png font_embed //go:embed miwebb.white.png font_embed
var fs embed.FS 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 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 // constant for mm is just to ensure you don't change it without noting the scale factor that depends on it
const scaleUnit = "mm" scaleUnit = "mm"
const scaleFactor = 72.0 / 25.4 scaleFactor = 72.0 / 25.4
)
type CoverSettings struct { type CoverSettings struct {
Number string Number string
@@ -42,12 +64,16 @@ type CoverSettings struct {
HLColor Color 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) { func GenerateInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
pdf, err := GenerateFrontCover(settings) pdf, err := GenerateFrontCover(settings)
if err != nil { if err != nil {
return nil, err 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() pdf.AddPage()
drawCustomerName(pdf, settings) drawCustomerName(pdf, settings)
@@ -58,6 +84,8 @@ func GenerateInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
return pdf, nil 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) { func GenerateFrontCover(settings CoverSettings) (*fpdf.Fpdf, error) {
pdf, err := generateBaseInvoice(settings) pdf, err := generateBaseInvoice(settings)
if err != nil { if err != nil {
@@ -74,6 +102,7 @@ func GenerateFrontCover(settings CoverSettings) (*fpdf.Fpdf, error) {
return pdf, nil return pdf, nil
} }
// GenerateBackCover generates a very plain cover containing just the customer name
func GenerateBackCover(settings CoverSettings) (*fpdf.Fpdf, error) { func GenerateBackCover(settings CoverSettings) (*fpdf.Fpdf, error) {
pdf, err := generateBaseInvoice(settings) pdf, err := generateBaseInvoice(settings)
if err != nil { if err != nil {
@@ -83,6 +112,12 @@ func GenerateBackCover(settings CoverSettings) (*fpdf.Fpdf, error) {
return pdf, err 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) { func generateBaseInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
pdf := fpdf.New("P", scaleUnit, "A4", "") 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 { if err := addEmbeddedFont(pdf, "Mark Light", "", "Mark-Light"); err != nil {
return nil, fmt.Errorf("error adding font: %w", err) 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 { if err != nil {
return nil, fmt.Errorf("error adding logo: %w", err) 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.SetAutoPageBreak(false, 0)
pdf.SetHeaderFunc(func() { pdf.SetHeaderFunc(func() {
// Fills the entire page with the background color
bgColor.Fill(pdf) bgColor.Fill(pdf)
pdf.Rect(0, 0, pageWidth, pageHeight, "F") 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() pdf.AddPage()
@@ -117,11 +154,14 @@ func generateBaseInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
return pdf, nil 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) { func drawInvoiceNumber(pdf *fpdf.Fpdf, settings CoverSettings) {
_, pageHeight := pdf.GetPageSize() _, pageHeight := pdf.GetPageSize()
// Bottom left corner of the page, given margin
pdf.MoveTo(margin, pageHeight-margin) pdf.MoveTo(margin, pageHeight-margin)
pdf.SetFont("Mark Light", "", 9) pdf.SetFont(fontLight, "", 9)
textColor.Text(pdf) textColor.Text(pdf)
pdf.TransformBegin() pdf.TransformBegin()
@@ -130,37 +170,44 @@ func drawInvoiceNumber(pdf *fpdf.Fpdf, settings CoverSettings) {
pdf.CellFormat(0, 1, invoiceText, "", 0, "LT", false, 0, "") pdf.CellFormat(0, 1, invoiceText, "", 0, "LT", false, 0, "")
pdf.TransformEnd() pdf.TransformEnd()
// Calculate where the text has gone, so we can figure out the position of the line behind it
_, h := pdf.GetFontSize() _, 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 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 y := pageHeight - margin - pdf.GetStringWidth(invoiceText) - 5
textColor.Draw(pdf) textColor.Draw(pdf)
pdf.SetLineWidth(0.01) pdf.SetLineWidth(0.01)
// the 10 here is the length of the line segment, chosen arbitrarily
pdf.Line(x, y, x, y-10) 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) { func drawMiwebbLink(pdf *fpdf.Fpdf) {
pageWidth, pageHeight := pdf.GetPageSize() pageWidth, pageHeight := pdf.GetPageSize()
pdf.MoveTo(pageWidth-margin, pageHeight-margin) pdf.MoveTo(pageWidth-margin, pageHeight-margin)
pdf.SetFont("Mark Light", "", 9) pdf.SetFont(fontLight, "", 9)
textColor.Text(pdf) textColor.Text(pdf)
pdf.CellFormat(0, 0, "WWW.MIWEBB.COM", "", 0, "RB", false, 0, "https://www.miwebb.com") 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) { func drawCustomerName(pdf *fpdf.Fpdf, settings CoverSettings) {
ml, _, mr, _ := pdf.GetMargins() ml, _, mr, _ := pdf.GetMargins()
pw, ph := pdf.GetPageSize() pw, ph := pdf.GetPageSize()
// Calculate the width and height of the grid, as we'll need it to position it
width := pw - ml - mr 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 height := (width/gridXTicks - 1) * gridYTicks
text := strings.ToUpper(settings.CustomerName) text := strings.ToUpper(settings.CustomerName)
// Draw the grid with a weird gray color // Draw the grid with a weird gray color
gridColor.Draw(pdf) gridColor.Draw(pdf)
pdf.SetLineWidth(0.01) pdf.SetLineWidth(0.01)
// Calculates the top left corner of the grid
gridX, gridY := ml, (ph/2)-(height/2) gridX, gridY := ml, (ph/2)-(height/2)
drawGrid(pdf, gridX, gridY, width, height, gridXTicks, gridYTicks) drawGrid(pdf, gridX, gridY, width, height, gridXTicks, gridYTicks)
@@ -172,22 +219,25 @@ func drawCustomerName(pdf *fpdf.Fpdf, settings CoverSettings) {
1.2, gridNoduleColor, 1.2, gridNoduleColor,
0.25, settings.HLColor) 0.25, settings.HLColor)
pdf.SetFont("Mark Medium", "", 10) pdf.SetFont(fontMedium, "", 10)
// Estimate font size, and calculate line height immediately after // Estimate font size, and calculate line height immediately after
pdf.SetFontSize(estimateFontSize(pdf, text, width, height, gridMargin)) pdf.SetFontSize(estimateFontSize(pdf, text, width, height, gridPadding))
_, sz := pdf.GetFontSize() _, fontHeight := pdf.GetFontSize()
sz *= lineHeight 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: // Centered vertically:
// x = <gridX> + <margin> // x = <gridX> + <margin>
// y = <gridY> + <gridH>/2 - textHeight/2 // y = <gridY> + <gridH>/2 - textHeight/2
pdf.MoveTo(ml+gridPadding, ph/2-(textHeight/2))
pdf.MoveTo(ml+gridMargin, ph/2-(textHeight/2))
textColor.Text(pdf) 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) { func drawGrid(pdf *fpdf.Fpdf, x, y, w, h float64, xs, ys int) {
// Horizontal lines // Horizontal lines
for i := 0; i < ys; i++ { 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) pdf.Line(x, yOff, x+w, yOff)
} }
// Vertical line(pw/2)-(width/2)s // Vertical lines
for i := 0; i < xs; i++ { for i := 0; i < xs; i++ {
xOff := x + (w/float64(xs-1))*float64(i) xOff := x + (w/float64(xs-1))*float64(i)
pdf.Line(xOff, y, xOff, y+h) 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) { func drawNoduleLine(pdf *fpdf.Fpdf, x1, y1, x2, y2, squareSize float64, squareColor Color, lineWidth float64, lineColor Color) {
lineColor.Draw(pdf) lineColor.Draw(pdf)
pdf.SetLineWidth(lineWidth) 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") 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 { 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 maxWidth := containerWidth - 3*margin
maxHeight := containerHeight - 3*margin maxHeight := containerHeight - 3*margin
// Probably smallest we can start with // Probably smallest we can start with
for fontSize := 10.; fontSize <= maxFontSize; fontSize += 1 { for fontSize := 10.; fontSize <= maxFontSize; fontSize += 1 {
width, height := stringSize(pdf, text, (fontSize/scaleFactor)*lineHeight, fontSize) width, height := stringSize(pdf, text, (fontSize/scaleFactor)*lineHeight, fontSize)
// Return the last font size if this one overflows
if width >= maxWidth || height >= maxHeight { if width >= maxWidth || height >= maxHeight {
return fontSize - 1 return fontSize - 1
} }
@@ -228,6 +285,9 @@ func estimateFontSize(pdf *fpdf.Fpdf, text string, containerWidth, containerHeig
return maxFontSize 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) { func stringSize(pdf *fpdf.Fpdf, text string, lineHeight, fontSizePt float64) (width, height float64) {
if fontSizePt == 0 { if fontSizePt == 0 {
fontSizePt, _ = pdf.GetFontSize() fontSizePt, _ = pdf.GetFontSize()
@@ -245,6 +305,8 @@ func stringSize(pdf *fpdf.Fpdf, text string, lineHeight, fontSizePt float64) (wi
return 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 { func addEmbeddedFont(pdf *fpdf.Fpdf, family, style, path string) error {
jsonData, err := fs.ReadFile(fontsEmbedPrefix + path + ".json") jsonData, err := fs.ReadFile(fontsEmbedPrefix + path + ".json")
if err != nil { if err != nil {
@@ -263,6 +325,8 @@ func addEmbeddedFont(pdf *fpdf.Fpdf, family, style, path string) error {
return nil 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) { func addEmbeddedLogo(pdf *fpdf.Fpdf, name, path string) (*fpdf.ImageInfoType, error) {
reader, err := fs.Open(path) reader, err := fs.Open(path)
if err != nil { if err != nil {