text: (function (ctx) {
    'use strict';
  
    const PREFIXE = 'STEGANO---';
    const SUFFIXE = '---ONAGETS';
  
    const PIXELS_PAR_CARACTERE = 6;
    const OCTETS_PAR_CARACTERE = PIXELS_PAR_CARACTERE * 4; // RGBA
  
    ctx.Steganographie = class Steganographie {
      // ===== Interface =====
  
      decoder (image) {
        // Vérifier que l'image peut contenir au moins 1 caractère.
        const taillePixels = image.width * image.height;
        const tailleCaracteres = Math.floor(taillePixels / PIXELS_PAR_CARACTERE);
        if (tailleCaracteres < 1) {
          return ''; // L'image ne peut contenir aucun texte.
        }
  
        // Vérifier que la reconstitution du texte commence par le préfixe
        // (sinon il ne s'agit pas d'un texte, mais de bruit).
        const positionPrefixe = this.trouver(PREFIXE, 0, 1, image);
        if (positionPrefixe !== -1) {
          const debut = positionPrefixe + PREFIXE.length;
          let fin = 1 + tailleCaracteres - SUFFIXE.length;
  
          // Rechercher la position d'un suffixe qui marque la fin du texte.
          const positionSuffixe = this.trouver(SUFFIXE, debut, fin, image);
          if (positionSuffixe !== -1) {
            fin = positionSuffixe;
          } else {
            return null; // Erreur : il y a un texte mais sans suffixe.
          }
  
          // Il y a un préfixe et un suffixe : extraire le texte entre les deux,
          // puis remettre tout le texte à zéro dans l'image.
          const texte = this.extraire(debut, fin, image);
          this.remettreAZeroTexte(image);
          return texte;
        } else {
          return ''; // L'image ne contient aucun texte identifiable.
        }
      }
  
      encoder (texte, image) {
        // Encadrer le texte entre un préfixe et un suffixe.
        const texteComplet = `${PREFIXE}${texte}${SUFFIXE}`;
  
        // Vérifier que l'image peut contenir le texte complet.
        const taillePixels = image.width * image.height;
        const tailleCaracteres = Math.floor(taillePixels / PIXELS_PAR_CARACTERE);
        if (texteComplet.length > tailleCaracteres) {
          return "Place insuffisante pour écrire le texte dans l'image";
        } else {
          // Ecrire le texte en commençant au début de l'image.
          this.ecrire(texteComplet, 0, image);
        }
      }
  
      // ===== Implémentation : recherche, lecture, et écriture de texte =====
  
      trouver (texte, debut, fin, image) {
        // Avancer dans l'image pour trouver une correspondance avec un texte,
        // et retourner la position du début de celle-ci, ou la valeur -1.
  
        let indexCaracteres = 0;
        let positionCorrespondance = -1;
        for (let position = debut; position < fin; position++) {
          const caractere = this.extraire(position, position + 1, image);
          if (caractere === texte.charAt(indexCaracteres)) {
            if (indexCaracteres === 0) {
              positionCorrespondance = position;
            }
            indexCaracteres += 1;
            if (indexCaracteres === texte.length) {
              break; // Correspondance complète trouvée.
            }
          } else {
            indexCaracteres = 0;
            positionCorrespondance = -1;
          }
        }
        return positionCorrespondance;
      }
  
      extraire (debut, fin, image) {
        // Extraire le texte, caractère par caractère, entre début et fin.
        let texte = '';
        for (let idxCar = debut; idxCar < fin; idxCar++) {
          const position = idxCar * OCTETS_PAR_CARACTERE;
          const octets = this.extraireOctets(position, image);
          const caractere = this.reconstituerCaractere(octets);
          texte += caractere;
        }
        return texte;
      }
  
      ecrire (texte, debut, image) {
        // Ecrire le texte dans l'image, caractère par caractère ...
        for (let idxCar = 0; idxCar < texte.length; idxCar++) {
          const caractere = texte.charAt(idxCar);
          const position = (debut + idxCar) * OCTETS_PAR_CARACTERE;
  
          // ... et bit par bit.
          const bits = this.decomposerCaractere(caractere);
          for (let idxBit = 0; idxBit < bits.length; idxBit++) {
            const valeurBit = bits[idxBit];
            if (valeurBit !== -1) {
              image.data[position + idxBit] = this.ecrireBitPoidFaible(
                image.data[position + idxBit], valeurBit
              );
            }
          }
        }
      }
  
      remettreAZeroTexte (image) {
        // Remettre les bits de poids faible à zéro sur toute l'étendue
        // de texte à l'intérieur de l'image, pour effacer le texte précédent.
  
        const taillePixels = image.width * image.height;
        const tailleCaracteres = Math.floor(taillePixels / PIXELS_PAR_CARACTERE);
        for (let position = 0; position < tailleCaracteres; position++) {
          this.ecrire(String.fromCharCode(0), position, image);
        }
      }
  
      // ===== Implémentation : manipulation de bits =====
  
      decomposerCaractere (caractere) {
        // Décomposer les 16 bits de la valeur UTF-16 d'un caractère JavaScript,
        // pour fournir 24 bits de poids faible à insérer dans 24 octets
        // selon une progression RGBA de type rgBaRGBaRGBaRGBaRGBaRGBa
        // (lettre minuscule => bit inchangé (-1), majuscule => bit changé (0/1)).
  
        const valeur = caractere.charCodeAt(0);
        return [
          -1,
          -1,
          (valeur & 0b1000000000000000) >> 15,
          -1,
          (valeur & 0b0100000000000000) >> 14,
          (valeur & 0b0010000000000000) >> 13,
          (valeur & 0b0001000000000000) >> 12,
          -1,
          (valeur & 0b0000100000000000) >> 11,
          (valeur & 0b0000010000000000) >> 10,
          (valeur & 0b0000001000000000) >> 9,
          -1,
          (valeur & 0b0000000100000000) >> 8,
          (valeur & 0b0000000010000000) >> 7,
          (valeur & 0b0000000001000000) >> 6,
          -1,
          (valeur & 0b0000000000100000) >> 5,
          (valeur & 0b0000000000010000) >> 4,
          (valeur & 0b0000000000001000) >> 3,
          -1,
          (valeur & 0b0000000000000100) >> 2,
          (valeur & 0b0000000000000010) >> 1,
          (valeur & 0b0000000000000001),
          -1
        ];
      }
  
      ecrireBitPoidFaible (octet, bit) {
        // Modifier le bit de poid faible de l'octet.
  
        return ((octet & 0b11111110) | (bit & 0b00000001));
      }
  
      extraireOctets (position, image) {
        // Retourner 16 octets parmi les 24 octets suivants de l'image,
        // en suivant une progression RGBA de type rgBaRGBaRGBaRGBaRGBaRGBa
        // (lettre minuscule => octet ignoré, majuscule => octet lu).
  
        return [
          image.data[position + 2],
          image.data[position + 4],
          image.data[position + 5],
          image.data[position + 6],
          image.data[position + 8],
          image.data[position + 9],
          image.data[position + 10],
          image.data[position + 12],
          image.data[position + 13],
          image.data[position + 14],
          image.data[position + 16],
          image.data[position + 17],
          image.data[position + 18],
          image.data[position + 20],
          image.data[position + 21],
          image.data[position + 22]
        ];
      }
  
      reconstituerCaractere (octets) {
        // Reconstituer la valeur UTF-16 d'un caractère JavaScript depuis
        // les bits de poids faible d'une séquence de 16 octets.
  
        let valeur = 0;
        for (let idx = 0; idx < octets.length; idx++) {
          valeur |= ((octets[idx] & 0b00000001) << (15 - idx));
        }
        return String.fromCharCode(valeur);
      }
    };
  }(window.stegano));
  