Преглед на файлове

Generate PDF with os/exec process instead of js

the htmltopdf.js library renders all PDF text as images instead of
vectors, and Go does not have a reliable native library for generating
PDFs that is actively maintained. The best solution appears to be to
make a system call to wkhtmltopdf, but now there is a hidden package
dependency that is not checked at build time and exists in another
process. Maybe in the future a C library can be used and linked with the
Go binary.
master
Immanuel Onyeka преди 1 година
родител
ревизия
ba9522a883
променени са 6 файла, в които са добавени 231 реда и са изтрити 10 реда
  1. +1
    -0
      components/dropdown.vue
  2. +57
    -3
      components/estimate-test.vue
  3. +2
    -1
      components/settings.vue
  4. +94
    -2
      skouter.go
  5. +14
    -0
      views/pdf.tpl
  6. +63
    -4
      views/test.tpl

+ 1
- 0
components/dropdown.vue Целия файл

@@ -17,6 +17,7 @@
margin-top: 5px;
z-index: 3;
border: 1px solid black;
top: 100%;
}
.entry {


+ 57
- 3
components/estimate-test.vue Целия файл

@@ -1,5 +1,6 @@
<template>
<div id="pdf-doc" ref="doc" v-if="estimate">
<div class="disclaimer"><p>Actual costs may vary from estimates after approval. Get an official quote before choosing a loan.</p></div>

<header class="heading">

@@ -11,14 +12,18 @@
<h4>{{user.firstName + " " + user.lastName}}</h4>
<span>{{user.email}}</span>
<span>{{user.phone}}</span>
<small>{{user.phone}}</small>
<small>{{user.address.street}}</small>
<small>
{{`${user.address.city}, ${user.address.region} ${user.address.zip}`}}
</small>
</div>
<img :src="avatar"/>
</div>

</header>

<button @click="makePDF">Generate</button>
<button @click="getPdf">Generate</button>
<a :href="pdfLink" v-if="pdfLink" download="estimate.pdf">download </a>
</div>
</template>

@@ -30,6 +35,7 @@ const doc = ref(null)
const props = defineProps(['token', 'estimate', 'user'])
const estimate = ref(null)
const estimates = ref(null)
const pdfLink = ref('')

const letterhead = computed(() => {
if (!props.user.letterhead) return null
@@ -43,7 +49,11 @@ const avatar = computed(() => {
})

function makePDF() {
html2pdf(doc.value)
var opt = {
image: { type: 'png', quality: 1 },
}

html2pdf(doc.value, opt)
}


@@ -65,18 +75,62 @@ function getEstimates() {
})
}

function getPdf() {
fetch(`/api/pdf`,
{method: 'GET',
headers: {
"Accept": "application/json",
"Authorization": `Bearer ${props.token}`,
},
}).then(response => {
if (response.ok) { return response.blob() }
else {
return null
}
}).then (result => {
if (!result) return
pdfLink.value = URL.createObjectURL(result)
})
}

onMounted(() => {
getEstimates().then(() => estimate.value = estimates.value[0])
})
</script>

<style scoped>
#pdf-doc {
margin: 4px 30px;
}

.disclaimer {
font-weight: bold;
border-bottom: 1px solid lightgrey;
margin-bottom: 20px;
color: var(--text);
}

.disclaimer p {
margin: 5px 0;
}

h4 {
margin: 4px 0;
}

header.heading {
display: flex;
justify-content: space-between;
}

.user-info {
display: flex;
flex-flow: column;
}

#pdf-doc header.heading > div {
display: flex;
gap: 8px;
text-align: right;
}
</style>

+ 2
- 1
components/settings.vue Целия файл

@@ -31,7 +31,7 @@
<label for="">NMLS ID</label>
<input type="text">
<label for="">Branch ID</label>
<input type="text" :value="user.branchId">
<input type="text" :value="user.branchId" disabled>

<select id="" name="" :value="user.country">
<option value="USA">USA</option>
@@ -288,5 +288,6 @@ watch(props.user, (u) => {
div.address-entry {
display: flex;
flex-flow: column;
position: relative;
}
</style>

+ 94
- 2
skouter.go Целия файл

@@ -2,6 +2,7 @@ package main

import (
"os"
"os/exec"
"net/http"
"net/mail"
"log"
@@ -12,6 +13,7 @@ import (
_ "github.com/go-sql-driver/mysql"
"fmt"
"encoding/json"
"encoding/base64"
"strconv"
"bytes"
"time"
@@ -811,7 +813,8 @@ func queryUsers(db *sql.DB, id int) ( []User, error ) {
u.status,
u.verified,
u.role,
u.address
u.address,
u.phone
FROM user u WHERE u.id = CASE @e := ? WHEN 0 THEN u.id ELSE @e END
`
rows, err = db.Query(query, id)
@@ -838,6 +841,7 @@ func queryUsers(db *sql.DB, id int) ( []User, error ) {
&user.Verified,
&user.Role,
&user.Address.Id,
&user.Phone,
)
err != nil {
return users, err
@@ -1807,7 +1811,7 @@ func showPDF(w http.ResponseWriter, r *http.Request) {

// p := r.URL.Path
db, err := sql.Open("mysql",
fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/skouter",
fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/skouter_dev",
os.Getenv("DBUser"),
os.Getenv("DBPass")))

@@ -1829,18 +1833,102 @@ func showPDF(w http.ResponseWriter, r *http.Request) {
info := struct {
Title string
Name string
Avatar string
Letterhead string
User User
}{
Title: "test PDF",
Name: "idk-random-name",
User: users[0],
}
avatar, err := fetchAvatar(db, info.User.Id)
letterhead, err := fetchLetterhead(db, info.User.Id)
info.Avatar =
base64.StdEncoding.EncodeToString(avatar)
info.Letterhead =
base64.StdEncoding.EncodeToString(letterhead)
err = pa.Execute(w, info)
if err != nil {fmt.Println(err)}

}

func getPdf(w http.ResponseWriter, db *sql.DB, r *http.Request) {
var err error
cmd := exec.Command("wkhtmltopdf", "-", "-")
stdout, err := cmd.StdoutPipe()
if err != nil {
w.WriteHeader(500);
log.Println(err)
return
}
stdin, err := cmd.StdinPipe()
if err != nil {
w.WriteHeader(500);
log.Println(err)
return
}

if err := cmd.Start(); err != nil {
log.Fatal(err)
}
var pa = template.Must(template.ParseFiles("views/pdf.tpl",
"views/test.tpl"))
// claims, err := getClaims(r)
if err != nil {
w.WriteHeader(500);
log.Println(err)
return
}
users, err := queryUsers(db, 1)
info := struct {
Title string
Name string
Avatar string
Letterhead string
User User
}{
Title: "test PDF",
Name: "idk-random-name",
User: users[0],
}
avatar, err := fetchAvatar(db, info.User.Id)
letterhead, err := fetchLetterhead(db, info.User.Id)
info.Avatar =
base64.StdEncoding.EncodeToString(avatar)
info.Letterhead =
base64.StdEncoding.EncodeToString(letterhead)
err = pa.Execute(stdin, info)
if err != nil {
w.WriteHeader(500);
log.Println(err)
return
}
stdin.Close()
buf, err := io.ReadAll(stdout)
if _, err := w.Write(buf); err != nil {
w.WriteHeader(500);
log.Println(err)
return
}
if err := cmd.Wait(); err != nil {
// w.WriteHeader(500)
log.Println(err)
return
}
}

func clipLetterhead(w http.ResponseWriter, db *sql.DB, r *http.Request) {
var validTypes []string = []string{"image/png", "image/jpeg"}
var isValidType bool
@@ -1966,6 +2054,10 @@ func api(w http.ResponseWriter, r *http.Request) {
r.Method == http.MethodPost &&
guard(r, 1):
summarize(w, db, r)
case match(p, "/api/pdf", &args) &&
r.Method == http.MethodGet &&
guard(r, 1):
getPdf(w, db, r)
default:
http.Error(w, "Invalid route or token", 404)
}


+ 14
- 0
views/pdf.tpl Целия файл

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<head>
<meta charset='utf-8'>
<meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="">
</head>

<body>
{{block "header" .}}
{{end}}

{{template "main" .}}
</body>

+ 63
- 4
views/test.tpl Целия файл

@@ -4,8 +4,67 @@
{{end}}

{{define "main"}}
<main class='fade-in-2'>
<div>hello world {{.User.Title}}</div>
<section>TESTING</section>
</main>
<div id="pdf-doc" ref="doc" v-if="estimate">
<div class="disclaimer"><p>Actual costs may vary from estimates after approval. Get an official quote before choosing a loan.</p></div>

<header class="heading">

<img src="data:image/png;base64,{{.Letterhead}}" />

<div>
<div class="user-info">
<h4>{{.User.FirstName}} {{.User.LastName}}</h4>
<span>{{.User.Email}}</span>
<span>{{.User.Phone}}</span>
<small>{{.User.Address.Street}}</small>
<small>
{{.User.Address.City}}, {{.User.Address.Region}} {{.User.Address.Zip}}
</small>
</div>
<img src="data:image/png;base64,{{.Avatar}}" />
</div>

</header>

<style scoped>
#pdf-doc {
margin: 4px 30px;
}

.disclaimer {
font-weight: bold;
border-bottom: 1px solid lightgrey;
margin-bottom: 20px;
color: var(--text);
}

.disclaimer p {
margin: 5px 0;
}

h4 {
margin: 4px 0;
}

header.heading {
display: flex;
justify-content: space-between;
}

.user-info {
display: flex;
flex-flow: column;
}

#pdf-doc header.heading > div {
display: flex;
text-align: right;
}

header.heading .user-info {
margin-right: 8px;
}
</style>
{{end}}



Loading…
Отказ
Запис