En la actualidad, muchas de las aplicaciones que se hacen para el móvil tienen que ver con el trabajo exterior a la empresas y casi siempre es relacionado con el Cliente, de tal forma que es muy habitual la recogida de la firma del Cliente, como garantía de la aceptación del servicio.
En este caso, el ejemplo se basa en mostrar lo sencillo que es la recepción de la firma (Cliente, Técnico, etc.) en esta plataforma de React.
Objetivo
Mostrar un ejemplo básico de una aplicación realizada en React para la captura de la firma manuscrita del (Cliente, Técnico, etc.)
DEMO: https://fhumanes.com/signature-test/
(1) (2) (3) Pasos a seguir en el ejemplo
He hecho un programa PHPRunner para que podáis comprobar que el contenido está almacenado en la base de datos. https://fhumanes.com/signature-php
Solución Técnica
He querido dejar muy, muy simple el ejemplo para que podáis ver lo sencillo que es trabajar en esta plataforma.
La firma se recoge en formato PNG y se envía al server como un campo más de un formulario en formato «Base 64». Como son imágenes pequeñas, no hay ningún problema en utilizar este sistema de transformación a base 64.
El código más relevante de React es el fichero App.js:
import React, { useRef, useState } from 'react'; import SignatureCanvas from 'react-signature-canvas'; import axios from 'axios'; import Config from './config'; // Función para recortar manualmente el canvas const trimCanvas = (canvas) => { const ctx = canvas.getContext('2d'); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; let top = 0, bottom = canvas.height, left = 0, right = canvas.width; // Encontrar el borde superior topLoop: for (; top < bottom; top++) { for (let x = 0; x < right; x++) { const alpha = data[(top * canvas.width + x) * 4 + 3]; if (alpha > 0) break topLoop; } } // Encontrar el borde inferior bottomLoop: for (; bottom > top; bottom--) { for (let x = 0; x < right; x++) { const alpha = data[((bottom - 1) * canvas.width + x) * 4 + 3]; if (alpha > 0) break bottomLoop; } } // Encontrar el borde izquierdo leftLoop: for (; left < right; left++) { for (let y = top; y < bottom; y++) { const alpha = data[(y * canvas.width + left) * 4 + 3]; if (alpha > 0) break leftLoop; } } // Encontrar el borde derecho rightLoop: for (; right > left; right--) { for (let y = top; y < bottom; y++) { const alpha = data[(y * canvas.width + (right - 1)) * 4 + 3]; if (alpha > 0) break rightLoop; } } // Crear nuevo canvas con las dimensiones recortadas const trimmedCanvas = document.createElement('canvas'); trimmedCanvas.width = right - left; trimmedCanvas.height = bottom - top; const trimmedCtx = trimmedCanvas.getContext('2d'); trimmedCtx.drawImage( canvas, left, top, right - left, bottom - top, 0, 0, right - left, bottom - top ); return trimmedCanvas; }; const SignaturePad = () => { const sigCanvas = useRef(null); const [imageURL, setImageURL] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [submitMessage, setSubmitMessage] = useState(''); // Limpiar el canvas de firma const clearSignature = () => { sigCanvas.current.clear(); setImageURL(null); setSubmitMessage(''); }; // Guardar la firma como imagen PNG en Base64 /* // Reduciendo el tamaño de la imagen const saveSignature = () => { if (sigCanvas.current.isEmpty()) { alert('Por favor, proporciona tu firma primero'); return; } const canvas = sigCanvas.current.getCanvas(); const trimmedCanvas = trimCanvas(canvas); const image = trimmedCanvas.toDataURL('image/png'); setImageURL(image); setSubmitMessage('Firma guardada y recortada, lista para enviar'); }; */ const saveSignature = () => { if (sigCanvas.current.isEmpty()) { alert('Por favor, proporciona tu firma primero'); return; } // Enfoque alternativo const canvas = sigCanvas.current.getCanvas(); const image = canvas.toDataURL('image/png'); setImageURL(image); setSubmitMessage('Firma guardada, lista para enviar'); }; // Enviar la firma al servidor const submitSignature = async () => { if (!imageURL) { alert('Por favor, guarda tu firma primero'); return; } setIsSubmitting(true); setSubmitMessage('Enviando firma...'); try { // Extraer solo la parte Base64 de la URL de datos const base64Image = imageURL.split(',')[1]; const url = Config.RUTA_API + "/saveFirma"; const formData = new FormData(); formData.append('signature', base64Image); formData.append('timestamp', new Date().toISOString()); const config = { headers: { "Content-Type": "application/x-www-form-urlencoded", "Authorization": Config.AUTHORIZATION }, }; // Reemplaza esta URL con tu endpoint real const response = await axios.post( url, formData, config ); setSubmitMessage('Firma enviada con éxito!'); console.log('Respuesta del servidor:', response.data); } catch (error) { console.error('Error al enviar la firma:', error); setSubmitMessage('Error al enviar la firma'); } finally { setIsSubmitting(false); } }; return ( <div style={styles.container}> <h1>Captura de Firma</h1> <div style={styles.signatureContainer}> <SignatureCanvas ref={sigCanvas} canvasProps={{ width: 500, height: 200, className: 'signature-canvas', style: styles.canvas }} penColor="black" /> </div> <div style={styles.buttonGroup}> <button style={styles.button} onClick={clearSignature}> Limpiar Firma </button> <button style={styles.button} onClick={saveSignature}> Guardar Firma </button> <button style={styles.button} onClick={submitSignature} disabled={isSubmitting || !imageURL} > {isSubmitting ? 'Enviando...' : 'Enviar Firma'} </button> </div> {submitMessage && ( <p style={submitMessage.includes('éxito') ? styles.success : styles.message}> {submitMessage} </p> )} {imageURL && ( <div style={styles.preview}> <h3>Previsualización de la firma:</h3> <img src={imageURL} alt="Firma guardada" style={styles.previewImage} /> </div> )} </div> ); }; // Estilos const styles = { container: { maxWidth: '600px', margin: '0 auto', padding: '20px', fontFamily: 'Arial, sans-serif', textAlign: 'center', }, signatureContainer: { border: '1px solid #ccc', borderRadius: '4px', margin: '20px 0', backgroundColor: '#f9f9f9', }, canvas: { backgroundColor: '#f9f9f9', }, buttonGroup: { display: 'flex', justifyContent: 'center', gap: '10px', margin: '20px 0', }, button: { padding: '10px 15px', backgroundColor: '#4CAF50', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '16px', }, preview: { marginTop: '30px', }, previewImage: { border: '1px solid #ddd', borderRadius: '4px', padding: '5px', maxWidth: '100%', }, message: { color: '#666', margin: '10px 0', }, success: { color: 'green', margin: '10px 0', fontWeight: 'bold', }, }; export default SignaturePad;
El ejemplo que recoge los datos está hecho en PHP (sin integrar con PHPRunner), es decir, «puro» PHP.
La clase que gestiona los accesos a la base de datos es «Dbsignature.php»:
<?php // include_once '../include/Config.php'; // Configuration Rest Api /** * * @About: Gestión de Datos de Gestión de firmas * @File: Dbsignature.php * @Date: $Date:$ April 2025 * @Version: $Rev:$ 1.0 * @Developer: fernando humanes **/ use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Selective\BasePath\BasePathMiddleware; use Slim\Factory\AppFactory; class Dbsignature { private $connDB; function __construct() { /* You should enable error reporting for mysqli before attempting to make a connection */ mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); $mysqli = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME); /* Set the desired charset after establishing a connection */ $mysqli->set_charset('utf8mb4'); $this->connDB = $mysqli; // No se usa esta conexión si está integrado con PHPRunner } private function responseKO ($data_arr) { global $errorMessages; $data_arr['error'] = true; $data_arr["message_num"] = '005'; $data_arr["message"] = str_replace("?",$this->connDB->error,$errorMessages['005']); $this->connDB->close(); return $data_arr; } private function responseOK ($data_arr) { global $errorMessages; $data_arr['error'] = false; $data_arr["message_num"] = '006'; $data_arr["message"] = $errorMessages['006']; $this->connDB->close(); return $data_arr; } /* * ADD de Firmas */ public function addSignature($param, Request $request,Response $response) { global $errorMessages; $signature = $param['signature']; $timestamp = $param['timestamp']; // Decodificar la cadena Base64 $imageData = base64_decode($signature); $filename = uniqid('firma_') . '.png'; // Convesión de fecha y adaptación a zona Horaria $dt_obj = new DateTime($timestamp ." UTC"); // La fecha la recibimos en formato UTC $dt_obj->setTimezone(new DateTimeZone('Europe/Madrid')); // La adaptamos al formato del servidor $timestamp = date_format($dt_obj, 'Y-m-d H:i:s'); $sql = " INSERT INTO signature_react (`file`, `nameFile`, `creationDate`) VALUES (?,?,?)"; $data_arr = array(); $stmt = $this->connDB->prepare($sql); $stmt->bind_param('bss',$imageData, $filename, $timestamp); // Para enviar el BLOB en bloques (útil para archivos grandes) $stmt->send_long_data(0, $imageData); $stmt->execute(); return $this->responseOK($data_arr); } }
Como siempre, os dejo los fuentes para que podáis configurarlos en vuestro PC y ajustéis lo que necesitéis.