, ,

Fini les impressions ! Signez vos documents directement dans Google Docs

En tant qu’utilisateurs de Google Workspace, nous avons tous connu ce moment : vous recevez un document important dans Google Docs – un contrat, une facture, une autorisation – et là, on vous demande de le « signer et de le renvoyer ».

Commence alors le parcours habituel : télécharger le PDF, l’imprimer, le signer avec un vrai stylo (un objet presque antique !), le scanner, et enfin le renvoyer par e-mail. C’est long, fastidieux et pas très écologique.

Et si je vous disais que vous pouviez créer votre propre bouton « Insérer la signature » directement dans Google Docs ?

Aujourd’hui, nous allons mettre les mains dans le moteur de Google Workspace grâce à Google Apps Script pour créer un outil simple mais incroyablement pratique : un module de signature manuscrite.

Pas de panique si vous n’avez jamais codé ! Ce tutoriel est conçu pour être suivi étape par étape.

Qu’est-ce que Google Apps Script ?

Pensez à Google Apps Script comme à la « baguette magique » de Google Workspace. C’est un langage de script (basé sur JavaScript) qui vous permet d’automatiser des tâches, de créer des menus personnalisés et de faire communiquer vos applications (Docs, Sheets, Gmail, etc.) entre elles.

Aujourd’hui, nous allons l’utiliser pour ajouter un menu à Google Docs qui ouvrira une petite fenêtre où vous pourrez dessiner votre signature et l’insérer d’un clic.

Menu Signature
Menu Signature

Le tutoriel : Créer votre module de signature

Suivez simplement ces étapes. En moins de 5 minutes, votre outil sera prêt.

Étape 1 : Ouvrir l’éditeur de script

  1. Ouvrez un nouveau document Google Docs (ou un document existant où vous souhaitez ajouter cette fonctionnalité).
  2. Dans le menu supérieur, cliquez sur Extensions > Apps Script.

Une nouvelle fenêtre (l’éditeur de script) va s’ouvrir. Donnez un nom à votre projet en haut à gauche (par exemple, « Mon Module Signature »).

Étape 2 : Le code « Cerveau » (Code.gs)

L’éditeur vous ouvre par défaut un fichier nommé Code.gs. C’est le script « côté serveur », celui qui gère la création du menu et l’insertion de l’image.

  1. Effacez tout le contenu présent dans le fichier Code.gs.
  2. Copiez l’intégralité du code ci-dessous et collez-le dans le fichier Code.gs.

JavaScript

/**
 * @file        Code.gs
 * @description Script principal (côté serveur) pour le module d'insertion
 *              de signature dans Google Docs.
 * Ce fichier gère :
 * - La création du menu personnalisé à l'ouverture.
 * - L'affichage de la modale (interface utilisateur HTML).
 * - La réception et l'insertion de l'image de signature
 * dans le document actif.
 *
 * @author      Fabrice Faucheux
 * @version     1.0.0
 * @date        16/11/2025
 *
 * @license     MIT
 *
 * @OnlyCurrentDoc
 */

/**
 * --- LICENCE MIT ---
 *
 * Copyright (c) 2025 Fabrice Faucheux
 *
 * L'autorisation est accordée, sans frais, à toute personne obtenant une copie
 * de ce logiciel et des fichiers de documentation associés (le "Logiciel"),
 * de traiter le Logiciel sans restriction, y compris, sans s'y limiter,
 * les droits d'utiliser, de copier, de modifier, de fusionner, de publier,
 * de distribuer, de sous-licencier et/ou de vendre des copies du Logiciel,
 * et de permettre aux personnes auxquelles le Logiciel est fourni de le faire,
 * sous réserve des conditions suivantes :
 *
 * La notification de copyright ci-dessus et la présente notification d'autorisation
 * doivent être incluses dans toutes les copies ou parties substantielles du Logiciel.
 *
 * LE LOGICIEL EST FOURNI "TEL QUEL", SANS GARANTIE D'AUCUNE SORTE, EXPRESSE OU
 * IMPLICITE, Y COMPRIS, MAIS SANS S'Y LIMITER, LES GARANTIES DE QUALITÉ MARCHANDE,
 * D'ADÉQUATION À UN USAGE PARTICULIER ET D'ABSENCE DE CONTREFAÇON.
 * EN AUCUN CAS, LES AUTEURS OU TITULAIRES DE DROITS D'AUTEUR NE SERONT RESPONSABLES
 * DE TOUTE RÉCLAMATION, DOMMAGE OU AUTRE RESPONSABILITÉ, QUE CE SOIT DANS LE CADRE
 * D'UNE ACTION CONTRACTUELLE, DÉLICTUELLE OU AUTRE, DÉCOULANT DE, OU LIÉE AU
 * LOGICIEL OU À L'UTILISATION OU À D'AUTRES OPÉRATIONS DANS LE LOGICIEL.
 */

/**
 * Constante globale pour le nom du menu.
 * Utiliser des constantes pour les chaînes de caractères répétées améliore la maintenance.
 */
const NOM_MENU = '✍️ Signature';
const FICHIER_HTML_MODALE = 'Signature';

/**
 * S'exécute à l'ouverture du document pour ajouter le menu personnalisé.
 * @param {object} e - L'objet événement (non utilisé ici, mais standard pour onOpen).
 */
function onOpen(e) {
  DocumentApp.getUi()
    .createMenu(NOM_MENU)
    .addItem('Insérer la signature', 'afficherModaleSignature')
    .addToUi();
}

/**
 * Affiche la boîte de dialogue modale (pop-up) en utilisant le fichier HTML.
 */
function afficherModaleSignature() {
  try {
    // Crée la sortie HTML à partir du fichier spécifié
    const sortieHtml = HtmlService.createHtmlOutputFromFile(FICHIER_HTML_MODALE)
      .setWidth(400)
      .setHeight(300); // Hauteur suffisante pour le canvas et les boutons

    // Affiche la modale
    DocumentApp.getUi().showModalDialog(sortieHtml, 'Insérer votre signature');
    
  } catch (erreur) {
    // Capture l'erreur (par exemple, si le fichier 'Signature.html' n'existe pas)
    Logger.log(`Erreur lors de l'ouverture de la modale : ${erreur.message}`);
    // Informe l'utilisateur d'un problème
    DocumentApp.getUi().alert(
      'Erreur Critique',
      `Impossible d'ouvrir le module de signature. Veuillez contacter l'administrateur. (Erreur : ${erreur.message})`,
      DocumentApp.getUi().ButtonSet.OK
    );
  }
}

/**
 * Insère l'image de la signature (encodée en Base64) dans le document.
 * Cette fonction est appelée depuis le client (HTML).
 *
 * @param {string} donneesImage - L'image encodée en Base64 (data URL, format 'data:image/png;base64,...').
 * @returns {object} Un objet confirmant le succès (utile pour le client).
 * @throws {Error} Renvoie une erreur si les données sont invalides ou si l'insertion échoue.
 */
function insererSignature(donneesImage) {
  try {
    // 1. Validation robuste des données d'entrée
    if (!donneesImage || typeof donneesImage !== 'string' || !donneesImage.startsWith('data:image/png;base64,')) {
      throw new Error("Format de données d'image non valide. Attendu : 'data:image/png;base64,...'");
    }

    // 2. Extraction des données Base64 pures
    const partieBase64 = donneesImage.split(',')[1];
    if (!partieBase64) {
      throw new Error("Données Base64 corrompues ou invalides.");
    }

    // 3. Décodage et création du Blob (l'objet fichier)
    const objetBinaire = Utilities.newBlob(
      Utilities.base64Decode(partieBase64),
      'image/png', // MimeType explicite
      'signature.png' // Nom de fichier (bonne pratique)
    );

    // 4. Insertion dans le document
    const doc = DocumentApp.getActiveDocument();
    const curseur = doc.getCursor();

    let imageInseree;

    // Insère l'image à la position du curseur
    if (curseur) {
      imageInseree = curseur.insertInlineImage(objetBinaire);
    } else {
      // Sinon, l'ajoute à la fin du corps du document
      imageInseree = doc.getBody().appendImage(objetBinaire);
    }

    // 5. Redimensionnement proportionnel
    // Le canvas fait 340x160. Si nous fixons la largeur à 150px :
    const largeurVoulue = 150;
    const ratio = largeurVoulue / imageInseree.getWidth(); // Ratio basé sur la largeur
    const hauteurVoulue = imageInseree.getHeight() * ratio;

    imageInseree.setWidth(largeurVoulue);
    imageInseree.setHeight(hauteurVoulue);

    // Retourne un objet succès (sera reçu par 'withSuccessHandler' côté client)
    return { statut: 'succès', largeur: largeurVoulue, hauteur: hauteurVoulue };

  } catch (erreur) {
    Logger.log(`Échec de l'insertion de la signature : ${erreur.message}`);
    // Renvoie l'erreur au client (sera attrapée par 'withFailureHandler')
    throw new Error(`Échec de l'insertion : ${erreur.message}`);
  }
}
  1. N’oubliez pas de sauvegarder en cliquant sur l’icône en forme de disquette (ou Ctrl+S / Cmd+S).

Étape 3 : Le code « Visage » (Signature.html)

Maintenant, nous avons besoin de créer la fenêtre pop-up (la « modale ») que l’utilisateur verra.

  1. Dans l’éditeur Apps Script, cliquez sur le signe + à côté de « Fichiers » dans la barre latérale gauche.
  2. Choisissez HTML.
  3. Dans la pop-up, nommez le fichier Signature (avec la majuscule, c’est important car le Code.gs y fait référence) et appuyez sur Entrée.
  4. Effacez le contenu de base de ce nouveau fichier Signature.html.
  5. Copiez l’intégralité du code ci-dessous et collez-le dans Signature.html.

HTML

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
    <style>
      /* Importe la police Roboto (standard Google) */
      @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap');

      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }

      body {
        /* Utilise la police Google */
        font-family: 'Roboto', Arial, sans-serif;
        font-size: 14px;
        
        /* Le corps d'une modale Google est blanc */
        background-color: #ffffff;
        color: #202124; /* Noir standard de Google */
        
        /* Padding standard de 24px pour une modale */
        padding: 24px;
        text-align: center;
        user-select: none;
      }

      h3 {
        /* Taille de titre pour une modale simple */
        font-size: 18px;
        font-weight: 500;
        color: #202124;
        margin-bottom: 20px; /* Espace de 20px avant le canvas */
      }

      canvas {
        /* Bordure standard de Google */
        border: 1px solid #dadce0;
        /* Rayon de bordure standard */
        border-radius: 4px;
        cursor: crosshair;
        touch-action: none;
        display: block;
        margin: 0 auto 20px; /* Maintient le centrage, 20px en bas */
        background-color: #fff;
      }

      /* --- Styles des Boutons Google --- */

      button {
        font-family: 'Roboto', Arial, sans-serif;
        font-size: 14px;
        font-weight: 500;
        /* Rayon standard de 4px */
        border-radius: 4px;
        /* Padding exact de Google */
        padding: 9px 23px;
        cursor: pointer;
        border: 1px solid transparent;
        transition: all 0.2s;
        user-select: none;
        margin: 0 4px; /* Espace de 8px (4+4) entre les boutons */
      }

      /* Bouton "Insérer" (Primaire, Bleu) */
      #btn-inserer {
        background-color: #1a73e8; /* Bleu Google */
        color: #ffffff;
        border-color: #1a73e8;
      }
      #btn-inserer:hover {
        background-color: #1764cc;
        border-color: #1764cc;
      }
      #btn-inserer:focus {
        box-shadow: 0 0 0 3px #c6dafc; /* Halo de focus Google */
        outline: none;
      }

      /* État désactivé pour le bouton primaire */
      #btn-inserer:disabled {
        background-color: #AECBFA; /* Bleu Google désactivé */
        border-color: #AECBFA;
        color: #ffffff;
        cursor: not-allowed;
        opacity: 0.9; /* Opacité légère */
      }
      /* On retire l'effet de survol quand il est désactivé */
      #btn-inserer:disabled:hover {
        background-color: #AECBFA;
        border-color: #AECBFA;
      }


      /* Bouton "Effacer" (Secondaire, Blanc) */
      #btn-effacer {
        background-color: #ffffff;
        color: #1a73e8; /* Texte bleu (couleur d'action) */
        border: 1px solid #dadce0; /* Bordure grise standard */
        
        /* Léger ajustement de padding car la bordure prend 1px */
        padding: 8px 23px; 
      }
      #btn-effacer:hover {
        background-color: #f8fafe; /* Survol bleu très léger */
        border-color: #c6dafc;
      }
      #btn-effacer:focus {
        border-color: #1a73e8; /* Bordure bleue au focus */
        box-shadow: 0 0 0 2px #c6dafc;
        outline: none;
      }

      /* --- Zone d'erreur --- */
      #zone-erreur {
        color: #d93025; /* Rouge erreur Google */
        margin-top: 16px;
        font-size: 13px;
        height: 1.2em;
        text-align: center; /* Reste centré avec le layout */
      }
    </style>
</head>
<body>
  <h3>✍️ Signez ci-dessous</h3>
  
  <canvas id="canvas-signature" width="340" height="160"></canvas>
  
  <div>
    <button id="btn-effacer">Effacer</button>
    <button id="btn-inserer">Insérer</button>
  </div>
  
  <div id="zone-erreur"></div>

<script>
  /**
   * Fichier : Signature.html (JavaScript client)
   * Description : Gère la capture de la signature sur le canvas et l'envoi au serveur.
   */

  // Attend que le DOM (la page) soit entièrement chargé
  document.addEventListener('DOMContentLoaded', () => {

    // --- Références DOM ---
    const canvas = document.getElementById('canvas-signature');
    const contexte = canvas.getContext('2d');
    const boutonEffacer = document.getElementById('btn-effacer');
    const boutonInserer = document.getElementById('btn-inserer');
    const zoneErreur = document.getElementById('zone-erreur');

    // --- État de l'application ---
    let dessinEnCours = false; // Vrai si le clic est maintenu
    let estVide = true;        // Vrai si le canvas est vierge

    // Configuration initiale du pinceau
    contexte.lineWidth = 2;
    contexte.lineCap = 'round';
    contexte.strokeStyle = '#000'; // Couleur de la signature

    // Désactive le bouton d'insertion au démarrage
    boutonInserer.disabled = true;

    // --- Fonctions de dessin ---

    /**
     * Récupère les coordonnées (souris ou tactile) relatives au canvas.
     * @param {Event} evenement - L'événement 'move' ou 'start'.
     * @returns {object} { x, y }
     */
    const getCoordonnees = (evenement) => {
      const rect = canvas.getBoundingClientRect();
      const clientX = evenement.clientX || evenement.touches[0].clientX;
      const clientY = evenement.clientY || evenement.touches[0].clientY;
      return {
        x: clientX - rect.left,
        y: clientY - rect.top
      };
    };

    /**
     * Démarre le dessin (mousedown / touchstart)
     */
    const demarrerDessin = (evenement) => {
      dessinEnCours = true;
      const { x, y } = getCoordonnees(evenement);
      contexte.beginPath(); // Important pour ne pas lier le dernier trait
      contexte.moveTo(x, y);
    };

    /**
     * Arrête le dessin (mouseup / touchend / mouseout)
     */
    const arreterDessin = () => {
      dessinEnCours = false;
    };

    /**
     * Dessine sur le canvas (mousemove / touchmove)
     */
    const dessiner = (evenement) => {
      if (!dessinEnCours) return;
      
      // Empêche le défilement de la page sur mobile pendant le dessin
      evenement.preventDefault(); 

      const { x, y } = getCoordonnees(evenement);
      contexte.lineTo(x, y);
      contexte.stroke();

      // Si c'est le premier trait, active le bouton "Insérer"
      if (estVide) {
        estVide = false;
        boutonInserer.disabled = false;
      }
    };

    // --- Écouteurs d'événements (Méthode ES6) ---
    
    // Démarrage (Souris et Tactile)
    ['mousedown', 'touchstart'].forEach(typeEvenement => {
      canvas.addEventListener(typeEvenement, demarrerDessin, { passive: false });
    });
    
    // Arrêt
    ['mouseup', 'touchend', 'mouseout'].forEach(typeEvenement => {
      canvas.addEventListener(typeEvenement, arreterDessin);
    });
    
    // Mouvement
    ['mousemove', 'touchmove'].forEach(typeEvenement => {
      canvas.addEventListener(typeEvenement, dessiner, { passive: false });
    });

    // --- Fonctions des boutons ---

    /**
     * Efface le contenu du canvas et réinitialise l'état.
     */
    const effacerCanvas = () => {
      contexte.clearRect(0, 0, canvas.width, canvas.height);
      estVide = true;
      boutonInserer.disabled = true; // Désactive à nouveau le bouton
      zoneErreur.textContent = '';  // Efface les anciennes erreurs
    };

    /**
     * Envoie la signature au serveur Apps Script.
     */
    const enregistrerSignature = () => {
      if (estVide) return; // Sécurité supplémentaire

      // Retour visuel : désactivation des boutons pendant l'envoi
      boutonInserer.disabled = true;
      boutonEffacer.disabled = true;
      boutonInserer.textContent = 'Envoi...';
      zoneErreur.textContent = '';

      // Récupère l'image au format PNG Base64
      const donneesImage = canvas.toDataURL('image/png');

      google.script.run
        .withSuccessHandler(gererSucces)
        .withFailureHandler(gererEchec)
        .insererSignature(donneesImage);
    };
    
    /**
     * Gère le succès de l'insertion (ferme la modale).
     */
    const gererSucces = (resultatServeur) => {
      // resultatServeur contient { statut: 'succès', ... }
      // console.log('Insertion réussie:', resultatServeur);
      google.script.host.close();
      // Les boutons n'ont pas besoin d'être réactivés car la fenêtre se ferme.
    };

    /**
     * Gère l'échec de l'insertion (affiche l'erreur à l'utilisateur).
     */
    const gererEchec = (erreur) => {
      console.error(erreur); // Log complet dans la console
      // Affiche un message clair au lieu d'un 'alert' bloquant
      zoneErreur.textContent = `Erreur : ${erreur.message}`;
      
      // Réactive les boutons pour permettre une nouvelle tentative
      boutonInserer.disabled = false; // (estVide est toujours false)
      boutonEffacer.disabled = false;
      boutonInserer.textContent = 'Insérer';
    };

    // --- Connexion des boutons aux fonctions ---
    boutonEffacer.addEventListener('click', effacerCanvas);
    boutonInserer.addEventListener('click', enregistrerSignature);
  });
</script>
</body>
</html>
  1. Sauvegardez ce fichier (Ctrl+S / Cmd+S).

C’est prêt ! Voyons la magie opérer

Tout est en place. Voici comment l’utiliser :

  1. Revenez à votre onglet Google Docs.
  2. Actualisez (rechargez) la page de votre document. C’est crucial pour que le script onOpen s’exécute et crée le menu.
  3. Après le rechargement, regardez le menu : un nouveau bouton « ✍️ Signature » est apparu !
  4. Cliquez sur « ✍️ Signature » puis sur « Insérer la signature ».
  5. La première fois, Google vous demandera l’autorisation d’exécuter le script. C’est normal. Acceptez les autorisations (le script a besoin d’accéder au document pour y insérer l’image).
  6. Une fois l’autorisation donnée, ré-ouvrez le menu et cliquez à nouveau sur « Insérer la signature ».

La fenêtre pop-up va s’afficher :

capture d’écran 2025 11 16 à 09.24.33
Fenêtre de signature
  1. Signez dans le cadre blanc avec votre souris (ou votre doigt sur un appareil tactile).
  2. Cliquez sur « Insérer ».

La fenêtre se ferme et… votre signature est insérée directement dans le document, à l’endroit où se trouvait votre curseur ! Elle est même redimensionnée pour s’intégrer parfaitement.

Comment ça marche (pour les curieux) ?

Très simplement :

  1. Code.gs (le cerveau) : La fonction onOpen crée le menu. La fonction afficherModaleSignature montre la fenêtre (le fichier HTML). La fonction insererSignature reçoit les données de l’image, les décode et les insère dans le document à la position du curseur.
  2. Signature.html (le visage) : C’est la fenêtre pop-up. Le <canvas> est la zone de dessin. Le JavaScript (dans la balise <script>) capture vos mouvements de souris, dessine les traits, et désactive/active les boutons « Effacer » et « Insérer ».
  3. La communication : Quand vous cliquez sur « Insérer », le JavaScript du HTML convertit votre dessin en une longue chaîne de texte (en Base64) et l’envoie au « cerveau » (Code.gs) grâce à google.script.run. Le cerveau prend alors le relais pour l’insérer.

Et voilà ! Vous avez non seulement résolu un problème agaçant, mais vous avez aussi fait vos premiers pas avec la puissance de Google Apps Script.

J’espère que cette astuce vous fera gagner autant de temps qu’à moi.

Dites-moi dans les commentaires : Avez-vous réussi ? Quelles autres tâches répétitives dans Google Workspace aimeriez-vous automatiser ? Posez-moi vos questions, j’y répondrai avec plaisir !