├── .gitignore ├── wrangler.toml ├── package.json ├── .github └── workflows │ └── deploy.yml ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | worker/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "generate" 2 | account_id = "2311a34ed4b9a31a0c6a154fb061327a" # invoice.workers.dev 3 | type = "webpack" 4 | workers_dev = true 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "generate-invoice", 4 | "version": "1.0.0", 5 | "description": "Generating PDF invoices with Cloudflare Workers.", 6 | "main": "index.js", 7 | "author": "Adam Schwartz ", 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | name: Deploy 12 | steps: 13 | - uses: actions/checkout@master 14 | - name: Publish 15 | uses: cloudflare/wrangler-action@1.0.0 16 | with: 17 | apiKey: ${{ secrets.apiKey }} 18 | email: ${{ secrets.email }} 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generate an Invoice PDF using Cloudflare Workers 2 | 3 | The open-source PDF generation service powering [Lazy Invoice](https://lazy.invoice.workers.dev) ([code](https://github.com/adamschwartz/lazy.invoice.workers.dev/)), an open-source website for quickly generating PDF invoices on the fly. Powered by Cloudflare Workers. 4 | 5 | [Example PDF →](https://generate.invoice.workers.dev/?company=Ginza+Natsuno%0D%0A4F+Marunouchi+1-5-1%0D%0AChiyoda-ku%2C+Tokyo%2C+Japan&customer=Sukiyabashi+Jiro%0D%0A2-15%2C+Ginza+4-chome%0D%0ACity+Chuo%2C+Tokyo%2C+Japan&description=4+pairs+of+rounded+red+sandalwood+chopsticks&amount=4+×+¥8%2C000+per+pair&total=¥32%2C000&number=373267) 6 | 7 | 8 | ## API 9 | 10 | Requests are made as `GET` against `http://generate.invoice.workers.dev`. To customize the PDF, set at least the query params `company`, `customer`, `description`, `amount`, and `total`, for example: 11 | 12 |
https://generate.invoice.workers.dev/
13 |   ?company=Ginza Natsuno \n4F Marunouchi 1-5-1 \nChiyoda-ku, Tokyo, Japan
14 |   &customer=Sukiyabashi Jiro \n2-15, Ginza 4-chome \nCity Chuo, Tokyo, Japan
15 |   &description=4 pairs of rounded red sandalwood chopsticks
16 |   &amount=4 × ¥8,000 per pair
17 |   &total=¥32,000
18 | 19 | [View this example PDF](https://generate.invoice.workers.dev/?company=Ginza+Natsuno%0D%0A4F+Marunouchi+1-5-1%0D%0AChiyoda-ku%2C+Tokyo%2C+Japan&customer=Sukiyabashi+Jiro%0D%0A2-15%2C+Ginza+4-chome%0D%0ACity+Chuo%2C+Tokyo%2C+Japan&description=4+pairs+of+rounded+red+sandalwood+chopsticks&amount=4+×+¥8%2C000+per+pair&total=¥32%2C000&number=373267) 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // TODO - add { "jspdf": "1.5.3" } dependency to package.json and make this work 2 | // import jsPDF from 'jspdf/dist/jspdf.node.min.js' 3 | import jsPDF from './vendor/jspdf-with-shimmed-globals.js' 4 | 5 | addEventListener('fetch', event => { 6 | event.respondWith(handleRequest(event)) 7 | }) 8 | 9 | async function handleRequest(event) { 10 | const doc = new jsPDF({ 11 | orientation: 'p', 12 | format: 'a4' 13 | }) 14 | 15 | const pageWidth = doc.internal.pageSize.width 16 | const ppi = 3 17 | 18 | const now = new Date() 19 | const dateToday = (now.getMonth() + 1) + '/' + now.getDate() + '/' + now.getFullYear() 20 | 21 | let currentFontType = 'normal' 22 | const setFontType = (val) => { 23 | doc.setFontType(val) 24 | currentFontType = val 25 | } 26 | 27 | const t = {} 28 | const searchParams = new URL(event.request.url).searchParams 29 | const textParams = ['company', 'customer', 'number', 'date', 'description', 'amount', 'total'] 30 | textParams.forEach(param => t[param] = searchParams.get(param) || '') 31 | 32 | const docText = (x, y, text) => { 33 | if (x > 0) return doc.text(x, y, text) 34 | return doc.text(pageWidth + x, y, text, null, null, 'right') 35 | } 36 | 37 | const getLines = (text, start, end) => text.replace(/\\n/g, '\n').split('\n').slice(start, end) 38 | 39 | doc.setFont('helvetica') 40 | setFontType('bold') 41 | doc.setFontSize(14) 42 | docText(20, 24, getLines(t.company || 'Company', 0, 1)) 43 | docText(-20, 24, t.number ? 'Invoice #' + t.number : 'INVOICE') 44 | 45 | setFontType('normal') 46 | doc.setFontSize(10) 47 | doc.setLineHeightFactor(1.3) 48 | docText(20, 30, getLines(t.company, 1)) 49 | docText(-20, 30, t.date || dateToday) 50 | 51 | docText(20, 60, getLines(t.customer || 'Customer', 0)) 52 | 53 | setFontType('bold') 54 | docText(20, 98, 'Description') 55 | doc.text(pageWidth - 20, 98, 'Amount', null, null, 'right') 56 | 57 | doc.setLineWidth(.333) 58 | doc.line(20, 102, pageWidth - 20, 102) 59 | 60 | setFontType('normal') 61 | docText(20, 108, t.description || 'Products and services') 62 | docText(-20, 108, t.amount || '$1') 63 | 64 | const formatTotal = (amount) => { 65 | let str = (amount + '').replace(/[^0-9\.\,]/g, '') 66 | let num = parseFloat(str, 10) 67 | if (Math.floor(num) === num) return num + '' 68 | return num.toFixed(2) 69 | } 70 | 71 | const totalAmount = t.total || '$' + formatTotal(t.amount || '$1') 72 | setFontType('bold') 73 | docText(-20, 128, 'Total ' + totalAmount) 74 | 75 | const output = doc.output('arraybuffer') 76 | 77 | const headers = new Headers() 78 | headers.set('Content-Type', ' application/pdf') 79 | 80 | return new Response(output, { headers }) 81 | } 82 | --------------------------------------------------------------------------------