update signature validation

pull/475/head
Alex Turchyn 7 months ago committed by Pete Matsyburka
parent 7e70f4fb14
commit b0110d9340

@ -1,5 +1,6 @@
import SignaturePad from 'signature_pad'
import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas'
import { isValidSignatureCanvas } from './submission_form/validate_signature'
window.customElements.define('draw-signature', class extends HTMLElement {
connectedCallback () {
@ -43,6 +44,8 @@ window.customElements.define('draw-signature', class extends HTMLElement {
return response
})
}).catch(error => {
console.log(error)
}).finally(() => {
this.submitButton.disabled = false
})
@ -65,26 +68,26 @@ window.customElements.define('draw-signature', class extends HTMLElement {
}
async submitImage () {
return new Promise((resolve, reject) => {
cropCanvasAndExportToPNG(this.canvas, { errorOnTooSmall: true }).then(async (blob) => {
const file = new File([blob], 'signature.png', { type: 'image/png' })
const formData = new FormData()
formData.append('file', file)
formData.append('submitter_slug', this.dataset.slug)
formData.append('name', 'attachments')
formData.append('remember_signature', 'true')
return fetch('/api/attachments', {
method: 'POST',
body: formData
}).then((resp) => resp.json()).then((attachment) => {
return resolve(attachment)
})
}).catch((error) => {
return reject(error)
})
if (!isValidSignatureCanvas(this.pad.toData())) {
alert('Signature is too small or simple. Please redraw.')
return Promise.reject(new Error('Image too small or simple'))
}
return cropCanvasAndExportToPNG(this.canvas).then(async (blob) => {
const file = new File([blob], 'signature.png', { type: 'image/png' })
const formData = new FormData()
formData.append('file', file)
formData.append('submitter_slug', this.dataset.slug)
formData.append('name', 'attachments')
formData.append('remember_signature', 'true')
return fetch('/api/attachments', {
method: 'POST',
body: formData
}).then(resp => resp.json())
})
}

@ -1,4 +1,4 @@
function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSmall: false }) {
function cropCanvasAndExportToPNG (canvas) {
const ctx = canvas.getContext('2d')
const width = canvas.width
@ -33,10 +33,6 @@ function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSma
croppedCanvas.height = croppedHeight
const croppedCtx = croppedCanvas.getContext('2d')
if (errorOnTooSmall && (croppedWidth < 20 || croppedHeight < 20)) {
return Promise.reject(new Error('Image too small'))
}
croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight)
return new Promise((resolve, reject) => {

@ -1445,11 +1445,7 @@ export default {
this.isSubmittingComplete = false
})
}).catch(error => {
if (error?.message === 'Image too small') {
alert(this.t('signature_is_too_small_please_redraw'))
} else {
console.log(error)
}
console.log(error)
}).finally(() => {
this.isSubmitting = false
this.isSubmittingComplete = false

@ -93,7 +93,7 @@ const en = {
reupload: 'Reupload',
upload: 'Upload',
files: 'Files',
signature_is_too_small_please_redraw: 'Signature is too small. Please redraw.',
signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.',
wait_countdown_seconds: 'Wait {countdown} seconds'
}
@ -191,7 +191,7 @@ const es = {
reupload: 'Volver a subir',
upload: 'Subir',
files: 'Archivos',
signature_is_too_small_please_redraw: 'La firma es demasiado pequeña. Por favor, dibújala de nuevo.',
signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.',
wait_countdown_seconds: 'Espera {countdown} segundos'
}
@ -289,7 +289,7 @@ const it = {
reupload: 'Ricarica',
upload: 'Carica',
files: 'File',
signature_is_too_small_please_redraw: 'La firma è troppo piccola. Ridisegnala per favore.',
signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.',
wait_countdown_seconds: 'Attendi {countdown} secondi'
}
@ -387,7 +387,7 @@ const de = {
reupload: 'Erneut hochladen',
upload: 'Hochladen',
files: 'Dateien',
signature_is_too_small_please_redraw: 'Die Unterschrift ist zu klein. Bitte erneut zeichnen.',
signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte erneut zeichnen.',
wait_countdown_seconds: 'Warte {countdown} Sekunden'
}
@ -485,7 +485,7 @@ const fr = {
reupload: 'Recharger',
upload: 'Télécharger',
files: 'Fichiers',
signature_is_too_small_please_redraw: 'La signature est trop petite. Veuillez la redessiner.',
signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.',
wait_countdown_seconds: 'Attendez {countdown} secondes'
}
@ -583,7 +583,7 @@ const pl = {
reupload: 'Ponowne przesłanie',
upload: 'Przesyłanie',
files: 'Pliki',
signature_is_too_small_please_redraw: 'Podpis jest zbyt mały. Proszę narysować go ponownie.'
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.'
}
const uk = {
@ -680,7 +680,7 @@ const uk = {
reupload: 'Перезавантажити',
upload: 'Завантажити',
files: 'Файли',
signature_is_too_small_please_redraw: 'Підпис занадто малий. Будь ласка, перемалюйте його.',
signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.',
wait_countdown_seconds: 'Зачекайте {countdown} секунд'
}
@ -778,7 +778,7 @@ const cs = {
reupload: 'Znovu nahrát',
upload: 'Nahrát',
files: 'Soubory',
signature_is_too_small_please_redraw: 'Podpis je příliš malý. Prosím, překreslete ho.',
signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.',
wait_countdown_seconds: 'Počkejte {countdown} sekund'
}
@ -876,7 +876,7 @@ const pt = {
reupload: 'Reenviar',
upload: 'Carregar',
files: 'Arquivos',
signature_is_too_small_please_redraw: 'A assinatura é muito pequena. Por favor, redesenhe-a.',
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
wait_countdown_seconds: 'Aguarde {countdown} segundos'
}
@ -975,7 +975,7 @@ const he = {
reupload: 'העלה שוב',
upload: 'העלאה',
files: 'קבצים',
signature_is_too_small_please_redraw: 'החתימה קטנה מדי. אנא צייר מחדש.',
signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.',
wait_countdown_seconds: 'המתן {countdown} שניות'
}
@ -1074,7 +1074,7 @@ const nl = {
reupload: 'Opnieuw uploaden',
upload: 'Uploaden',
files: 'Bestanden',
signature_is_too_small_please_redraw: 'De handtekening is te klein. Teken deze opnieuw, alstublieft.',
signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.',
wait_countdown_seconds: 'Wacht {countdown} seconden'
}
@ -1172,7 +1172,7 @@ const ar = {
reupload: 'إعادة التحميل',
upload: 'تحميل',
files: 'الملفات',
signature_is_too_small_please_redraw: 'التوقيع صغير جدًا. يرجى إعادة الرسم.',
signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.',
wait_countdown_seconds: 'انتظر {countdown} ثانية'
}
@ -1269,7 +1269,7 @@ const ko = {
reupload: '다시 업로드',
upload: '업로드',
files: '파일',
signature_is_too_small_please_redraw: '서명이 너무 작니다. 다시 그려주세요.',
signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.',
wait_countdown_seconds: '{countdown}초 기다리세요'
}

@ -118,11 +118,17 @@
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
class="mx-auto bg-white border border-base-300 rounded max-h-44"
>
<canvas
v-show="!modelValue && !computedPreviousValue"
ref="canvas"
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
/>
<div class="relative">
<div
v-if="!isDrawInitials"
class="absolute top-0 right-0 left-0 bottom-0"
/>
<canvas
v-show="!modelValue && !computedPreviousValue"
ref="canvas"
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
/>
</div>
<input
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
id="initials_text_input"

@ -292,6 +292,7 @@
<script>
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue'
import { cropCanvasAndExportToPNG } from './crop_canvas'
import { isValidSignatureCanvas } from './validate_signature'
import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on'
import FileDropzone from './dropzone'
@ -680,8 +681,14 @@ export default {
return Promise.resolve({})
}
return new Promise((resolve, reject) => {
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => {
if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
return Promise.reject(new Error('Image too small or simple'))
}
return new Promise((resolve) => {
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
const file = new File([blob], 'signature.png', { type: 'image/png' })
if (this.dryRun) {
@ -717,12 +724,6 @@ export default {
return resolve(attachment)
})
}
}).catch((error) => {
if (error.message === 'Image too small' && this.field.required === false) {
return resolve({})
} else {
return reject(error)
}
})
})
}

@ -0,0 +1,38 @@
function isValidSignatureCanvas (data) {
if (data.length === 0) return false
const strokes = data.filter(stroke => Array.isArray(stroke.points) && stroke.points.length > 2)
if (strokes.length === 0) return false
let skippedStraightLine = 0
const validStrokes = strokes.filter(stroke => {
const points = stroke.points
const first = points[0]
const last = points[points.length - 1]
const A = last.y - first.y
const B = first.x - last.x
const C = last.x * first.y - first.x * last.y
const lineLength = Math.sqrt(A * A + B * B)
const totalDeviation = points.reduce((sum, p) => {
const distanceToLine = Math.abs(A * p.x + B * p.y + C) / lineLength
return sum + distanceToLine
}, 0)
const avgDeviation = totalDeviation / points.length
if (avgDeviation < 3 && skippedStraightLine < 2) {
skippedStraightLine++
return false
}
return true
})
return validStrokes.length > 0
}
export { isValidSignatureCanvas }

@ -4,21 +4,49 @@ module SigningFormHelper
module_function
def draw_canvas
page.find('canvas').click([], { x: 150, y: 100 })
page.execute_script <<~JS
const canvas = document.getElementsByTagName('canvas')[0];
const ctx = canvas.getContext('2d');
const rect = canvas.getBoundingClientRect();
ctx.beginPath();
ctx.moveTo(150, 100);
ctx.lineTo(450, 100);
ctx.stroke();
const startX = rect.left + 50;
const startY = rect.top + 100;
ctx.beginPath();
ctx.moveTo(150, 100);
ctx.lineTo(150, 150);
ctx.stroke();
const amplitude = 20;
const wavelength = 30;
const length = 300;
function dispatchPointerEvent(type, x, y) {
const event = new PointerEvent(type, {
pointerId: 1,
pointerType: 'pen',
isPrimary: true,
clientX: x,
clientY: y,
bubbles: true,
pressure: 0.5
});
canvas.dispatchEvent(event);
}
dispatchPointerEvent('pointerdown', startX, startY);
let x = 0;
function drawStep() {
if (x > length) {
dispatchPointerEvent('pointerup', startX + x, startY);
return;
}
const y = startY + amplitude * Math.sin((x / wavelength) * 2 * Math.PI);
dispatchPointerEvent('pointermove', startX + x, y);
x += 5;
requestAnimationFrame(drawStep);
}
drawStep();
JS
sleep 0.5
end

@ -360,6 +360,19 @@ RSpec.describe 'Signing Form', type: :system do
expect(field_value(submitter, 'Signature')).to be_present
end
it 'shows an error message if the canvas is not drawn or too simple' do
visit submit_form_path(slug: submitter.slug)
find('#expand_form_button').click
page.find('canvas').click([], { x: 150, y: 100 })
alert_text = page.accept_alert do
click_button 'Sign and Complete'
end
expect(alert_text).to eq 'Signature is too small or simple. Please redraw.'
end
it 'completes the form if the canvas is typed' do
visit submit_form_path(slug: submitter.slug)

Loading…
Cancel
Save