├── 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 |
118 | 119 | 🇫🇷 Le gouvernement a publié son propre générateur ! 120 | 121 | Il a été développé à partir de mon code (🥳), la confidentialité de vos données reste donc identique, et 122 | il vous permettra de présenter votre attestation sur smartphone. 123 |
124 | 125 | 126 | Mon outil restera en place mais je ne continuerai pas à le maintenir, je l'avais créé afin de faciliter le 127 | remplissage des attestations, maintenant il ne me semble plus nécessaire. 128 |