├── README.md ├── package.json ├── worker.js └── wrangler.toml /README.md: -------------------------------------------------------------------------------- 1 | # Generate an Invoice PDF using Cloudflare Workers 2 | 3 | ## Direct PDF generation 4 | 5 | Visit https://generate.invoice.workers.dev to view a PDF generated with all of the default input values. 6 | 7 | To customize, set query params `company`, `customer`, `description`, `amount`, and `total`: 8 | 9 |
10 | https://generate.invoice.workers.dev/ 11 | ?company=Ginza Natsuno \n4F Marunouchi 1-5-1 \nChiyoda-ku, Tokyo, Japan 12 | &customer=Sukiyabashi Jiro \n2-15, Ginza 4-chome \nCity Chuo, Tokyo, Japan 13 | &description=4 pairs of rounded red sandalwood chopsticks 14 | &amount=4 × ¥8,000 per pair 15 | &total=¥32,000 16 |17 | 18 | [View 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) 19 | 20 | ## Website 21 | 22 | You can also generate a PDF using a UI by visiting [lazy.invoice.workers.dev](https://lazy.invoice.workers.dev), for which this backend Cloudflare Worker script was built. 23 | 24 | ## Customizing this project 25 | 26 | This project uses the [Workers CLI](https://workers.cloudflare.com/docs/quickstart/cli-setup/) in order to build and publish your script to Cloudflare Workers. 27 | 28 | To customize this project for your own purposes, follow these steps: 29 | 30 | 1) Install the [Workers CLI](https://workers.cloudflare.com/docs/quickstart/cli-setup/). 31 | 2) Clone the repo into a new directory. 32 | 3) Follow the [Workers configuration instructions](https://workers.cloudflare.com/docs/quickstart/configuring-and-publishing/) to configure the CLI. Essentially this amounts to running one Workers CLI command and then editing the `wrangler.toml` file in this project with your Cloudflare Account ID and the secondary subdomain of your choosing for deployment. 33 | 34 | ## License 35 | 36 | MIT 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "worker-generate-invoice-pdf", 3 | "private": "true", 4 | "version": "0.0.1", 5 | "main": "worker.js", 6 | "dependencies": { 7 | "jspdf": "1.5.3" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /worker.js: -------------------------------------------------------------------------------- 1 | // Ensure globals are available when jsPDF is required 2 | const window = { document: { createElementNS: () => { return {} } } } 3 | const navigator = {} 4 | 5 | const jsPDF = require('jspdf/dist/jspdf.node.min') 6 | 7 | addEventListener('fetch', event => { 8 | event.respondWith(handleRequest(event)) 9 | }) 10 | 11 | async function handleRequest(event) { 12 | const doc = new jsPDF({ 13 | orientation: 'p', 14 | format: 'a4' 15 | }) 16 | 17 | const pageWidth = doc.internal.pageSize.width 18 | const ppi = 3 19 | 20 | const now = new Date() 21 | const dateToday = (now.getMonth() + 1) + '/' + now.getDate() + '/' + now.getFullYear() 22 | 23 | let currentFontType = 'normal' 24 | const setFontType = (val) => { 25 | doc.setFontType(val) 26 | currentFontType = val 27 | } 28 | 29 | const t = {} 30 | const searchParams = new URL(event.request.url).searchParams 31 | const textParams = ['company', 'customer', 'number', 'date', 'description', 'amount', 'total'] 32 | textParams.forEach(param => t[param] = searchParams.get(param) || '') 33 | 34 | const docText = (x, y, text) => { 35 | if (x > 0) return doc.text(x, y, text) 36 | return doc.text(pageWidth + x, y, text, null, null, 'right') 37 | } 38 | 39 | const getLines = (text, start, end) => text.replace(/\\n/g, '\n').split('\n').slice(start, end) 40 | 41 | doc.setFont('helvetica') 42 | setFontType('bold') 43 | doc.setFontSize(14) 44 | docText(20, 24, getLines(t.company || 'Company', 0, 1)) 45 | docText(-20, 24, t.number ? 'Invoice #' + t.number : 'INVOICE') 46 | 47 | setFontType('normal') 48 | doc.setFontSize(10) 49 | doc.setLineHeightFactor(1.3) 50 | docText(20, 30, getLines(t.company, 1)) 51 | docText(-20, 30, t.date || dateToday) 52 | 53 | docText(20, 60, getLines(t.customer || 'Customer', 0)) 54 | 55 | setFontType('bold') 56 | docText(20, 98, 'Description') 57 | doc.text(pageWidth - 20, 98, 'Amount', null, null, 'right') 58 | 59 | doc.setLineWidth(.333) 60 | doc.line(20, 102, pageWidth - 20, 102) 61 | 62 | setFontType('normal') 63 | docText(20, 108, t.description || 'Products and services') 64 | docText(-20, 108, t.amount || '$1') 65 | 66 | const formatTotal = (amount) => { 67 | let str = (amount + '').replace(/[^0-9\.\,]/g, '') 68 | let num = parseFloat(str, 10) 69 | if (Math.floor(num) === num) return num + '' 70 | return num.toFixed(2) 71 | } 72 | 73 | const totalAmount = t.total || '$' + formatTotal(t.amount || '$1') 74 | setFontType('bold') 75 | docText(-20, 128, 'Total ' + totalAmount) 76 | 77 | const output = doc.output('arraybuffer') 78 | 79 | const headers = new Headers() 80 | headers.set('Content-Type', ' application/pdf') 81 | 82 | return new Response(output, { headers }) 83 | } 84 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "YOUR_SCRIPT_NAME" 2 | account_id= "YOUR_ACOUNT_ID" 3 | type = "webpack" --------------------------------------------------------------------------------