Retour au blog

Images manquantes lors de l'export Markdown vers Word ? Comment nous avons résolu les problèmes de cross-origin et de compatibilité WebP

Équipe MD-TO

Lors du développement de la fonctionnalité d’export Markdown vers Word sur md-to.com, nous avons rencontré un bug frustrant : les images dans le contenu Markdown des utilisateurs disparaissaient après l’export vers Word.

La cause profonde impliquait deux couches : la politique de sécurité du navigateur (CORS) et la compatibilité des formats d’image (WebP). Cet article documente le processus de débogage et notre solution finale.


Le symptôme

Un utilisateur colle du Markdown comme ceci dans l’éditeur :

![Interface éditeur](https://example.com/screenshot.webp)
![Icône du site](https://example.com/icon.png)

L’aperçu affiche les deux images correctement. Mais dans le fichier Word exporté, les deux images ont disparu — remplacées par un texte de substitution comme [Interface éditeur] et [Icône du site].


Problème 1 : Word ne supporte pas WebP

Cause profonde

Nous utilisons la bibliothèque docx pour générer les documents Word. Son composant ImageRun ne supporte que quatre formats d’image :

type DocxImageType = 'jpg' | 'png' | 'gif' | 'bmp';

Les sites web modernes utilisent massivement le WebP (25-35 % plus petit que le PNG). Lorsque notre code rencontrait un format non supporté, il levait une erreur Unsupported image type et l’image était silencieusement ignorée.

Solution : Conversion automatique via Canvas

Les navigateurs affichent nativement le WebP, nous pouvons donc utiliser l’API Canvas pour le convertir en PNG :

function loadImageViaCanvas(
  source: Blob | string,
): Promise<{ blob: Blob; width: number; height: number }> {
  return new Promise((resolve, reject) => {
    const img = new Image();
    const isBlobSource = source instanceof Blob;
    if (!isBlobSource) {
      img.crossOrigin = 'anonymous';
    }
    const blobUrl = isBlobSource ? URL.createObjectURL(source) : null;

    img.onload = () => {
      if (blobUrl) URL.revokeObjectURL(blobUrl);
      const { naturalWidth: width, naturalHeight: height } = img;
      const canvas = document.createElement('canvas');
      canvas.width = width;
      canvas.height = height;
      const ctx = canvas.getContext('2d')!;
      ctx.drawImage(img, 0, 0);
      canvas.toBlob(
        (pngBlob) => {
          if (pngBlob) resolve({ blob: pngBlob, width, height });
          else reject(new Error('Canvas toBlob failed'));
        },
        'image/png',
      );
    };
    img.onerror = () => reject(new Error('Failed to load image'));
    img.src = blobUrl ?? (source as string);
  });
}

Cette fonction accepte deux types d’entrée :

  • Blob : pour la conversion de format pure des images déjà téléchargées
  • Chaîne URL : pour le contournement CORS (plus de détails ci-dessous)

Quel que soit le format d’entrée (WebP, AVIF, etc.), la sortie est toujours en PNG — que Word gère parfaitement.


Problème 2 : CORS bloque le téléchargement des images cross-origin

Après avoir corrigé le problème WebP, nous avons découvert un problème plus fondamental : les images cross-origin ne pouvaient tout simplement pas être téléchargées.

Cause profonde

Notre code utilise fetch() pour obtenir les données de l’image :

const response = await fetch(imageUrl);
const blob = await response.blob();

Lorsque l’URL de l’image est sur un domaine différent du site web (par ex., le site est md-to.com, l’image est sur example.com), la politique CORS (Cross-Origin Resource Sharing) du navigateur bloque la requête :

Access to fetch at 'https://example.com/image.png'
from origin 'https://md-to.com' has been blocked by CORS policy

Pourquoi l’aperçu fonctionne mais pas l’export ?

Parce que le navigateur applique différents niveaux de sécurité selon les opérations :

OpérationCross-origin autorisé ?Pourquoi
Rendu <img src="externe">✅ OuiLe navigateur affiche les images sans CORS
Accès aux données via fetch()❌ BloquéLa lecture de données binaires en JS nécessite CORS
Export <img crossOrigin> + Canvas❌ BloquéUn canvas « teinté » ne peut pas exporter de données
fetch({mode: 'no-cors'})❌ IllisibleRetourne un corps vide

L’aperçu utilise des balises <img> (rendu cross-origin autorisé), tandis que l’export nécessite que JavaScript lise les données binaires de l’image (bloqué par CORS). C’est pourquoi vous pouvez « voir l’image mais pas l’exporter ».

Pourquoi le Base64 n’aide pas non plus

Vous pourriez penser : « Il suffit de convertir les images en Base64 ! »

Mais le Base64 n’est qu’un format d’encodage, pas une méthode d’acquisition de données. Le problème n’est pas « dans quel format stocker » — c’est que « le navigateur ne vous laisse pas accéder aux octets bruts ». Que vous vouliez encoder en Base64, ArrayBuffer ou autre, la première étape est d’obtenir les données — et CORS bloque cela complètement.

Solution : Proxy d’images côté serveur

Puisqu’il n’y a pas de contournement côté navigateur, nous laissons le serveur récupérer les images. Les serveurs n’ont pas de restrictions CORS et peuvent librement requêter n’importe quelle URL.

Nous avons créé une Edge Function légère comme proxy d’images :

Navigateur → /api/image-proxy?url=https://example.com/image.png → Notre serveur → example.com
                                                                   ↑ Pas de restriction CORS

Fonction proxy (déployée en edge) :

export async function onRequest({ request }) {
  const { searchParams } = new URL(request.url);
  const target = searchParams.get('url');

  // Sécurité : uniquement http(s), uniquement types image, limite de taille
  const upstream = await fetch(target);
  const contentType = upstream.headers.get('content-type');

  return new Response(await upstream.arrayBuffer(), {
    headers: {
      'Content-Type': contentType,
      'Cache-Control': 'public, max-age=86400',
      'Access-Control-Allow-Origin': '*',
    },
  });
}

Routage automatique côté client :

function resolveImageSrc(src: string): string {
  const resolved = new URL(src, window.location.href).toString();

  // Router les images cross-origin via le proxy
  if (new URL(resolved).origin !== window.location.origin) {
    return `/api/image-proxy?url=${encodeURIComponent(resolved)}`;
  }

  return resolved;
}

Toutes les requêtes d’images cross-origin deviennent désormais des requêtes same-origin — plus de problèmes CORS.


Architecture finale

URL de l'image dans le Markdown


  resolveImageSrc()

        ├── Same-origin → Fetch direct

        └── Cross-origin → /api/image-proxy?url=xxx


                        Edge Function transfert


                          Blob obtenu


                    getDocxImageType() vérifie le format

                    ┌─────────┴──────────┐
                    ▼                    ▼
              jpg/png/gif/bmp      webp/avif/autre
              Intégrer directement  Canvas → PNG → Intégrer

Environnement de développement

Un middleware proxy correspondant a été ajouté au serveur de développement Vite, assurant un comportement cohérent entre le développement et la production.

Mesures de sécurité

Le proxy d’images n’est pas un proxy ouvert sans restrictions :

  • N’autorise que les protocoles http:// et https://
  • Ne transmet que les Content-Types d’images (png/jpeg/gif/bmp/webp/avif/svg)
  • Limite de 10 Mo par image
  • Cache de 24 heures sur les réponses

Résumé

ProblèmeCauseSolution
Les images WebP n’apparaissent pas dans WordLa bibliothèque docx ne supporte pas WebPL’API Canvas convertit en PNG
Les images cross-origin ne peuvent pas être téléchargéesPolitique de sécurité CORS du navigateurProxy Edge Function côté serveur
Les images échouent aussi en environnement de devlocalhost → domaine externe est cross-originMiddleware proxy du serveur dev Vite

Point clé : Le modèle de sécurité du navigateur distingue « rendu » et « accès aux données ». Ce n’est pas parce qu’une balise <img> peut afficher une image cross-origin que JavaScript peut lire ses données. Lorsque vous avez besoin d’un accès côté client aux ressources cross-origin, un proxy côté serveur est pratiquement la seule approche fiable.


Envie d’essayer ? Rendez-vous sur md-to.com Markdown vers Word, collez du Markdown avec des images externes, et exportez.