├── LICENCE ├── README.md ├── certificate.js ├── certificate.pdf ├── favicons ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── mstile-150x150.png ├── safari-pinned-tab.svg └── site.webmanifest └── index.html /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) Johann Pardanaud 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [🇫🇷 This project was reused by the French governement to create their own certificate generator. 🇫🇷](https://github.com/LAB-MI/deplacement-covid-19) 2 | 3 | The code was written by using these awesome open source projects: 4 | 5 | - [PDF-LIB](https://pdf-lib.js.org/) 6 | - [Signature Pad](https://github.com/szimek/signature_pad) 7 | - [Bootstrap](https://getbootstrap.com/) 8 | - [Font Awesome](https://fontawesome.com/license) 9 | - [Twemoji](https://twemoji.twitter.com/) 10 | -------------------------------------------------------------------------------- /certificate.js: -------------------------------------------------------------------------------- 1 | const { PDFDocument, StandardFonts } = PDFLib 2 | 3 | const $ = (...args) => document.querySelector(...args) 4 | const $$ = (...args) => document.querySelectorAll(...args) 5 | const signaturePad = new SignaturePad($('#field-signature'), { minWidth: 1, maxWidth: 3 }) 6 | 7 | function hasProfile() { 8 | return localStorage.getItem('name') !== null 9 | } 10 | 11 | function saveProfile() { 12 | for (field of $$('#form-profile input:not([disabled]):not([type=checkbox])')) { 13 | localStorage.setItem(field.id.substring('field-'.length), field.value) 14 | } 15 | 16 | localStorage.setItem('signature', signaturePad.toDataURL()) 17 | } 18 | 19 | function getProfile() { 20 | const fields = {} 21 | for (let i = 0; i < localStorage.length; i++){ 22 | const name = localStorage.key(i) 23 | fields[name] = localStorage.getItem(name) 24 | } 25 | return fields 26 | } 27 | 28 | async function generatePdf(profile, reason) { 29 | const url = 'certificate.pdf?v=bfc885e5326a9e0d3184aed9d183bca20a9cd76f' 30 | const existingPdfBytes = await fetch(url).then(res => res.arrayBuffer()) 31 | 32 | const pdfDoc = await PDFDocument.load(existingPdfBytes) 33 | const page = pdfDoc.getPages()[0] 34 | 35 | const font = await pdfDoc.embedFont(StandardFonts.Helvetica) 36 | const drawText = (text, x, y, size = 11) => { 37 | page.drawText(text, {x, y, size, font}) 38 | } 39 | 40 | drawText(profile.name, 125, 685) 41 | drawText(profile.birthday, 125, 661) 42 | drawText(profile.birthplace || '', 95, 637) 43 | drawText(`${profile.address} ${profile.zipcode} ${profile.town}`, 140, 613) 44 | 45 | switch (reason) { 46 | case 'work': 47 | drawText('x', 76.5, 526, 20) 48 | break 49 | case 'groceries': 50 | drawText('x', 76.5, 476.5, 20) 51 | break 52 | case 'health': 53 | drawText('x', 76.5, 436, 20) 54 | break 55 | case 'family': 56 | drawText('x', 76.5, 399.5, 20) 57 | break 58 | case 'sport': 59 | drawText('x', 76.5, 344, 20) 60 | break 61 | case 'notification': 62 | drawText('x', 76.5, 297, 20) 63 | break 64 | case 'mission': 65 | drawText('x', 76.5, 261, 20) 66 | break 67 | } 68 | 69 | drawText(profile['done-at'] || profile.town, 110, 225) 70 | 71 | if (reason !== '') { 72 | const date = [ 73 | String((new Date).getDate()).padStart(2, '0'), 74 | String((new Date).getMonth() + 1).padStart(2, '0'), 75 | String((new Date).getFullYear()), 76 | ].join('/') 77 | 78 | drawText(date, 105, 201) 79 | drawText(String((new Date).getHours()).padStart(2, '0'), 195, 201) 80 | 81 | // Round the minutes to the lower X0 or X5 value, so it feels more human. 82 | const minutes = Math.floor((new Date).getMinutes() / 5) * 5; 83 | drawText(String(minutes).padStart(2, '0'), 225, 201) 84 | } 85 | 86 | const signatureArrayBuffer = await fetch(profile.signature).then(res => res.arrayBuffer()) 87 | const signatureImage = await pdfDoc.embedPng(signatureArrayBuffer) 88 | const signatureDimensions = signatureImage.scale(1 / (signatureImage.width / 80)) 89 | 90 | page.drawImage(signatureImage, { 91 | x: page.getWidth() - signatureDimensions.width - 380, 92 | y: 130, 93 | width: signatureDimensions.width, 94 | height: signatureDimensions.height, 95 | }) 96 | 97 | const pdfBytes = await pdfDoc.save() 98 | return new Blob([pdfBytes], {type: 'application/pdf'}) 99 | } 100 | 101 | function downloadBlob(blob, fileName) { 102 | const link = document.createElement('a') 103 | var url = URL.createObjectURL(blob) 104 | link.href = url 105 | link.download = fileName 106 | link.click() 107 | } 108 | 109 | function getAndSaveReason() { 110 | const {value} = $('input[name="field-reason"]:checked') 111 | localStorage.setItem('last-reason', value) 112 | return value 113 | } 114 | 115 | function restoreReason() { 116 | const value = localStorage.getItem('last-reason') 117 | if (value === null) { 118 | return 119 | } 120 | 121 | $(`#radio-${value}`).checked = true 122 | } 123 | 124 | // see: https://stackoverflow.com/a/32348687/1513045 125 | function isFacebookBrowser() { 126 | const ua = navigator.userAgent || navigator.vendor || window.opera 127 | return (ua.indexOf("FBAN") > -1) || (ua.indexOf("FBAV") > -1) 128 | } 129 | 130 | function applyDoneAt() { 131 | const { checked } = $('#check-same-town') 132 | $('#group-done-at').style.display = checked ? 'none' : 'block'; 133 | $('#field-done-at').disabled = checked; 134 | } 135 | 136 | if (isFacebookBrowser()) { 137 | $('#alert-facebook').style.display = 'block'; 138 | } 139 | 140 | $('#alert-official .close').addEventListener('click', ({ target }) => { 141 | target.offsetParent.style.display = 'none' 142 | localStorage.setItem('dismiss-official-alert', true) 143 | }) 144 | 145 | if (localStorage.getItem('dismiss-official-alert')) { 146 | $('#alert-official').style.display = 'none' 147 | } 148 | 149 | if (hasProfile()) { 150 | $('#form-generate').style.display = 'block' 151 | } else { 152 | $('#form-profile').style.display = 'block' 153 | } 154 | 155 | $('#form-profile').addEventListener('submit', event => { 156 | event.preventDefault() 157 | saveProfile() 158 | location.reload() 159 | }) 160 | 161 | $('#date-selector').addEventListener('change', ({ target }) => { 162 | $('#field-birthday').value = target.value.split('-').reverse().join('/') 163 | }) 164 | 165 | $('#check-same-town').addEventListener('change', applyDoneAt) 166 | applyDoneAt() 167 | 168 | const formWidth = $('#form-profile').offsetWidth 169 | $('#field-signature').width = formWidth 170 | $('#field-signature').height = formWidth / 1.5 171 | 172 | $('#reset-signature').addEventListener('click', () => signaturePad.clear()) 173 | 174 | $('#form-generate').addEventListener('submit', async event => { 175 | event.preventDefault() 176 | 177 | const button = event.target.querySelector('button[type=submit]') 178 | button.disabled = true 179 | 180 | const reason = getAndSaveReason() 181 | const profile = getProfile() 182 | 183 | if (profile.birthplace === undefined) { 184 | const birthplace = prompt([ 185 | `La nouvelle attestation, en date du 25 mars, exige maintenant le lieu de naissance et votre profil ne contient`, 186 | `actuellement pas cette information, merci de compléter :`, 187 | ].join(' ')) 188 | 189 | if (birthplace) { 190 | profile.birthplace = birthplace 191 | localStorage.setItem('birthplace', birthplace) 192 | } 193 | } 194 | 195 | const pdfBlob = await generatePdf(profile, reason) 196 | button.disabled = false 197 | 198 | downloadBlob(pdfBlob, 'attestation.pdf') 199 | }) 200 | 201 | restoreReason() 202 | -------------------------------------------------------------------------------- /certificate.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nesk/covid-19-certificate/105ddfaa84af685067d62972505cc6f821d6c5b3/certificate.pdf -------------------------------------------------------------------------------- /favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nesk/covid-19-certificate/105ddfaa84af685067d62972505cc6f821d6c5b3/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nesk/covid-19-certificate/105ddfaa84af685067d62972505cc6f821d6c5b3/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nesk/covid-19-certificate/105ddfaa84af685067d62972505cc6f821d6c5b3/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #603cba 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nesk/covid-19-certificate/105ddfaa84af685067d62972505cc6f821d6c5b3/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nesk/covid-19-certificate/105ddfaa84af685067d62972505cc6f821d6c5b3/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nesk/covid-19-certificate/105ddfaa84af685067d62972505cc6f821d6c5b3/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | COVID-19 – Générateur d'attestation de déplacement 18 | 19 | 108 | 109 | 110 |

COVID-19 – Générateur d'attestation de déplacement

111 | 112 | 131 | 132 | 206 | 207 | 271 | 272 | 281 | 282 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | --------------------------------------------------------------------------------