8 Commits

Author SHA1 Message Date
0f59ad2320 Update live server
All checks were successful
continuous-integration/drone Build is passing
2022-09-02 14:46:30 +02:00
83d38ef427 Add more possible buttons 2022-09-02 14:36:26 +02:00
e4cc2c31ec Fix small optimization issue that suddenly popped up
All checks were successful
continuous-integration/drone/push Build is passing
2022-04-20 11:11:38 +02:00
fdc9b1a82f Add Gitlab CI
All checks were successful
continuous-integration/drone/push Build is passing
2022-04-20 11:02:22 +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
27 changed files with 4775 additions and 3817 deletions

View File

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

55
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,55 @@
stages:
- build
- deploy
# cache using branch name
# https://gitlab.com/help/ci/caching/index.md
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- frontend/node_modules
build:blobs:
image: golang:1.17
stage: build
script:
- mkdir blobs/
- make wasm
- make build-cross-clis
- mv covergen.wasm dist blobs/
# Grab the wasm shim from the docker image
- cp /usr/local/go/misc/wasm/wasm_exec.js blobs/wasm_exec.js
artifacts:
paths:
- blobs/
expire_in: 30 days
build:frontend:
image: node:16
stage: build
needs:
- build:blobs
before_script:
- cd frontend/
- corepack npm install --immutable
script:
- mv ../blobs/* public
- corepack npm run build
artifacts:
paths:
- frontend/dist/
expire_in: 30 days
deploy:
stage: deploy
image: alpine
needs:
- build:frontend
before_script:
- apk add lftp
script:
- cd frontend/
- lftp "$DEPLOY_USER_PASS@vps17.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
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
.PHONY: wasm-frontend
wasm-frontend: wasm
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 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.

View File

@@ -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")

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"
)
// 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
frontend/.gitignore vendored
View File

@@ -4,5 +4,5 @@ dist
dist-ssr
*.local
.yarn/install-state.gz
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">
<head>
<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" />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<title>NVLS | Covergen</title>
<title>MiWebb | Covergen</title>
</head>
<body class="bg-gray-200">
<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",
"build": "vue-tsc --noEmit && vite build",
"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:style": "eslint --ext .ts,.vue --ignore-path .gitignore .",
"lint:fix": "yarn lint:style --fix"
"lint:fix": "npm run lint:style --fix"
},
"dependencies": {
"@tailwindcss/forms": "^0.4.0",
"pinia": "^2.0.9",
"tailwindcss": "^3.0.15",
"vue": "^3.2.25"
"@tailwindcss/forms": "^0.5.0",
"pinia": "^2.0.13",
"tailwindcss": "^3.0.24",
"vue": "^3.2.33"
},
"devDependencies": {
"@types/golang-wasm-exec": "^1.15.0",
"@types/node": "^17.0.10",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",
"@vitejs/plugin-vue": "^2.0.0",
"@types/node": "^17.0.25",
"@typescript-eslint/eslint-plugin": "^5.20.0",
"@typescript-eslint/parser": "^5.20.0",
"@vitejs/plugin-vue": "^2.3.1",
"@vue/eslint-config-typescript": "^10.0.0",
"autoprefixer": "^10.4.2",
"eslint": "^8.7.0",
"eslint-config-prettier": "^8.3.0",
"autoprefixer": "^10.4.4",
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier-vue": "^3.1.0",
"eslint-plugin-vue": "^8.3.0",
"postcss": "^8.4.5",
"prettier": "^2.5.1",
"typescript": "^4.4.4",
"vite": "^2.7.2",
"vue-tsc": "^0.29.8"
"eslint-plugin-vue": "^8.6.0",
"postcss": "^8.4.12",
"prettier": "^2.6.2",
"typescript": "^4.6.3",
"vite": "^2.9.5",
"vue-tsc": "^0.34.7"
}
}

View File

@@ -0,0 +1,9 @@
AddType application/wasm .wasm
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule (.*) /index.html [QSA,L]
</IfModule>

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">
import Logo from '@/icons/Logo.vue';
const downloads = [
{ name: 'Linux', link: './dist/covergen.linux-amd64' },
{ name: 'Darwin', link: './dist/covergen.darwin-amd64' },
];
</script>
<template>
<div class="w-full mx-auto px-8">
<div class="relative flex items-center justify-between h-16">
<div class="flex-1 flex items-stretch justify-between">
<div class="flex-shrink-0 flex items-center">
<img
src="../assets/logo.png"
alt="covergen logo"
class="h-8 w-auto mr-2"
/>
<Logo class="h-12" />
<h1>CoverGen</h1>
</div>
<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>
<a
v-for="dl in downloads"

View File

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

View File

@@ -21,6 +21,17 @@ const possibleLabels = [
'Rosebud',
'You show that customer',
'Do it for Berend',
'To the bad cave!',
'The more you earn, the more you learn',
'Fortune sides with him who dares',
'Up, Up, Down, Down, Left, Right, Left, Right, B, A.',
'Show me the money',
'Something for nothing',
'There is no cow level',
'WhatIsBestInLife',
'RealMenDrillDeep',
'WhySoSerious',
'IAmIronMan',
];
const label = randomLabel(possibleLabels);
@@ -41,6 +52,11 @@ function doRender() {
}
}
}
function onClick() {
label.update();
doRender();
}
</script>
<template>
@@ -62,9 +78,9 @@ function doRender() {
<button
class="px-4 py-2 bg-blue-500 rounded-lg hover:bg-blue-600 text-white border-2 active:border-blue-500 focus:outline focus:outline-2 focus:outline-blue-500"
@click="label.update() && doRender()"
@click="onClick"
>
{{ label.label.value }}
</button>
</div>
</template>
</template>

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();
WebAssembly.instantiateStreaming(fetch('covergen.wasm'), go.importObject).then(
(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 {
const result = window.generateCover(args);
if ('error' in result) {
@@ -35,6 +43,11 @@ export function generateCover(args: CoverArgs): File {
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): {
front: 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[]) {
const label = ref<string>();
const randomLabel = () => {
@@ -12,7 +18,11 @@ export function randomLabel(options: string[]) {
label.value = randomLabel();
return {
label: readonly(label),
update: () => (label.value = randomLabel()),
// This type refinement is proper as the `undefined` value can never happen from this point onwards
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,
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noUnusedLocals": true,
"lib": ["esnext", "dom"],
"paths": {
"@/*": ["./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,12 +3,12 @@ import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
export default defineConfig({
plugins: [vue()],
base: mode === 'production' ? '/covergen/' : '/',
base: '/',
resolve: {
alias: {
'@': 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"
// 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))
}

View File

@@ -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 {