Génération de Posters pour Mini-Programmes et Partage sur Moments WeChat

I. Choix de Solution: Trois Approches Principales

Approche Principe Avantages Inconvénients Cas d'usage
Canvas Natif Appel direct aux Canvas API côté client Flexible, aperçu en temps réel, pas de serveur nécessaire Code complexe, problèmes de compatibilité Posters hautement personnalisés
Composant Painter Piloté par configuraiton JSON, encapsulation Canvas Simple à prendre en main, syntaxe CSS-like Personnalisation limitée, maintenance réduite Modèles de posters standardisés
Génération Serveur Backend utilise des bibliothèques comme Node-canvas Style parfaitement cohérent, pas de charge client Coût serveur, pas d'aperçu en temps réel Campagnes marketing à fort trafic

Pour les modèles simples, utilisez Painter ; pour les personnalisations copmlexes, Canvas natif ; pour les hauts volumes, génération côté serveur.

II. Dessin avec Canvas Natif: Processus en Trois Étapes

2.1 Lier le Nœud Canvas

La nouvelle interface Canvas 2D (bibliothèque 2.9.0+) nécessite d'utiliser la méthode node :

<canvas type="2d" id="afficheCanvas" style="width: 300px; height: 400px;"></canvas>

Page({
  data: {
    largeurCanvas: 300,
    hauteurCanvas: 400,
    ratioPixel: 1
  },

  onLoad() {
    // Obtenir le ratio de pixels pour un affichage haute définition
    const sysInfo = wx.getSystemInfoSync();
    this.setData({ ratioPixel: sysInfo.ratioPixel });

    // Initialiser le Canvas
    this.initialiserCanvas();
  },

  initialiserCanvas() {
    const query = wx.createSelectorQuery();
    query.select('#afficheCanvas')
      .fields({ node: true, size: true })
      .exec((res) => {
        const canvas = res[0].node;
        const ctx = canvas.getContext('2d');

        // Définir les dimensions haute définition
        const dpr = this.data.ratioPixel;
        canvas.width = this.data.largeurCanvas * dpr;
        canvas.height = this.data.hauteurCanvas * dpr;

        // Mettre à l'échelle pour les écrans HD
        ctx.scale(dpr, dpr);

        this.canvas = canvas;
        this.ctx = ctx;

        // Commencer le dessin
        this.dessinerAffiche();
      });
  }
});

2.2 Dessiner le Contenu

Les images réseau doivent être téléchargées localement avant d'être utilisées dans Canvas :

async dessinerAffiche() {
  const ctx = this.ctx;
  const { largeurCanvas, hauteurCanvas } = this.data;

  // 1. Dessiner l'arrière-plan
  ctx.fillStyle = '#ffffff';
  ctx.fillRect(0, 0, largeurCanvas, hauteurCanvas);

  // 2. Dessiner l'image de fond (nécessite un téléchargement préalable)
  await this.ajouterImage('https://exemple.com/fond.jpg');

  // 3. Dessiner l'avatar utilisateur (découpage circulaire)
  await this.ajouterImageRonde('https://exemple.com/avatar.jpg', 20, 20, 60);

  // 4. Dessiner le texte
  ctx.fillStyle = '#333333';
  ctx.font = '16px "PingFang SC", sans-serif';
  ctx.textAlign = 'left';
  ctx.fillText('Pseudo utilisateur', 90, 50);

  // 5. Dessiner un texte multiligne
  this.dessinerTexteMultiligne('Contenu du texte qui doit être automatiquement renvoyé à la ligne en fonction de la largeur.', 20, 100, largeurCanvas - 40, 24);

  // 6. Dessiner le QR code
  await this.ajouterImage('https://exemple.com/qrcode.jpg', 230, 320, 60, 60);
}

// Méthode utilitaire: ajouter une image standard
ajouterImage(src, x, y, w, h) {
  return new Promise((resolve, reject) => {
    wx.getImageInfo({
      src,
      success: (res) => {
        const img = this.canvas.createImage();
        img.src = res.path;
        img.onload = () => {
          this.ctx.drawImage(img, x, y, w, h);
          resolve();
        };
        img.onerror = reject;
      },
      fail: reject
    });
  });
}

// Méthode utilitaire: ajouter une image circulaire
ajouterImageRonde(src, x, y, taille) {
  return new Promise((resolve, reject) => {
    wx.getImageInfo({
      src,
      success: (res) => {
        const img = this.canvas.createImage();
        img.src = res.path;
        img.onload = () => {
          this.ctx.save();
          this.ctx.beginPath();
          this.ctx.arc(x + taille/2, y + taille/2, taille/2, 0, Math.PI * 2);
          this.ctx.clip();
          this.ctx.drawImage(img, x, y, taille, taille);
          this.ctx.restore();
          resolve();
        };
        img.onerror = reject;
      },
      fail: reject
    });
  });
}

// Méthode utilitaire: dessiner un texte avec retour automatique à la ligne
dessinerTexteMultiligne(texte, x, y, largeurMax, hauteurLigne) {
  const ctx = this.ctx;
  let ligne = '';
  let yActuel = y;

  for (let i = 0; i < texte.length; i++) {
    const testLigne = ligne + texte[i];
    const metrics = ctx.measureText(testLigne);

    if (metrics.width > largeurMax && i > 0) {
      ctx.fillText(ligne, x, yActuel);
      ligne = texte[i];
      yActuel += hauteurLigne;
    } else {
      ligne = testLigne;
    }
  }
  ctx.fillText(ligne, x, yActuel);
}

2.3 Exporter en Image

Une fois le dessin terminé, exporter en fichier temporaire :

exporterAffiche() {
  wx.canvasToTempFilePath({
    canvas: this.canvas,  // Pour la nouvelle API, passer l'instance canvas
    fileType: 'jpg',
    quality: 0.9,
    success: (res) => {
      this.setData({ cheminAffiche: res.tempFilePath });
      console.log('Affiche générée avec succès:', res.tempFilePath);
    },
    fail: (err) => {
      console.error('Échec de l\'export:', err);
    }
  });
}

III. Sauvegarde dans la Galerie: La Gestion des Permissions

La sauvegarde d'images nécessite la permission scope.writePhotosAlbum. Le traitement du refus par l'utilisateur est un point crucial :

sauvegarderDansGalerie() {
  const cheminAffiche = this.data.cheminAffiche;
  if (!cheminAffiche) {
    wx.showToast({ title: 'Veuillez d\'abord générer l\'affiche', icon: 'none' });
    return;
  }

  // Vérifier d'abord l'état des permissions
  wx.getSetting({
    success: (res) => {
      // Déjà autorisé: sauvegarder directement
      if (res.authSetting['scope.writePhotosAlbum']) {
        this.effectuerSauvegarde(cheminAffiche);
        return;
      }

      // Non autorisé: demander la permission
      wx.authorize({
        scope: 'scope.writePhotosAlbum',
        success: () => {
          this.effectuerSauvegarde(cheminAffiche);
        },
        fail: () => {
          // Après un refus, authorize ne réaffichera pas la popup
          // Il faut guider manuellement vers les paramètres
          this.afficherModalAutorisation();
        }
      });
    }
  });
}

effectuerSauvegarde(cheminFichier) {
  wx.saveImageToPhotosAlbum({
    filePath: cheminFichier,
    success: () => {
      wx.showToast({ title: 'Sauvegarde réussie', icon: 'success' });
    },
    fail: (err) => {
      // Gérer différents cas d'échec
      if (err.errMsg.includes('auth deny')) {
        this.afficherModalAutorisation();
      }
    }
  });
}

afficherModalAutorisation() {
  wx.showModal({
    title: 'Permission de galerie requise',
    content: 'Veuillez activer la permission de sauvegarder des images dans la galerie depuis les paramètres',
    confirmText: 'Aller aux paramètres',
    success: (res) => {
      if (res.confirm) {
        // Ouvrir la page des paramètres
        wx.openSetting({
          success: (setRes) => {
            if (setRes.authSetting['scope.writePhotosAlbum']) {
              this.effectuerSauvegarde(this.data.cheminAffiche);
            }
          }
        });
      }
    }
  });
}

IV. Partage sur Moments WeChat: Configuration et Guide

Les mini-programmes ne peuvent pas déclencher le partage sur Moments WeChat par programmation - l'utilisateur doit cliquer manuellement sur le menu en haut à droite.

4.1 Activer le menu de partage

onLoad() {
  wx.showShareMenu({
    withShareTicket: true,
    menus: ['shareAppMessage', 'shareTimeline']  // Doit inclure shareTimeline
  });
}

4.2 Définir le contenu de partage

// Partager avec des amis
onShareAppMessage() {
  return {
    title: 'Ce mini-programme est génial',
    path: '/pages/index/index',
    imageUrl: '/assets/share.png'
  };
}

// Partager sur Moments
onShareTimeline() {
  return {
    title: 'Je recommande ce mini-programme',
    query: 'from=timeline',  // Pour le suivi des données, n'affecte pas le routage
    imageUrl: '/assets/timeline.png'  // Format recommandé 5:4, max 1MB
  };
}

4.3 Ajouter un bouton pour guider l'utilisateur

<button class="btn-partage" open-type="share">Partager avec des amis</button>

V. Conseils d'Évitage: Expériences et Pièges Courants

Piège 1: Export de Canvas vide

Cause: draw() est asynchrone, il faut attendre la fin du dessin avant d'exporter.

// ❌ Incorrect: appeler l'export immédiatement
ctx.draw();
wx.canvasToTempFilePath({ ... });

// ✅ Correct: utiliser callback ou setTimeout
ctx.draw();
setTimeout(() => {
  wx.canvasToTempFilePath({ ... });
}, 300);

Piège 2: Images floues sur écrans HD

Cause: Le pixel réel du Canvas est le pixel logique × ratioPixel, mais la taille de style est la taille logique.

// ❌ Incorrect: définir directement la taille logique
canvas.width = 300;
canvas.height = 400;

// ✅ Correct: mettre à l'échelle selon le ratio de pixels
const dpr = wx.getSystemInfoSync().ratioPixel;
canvas.width = 300 * dpr;
canvas.height = 400 * dpr;
ctx.scale(dpr, dpr);  // Également mettre à l'échelle le dessin

Piège 3: Échec du téléchargement d'images réseau

Cause: Le domaine n'est pas dans la liste blanche des téléchargements.

Solution: Se connecter à la plateforme WeChat → Gestion du développement → Domaines serveur → Ajouter le domaine d'images à downloadFile legal domain.

Piège 4: Extension de fichier incorrecte sur Android

Cause: Certains appareils Android retournent un tempFilePath sans extension dans downloadFile.

// ✅ Spécifier filePath avec extension forcée
const filePath = wx.env.USER_DATA_PATH + '/affiche.jpg';
wx.downloadFile({
  url: urlAffiche,
  filePath,  // Spécifier le chemin local et l'extension
  success: (res) => {
    wx.saveImageToPhotosAlbum({ filePath: res.filePath });
  }
});

Piège 5: Positionnement de retour à la ligne incorrect sur iOS

Cause: ctx.measureText() se comporte différemment sur iOS et Android.

Solution: Laisser une marge dans le calcul de retour à la ligne, par exemple largeur * 0.95.

Piège 6: Après un refus, la permission ne demande plus

Cause: Conception de WeChat, après un refus, authorize échoue silencieusement.

Solution: Il faut utiliser <button open-type="openSetting"> ou wx.openSetting() pour guider manuellement.

Piège 7: Limitations du mode page unique Moments

Après partage sur Moments, l'ouvre dans un "mode page unique" avec plusieurs restrictions:

  • Interfaces de connexion wx.login()
  • Sélection d'images wx.chooseImage()
  • Sauvegarde d'images wx.saveImageToPhotosAlbum()
  • Navigation entre pages wx.navigateTo()
  • Composants tabBar

Solution: Vérifier via le scénario scene === 1154 et masquer les fonctionnalités indisponibles en mode page unique.

VI. Exemple Complet: Génération + Sauvegarde + Partage

// pages/affiche/affiche.js
Page({
  data: {
    cheminAffiche: '',
    largeurCanvas: 300,
    hauteurCanvas: 420,
    ratioPixel: 1
  },

  onLoad() {
    this.setData({ ratioPixel: wx.getSystemInfoSync().ratioPixel });
    this.initialiserCanvas();

    // Activer le menu de partage
    wx.showShareMenu({
      withShareTicket: true,
      menus: ['shareAppMessage', 'shareTimeline']
    });
  },

  async initialiserCanvas() {
    const query = wx.createSelectorQuery();
    query.select('#affiche')
      .fields({ node: true, size: true })
      .exec((res) => {
        const canvas = res[0].node;
        const ctx = canvas.getContext('2d');
        const dpr = this.data.ratioPixel;

        canvas.width = this.data.largeurCanvas * dpr;
        canvas.height = this.data.hauteurCanvas * dpr;
        ctx.scale(dpr, dpr);

        this.canvas = canvas;
        this.ctx = ctx;

        this.genererAffiche();
      });
  },

  async genererAffiche() {
    wx.showLoading({ title: 'Génération en cours...' });

    try {
      // Arrière-plan
      this.ctx.fillStyle = '#f5f5f5';
      this.ctx.fillRect(0, 0, this.data.largeurCanvas, this.data.hauteurCanvas);

      // Télécharger et dessiner l'image de fond
      await this.telechargerEtAjouterImage('https://exemple.com/fond.jpg', 0, 0, 300, 200);

      // Dessiner l'avatar
      await this.telechargerEtAjouterImageRonde('https://exemple.com/avatar.jpg', 20, 220, 50);

      // Dessiner le texte
      this.ctx.fillStyle = '#333';
      this.ctx.font = '14px sans-serif';
      this.ctx.fillText('Pseudo utilisateur', 80, 245);

      // Dessiner le texte descriptif
      this.dessinerTexteMultiligne('Générez votre affiche personnalisée et invitez vos amis à rejoindre', 20, 290, 260, 20);

      // Dessiner le QR code
      await this.telechargerEtAjouterImage('https://exemple.com/qr.jpg', 110, 320, 80, 80);

      wx.hideLoading();
      this.exporterAffiche();
    } catch (err) {
      wx.hideLoading();
      wx.showToast({ title: 'Échec de la génération', icon: 'none' });
    }
  },

  telechargerEtAjouterImage(src, x, y, w, h) {
    return new Promise((resolve, reject) => {
      wx.getImageInfo({
        src,
        success: (res) => {
          const img = this.canvas.createImage();
          img.src = res.path;
          img.onload = () => {
            this.ctx.drawImage(img, x, y, w, h);
            resolve();
          };
          img.onerror = reject;
        },
        fail: reject
      });
    });
  },

  telechargerEtAjouterImageRonde(src, x, y, taille) {
    return new Promise((resolve, reject) => {
      wx.getImageInfo({
        src,
        success: (res) => {
          const img = this.canvas.createImage();
          img.src = res.path;
          img.onload = () => {
            this.ctx.save();
            this.ctx.beginPath();
            this.ctx.arc(x + taille/2, y + taille/2, taille/2, 0, Math.PI * 2);
            this.ctx.clip();
            this.ctx.drawImage(img, x, y, taille, taille);
            this.ctx.restore();
            resolve();
          };
          img.onerror = reject;
        },
        fail: reject
      });
    });
  },

  dessinerTexteMultiligne(texte, x, y, largeurMax, hauteurLigne) {
    const ctx = this.ctx;
    let ligne = '';
    let yActuel = y;
    for (let i = 0; i < texte.length; i++) {
      const testLigne = ligne + texte[i];
      if (ctx.measureText(testLigne).width > largeurMax && i > 0) {
        ctx.fillText(ligne, x, yActuel);
        ligne = texte[i];
        yActuel += hauteurLigne;
      } else {
        ligne = testLigne;
      }
    }
    ctx.fillText(ligne, x, yActuel);
  },

  exporterAffiche() {
    wx.canvasToTempFilePath({
      canvas: this.canvas,
      fileType: 'jpg',
      quality: 0.9,
      success: (res) => {
        this.setData({ cheminAffiche: res.tempFilePath });
      }
    });
  },

  sauvegarderAffiche() {
    const chemin = this.data.cheminAffiche;
    if (!chemin) return;

    wx.getSetting({
      success: (res) => {
        if (!res.authSetting['scope.writePhotosAlbum']) {
          wx.authorize({
            scope: 'scope.writePhotosAlbum',
            success: () => this._effectuerSauvegarde(chemin),
            fail: () => this._afficherModalAutorisation()
          });
        } else {
          this._effectuerSauvegarde(chemin);
        }
      }
    });
  },

  _effectuerSauvegarde(cheminFichier) {
    wx.saveImageToPhotosAlbum({
      filePath: cheminFichier,
      success: () => wx.showToast({ title: 'Sauvegardé avec succès', icon: 'success' }),
      fail: (err) => err.errMsg.includes('deny') && this._afficherModalAutorisation()
    });
  },

  _afficherModalAutorisation() {
    wx.showModal({
      title: 'Permission de galerie requise',
      content: 'Veuillez activer la permission de sauvegarder des images dans la galerie',
      confirmText: 'Aller aux paramètres',
      success: (res) => res.confirm && wx.openSetting()
    });
  },

  guiderPartageTimeline() {
    wx.showToast({ title: 'Veuillez cliquer sur ··· en haut à droite pour partager sur Moments', icon: 'none' });
  },

  onShareAppMessage() {
    return {
      title: 'Générez votre affiche personnalisée',
      path: '/pages/affiche/affiche',
      imageUrl: this.data.cheminAffiche
    };
  },

  onShareTimeline() {
    return {
      title: 'Générez votre affiche personnalisée',
      query: 'from=timeline',
      imageUrl: this.data.cheminAffiche
    };
  }
});

Étiquettes: Mini-programmes WeChat Canvas Partage réseau Génération d'images Permissions utilisateur

Publié le 24 juin à 22h08