commit ec3dbbaf3a73ccd300b105756d62b1971d4e7206 Author: Jur van den Berg Date: Fri Dec 17 21:21:58 2021 +0100 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..730aaac --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +covergen +!covergen/ +*.pdf diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5992101 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +wasm: + GOOS=js GOARCH=wasm go build -o assets/covergen.wasm ./cmd/wasm/main.go +.PHONY: wasm diff --git a/assets/covergen.wasm b/assets/covergen.wasm new file mode 100755 index 0000000..d6dceca Binary files /dev/null and b/assets/covergen.wasm differ diff --git a/assets/index.html b/assets/index.html new file mode 100644 index 0000000..0b12cd7 --- /dev/null +++ b/assets/index.html @@ -0,0 +1,53 @@ + + + + + + + + +
+ +
+
+ +
+
+ +
+ + + \ No newline at end of file diff --git a/assets/wasm_exec.js b/assets/wasm_exec.js new file mode 100644 index 0000000..48164c3 --- /dev/null +++ b/assets/wasm_exec.js @@ -0,0 +1,633 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +(() => { + // Map multiple JavaScript environments to a single common API, + // preferring web standards over Node.js API. + // + // Environments considered: + // - Browsers + // - Node.js + // - Electron + // - Parcel + // - Webpack + + if (typeof global !== "undefined") { + // global already exists + } else if (typeof window !== "undefined") { + window.global = window; + } else if (typeof self !== "undefined") { + self.global = self; + } else { + throw new Error("cannot export Go (neither global, window nor self is defined)"); + } + + if (!global.require && typeof require !== "undefined") { + global.require = require; + } + + if (!global.fs && global.require) { + const fs = require("fs"); + if (typeof fs === "object" && fs !== null && Object.keys(fs).length !== 0) { + global.fs = fs; + } + } + + const enosys = () => { + const err = new Error("not implemented"); + err.code = "ENOSYS"; + return err; + }; + + if (!global.fs) { + let outputBuf = ""; + global.fs = { + constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused + writeSync(fd, buf) { + outputBuf += decoder.decode(buf); + const nl = outputBuf.lastIndexOf("\n"); + if (nl != -1) { + console.log(outputBuf.substr(0, nl)); + outputBuf = outputBuf.substr(nl + 1); + } + return buf.length; + }, + write(fd, buf, offset, length, position, callback) { + if (offset !== 0 || length !== buf.length || position !== null) { + callback(enosys()); + return; + } + const n = this.writeSync(fd, buf); + callback(null, n); + }, + chmod(path, mode, callback) { callback(enosys()); }, + chown(path, uid, gid, callback) { callback(enosys()); }, + close(fd, callback) { callback(enosys()); }, + fchmod(fd, mode, callback) { callback(enosys()); }, + fchown(fd, uid, gid, callback) { callback(enosys()); }, + fstat(fd, callback) { callback(enosys()); }, + fsync(fd, callback) { callback(null); }, + ftruncate(fd, length, callback) { callback(enosys()); }, + lchown(path, uid, gid, callback) { callback(enosys()); }, + link(path, link, callback) { callback(enosys()); }, + lstat(path, callback) { callback(enosys()); }, + mkdir(path, perm, callback) { callback(enosys()); }, + open(path, flags, mode, callback) { callback(enosys()); }, + read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, + readdir(path, callback) { callback(enosys()); }, + readlink(path, callback) { callback(enosys()); }, + rename(from, to, callback) { callback(enosys()); }, + rmdir(path, callback) { callback(enosys()); }, + stat(path, callback) { callback(enosys()); }, + symlink(path, link, callback) { callback(enosys()); }, + truncate(path, length, callback) { callback(enosys()); }, + unlink(path, callback) { callback(enosys()); }, + utimes(path, atime, mtime, callback) { callback(enosys()); }, + }; + } + + if (!global.process) { + global.process = { + getuid() { return -1; }, + getgid() { return -1; }, + geteuid() { return -1; }, + getegid() { return -1; }, + getgroups() { throw enosys(); }, + pid: -1, + ppid: -1, + umask() { throw enosys(); }, + cwd() { throw enosys(); }, + chdir() { throw enosys(); }, + } + } + + if (!global.crypto && global.require) { + const nodeCrypto = require("crypto"); + global.crypto = { + getRandomValues(b) { + nodeCrypto.randomFillSync(b); + }, + }; + } + if (!global.crypto) { + throw new Error("global.crypto is not available, polyfill required (getRandomValues only)"); + } + + if (!global.performance) { + global.performance = { + now() { + const [sec, nsec] = process.hrtime(); + return sec * 1000 + nsec / 1000000; + }, + }; + } + + if (!global.TextEncoder && global.require) { + global.TextEncoder = require("util").TextEncoder; + } + if (!global.TextEncoder) { + throw new Error("global.TextEncoder is not available, polyfill required"); + } + + if (!global.TextDecoder && global.require) { + global.TextDecoder = require("util").TextDecoder; + } + if (!global.TextDecoder) { + throw new Error("global.TextDecoder is not available, polyfill required"); + } + + // End of polyfills for common API. + + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); + + global.Go = class { + constructor() { + this.argv = ["js"]; + this.env = {}; + this.exit = (code) => { + if (code !== 0) { + console.warn("exit code:", code); + } + }; + this._exitPromise = new Promise((resolve) => { + this._resolveExitPromise = resolve; + }); + this._pendingEvent = null; + this._scheduledTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; + + const setInt64 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); + } + + const getInt64 = (addr) => { + const low = this.mem.getUint32(addr + 0, true); + const high = this.mem.getInt32(addr + 4, true); + return low + high * 4294967296; + } + + const loadValue = (addr) => { + const f = this.mem.getFloat64(addr, true); + if (f === 0) { + return undefined; + } + if (!isNaN(f)) { + return f; + } + + const id = this.mem.getUint32(addr, true); + return this._values[id]; + } + + const storeValue = (addr, v) => { + const nanHead = 0x7FF80000; + + if (typeof v === "number" && v !== 0) { + if (isNaN(v)) { + this.mem.setUint32(addr + 4, nanHead, true); + this.mem.setUint32(addr, 0, true); + return; + } + this.mem.setFloat64(addr, v, true); + return; + } + + if (v === undefined) { + this.mem.setFloat64(addr, 0, true); + return; + } + + let id = this._ids.get(v); + if (id === undefined) { + id = this._idPool.pop(); + if (id === undefined) { + id = this._values.length; + } + this._values[id] = v; + this._goRefCounts[id] = 0; + this._ids.set(v, id); + } + this._goRefCounts[id]++; + let typeFlag = 0; + switch (typeof v) { + case "object": + if (v !== null) { + typeFlag = 1; + } + break; + case "string": + typeFlag = 2; + break; + case "symbol": + typeFlag = 3; + break; + case "function": + typeFlag = 4; + break; + } + this.mem.setUint32(addr + 4, nanHead | typeFlag, true); + this.mem.setUint32(addr, id, true); + } + + const loadSlice = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + return new Uint8Array(this._inst.exports.mem.buffer, array, len); + } + + const loadSliceOfValues = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + const a = new Array(len); + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8); + } + return a; + } + + const loadString = (addr) => { + const saddr = getInt64(addr + 0); + const len = getInt64(addr + 8); + return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); + } + + const timeOrigin = Date.now() - performance.now(); + this.importObject = { + go: { + // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) + // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported + // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). + // This changes the SP, thus we have to update the SP used by the imported function. + + // func wasmExit(code int32) + "runtime.wasmExit": (sp) => { + sp >>>= 0; + const code = this.mem.getInt32(sp + 8, true); + this.exited = true; + delete this._inst; + delete this._values; + delete this._goRefCounts; + delete this._ids; + delete this._idPool; + this.exit(code); + }, + + // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) + "runtime.wasmWrite": (sp) => { + sp >>>= 0; + const fd = getInt64(sp + 8); + const p = getInt64(sp + 16); + const n = this.mem.getInt32(sp + 24, true); + fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); + }, + + // func resetMemoryDataView() + "runtime.resetMemoryDataView": (sp) => { + sp >>>= 0; + this.mem = new DataView(this._inst.exports.mem.buffer); + }, + + // func nanotime1() int64 + "runtime.nanotime1": (sp) => { + sp >>>= 0; + setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); + }, + + // func walltime1() (sec int64, nsec int32) + "runtime.walltime1": (sp) => { + sp >>>= 0; + const msec = (new Date).getTime(); + setInt64(sp + 8, msec / 1000); + this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); + }, + + // func scheduleTimeoutEvent(delay int64) int32 + "runtime.scheduleTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this._nextCallbackTimeoutID; + this._nextCallbackTimeoutID++; + this._scheduledTimeouts.set(id, setTimeout( + () => { + this._resume(); + while (this._scheduledTimeouts.has(id)) { + // for some reason Go failed to register the timeout event, log and try again + // (temporary workaround for https://github.com/golang/go/issues/28975) + console.warn("scheduleTimeoutEvent: missed timeout event"); + this._resume(); + } + }, + getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early + )); + this.mem.setInt32(sp + 16, id, true); + }, + + // func clearTimeoutEvent(id int32) + "runtime.clearTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this.mem.getInt32(sp + 8, true); + clearTimeout(this._scheduledTimeouts.get(id)); + this._scheduledTimeouts.delete(id); + }, + + // func getRandomData(r []byte) + "runtime.getRandomData": (sp) => { + sp >>>= 0; + crypto.getRandomValues(loadSlice(sp + 8)); + }, + + // func finalizeRef(v ref) + "syscall/js.finalizeRef": (sp) => { + sp >>>= 0; + const id = this.mem.getUint32(sp + 8, true); + this._goRefCounts[id]--; + if (this._goRefCounts[id] === 0) { + const v = this._values[id]; + this._values[id] = null; + this._ids.delete(v); + this._idPool.push(id); + } + }, + + // func stringVal(value string) ref + "syscall/js.stringVal": (sp) => { + sp >>>= 0; + storeValue(sp + 24, loadString(sp + 8)); + }, + + // func valueGet(v ref, p string) ref + "syscall/js.valueGet": (sp) => { + sp >>>= 0; + const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 32, result); + }, + + // func valueSet(v ref, p string, x ref) + "syscall/js.valueSet": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); + }, + + // func valueDelete(v ref, p string) + "syscall/js.valueDelete": (sp) => { + sp >>>= 0; + Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); + }, + + // func valueIndex(v ref, i int) ref + "syscall/js.valueIndex": (sp) => { + sp >>>= 0; + storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); + }, + + // valueSetIndex(v ref, i int, x ref) + "syscall/js.valueSetIndex": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + "syscall/js.valueCall": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const m = Reflect.get(v, loadString(sp + 16)); + const args = loadSliceOfValues(sp + 32); + const result = Reflect.apply(m, v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, result); + this.mem.setUint8(sp + 64, 1); + } catch (err) { + storeValue(sp + 56, err); + this.mem.setUint8(sp + 64, 0); + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + "syscall/js.valueInvoke": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.apply(v, undefined, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + "syscall/js.valueNew": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.construct(v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueLength(v ref) int + "syscall/js.valueLength": (sp) => { + sp >>>= 0; + setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); + }, + + // valuePrepareString(v ref) (ref, int) + "syscall/js.valuePrepareString": (sp) => { + sp >>>= 0; + const str = encoder.encode(String(loadValue(sp + 8))); + storeValue(sp + 16, str); + setInt64(sp + 24, str.length); + }, + + // valueLoadString(v ref, b []byte) + "syscall/js.valueLoadString": (sp) => { + sp >>>= 0; + const str = loadValue(sp + 8); + loadSlice(sp + 16).set(str); + }, + + // func valueInstanceOf(v ref, t ref) bool + "syscall/js.valueInstanceOf": (sp) => { + sp >>>= 0; + this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); + }, + + // func copyBytesToGo(dst []byte, src ref) (int, bool) + "syscall/js.copyBytesToGo": (sp) => { + sp >>>= 0; + const dst = loadSlice(sp + 8); + const src = loadValue(sp + 32); + if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + // func copyBytesToJS(dst ref, src []byte) (int, bool) + "syscall/js.copyBytesToJS": (sp) => { + sp >>>= 0; + const dst = loadValue(sp + 8); + const src = loadSlice(sp + 16); + if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + "debug": (value) => { + console.log(value); + }, + } + }; + } + + async run(instance) { + if (!(instance instanceof WebAssembly.Instance)) { + throw new Error("Go.run: WebAssembly.Instance expected"); + } + this._inst = instance; + this.mem = new DataView(this._inst.exports.mem.buffer); + this._values = [ // JS values that Go currently has references to, indexed by reference id + NaN, + 0, + null, + true, + false, + global, + this, + ]; + this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map([ // mapping from JS values to reference ids + [0, 1], + [null, 2], + [true, 3], + [false, 4], + [global, 5], + [this, 6], + ]); + this._idPool = []; // unused ids that have been garbage collected + this.exited = false; // whether the Go program has exited + + // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. + let offset = 4096; + + const strPtr = (str) => { + const ptr = offset; + const bytes = encoder.encode(str + "\0"); + new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); + offset += bytes.length; + if (offset % 8 !== 0) { + offset += 8 - (offset % 8); + } + return ptr; + }; + + const argc = this.argv.length; + + const argvPtrs = []; + this.argv.forEach((arg) => { + argvPtrs.push(strPtr(arg)); + }); + argvPtrs.push(0); + + const keys = Object.keys(this.env).sort(); + keys.forEach((key) => { + argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); + }); + argvPtrs.push(0); + + const argv = offset; + argvPtrs.forEach((ptr) => { + this.mem.setUint32(offset, ptr, true); + this.mem.setUint32(offset + 4, 0, true); + offset += 8; + }); + + // The linker guarantees global data starts from at least wasmMinDataAddr. + // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. + const wasmMinDataAddr = 4096 + 8192; + if (offset >= wasmMinDataAddr) { + throw new Error("total length of command line and environment variables exceeds limit"); + } + + this._inst.exports.run(argc, argv); + if (this.exited) { + this._resolveExitPromise(); + } + await this._exitPromise; + } + + _resume() { + if (this.exited) { + throw new Error("Go program has already exited"); + } + this._inst.exports.resume(); + if (this.exited) { + this._resolveExitPromise(); + } + } + + _makeFuncWrapper(id) { + const go = this; + return function () { + const event = { id: id, this: this, args: arguments }; + go._pendingEvent = event; + go._resume(); + return event.result; + }; + } + } + + if ( + typeof module !== "undefined" && + global.require && + global.require.main === module && + global.process && + global.process.versions && + !global.process.versions.electron + ) { + if (process.argv.length < 3) { + console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); + process.exit(1); + } + + const go = new Go(); + go.argv = process.argv.slice(2); + go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env); + go.exit = process.exit; + WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { + process.on("exit", (code) => { // Node.js exits if no event handler is pending + if (code === 0 && !go.exited) { + // deadlock, make Go print error and stack traces + go._pendingEvent = { id: 0 }; + go._resume(); + } + }); + return go.run(result.instance); + }).catch((err) => { + console.error(err); + process.exit(1); + }); + } +})(); diff --git a/cmd/covergen/main.go b/cmd/covergen/main.go new file mode 100644 index 0000000..faa433b --- /dev/null +++ b/cmd/covergen/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "math/rand" + "os" + "strings" + "time" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/pflag" + + "covergen/pkg/covergen" +) + +var colors = []string{"red", "blue", "yellow", "green"} +var colorMap = map[string]covergen.Color{ + "red": {255, 85, 0}, + "blue": {59, 180, 255}, + "yellow": {255, 230, 0}, + "green": {76, 177, 110}, +} + +var number = pflag.StringP("number", "n", "", "Invoice or quotation number") +var customer = pflag.StringP("customer", "c", "", "Customer name for cover") +var numberPrefix = pflag.String("number-prefix", "offerte", "Prefix to use for number") +var color = pflag.String("color", "random", "Selects a color to use for the grid highlight. Valid choices: ['random', 'red', 'blue', 'yellow', 'green']") +var output = pflag.StringP("output", "o", "cover.pdf", "File to output to") + +func init() { + log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger() + pflag.Parse() + + if *number == "" { + log.Error().Msg("Missing (invoice) number flag") + pflag.Usage() + os.Exit(1) + } + + if *customer == "" { + log.Error().Msg("Missing customer name flag") + pflag.Usage() + os.Exit(1) + } + + if *color != "random" { + if _, ok := colorMap[*color]; !ok { + log.Error().Msgf("Color %s is not defined", *color) + pflag.Usage() + os.Exit(1) + } + } + + if *output == "" { + log.Error().Msg("Invalid output flag") + pflag.Usage() + os.Exit(1) + } +} + +func main() { + rand.Seed(time.Now().UnixNano()) + + var chosenColor covergen.Color + if *color == "random" { + chosenColor = colorMap[colors[rand.Intn(len(colors))]] + } else { + chosenColor = colorMap[*color] + } + + *customer = strings.ReplaceAll(*customer, "\\n", "\n") + + pdf, err := covergen.GenerateInvoice(covergen.CoverSettings{ + Number: *number, + NumberPrefix: *numberPrefix, + CustomerName: *customer, + HLColor: chosenColor, + }) + + if err != nil { + log.Fatal().Err(err).Msg("Failed to render invoice") + } + + if err := pdf.OutputFileAndClose(*output); err != nil { + log.Fatal().Err(err).Msg("Failed to write invoice to disk") + } +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..b2923ae --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,14 @@ +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 + } +} diff --git a/cmd/wasm/main.go b/cmd/wasm/main.go new file mode 100644 index 0000000..041ddf9 --- /dev/null +++ b/cmd/wasm/main.go @@ -0,0 +1,118 @@ +package main + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "syscall/js" + + "github.com/thegrumpylion/jsref" + + "covergen/pkg/covergen" +) + +type jsmap = map[string]interface{} + +type covergenArgs struct { + Customer string `jsref:"customer"` + Number string `jsref:"number"` + NumberPrefix string `jsref:"numberPrefix"` + HLColor string `jsref:"hlColor"` +} + +func parseHexColor(s string) (c covergen.Color, err error) { + switch len(s) { + case 7: + _, err = fmt.Sscanf(s, "#%02x%02x%02x", &c.R, &c.G, &c.B) + case 4: + _, err = fmt.Sscanf(s, "#%1x%1x%1x", &c.R, &c.G, &c.B) + // Double the hex digits: + c.R *= 17 + c.G *= 17 + c.B *= 17 + default: + err = fmt.Errorf("invalid length, must be 7 or 4") + + } + return +} + +func settingsFromValue(arg js.Value) (*covergen.CoverSettings, error) { + settings := covergenArgs{ + NumberPrefix: "offerte", + } + err := jsref.Unmarshal(&settings, arg) + if err != nil { + return nil, err + } + + if settings.Number == "" { + return nil, errors.New("number: shouldn't be empty") + } + + if settings.Customer == "" { + return nil, errors.New("customer: shouldn't be empty") + } + + color := covergen.Color{R: 255} + if settings.HLColor != "" { + color, err = parseHexColor(settings.HLColor) + if err != nil { + return nil, fmt.Errorf("hlColor: %w", err) + } + } + + return &covergen.CoverSettings{ + Number: settings.Number, + NumberPrefix: settings.NumberPrefix, + CustomerName: settings.Customer, + HLColor: color, + }, nil +} + +func generateCover(this js.Value, args []js.Value) interface{} { + defer func() { + if r := recover(); r != nil { + fmt.Println("recovered", r) + } + }() + + if len(args) != 1 { + return jsmap{"error": "missing argument"} + } + + arg := args[0] + if arg.Type() != js.TypeObject { + return jsmap{"error": "expected object"} + } + + settings, err := settingsFromValue(arg) + if err != nil { + return jsmap{"error": err.Error()} + } + + pdf, err := covergen.GenerateInvoice(*settings) + if err != nil { + return jsmap{"error": err.Error()} + } + + var buf bytes.Buffer + if err = pdf.Output(&buf); err != nil { + return jsmap{"error": err.Error()} + } + + s := buf.Bytes() + sum := sha256.Sum256(s) + fmt.Println("shasum", hex.EncodeToString(sum[:])) + + ta := js.Global().Get("Uint8Array").New(len(s)) + js.CopyBytesToJS(ta, s) + return ta +} + +func main() { + js.Global().Set("covergen", js.FuncOf(generateCover)) + <-make(chan bool) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6989d1f --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module covergen + +go 1.16 + +require ( + github.com/go-pdf/fpdf v0.5.0 + github.com/rs/zerolog v1.26.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/thegrumpylion/jsref v0.0.0-20201010132516-cde94ac58b50 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..14b33b4 --- /dev/null +++ b/go.sum @@ -0,0 +1,53 @@ +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-pdf/fpdf v0.5.0 h1:GHpcYsiDV2hdo77VTOuTF9k1sN8F8IY7NjnCo9x+NPY= +github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= +github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.26.0 h1:ORM4ibhEZeTeQlCojCK2kPz1ogAY4bGs4tD+SaAdGaE= +github.com/rs/zerolog v1.26.0/go.mod h1:yBiM87lvSqX8h0Ww4sdzNSkVYZ8dL2xjZJG1lAuGZEo= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/thegrumpylion/jsref v0.0.0-20201010132516-cde94ac58b50 h1:+VI/5fDcWqq0+yXPIAR2y3FJuTT05VBgcev7P8jY1q0= +github.com/thegrumpylion/jsref v0.0.0-20201010132516-cde94ac58b50/go.mod h1:SQhhmnL4j/8Axo6GXCMJkuWVpi90x42s7KNRfVjYH2E= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/covergen/color.go b/pkg/covergen/color.go new file mode 100644 index 0000000..0bb27bc --- /dev/null +++ b/pkg/covergen/color.go @@ -0,0 +1,21 @@ +package covergen + +import "github.com/go-pdf/fpdf" + +type Color struct { + R uint8 + G uint8 + B uint8 +} + +func (c Color) Text(pdf *fpdf.Fpdf) { + pdf.SetTextColor(int(c.R), int(c.G), int(c.B)) +} + +func (c Color) Draw(pdf *fpdf.Fpdf) { + pdf.SetDrawColor(int(c.R), int(c.G), int(c.B)) +} + +func (c Color) Fill(pdf *fpdf.Fpdf) { + pdf.SetFillColor(int(c.R), int(c.G), int(c.B)) +} diff --git a/pkg/covergen/font_embed/Mark-Light.json b/pkg/covergen/font_embed/Mark-Light.json new file mode 100644 index 0000000..39bd52f --- /dev/null +++ b/pkg/covergen/font_embed/Mark-Light.json @@ -0,0 +1 @@ +{"Tp":"TrueType","Name":"Mark-Light","Desc":{"Ascent":790,"Descent":-210,"CapHeight":700,"Flags":32,"FontBBox":{"Xmin":-463,"Ymin":-239,"Xmax":1072,"Ymax":954},"ItalicAngle":0,"StemV":70,"MissingWidth":599},"Up":-309,"Ut":52,"Cw":[599,599,599,599,599,599,599,599,599,599,599,599,599,599,599,599,599,599,599,599,599,599,599,599,599,599,599,599,599,599,599,599,265,260,507,728,700,956,690,351,400,400,433,600,244,418,260,492,600,600,600,600,600,600,600,600,600,600,260,244,600,600,600,424,990,703,700,764,801,671,644,819,783,271,576,684,602,930,785,843,666,843,671,672,608,755,693,1104,687,632,664,369,492,369,599,468,599,622,623,512,622,561,438,618,599,239,240,521,239,965,599,596,623,622,407,487,452,593,515,863,503,514,533,420,351,420,599,599,700,599,244,599,411,705,599,599,599,599,672,310,1130,599,664,599,599,244,244,411,411,437,601,813,599,674,487,310,991,599,533,632,265,260,599,700,599,700,599,655,599,949,599,499,599,418,440,599,384,599,599,599,599,599,599,375,599,599,599,499,599,599,599,424,703,703,703,703,703,703,1133,764,671,671,671,671,271,271,271,271,888,785,843,843,843,843,843,600,843,755,755,755,755,632,666,608,622,622,622,622,622,622,910,512,561,561,561,561,239,239,239,239,595,599,596,596,596,596,596,599,596,593,593,593,593,514,623,514],"Enc":"cp1252","Diff":"","File":"Mark-Light.z","Size1":0,"Size2":0,"OriginalSize":49024,"N":0,"DiffN":0} \ No newline at end of file diff --git a/pkg/covergen/font_embed/Mark-Light.z b/pkg/covergen/font_embed/Mark-Light.z new file mode 100644 index 0000000..46601c4 Binary files /dev/null and b/pkg/covergen/font_embed/Mark-Light.z differ diff --git a/pkg/covergen/font_embed/Mark-Medium.json b/pkg/covergen/font_embed/Mark-Medium.json new file mode 100644 index 0000000..0dbbbc7 --- /dev/null +++ b/pkg/covergen/font_embed/Mark-Medium.json @@ -0,0 +1 @@ +{"Tp":"TrueType","Name":"Mark-Medium","Desc":{"Ascent":790,"Descent":-210,"CapHeight":700,"Flags":32,"FontBBox":{"Xmin":-491,"Ymin":-237,"Xmax":1083,"Ymax":994},"ItalicAngle":0,"StemV":70,"MissingWidth":605},"Up":-368,"Ut":68,"Cw":[605,605,605,605,605,605,605,605,605,605,605,605,605,605,605,605,605,605,605,605,605,605,605,605,605,605,605,605,605,605,605,605,270,265,556,740,700,992,727,371,425,425,475,600,253,423,265,504,600,600,600,600,600,600,600,600,600,600,265,253,600,600,600,445,986,724,702,766,794,682,652,820,794,303,575,712,615,944,796,847,662,847,686,676,604,765,705,1090,678,664,646,388,504,388,605,470,605,624,624,514,624,560,422,616,605,269,272,543,269,945,605,611,624,624,430,506,454,598,531,837,526,549,521,440,371,440,605,605,700,605,253,605,448,815,605,605,605,605,676,319,1127,605,646,605,605,253,253,448,448,423,589,807,605,790,506,319,968,605,521,664,270,265,605,700,605,700,605,654,605,956,605,531,605,423,509,605,421,605,605,605,605,605,605,328,605,605,605,531,605,605,605,445,724,724,724,724,724,724,1159,766,682,682,682,682,303,303,303,303,851,796,847,847,847,847,847,600,847,765,765,765,765,664,662,621,624,624,624,624,624,624,882,514,560,560,560,560,269,269,269,269,608,605,611,611,611,611,611,605,611,598,598,598,598,549,624,549],"Enc":"cp1252","Diff":"","File":"Mark-Medium.z","Size1":0,"Size2":0,"OriginalSize":47520,"N":0,"DiffN":0} \ No newline at end of file diff --git a/pkg/covergen/font_embed/Mark-Medium.z b/pkg/covergen/font_embed/Mark-Medium.z new file mode 100644 index 0000000..d7c2c02 Binary files /dev/null and b/pkg/covergen/font_embed/Mark-Medium.z differ diff --git a/pkg/covergen/fonts/Mark-Light.ttf b/pkg/covergen/fonts/Mark-Light.ttf new file mode 100644 index 0000000..192f3b5 Binary files /dev/null and b/pkg/covergen/fonts/Mark-Light.ttf differ diff --git a/pkg/covergen/fonts/Mark-Medium.ttf b/pkg/covergen/fonts/Mark-Medium.ttf new file mode 100644 index 0000000..2ec17a8 Binary files /dev/null and b/pkg/covergen/fonts/Mark-Medium.ttf differ diff --git a/pkg/covergen/fonts/cp1252.map b/pkg/covergen/fonts/cp1252.map new file mode 100644 index 0000000..dd490e5 --- /dev/null +++ b/pkg/covergen/fonts/cp1252.map @@ -0,0 +1,251 @@ +!00 U+0000 .notdef +!01 U+0001 .notdef +!02 U+0002 .notdef +!03 U+0003 .notdef +!04 U+0004 .notdef +!05 U+0005 .notdef +!06 U+0006 .notdef +!07 U+0007 .notdef +!08 U+0008 .notdef +!09 U+0009 .notdef +!0A U+000A .notdef +!0B U+000B .notdef +!0C U+000C .notdef +!0D U+000D .notdef +!0E U+000E .notdef +!0F U+000F .notdef +!10 U+0010 .notdef +!11 U+0011 .notdef +!12 U+0012 .notdef +!13 U+0013 .notdef +!14 U+0014 .notdef +!15 U+0015 .notdef +!16 U+0016 .notdef +!17 U+0017 .notdef +!18 U+0018 .notdef +!19 U+0019 .notdef +!1A U+001A .notdef +!1B U+001B .notdef +!1C U+001C .notdef +!1D U+001D .notdef +!1E U+001E .notdef +!1F U+001F .notdef +!20 U+0020 space +!21 U+0021 exclam +!22 U+0022 quotedbl +!23 U+0023 numbersign +!24 U+0024 dollar +!25 U+0025 percent +!26 U+0026 ampersand +!27 U+0027 quotesingle +!28 U+0028 parenleft +!29 U+0029 parenright +!2A U+002A asterisk +!2B U+002B plus +!2C U+002C comma +!2D U+002D hyphen +!2E U+002E period +!2F U+002F slash +!30 U+0030 zero +!31 U+0031 one +!32 U+0032 two +!33 U+0033 three +!34 U+0034 four +!35 U+0035 five +!36 U+0036 six +!37 U+0037 seven +!38 U+0038 eight +!39 U+0039 nine +!3A U+003A colon +!3B U+003B semicolon +!3C U+003C less +!3D U+003D equal +!3E U+003E greater +!3F U+003F question +!40 U+0040 at +!41 U+0041 A +!42 U+0042 B +!43 U+0043 C +!44 U+0044 D +!45 U+0045 E +!46 U+0046 F +!47 U+0047 G +!48 U+0048 H +!49 U+0049 I +!4A U+004A J +!4B U+004B K +!4C U+004C L +!4D U+004D M +!4E U+004E N +!4F U+004F O +!50 U+0050 P +!51 U+0051 Q +!52 U+0052 R +!53 U+0053 S +!54 U+0054 T +!55 U+0055 U +!56 U+0056 V +!57 U+0057 W +!58 U+0058 X +!59 U+0059 Y +!5A U+005A Z +!5B U+005B bracketleft +!5C U+005C backslash +!5D U+005D bracketright +!5E U+005E asciicircum +!5F U+005F underscore +!60 U+0060 grave +!61 U+0061 a +!62 U+0062 b +!63 U+0063 c +!64 U+0064 d +!65 U+0065 e +!66 U+0066 f +!67 U+0067 g +!68 U+0068 h +!69 U+0069 i +!6A U+006A j +!6B U+006B k +!6C U+006C l +!6D U+006D m +!6E U+006E n +!6F U+006F o +!70 U+0070 p +!71 U+0071 q +!72 U+0072 r +!73 U+0073 s +!74 U+0074 t +!75 U+0075 u +!76 U+0076 v +!77 U+0077 w +!78 U+0078 x +!79 U+0079 y +!7A U+007A z +!7B U+007B braceleft +!7C U+007C bar +!7D U+007D braceright +!7E U+007E asciitilde +!7F U+007F .notdef +!80 U+20AC Euro +!82 U+201A quotesinglbase +!83 U+0192 florin +!84 U+201E quotedblbase +!85 U+2026 ellipsis +!86 U+2020 dagger +!87 U+2021 daggerdbl +!88 U+02C6 circumflex +!89 U+2030 perthousand +!8A U+0160 Scaron +!8B U+2039 guilsinglleft +!8C U+0152 OE +!8E U+017D Zcaron +!91 U+2018 quoteleft +!92 U+2019 quoteright +!93 U+201C quotedblleft +!94 U+201D quotedblright +!95 U+2022 bullet +!96 U+2013 endash +!97 U+2014 emdash +!98 U+02DC tilde +!99 U+2122 trademark +!9A U+0161 scaron +!9B U+203A guilsinglright +!9C U+0153 oe +!9E U+017E zcaron +!9F U+0178 Ydieresis +!A0 U+00A0 space +!A1 U+00A1 exclamdown +!A2 U+00A2 cent +!A3 U+00A3 sterling +!A4 U+00A4 currency +!A5 U+00A5 yen +!A6 U+00A6 brokenbar +!A7 U+00A7 section +!A8 U+00A8 dieresis +!A9 U+00A9 copyright +!AA U+00AA ordfeminine +!AB U+00AB guillemotleft +!AC U+00AC logicalnot +!AD U+00AD hyphen +!AE U+00AE registered +!AF U+00AF macron +!B0 U+00B0 degree +!B1 U+00B1 plusminus +!B2 U+00B2 twosuperior +!B3 U+00B3 threesuperior +!B4 U+00B4 acute +!B5 U+00B5 mu +!B6 U+00B6 paragraph +!B7 U+00B7 periodcentered +!B8 U+00B8 cedilla +!B9 U+00B9 onesuperior +!BA U+00BA ordmasculine +!BB U+00BB guillemotright +!BC U+00BC onequarter +!BD U+00BD onehalf +!BE U+00BE threequarters +!BF U+00BF questiondown +!C0 U+00C0 Agrave +!C1 U+00C1 Aacute +!C2 U+00C2 Acircumflex +!C3 U+00C3 Atilde +!C4 U+00C4 Adieresis +!C5 U+00C5 Aring +!C6 U+00C6 AE +!C7 U+00C7 Ccedilla +!C8 U+00C8 Egrave +!C9 U+00C9 Eacute +!CA U+00CA Ecircumflex +!CB U+00CB Edieresis +!CC U+00CC Igrave +!CD U+00CD Iacute +!CE U+00CE Icircumflex +!CF U+00CF Idieresis +!D0 U+00D0 Eth +!D1 U+00D1 Ntilde +!D2 U+00D2 Ograve +!D3 U+00D3 Oacute +!D4 U+00D4 Ocircumflex +!D5 U+00D5 Otilde +!D6 U+00D6 Odieresis +!D7 U+00D7 multiply +!D8 U+00D8 Oslash +!D9 U+00D9 Ugrave +!DA U+00DA Uacute +!DB U+00DB Ucircumflex +!DC U+00DC Udieresis +!DD U+00DD Yacute +!DE U+00DE Thorn +!DF U+00DF germandbls +!E0 U+00E0 agrave +!E1 U+00E1 aacute +!E2 U+00E2 acircumflex +!E3 U+00E3 atilde +!E4 U+00E4 adieresis +!E5 U+00E5 aring +!E6 U+00E6 ae +!E7 U+00E7 ccedilla +!E8 U+00E8 egrave +!E9 U+00E9 eacute +!EA U+00EA ecircumflex +!EB U+00EB edieresis +!EC U+00EC igrave +!ED U+00ED iacute +!EE U+00EE icircumflex +!EF U+00EF idieresis +!F0 U+00F0 eth +!F1 U+00F1 ntilde +!F2 U+00F2 ograve +!F3 U+00F3 oacute +!F4 U+00F4 ocircumflex +!F5 U+00F5 otilde +!F6 U+00F6 odieresis +!F7 U+00F7 divide +!F8 U+00F8 oslash +!F9 U+00F9 ugrave +!FA U+00FA uacute +!FB U+00FB ucircumflex +!FC U+00FC udieresis +!FD U+00FD yacute +!FE U+00FE thorn +!FF U+00FF ydieresis diff --git a/pkg/covergen/miwebb.white.png b/pkg/covergen/miwebb.white.png new file mode 100644 index 0000000..3bb0905 Binary files /dev/null and b/pkg/covergen/miwebb.white.png differ diff --git a/pkg/covergen/render.go b/pkg/covergen/render.go new file mode 100644 index 0000000..2da662e --- /dev/null +++ b/pkg/covergen/render.go @@ -0,0 +1,247 @@ +package covergen + +import ( + "embed" + "fmt" + "strings" + + "github.com/go-pdf/fpdf" + "github.com/rs/zerolog/log" +) + +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 + +var bgColor = Color{25, 25, 25} +var textColor = Color{255, 255, 255} +var gridColor = Color{80, 80, 80} +var gridNoduleColor = Color{255, 255, 255} + +//go:embed miwebb.white.png font_embed +var fs embed.FS + +const fontsEmbedPrefix = "font_embed/" + +// 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 + +type CoverSettings struct { + Number string + NumberPrefix string + + CustomerName string + HLColor Color +} + +func GenerateInvoice(settings CoverSettings) (*fpdf.Fpdf, error) { + pdf := fpdf.New("P", scaleUnit, "A4", "") + + if err := addEmbeddedFont(pdf, "Mark Medium", "", "Mark-Medium"); err != nil { + log.Fatal().Err(err).Msg("Error adding font") + } + if err := addEmbeddedFont(pdf, "Mark Light", "", "Mark-Light"); err != nil { + log.Fatal().Err(err).Msg("Error adding font") + } + miwebbLogo, err := addEmbeddedLogo(pdf, miwebbLogoName, miwebbLogoPath) + if err != nil { + log.Fatal().Err(err).Msg("Error adding logo") + } + + pdf.SetMargins(margin, margin, margin) + pageWidth, pageHeight := pdf.GetPageSize() + pdf.SetAutoPageBreak(false, 0) + + pdf.SetHeaderFunc(func() { + bgColor.Fill(pdf) + pdf.Rect(0, 0, pageWidth, pageHeight, "F") + pdf.Image(miwebbLogoName, margin-5, margin, miwebbLogo.Width()*logoScale, miwebbLogo.Height()*logoScale, false, "", 0, "") + }) + + pdf.AddPage() + + drawCustomerName(pdf, settings) + drawInvoiceNumber(pdf, settings) + drawMiwebbLink(pdf) + + pdf.AddPage() + drawCustomerName(pdf, settings) + + if pdf.Err() { + return nil, pdf.Error() + } + + return pdf, nil +} +func drawInvoiceNumber(pdf *fpdf.Fpdf, settings CoverSettings) { + _, pageHeight := pdf.GetPageSize() + + pdf.MoveTo(margin, pageHeight-margin) + pdf.SetFont("Mark Light", "", 9) + textColor.Text(pdf) + + pdf.TransformBegin() + pdf.TransformRotate(90, margin, pageHeight-margin) + invoiceText := strings.ToUpper(fmt.Sprintf("%s %s", settings.NumberPrefix, settings.Number)) + pdf.CellFormat(0, 1, invoiceText, "", 0, "LT", false, 0, "") + pdf.TransformEnd() + + _, h := pdf.GetFontSize() + // X is 'margin + half text height' + x := float64(margin) + h/2 + // Y is 'where the text ended' + some gap + y := pageHeight - margin - pdf.GetStringWidth(invoiceText) - 5 + + textColor.Draw(pdf) + pdf.SetLineWidth(0.01) + pdf.Line(x, y, x, y-10) +} + +func drawMiwebbLink(pdf *fpdf.Fpdf) { + pageWidth, pageHeight := pdf.GetPageSize() + pdf.MoveTo(pageWidth-margin, pageHeight-margin) + + pdf.SetFont("Mark Light", "", 9) + textColor.Text(pdf) + pdf.CellFormat(0, 0, "WWW.MIWEBB.COM", "", 0, "RB", false, 0, "https://www.miwebb.com") +} + +func drawCustomerName(pdf *fpdf.Fpdf, settings CoverSettings) { + ml, _, mr, _ := pdf.GetMargins() + pw, ph := pdf.GetPageSize() + + width := pw - ml - mr + 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) + gridX, gridY := ml, (ph/2)-(height/2) + drawGrid(pdf, gridX, gridY, width, height, gridXTicks, gridYTicks) + + // Draw the highlight on the right + drawNoduleLine( + pdf, + gridX+width, gridY, + gridX+width, gridY+height, + 1.2, gridNoduleColor, + 0.25, settings.HLColor) + + pdf.SetFont("Mark Medium", "", 10) + // Estimate font size, and calculate line height immediately after + pdf.SetFontSize(estimateFontSize(pdf, text, width, height, gridMargin)) + _, sz := pdf.GetFontSize() + sz *= lineHeight + + _, textHeight := stringSize(pdf, text, sz, 0) + // Centered vertically: + // x = + + // y = + /2 - textHeight/2 + + pdf.MoveTo(ml+gridMargin, ph/2-(textHeight/2)) + textColor.Text(pdf) + pdf.MultiCell(width-2*gridMargin, sz, text, "", "ML", false) +} + +func drawGrid(pdf *fpdf.Fpdf, x, y, w, h float64, xs, ys int) { + // Horizontal lines + for i := 0; i < ys; i++ { + yOff := y + (h/float64(ys-1))*float64(i) + pdf.Line(x, yOff, x+w, yOff) + } + + // Vertical line(pw/2)-(width/2)s + for i := 0; i < xs; i++ { + xOff := x + (w/float64(xs-1))*float64(i) + pdf.Line(xOff, y, xOff, y+h) + } +} + +func drawNoduleLine(pdf *fpdf.Fpdf, x1, y1, x2, y2, squareSize float64, squareColor Color, lineWidth float64, lineColor Color) { + lineColor.Draw(pdf) + pdf.SetLineWidth(lineWidth) + pdf.Line(x1, y1, x2, y2) + + squareColor.Fill(pdf) + pdf.Rect(x1-(squareSize/2), y1-(squareSize/2), squareSize, squareSize, "F") + pdf.Rect(x2-(squareSize/2), y2-(squareSize/2), squareSize, squareSize, "F") +} + +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' + 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) + if width >= maxWidth || height >= maxHeight { + return fontSize - 1 + } + } + + return maxFontSize +} + +func stringSize(pdf *fpdf.Fpdf, text string, lineHeight, fontSizePt float64) (width, height float64) { + if fontSizePt == 0 { + fontSizePt, _ = pdf.GetFontSize() + } + + lines := strings.Split(text, "\n") + height = float64(len(lines)) * lineHeight + for _, line := range lines { + lineWidth := float64(pdf.GetStringSymbolWidth(line)) * fontSizePt / scaleFactor / 1000 + if lineWidth > width { + width = lineWidth + } + } + + return +} + +func addEmbeddedFont(pdf *fpdf.Fpdf, family, style, path string) error { + jsonData, err := fs.ReadFile(fontsEmbedPrefix + path + ".json") + if err != nil { + return err + } + zData, err := fs.ReadFile(fontsEmbedPrefix + path + ".z") + if err != nil { + return err + } + + pdf.AddFontFromBytes(family, style, jsonData, zData) + if pdf.Err() { + return pdf.Error() + } + + return nil +} + +func addEmbeddedLogo(pdf *fpdf.Fpdf, name, path string) (*fpdf.ImageInfoType, error) { + reader, err := fs.Open(path) + if err != nil { + return nil, err + } + + handle := pdf.RegisterImageOptionsReader(name, fpdf.ImageOptions{ + ImageType: "PNG", + ReadDpi: true, + }, reader) + + if pdf.Err() { + return nil, pdf.Error() + } + + return handle, nil +}